diff --git a/src/routes/http/api/onboarding.ts b/src/routes/http/api/onboarding.ts index f3d5721..a82c01c 100644 --- a/src/routes/http/api/onboarding.ts +++ b/src/routes/http/api/onboarding.ts @@ -55,30 +55,63 @@ export async function handleOnboarding( let liveSecret: string; let testSecret: string; + let liveProductId: string; + let testProductId: string; try { - const liveWebhook = await liveClient.webhooks.create({ - url: `${appUrl}/webhooks/payment/createdCheckout?mode=production`, - description: "Scrawn live payment webhook", - filter_types: ["payment.succeeded", "payment.failed"], - }); + const [liveWebhook, testWebhook, liveProduct, testProduct] = + await Promise.all([ + liveClient.webhooks.create({ + url: `${appUrl}/webhooks/payment/createdCheckout?mode=production`, + description: "Scrawn live payment webhook", + filter_types: ["payment.succeeded", "payment.failed"], + }), + testClient.webhooks.create({ + url: `${appUrl}/webhooks/payment/createdCheckout?mode=test`, + description: "Scrawn test payment webhook", + filter_types: ["payment.succeeded", "payment.failed"], + }), + liveClient.products.create({ + name: "Scrawn Billing", + price: { + type: "one_time_price", + currency: validated.currency, + price: 0, + pay_what_you_want: true, + purchasing_power_parity: false, + discount: 0, + }, + tax_category: "saas", + }), + testClient.products.create({ + name: "Scrawn Billing", + price: { + type: "one_time_price", + currency: validated.currency, + price: 0, + pay_what_you_want: true, + purchasing_power_parity: false, + discount: 0, + }, + tax_category: "saas", + }), + ]); + liveSecret = (await liveClient.webhooks.retrieveSecret(liveWebhook.id)) .secret; - - const testWebhook = await testClient.webhooks.create({ - url: `${appUrl}/webhooks/payment/createdCheckout?mode=test`, - description: "Scrawn test payment webhook", - filter_types: ["payment.succeeded", "payment.failed"], - }); testSecret = (await testClient.webhooks.retrieveSecret(testWebhook.id)) .secret; + liveProductId = liveProduct.product_id; + testProductId = testProduct.product_id; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); Sentry.captureException(error, { - extra: { context: "dodo webhook registration during onboarding" }, + extra: { + context: "dodo webhook/product registration during onboarding", + }, }); builder.setError(400, { type: "DodoApiError", - message: `Failed to register webhook with Dodo: ${errMsg}`, + message: `Failed to configure Dodo resources: ${errMsg}`, }); reply.code(400); return {}; @@ -87,8 +120,8 @@ export async function handleOnboarding( await upsertMetadata({ dodo_live_api_key: encrypt(validated.dodoLiveApiKey), dodo_test_api_key: encrypt(validated.dodoTestApiKey), - dodo_live_product_id: validated.dodoLiveProductId, - dodo_test_product_id: validated.dodoTestProductId, + dodo_live_product_id: liveProductId, + dodo_test_product_id: testProductId, dodo_live_webhook_secret: encrypt(liveSecret), dodo_test_webhook_secret: encrypt(testSecret), currency: validated.currency, diff --git a/src/zod/internals.ts b/src/zod/internals.ts index 938d40b..a0e6449 100644 --- a/src/zod/internals.ts +++ b/src/zod/internals.ts @@ -1,5 +1,13 @@ import { z } from "zod"; +const currencyMap = { + usd: "USD", + eur: "EUR", + gbp: "GBP", + inr: "INR", + jpy: "JPY", +} as const; + export interface FilterGroupOutput { logical: "AND" | "OR"; conditions: C[]; @@ -35,8 +43,8 @@ export function createFilterGroupSchema( export const onboardingSchema = z.object({ dodoLiveApiKey: z.string().min(1, "Dodo live API key is required"), dodoTestApiKey: z.string().min(1, "Dodo test API key is required"), - dodoLiveProductId: z.string().min(1, "Dodo live product ID is required"), - dodoTestProductId: z.string().min(1, "Dodo test product ID is required"), - currency: z.string().min(1, "Currency is required"), + currency: z + .enum(["usd", "eur", "gbp", "inr", "jpy"]) + .transform((c) => currencyMap[c]), redirectUrl: z.url("Redirect URL must be a valid URL"), });