Skip to content

Commit d4b8498

Browse files
authored
Merge pull request #79 from ScrawnDotDev/feat/auto-register-dodo-webhooks
feat: auto-register live/test dodo webhooks via SDK during onboarding
2 parents 668215a + 343a236 commit d4b8498

11 files changed

Lines changed: 93 additions & 38 deletions

File tree

‎.env.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/scrawn
22
CLICKHOUSE_URL=http://default:clickhouse@localhost:8123/scrawn
33

44
HMAC_SECRET=
5+
APP_URL=http://localhost:8060 # URL the Scrawn backend is hosted on
56

67
# SENTRY_DSN=
78
# LOG_LEVEL=info

‎.env.test‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ DODO_PAYMENTS_LIVE_API_KEY=
77
DODO_PAYMENTS_TEST_API_KEY=test_dodo_key
88
DODO_PAYMENTS_LIVE_PRODUCT_ID=
99
DODO_PAYMENTS_TEST_PRODUCT_ID=prod_test
10-
DODO_PAYMENTS_WEBHOOK_SECRET=test-webhook-secret
1110
REDIRECT_URL=http://localhost:18070
1211
TEST_API_KEY=
1312
APP_URL=http://localhost:18070

‎docker-compose.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ services:
5959
CLICKHOUSE_URL: http://default:clickhouse@clickhouse:8123/scrawn
6060
NODE_ENV: production
6161
HMAC_SECRET: ${HMAC_SECRET}
62+
APP_URL: ${APP_URL}
6263

6364
dashboard:
6465
profiles:

‎src/routes/gRPC/payment/paymentProvider.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function getDodoClient(
2929
liveClient = new DodoPayments({
3030
bearerToken: apiKey,
3131
environment: "live_mode",
32-
webhookKey: metadata?.dodo_webhook_secret ?? undefined,
32+
webhookKey: metadata?.dodo_live_webhook_secret ?? undefined,
3333
});
3434
return liveClient;
3535
}
@@ -45,7 +45,7 @@ export async function getDodoClient(
4545
testClient = new DodoPayments({
4646
bearerToken: apiKey,
4747
environment: "test_mode",
48-
webhookKey: metadata?.dodo_webhook_secret ?? undefined,
48+
webhookKey: metadata?.dodo_test_webhook_secret ?? undefined,
4949
});
5050
return testClient;
5151
}

‎src/routes/http/api/onboarding.ts‎

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { FastifyRequest, FastifyReply } from "fastify";
22
import * as Sentry from "@sentry/bun";
33
import { ZodError } from "zod";
4-
import { onboardingCronSchema } from "../../../zod/internals.ts";
4+
import DodoPayments from "dodopayments";
5+
import { onboardingSchema } from "../../../zod/internals.ts";
56
import {
67
createWideEventBuilder,
78
generateRequestId,
89
} from "../../../context/requestContext.ts";
910
import { logger } from "../../../errors/logger.ts";
10-
import { AuthError } from "../../../errors/auth.ts";
11+
import { AuthError } from "../../../errors/auth";
1112
import { authenticateHttpApiKey } from "../../../utils/authenticateHttpApiKey.ts";
1213
import {
1314
upsertMetadata,
@@ -18,7 +19,7 @@ import { clearClients } from "../../gRPC/payment/paymentProvider.ts";
1819
export async function handleOnboarding(
1920
request: FastifyRequest,
2021
reply: FastifyReply
21-
): Promise<{ crons: string[] }> {
22+
): Promise<Record<string, never>> {
2223
const builder = createWideEventBuilder(
2324
generateRequestId(),
2425
request.method,
@@ -30,14 +31,65 @@ export async function handleOnboarding(
3031
await authenticateHttpApiKey(authHeader);
3132

3233
const body = await request.body;
33-
const validated = onboardingCronSchema.parse(body);
34+
const validated = onboardingSchema.parse(body);
35+
36+
const appUrl = process.env.APP_URL;
37+
if (!appUrl) {
38+
builder.setError(500, {
39+
type: "ConfigError",
40+
message: "APP_URL environment variable is not set",
41+
});
42+
reply.code(500);
43+
return {};
44+
}
45+
46+
const liveClient = new DodoPayments({
47+
bearerToken: validated.dodoLiveApiKey,
48+
environment: "live_mode",
49+
});
50+
const testClient = new DodoPayments({
51+
bearerToken: validated.dodoTestApiKey,
52+
environment: "test_mode",
53+
});
54+
55+
let liveSecret: string;
56+
let testSecret: string;
57+
try {
58+
const liveWebhook = await liveClient.webhooks.create({
59+
url: `${appUrl}/webhooks/payment/createdCheckout?mode=production`,
60+
description: "Scrawn live payment webhook",
61+
filter_types: ["payment.succeeded", "payment.failed"],
62+
});
63+
liveSecret = (await liveClient.webhooks.retrieveSecret(liveWebhook.id))
64+
.secret;
65+
66+
const testWebhook = await testClient.webhooks.create({
67+
url: `${appUrl}/webhooks/payment/createdCheckout?mode=test`,
68+
description: "Scrawn test payment webhook",
69+
filter_types: ["payment.succeeded", "payment.failed"],
70+
});
71+
testSecret = (await testClient.webhooks.retrieveSecret(testWebhook.id))
72+
.secret;
73+
} catch (error) {
74+
const errMsg = error instanceof Error ? error.message : String(error);
75+
Sentry.captureException(error, {
76+
extra: { context: "dodo webhook registration during onboarding" },
77+
});
78+
builder.setError(400, {
79+
type: "DodoApiError",
80+
message: `Failed to register webhook with Dodo: ${errMsg}`,
81+
});
82+
reply.code(400);
83+
return {};
84+
}
3485

3586
await upsertMetadata({
3687
dodo_live_api_key: validated.dodoLiveApiKey,
3788
dodo_test_api_key: validated.dodoTestApiKey,
3889
dodo_live_product_id: validated.dodoLiveProductId,
3990
dodo_test_product_id: validated.dodoTestProductId,
40-
dodo_webhook_secret: validated.dodoWebhookSecret,
91+
dodo_live_webhook_secret: liveSecret,
92+
dodo_test_webhook_secret: testSecret,
4193
currency: validated.currency,
4294
redirect_url: validated.redirectUrl,
4395
});
@@ -47,7 +99,7 @@ export async function handleOnboarding(
4799
builder.setSuccess(200);
48100

49101
reply.code(201);
50-
return { crons: validated.crons };
102+
return {};
51103
} catch (error) {
52104
Sentry.captureException(error, {
53105
extra: { context: "onboarding route handler" },
@@ -59,7 +111,7 @@ export async function handleOnboarding(
59111
message: error.message,
60112
});
61113
reply.code(401);
62-
return { crons: [] };
114+
return {};
63115
}
64116

65117
if (error instanceof ZodError) {
@@ -71,7 +123,7 @@ export async function handleOnboarding(
71123
message: issues,
72124
});
73125
reply.code(400);
74-
return { crons: [] };
126+
return {};
75127
}
76128

77129
const err = error instanceof Error ? error : new Error(String(error));
@@ -80,7 +132,7 @@ export async function handleOnboarding(
80132
message: err.message,
81133
});
82134
reply.code(500);
83-
return { crons: [] };
135+
return {};
84136
} finally {
85137
logger.emit(builder.build());
86138
}
@@ -122,7 +174,8 @@ export async function handleGetConfig(
122174
dodo_test_api_key: maskApiKey(metadata.dodo_test_api_key),
123175
dodo_live_product_id: metadata.dodo_live_product_id,
124176
dodo_test_product_id: metadata.dodo_test_product_id,
125-
dodo_webhook_secret: maskApiKey(metadata.dodo_webhook_secret),
177+
dodo_live_webhook_secret: maskApiKey(metadata.dodo_live_webhook_secret),
178+
dodo_test_webhook_secret: maskApiKey(metadata.dodo_test_webhook_secret),
126179
currency: metadata.currency,
127180
redirect_url: metadata.redirect_url,
128181
};

‎src/routes/http/api/webhookEndpoints.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function toEndpointResponse(
6868
export async function handleCreateWebhookEndpoint(
6969
request: FastifyRequest,
7070
reply: FastifyReply
71-
): Promise<WebhookEndpointResponse | { error: string; crons?: never }> {
71+
): Promise<WebhookEndpointResponse | { error: string }> {
7272
const builder = createWideEventBuilder(
7373
generateRequestId(),
7474
request.method,

‎src/routes/http/createdCheckout.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,13 @@ export async function handleDodoWebhook(
7171
signature: string | undefined,
7272
timestamp: string | undefined,
7373
webhookId: string | undefined,
74+
mode: "production" | "test",
7475
builder: WideEventBuilder
7576
): Promise<WebhookResponse> {
7677
try {
77-
const client = await getDodoClient();
78+
const client = await getDodoClient(
79+
mode === "production" ? "production" : "test"
80+
);
7881
const headers = buildWebhookHeaders(signature, timestamp, webhookId);
7982
const webhookPayload = unwrapWebhookPayload(client, rawBody, headers);
8083

‎src/routes/http/registerWebhookRoutes.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ export async function registerWebhookRoutes(
3636
);
3737

3838
try {
39+
const mode = (request.query as Record<string, string>)?.mode;
40+
if (mode !== "production" && mode !== "test") {
41+
builder.setError(400, {
42+
type: "ValidationError",
43+
message:
44+
"Invalid or missing 'mode' query parameter. Must be 'production' or 'test'.",
45+
});
46+
reply.code(400);
47+
return { error: "Invalid mode query parameter" };
48+
}
49+
3950
const signatureHeader = request.headers["webhook-signature"];
4051
const timestampHeader = request.headers["webhook-timestamp"];
4152
const webhookIdHeader = request.headers["webhook-id"];
@@ -60,6 +71,7 @@ export async function registerWebhookRoutes(
6071
signature,
6172
timestamp,
6273
webhookId,
74+
mode,
6375
builder
6476
);
6577

‎src/storage/db/postgres/helpers/metadata.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export type UpsertMetadataInput = {
99
dodo_test_api_key?: string;
1010
dodo_live_product_id?: string;
1111
dodo_test_product_id?: string;
12-
dodo_webhook_secret?: string;
12+
dodo_live_webhook_secret?: string;
13+
dodo_test_webhook_secret?: string;
1314
currency?: string;
1415
redirect_url?: string;
1516
};
@@ -36,8 +37,10 @@ export async function upsertMetadata(
3637
setValues.dodo_live_product_id = input.dodo_live_product_id;
3738
if (input.dodo_test_product_id !== undefined)
3839
setValues.dodo_test_product_id = input.dodo_test_product_id;
39-
if (input.dodo_webhook_secret !== undefined)
40-
setValues.dodo_webhook_secret = input.dodo_webhook_secret;
40+
if (input.dodo_live_webhook_secret !== undefined)
41+
setValues.dodo_live_webhook_secret = input.dodo_live_webhook_secret;
42+
if (input.dodo_test_webhook_secret !== undefined)
43+
setValues.dodo_test_webhook_secret = input.dodo_test_webhook_secret;
4144
if (input.currency !== undefined) setValues.currency = input.currency;
4245
if (input.redirect_url !== undefined)
4346
setValues.redirect_url = input.redirect_url;

‎src/storage/db/postgres/schema.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,8 @@ export const metadataTable = pgTable("metadata", {
264264
dodo_test_api_key: text("dodo_test_api_key").notNull(),
265265
dodo_live_product_id: text("dodo_live_product_id").notNull(),
266266
dodo_test_product_id: text("dodo_test_product_id").notNull(),
267-
dodo_webhook_secret: text("dodo_webhook_secret").notNull(),
267+
dodo_live_webhook_secret: text("dodo_live_webhook_secret").notNull(),
268+
dodo_test_webhook_secret: text("dodo_test_webhook_secret").notNull(),
268269
currency: text("currency").notNull().default("usd"),
269270
redirect_url: text("redirect_url").notNull(),
270271
});

0 commit comments

Comments
 (0)