Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c151039
Create new tests
N2D4 Sep 23, 2025
e162ec4
s/offer/product
N2D4 Sep 23, 2025
8d167d8
Schema migration
N2D4 Sep 23, 2025
5fe50f9
Migration
N2D4 Sep 23, 2025
ccc90ea
Endpoint compat
N2D4 Sep 23, 2025
c4b860a
Fix tests
N2D4 Sep 23, 2025
b2da2f0
Remove validateCode migration
N2D4 Sep 23, 2025
4790eb3
Groups -> catalogs
N2D4 Sep 24, 2025
e72849e
fixes
N2D4 Sep 24, 2025
6548cd4
--recent-first flag
N2D4 Sep 24, 2025
687be84
Schema fuzzer
N2D4 Sep 25, 2025
c9a0726
Merge dev into rename-offer-to-product
N2D4 Sep 25, 2025
f81279e
fix tests
N2D4 Sep 26, 2025
5c7f3d9
more test fixes
N2D4 Sep 26, 2025
3e3b2a8
fix tests
N2D4 Sep 26, 2025
5bf4ede
Merge dev into rename-offer-to-product
N2D4 Sep 27, 2025
4be51b4
Merge dev into rename-offer-to-product
N2D4 Sep 29, 2025
e6e20ec
Merge dev into rename-offer-to-product
N2D4 Sep 30, 2025
4884446
Merge dev into rename-offer-to-product
N2D4 Oct 2, 2025
39f9b6d
Merge dev into rename-offer-to-product
N2D4 Oct 3, 2025
73bea8c
Merge branch 'dev' into rename-offer-to-product
N2D4 Oct 3, 2025
eb95114
Merge branch 'dev' into rename-offer-to-product
BilalG1 Oct 3, 2025
c50e96c
Fix outdated variables named "group"
N2D4 Oct 3, 2025
3d095e6
Fix import order
N2D4 Oct 3, 2025
680c971
Merge branch 'dev' into rename-offer-to-product
N2D4 Oct 4, 2025
7e88c9f
Fix tests
N2D4 Oct 4, 2025
9199a61
Fix tests again
N2D4 Oct 4, 2025
fb7ca6a
fix
N2D4 Oct 4, 2025
74e4a98
Merge branch 'dev' into rename-offer-to-product
N2D4 Oct 4, 2025
090a661
fixes
N2D4 Oct 4, 2025
7e191fd
fix tests
N2D4 Oct 4, 2025
e3227bc
fixes
N2D4 Oct 4, 2025
0cd66a3
fix tests (final! hopefully)
N2D4 Oct 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/CLAUDE-KNOWLEDGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,6 @@ A: This happens when packages haven't been built yet. Run these commands in orde
pnpm clean && pnpm i && pnpm codegen && pnpm build:packages
```
Then restart the dev server. This rebuilds all packages and generates the necessary TypeScript declarations.

## Q: How is backwards compatibility for the offer→product rename handled in the payments purchase APIs?
A: API v1 requests are routed through the `v2beta1` migration. The migration wraps the latest handlers, accepts legacy `offer_id`/`offer_inline` request fields, translates product-related errors back to the old offer error codes/messages, and augments responses (like `validate-code`) with `offer`/`conflicting_group_offers` aliases alongside the new `product` fields. Newer API versions keep the product-only contract.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- AlterTable
ALTER TABLE "OneTimePurchase"
RENAME COLUMN "offer" TO "product";

-- AlterTable
ALTER TABLE "OneTimePurchase"
RENAME COLUMN "offerId" TO "productId";

-- AlterTable
ALTER TABLE "Subscription"
RENAME COLUMN "offer" TO "product";

-- AlterTable
ALTER TABLE "Subscription"
RENAME COLUMN "offerId" TO "productId";
8 changes: 4 additions & 4 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -763,9 +763,9 @@ model Subscription {
tenancyId String @db.Uuid
customerId String
customerType CustomerType
offerId String?
productId String?
priceId String?
offer Json
product Json
quantity Int @default(1)

stripeSubscriptionId String?
Expand Down Expand Up @@ -802,9 +802,9 @@ model OneTimePurchase {
tenancyId String @db.Uuid
customerId String
customerType CustomerType
offerId String?
productId String?
priceId String?
offer Json
product Json
quantity Int
stripePaymentIntentId String?
createdAt DateTime @default(now())
Expand Down
12 changes: 6 additions & 6 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ async function seed() {
}
},
payments: {
groups: {
catalogs: {
plans: {
displayName: "Plans",
}
},
offers: {
products: {
team: {
groupId: "plans",
catalogId: "plans",
displayName: "Team",
customerType: "team",
serverOnly: false,
Expand All @@ -126,7 +126,7 @@ async function seed() {
}
},
growth: {
groupId: "plans",
catalogId: "plans",
displayName: "Growth",
customerType: "team",
serverOnly: false,
Expand All @@ -147,7 +147,7 @@ async function seed() {
}
},
free: {
groupId: "plans",
catalogId: "plans",
displayName: "Free",
customerType: "team",
serverOnly: false,
Expand All @@ -162,7 +162,7 @@ async function seed() {
}
},
"extra-admins": {
groupId: "plans",
catalogId: "plans",
displayName: "Extra Admins",
customerType: "team",
serverOnly: false,
Expand Down
8 changes: 6 additions & 2 deletions apps/backend/scripts/verify-data-integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async function main() {
const shouldSaveOutput = flags.includes("--save-output");
const shouldVerifyOutput = flags.includes("--verify-output");
const shouldSkipNeon = flags.includes("--skip-neon");
const recentFirst = flags.includes("--recent-first");


if (shouldSaveOutput) {
Expand Down Expand Up @@ -117,7 +118,9 @@ async function main() {
displayName: true,
description: true,
},
orderBy: {
orderBy: recentFirst ? {
updatedAt: "desc",
} : {
id: "asc",
},
});
Expand All @@ -126,7 +129,7 @@ async function main() {
console.log(`Starting at project ${startAt}.`);
}

const maxUsersPerProject = 10000;
const maxUsersPerProject = 100;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unintended Changes Impact Data Integrity Verification

This commit introduces several changes to the verify-data-integrity.ts script, which appear unrelated to an 'offer-to-product' rename. These include adding a --recent-first flag, reducing maxUsersPerProject from 10000 to 100, and an eslint-disable comment. The maxUsersPerProject reduction significantly alters the script's behavior, potentially processing fewer users and missing data integrity issues.

Fix in Cursor Fix in Web


const endAt = Math.min(startAt + count, projects.length);
for (let i = startAt; i < endAt; i++) {
Expand Down Expand Up @@ -264,6 +267,7 @@ async function main() {
console.log();
console.log();
}
// eslint-disable-next-line no-restricted-syntax
main().catch((...args) => {
console.error();
console.error();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
throw new StackAssertionError("Tenancy not found", { event });
}
const prisma = await getPrismaClientForTenancy(tenancy);
const offer = JSON.parse(metadata.offer || "{}");
const product = JSON.parse(metadata.product || "{}");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stripe webhook handlers will fail to process existing payment intents and subscriptions that still have the old metadata field names (metadata.offer and metadata.offerId), causing webhook processing failures for existing transactions.

View Details
📝 Patch Details
diff --git a/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx b/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
index 1fba69c1..69a51330 100644
--- a/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
+++ b/apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx
@@ -53,7 +53,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
       throw new StackAssertionError("Tenancy not found", { event });
     }
     const prisma = await getPrismaClientForTenancy(tenancy);
-    const product = JSON.parse(metadata.product || "{}");
+    const product = JSON.parse(metadata.product || metadata.offer || "{}");
     const qty = Math.max(1, Number(metadata.purchaseQuantity || 1));
     const stripePaymentIntentId = event.data.object.id;
     if (!metadata.customerId || !metadata.customerType) {
@@ -73,7 +73,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
         tenancyId: tenancy.id,
         customerId: metadata.customerId,
         customerType: typedToUppercase(metadata.customerType),
-        productId: metadata.productId || null,
+        productId: metadata.productId || metadata.offerId || null,
         priceId: metadata.priceId || null,
         stripePaymentIntentId,
         product,
@@ -81,7 +81,7 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
         creationSource: "PURCHASE_PAGE",
       },
       update: {
-        productId: metadata.productId || null,
+        productId: metadata.productId || metadata.offerId || null,
         priceId: metadata.priceId || null,
         product,
         quantity: qty,
diff --git a/apps/backend/src/lib/stripe.tsx b/apps/backend/src/lib/stripe.tsx
index 3ab57d28..49a01d6d 100644
--- a/apps/backend/src/lib/stripe.tsx
+++ b/apps/backend/src/lib/stripe.tsx
@@ -89,7 +89,7 @@ export async function syncStripeSubscriptions(stripe: Stripe, stripeAccountId: s
       },
       update: {
         status: subscription.status,
-        product: JSON.parse(subscription.metadata.product),
+        product: JSON.parse(subscription.metadata.product || subscription.metadata.offer || "{}"),
         quantity: item.quantity ?? 1,
         currentPeriodEnd: new Date(item.current_period_end * 1000),
         currentPeriodStart: new Date(item.current_period_start * 1000),
@@ -100,9 +100,9 @@ export async function syncStripeSubscriptions(stripe: Stripe, stripeAccountId: s
         tenancyId: tenancy.id,
         customerId,
         customerType,
-        productId: subscription.metadata.productId,
+        productId: subscription.metadata.productId || subscription.metadata.offerId,
         priceId: priceId ?? null,
-        product: JSON.parse(subscription.metadata.product),
+        product: JSON.parse(subscription.metadata.product || subscription.metadata.offer || "{}"),
         quantity: item.quantity ?? 1,
         stripeSubscriptionId: subscription.id,
         status: subscription.status,

Analysis

Stripe webhook handlers fail for existing payment intents and subscriptions with old metadata field names

What fails: Webhook processing crashes for existing Stripe objects that use metadata.offer and metadata.offerId instead of the new metadata.product and metadata.productId fields

How to reproduce:

  1. Existing Stripe payment intent with old metadata format triggers webhook
  2. JSON.parse(subscription.metadata.product) in syncStripeSubscriptions() throws "undefined is not valid JSON"
  3. One-time purchase webhooks store empty {} instead of actual product data

Result:

  • Subscription webhook processing fails with JSON parse error
  • One-time purchases lose product configuration data
  • Customer billing and subscription management broken for existing customers

Expected: Webhook handlers should fall back to old field names (metadata.offer, metadata.offerId) when new fields are missing, preserving existing customer data during the migration from offer-based to product-based naming

Files affected:

  • apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx (lines 56, 76, 84)
  • apps/backend/src/lib/stripe.tsx (lines 92, 103, 105)

const qty = Math.max(1, Number(metadata.purchaseQuantity || 1));
const stripePaymentIntentId = event.data.object.id;
Comment on lines +56 to 58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard JSON.parse and fix NaN quantity bug.

JSON.parse can throw on bad metadata; Math.max(1, Number(...)) returns NaN if the value is non‑numeric, causing Prisma write failures.

Apply:

-    const product = JSON.parse(metadata.product || "{}");
-    const qty = Math.max(1, Number(metadata.purchaseQuantity || 1));
+    const product = (() => {
+      try {
+        return metadata.product ? JSON.parse(metadata.product) : null;
+      } catch {
+        return null;
+      }
+    })();
+    const qty = (() => {
+      const n = Number(metadata.purchaseQuantity);
+      return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
+    })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const product = JSON.parse(metadata.product || "{}");
const qty = Math.max(1, Number(metadata.purchaseQuantity || 1));
const stripePaymentIntentId = event.data.object.id;
const product = (() => {
try {
return metadata.product ? JSON.parse(metadata.product) : null;
} catch {
return null;
}
})();
const qty = (() => {
const n = Number(metadata.purchaseQuantity);
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
})();
const stripePaymentIntentId = event.data.object.id;
🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx around
lines 56 to 58, guard the JSON.parse call and fix the quantity NaN issue: wrap
parsing of metadata.product in a try/catch (or use a safeParse helper) and
default product to {} if parse fails, and compute qty by converting
metadata.purchaseQuantity to a number (e.g., parseInt or Number), then if the
result is NaN or not finite fall back to 1 before applying Math.max(1,
parsedQty) so qty is never NaN; update the variables accordingly.

if (!metadata.customerId || !metadata.customerType) {
Expand All @@ -73,17 +73,17 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
tenancyId: tenancy.id,
customerId: metadata.customerId,
customerType: typedToUppercase(metadata.customerType),
offerId: metadata.offerId || null,
productId: metadata.productId || null,
priceId: metadata.priceId || null,
stripePaymentIntentId,
offer,
product,
quantity: qty,
creationSource: "PURCHASE_PAGE",
},
update: {
offerId: metadata.offerId || null,
productId: metadata.productId || null,
priceId: metadata.priceId || null,
offer,
product,
quantity: qty,
Comment on lines +76 to 87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Nullish-coalesce IDs; keep product nullable.

Use ?? to avoid treating valid falsy strings as null; align with priceId style. Ensure product can be null.

Apply:

-        productId: metadata.productId || null,
+        productId: metadata.productId ?? null,
...
-        product,
+        product,
...
-        productId: metadata.productId || null,
+        productId: metadata.productId ?? null,
...
-        product,
+        product,

Also consider updating adjacent priceId occurrences to use ?? for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
productId: metadata.productId || null,
priceId: metadata.priceId || null,
stripePaymentIntentId,
offer,
product,
quantity: qty,
creationSource: "PURCHASE_PAGE",
},
update: {
offerId: metadata.offerId || null,
productId: metadata.productId || null,
priceId: metadata.priceId || null,
offer,
product,
quantity: qty,
productId: metadata.productId ?? null,
priceId: metadata.priceId || null,
stripePaymentIntentId,
product,
quantity: qty,
creationSource: "PURCHASE_PAGE",
},
update: {
productId: metadata.productId ?? null,
priceId: metadata.priceId || null,
product,
quantity: qty,

}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getRenderedEnvironmentConfigQuery, overrideEnvironmentConfigOverride, validateEnvironmentConfigOverride } from "@/lib/config";
import { getRenderedEnvironmentConfigQuery, overrideEnvironmentConfigOverride } from "@/lib/config";
import { globalPrismaClient, rawQuery } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { environmentConfigSchema, getConfigOverrideErrors, migrateConfigOverride } from "@stackframe/stack-shared/dist/config/schema";
import { configOverrideCrud } from "@stackframe/stack-shared/dist/interface/crud/config";
import { yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
Expand All @@ -20,14 +21,10 @@ export const configOverridesCrudHandlers = createLazyProxy(() => createCrudHandl
throw e;
}

const validationResult = await validateEnvironmentConfigOverride({
environmentConfigOverride: parsedConfig,
branchId: auth.tenancy.branchId,
projectId: auth.tenancy.project.id,
});

if (validationResult.status === "error") {
throw new StatusError(StatusError.BadRequest, validationResult.error);
// TODO instead of doing this check here, we should change overrideEnvironmentConfigOverride to return the errors from its ensureNoConfigOverrideErrors call
const overrideError = await getConfigOverrideErrors(environmentConfigSchema, migrateConfigOverride("environment", parsedConfig));
if (overrideError.status === "error") {
throw new StatusError(StatusError.BadRequest, overrideError.error);
}

await overrideEnvironmentConfigOverride({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({
}
const prisma = await getPrismaClientForTenancy(auth.tenancy);

const { selectedPrice, conflictingGroupSubscriptions } = await validatePurchaseSession({
const { selectedPrice, conflictingCatalogSubscriptions } = await validatePurchaseSession({
prisma,
tenancy: auth.tenancy,
codeData: data,
Expand All @@ -53,18 +53,18 @@ export const POST = createSmartRouteHandler({
data: {
tenancyId: auth.tenancy.id,
customerId: data.customerId,
customerType: typedToUppercase(data.offer.customerType),
offerId: data.offerId,
customerType: typedToUppercase(data.product.customerType),
productId: data.productId,
priceId: price_id,
offer: data.offer,
product: data.product,
quantity,
creationSource: "TEST_MODE",
},
});
} else {
// Cancel conflicting subscriptions for TEST_MODE as well, then create new TEST_MODE subscription
if (conflictingGroupSubscriptions.length > 0) {
const conflicting = conflictingGroupSubscriptions[0];
if (conflictingCatalogSubscriptions.length > 0) {
const conflicting = conflictingCatalogSubscriptions[0];
if (conflicting.stripeSubscriptionId) {
const stripe = await getStripeForAccount({ tenancy: auth.tenancy });
await stripe.subscriptions.cancel(conflicting.stripeSubscriptionId);
Expand All @@ -85,11 +85,11 @@ export const POST = createSmartRouteHandler({
data: {
tenancyId: auth.tenancy.id,
customerId: data.customerId,
customerType: typedToUppercase(data.offer.customerType),
customerType: typedToUppercase(data.product.customerType),
status: "active",
offerId: data.offerId,
productId: data.productId,
priceId: price_id,
offer: data.offer,
product: data.product,
quantity,
currentPeriodStart: new Date(),
currentPeriodEnd: addInterval(new Date(), selectedPrice.interval!),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,24 @@ import { typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dis


type SelectedPrice = NonNullable<AdminTransaction['price']>;
type OfferWithPrices = {
type ProductWithPrices = {
displayName?: string,
prices?: Record<string, SelectedPrice & { serverOnly?: unknown, freeTrial?: unknown }> | "include-by-default",
} | null | undefined;

function resolveSelectedPriceFromOffer(offer: OfferWithPrices, priceId?: string | null): SelectedPrice | null {
if (!offer) return null;
function resolveSelectedPriceFromProduct(product: ProductWithPrices, priceId?: string | null): SelectedPrice | null {
if (!product) return null;
if (!priceId) return null;
const prices = offer.prices;
const prices = product.prices;
if (!prices || prices === "include-by-default") return null;
const selected = prices[priceId as keyof typeof prices] as (SelectedPrice & { serverOnly?: unknown, freeTrial?: unknown }) | undefined;
if (!selected) return null;
const { serverOnly: _serverOnly, freeTrial: _freeTrial, ...rest } = selected as any;
return rest as SelectedPrice;
}

function getOfferDisplayName(offer: OfferWithPrices): string | null {
return offer?.displayName ?? null;
function getProductDisplayName(product: ProductWithPrices): string | null {
return product?.displayName ?? null;
}


Expand Down Expand Up @@ -137,8 +137,8 @@ export const GET = createSmartRouteHandler({
customer_id: s.customerId,
quantity: s.quantity,
test_mode: s.creationSource === 'TEST_MODE',
offer_display_name: getOfferDisplayName(s.offer as OfferWithPrices),
price: resolveSelectedPriceFromOffer(s.offer as OfferWithPrices, s.priceId ?? null),
product_display_name: getProductDisplayName(s.product as ProductWithPrices),
price: resolveSelectedPriceFromProduct(s.product as ProductWithPrices, s.priceId ?? null),
status: s.status,
}));

Expand All @@ -153,7 +153,7 @@ export const GET = createSmartRouteHandler({
customer_id: i.customerId,
quantity: i.quantity,
test_mode: false,
offer_display_name: null,
product_display_name: null,
price: null,
status: null,
item_id: i.itemId,
Expand All @@ -170,8 +170,8 @@ export const GET = createSmartRouteHandler({
customer_id: o.customerId,
quantity: o.quantity,
test_mode: o.creationSource === 'TEST_MODE',
offer_display_name: getOfferDisplayName(o.offer as OfferWithPrices),
price: resolveSelectedPriceFromOffer(o.offer as OfferWithPrices, o.priceId ?? null),
product_display_name: getProductDisplayName(o.product as ProductWithPrices),
price: resolveSelectedPriceFromProduct(o.product as ProductWithPrices, o.priceId ?? null),
status: null,
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ensureOfferIdOrInlineOffer } from "@/lib/payments";
import { ensureProductIdOrInlineProduct } from "@/lib/payments";
import { getStripeForAccount } from "@/lib/stripe";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { CustomerType } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { adaptSchema, clientOrHigherAuthTypeSchema, inlineOfferSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { adaptSchema, clientOrHigherAuthTypeSchema, inlineProductSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { purchaseUrlVerificationCodeHandler } from "../verification-code-handler";
Expand All @@ -22,8 +22,8 @@ export const POST = createSmartRouteHandler({
body: yupObject({
customer_type: yupString().oneOf(["user", "team", "custom"]).defined(),
customer_id: yupString().defined(),
offer_id: yupString().optional(),
offer_inline: inlineOfferSchema.optional(),
product_id: yupString().optional(),
product_inline: inlineProductSchema.optional(),
}),
}),
response: yupObject({
Expand All @@ -36,10 +36,10 @@ export const POST = createSmartRouteHandler({
handler: async (req) => {
const { tenancy } = req.auth;
const stripe = await getStripeForAccount({ tenancy });
const offerConfig = await ensureOfferIdOrInlineOffer(tenancy, req.auth.type, req.body.offer_id, req.body.offer_inline);
const customerType = offerConfig.customerType;
const productConfig = await ensureProductIdOrInlineProduct(tenancy, req.auth.type, req.body.product_id, req.body.product_inline);
const customerType = productConfig.customerType;
if (req.body.customer_type !== customerType) {
throw new KnownErrors.OfferCustomerTypeDoesNotMatch(req.body.offer_id, req.body.customer_id, customerType, req.body.customer_type);
throw new KnownErrors.ProductCustomerTypeDoesNotMatch(req.body.product_id, req.body.customer_id, customerType, req.body.customer_type);
}

const stripeCustomerSearch = await stripe.customers.search({
Expand All @@ -66,8 +66,8 @@ export const POST = createSmartRouteHandler({
data: {
tenancyId: tenancy.id,
customerId: req.body.customer_id,
offerId: req.body.offer_id,
offer: offerConfig,
productId: req.body.product_id,
product: productConfig,
stripeCustomerId: stripeCustomer.id,
stripeAccountId: project?.stripeAccountId ?? throwErr("Stripe account not configured"),
},
Expand Down
Loading