Skip to content

Commit ad34cfe

Browse files
authored
Transactions page (#900)
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- RECURSEML_SUMMARY:START --> ## High-level PR Summary This PR adds a new `priceId` field to the `OneTimePurchase` and `Subscription` models in the database to store Stripe price identifiers. The change includes database schema updates, corresponding migration files, and modifications to payment processing logic to properly track and store price IDs throughout the purchase flow. The implementation consistently propagates the price ID from Stripe's API responses through various payment processing endpoints and webhooks handlers, ensuring the data is properly stored and synced with the database models. ⏱️ Estimated Review Time: 15-30 minutes <details> <summary>💡 Review Order Suggestion</summary> | Order | File Path | |-------|-----------| | 1 | `apps/backend/prisma/schema.prisma` | | 2 | `apps/backend/prisma/migrations/20250917193043_store_price_id/migration.sql` | | 3 | `apps/backend/src/lib/stripe.tsx` | | 4 | `apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx` | | 5 | `apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx` | | 6 | `apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx` | | 7 | `apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts` | </details> <!-- RECURSEML_SUMMARY:END --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Add `priceId` field to track Stripe price identifiers in purchase and subscription models, updating schema, payment logic, and tests. > > - **Database Changes**: > - Add `priceId` field to `OneTimePurchase` and `Subscription` models in `schema.prisma`. > - Update database schema with migration `20250917193043_store_price_id/migration.sql`. > - **Payment Processing**: > - Update `processStripeWebhookEvent()` in `webhooks/route.tsx` to handle `priceId`. > - Modify `POST` handlers in `purchase-session/route.tsx` and `test-mode-purchase-session/route.tsx` to include `priceId` in metadata. > - Update `syncStripeSubscriptions()` in `stripe.tsx` to sync `priceId`. > - **Testing**: > - Add tests in `stripe-webhooks.test.ts` to validate `priceId` handling in webhook and purchase flows. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for 4950494. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> ---- <!-- ELLIPSIS_HIDDEN --> <!-- RECURSEML_ANALYSIS:START --> ## Review by RecurseML _🔍 Review performed on [e48ffa6..4950494](e48ffa6...4950494d626199f28ccc823a1f475dfed56d924b)_ | &nbsp; Severity &nbsp; | &nbsp; Location &nbsp; | &nbsp; Issue &nbsp; | &nbsp; Delete &nbsp; | |:----------:|----------|-------|:--------:| | ![Medium](https://img.shields.io/badge/Medium-yellow?style=plastic) | [apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts:153](#900 (comment)) | The field `priceId` uses camelCase instead of the required snake_case for API parameters | [![](https://img.shields.io/badge/-lightgray?style=plastic&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDE5YzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY3SDZ2MTJ6TTE5IDRoLTMuNWwtMS0xaC01bC0xIDFINXYyaDE0VjR6Ii8+PC9zdmc+)](https://squash-322339097191.europe-west3.run.app/interactive/625f45934d9c89a3c90d718f357bd42d8eef4ee253fe1718db23788124856165/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=900) | | ![Medium](https://img.shields.io/badge/Medium-yellow?style=plastic) | [apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts:245](#900 (comment)) | The field `priceId` uses camelCase instead of the required snake_case for API parameters | [![](https://img.shields.io/badge/-lightgray?style=plastic&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDE5YzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY3SDZ2MTJ6TTE5IDRoLTMuNWwtMS0xaC01bC0xIDFINXYyaDE0VjR6Ii8+PC9zdmc+)](https://squash-322339097191.europe-west3.run.app/interactive/90fc6f93a3390e72b126cfe52084a56880cf1ea88395fa9c81a476ccb602d4af/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=900) | | ![Medium](https://img.shields.io/badge/Medium-yellow?style=plastic) | [apps/e2e/tests/backend/endpoints/api/v1/stripe-webhooks.test.ts:358](#900 (comment)) | The field `priceId` uses camelCase instead of the required snake_case for API parameters | [![](https://img.shields.io/badge/-lightgray?style=plastic&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik02IDE5YzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY3SDZ2MTJ6TTE5IDRoLTMuNWwtMS0xaC01bC0xIDFINXYyaDE0VjR6Ii8+PC9zdmc+)](https://squash-322339097191.europe-west3.run.app/interactive/c6ea18d2d6da35477d1eade56c71defa0e7a35512d4ca193d657c7b962868a39/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=900) | <details> <summary>✅ Files analyzed, no issues (4)</summary> • `apps/backend/src/lib/stripe.tsx` • `apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx` • `apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx` • `apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx` </details> <details> <summary>⏭️ Files skipped (trigger manually) (2)</summary> | &nbsp; Locations &nbsp; | &nbsp; Trigger Analysis &nbsp; | |-----------|:------------------:| `apps/backend/prisma/migrations/20250917193043_store_price_id/migration.sql` | [![Analyze](https://img.shields.io/badge/Analyze-238636?style=plastic)](https://squash-322339097191.europe-west3.run.app/interactive/8326b4d568a93d1c438396f9d3ef6137f1cc04d7c81e33912faf94418d97fb4c/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=900) `apps/backend/prisma/schema.prisma` | [![Analyze](https://img.shields.io/badge/Analyze-238636?style=plastic)](https://squash-322339097191.europe-west3.run.app/interactive/8eb33747d2d1aaddde3421800f1c6f1c94e8a47c61e10870ece1ba863766aa09/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=900) </details> [![Need help? Join our Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U) <!-- RECURSEML_ANALYSIS:END --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Persist the selected price ID with one-time purchases and subscriptions. - Carry the price ID through payment flows and Stripe metadata for better tracking and reporting. - Test mode purchases now also record the price ID. - Chores - Database migration adds a price ID field to purchase and subscription records. - Tests - Updated end-to-end tests to validate price ID handling in webhook and purchase flows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 2afaaea commit ad34cfe

21 files changed

Lines changed: 796 additions & 23 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "OneTimePurchase" ADD COLUMN "priceId" TEXT;
3+
4+
-- AlterTable
5+
ALTER TABLE "Subscription" ADD COLUMN "priceId" TEXT;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `customerType` to the `ItemQuantityChange` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "ItemQuantityChange" ADD COLUMN "customerType" "CustomerType";
9+
10+
UPDATE "ItemQuantityChange" AS iqc
11+
SET "customerType" = 'USER'
12+
FROM "ProjectUser" AS pu
13+
WHERE iqc."tenancyId" = pu."tenancyId"
14+
AND iqc."customerId" = pu."projectUserId"::text;
15+
16+
UPDATE "ItemQuantityChange" AS iqc
17+
SET "customerType" = 'TEAM'
18+
FROM "Team" AS t
19+
WHERE iqc."customerType" IS NULL
20+
AND iqc."tenancyId" = t."tenancyId"
21+
AND iqc."customerId" = t."teamId"::text;
22+
23+
UPDATE "ItemQuantityChange"
24+
SET "customerType" = 'CUSTOM'
25+
WHERE "customerType" IS NULL;
26+
27+
ALTER TABLE "ItemQuantityChange" ALTER COLUMN "customerType" SET NOT NULL;

apps/backend/prisma/schema.prisma

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ model Subscription {
764764
customerId String
765765
customerType CustomerType
766766
offerId String?
767+
priceId String?
767768
offer Json
768769
quantity Int @default(1)
769770
@@ -774,38 +775,40 @@ model Subscription {
774775
cancelAtPeriodEnd Boolean
775776
776777
creationSource PurchaseCreationSource
777-
createdAt DateTime @default(now())
778-
updatedAt DateTime @updatedAt
778+
createdAt DateTime @default(now())
779+
updatedAt DateTime @updatedAt
779780
780781
@@id([tenancyId, id])
781782
@@unique([tenancyId, stripeSubscriptionId])
782783
}
783784

784785
model ItemQuantityChange {
785-
id String @default(uuid()) @db.Uuid
786-
tenancyId String @db.Uuid
787-
customerId String
788-
itemId String
789-
quantity Int
790-
description String?
791-
expiresAt DateTime?
792-
createdAt DateTime @default(now())
786+
id String @default(uuid()) @db.Uuid
787+
tenancyId String @db.Uuid
788+
customerId String
789+
customerType CustomerType
790+
itemId String
791+
quantity Int
792+
description String?
793+
expiresAt DateTime?
794+
createdAt DateTime @default(now())
793795
794796
@@id([tenancyId, id])
795797
@@index([tenancyId, customerId, expiresAt])
796798
}
797799

798800
model OneTimePurchase {
799-
id String @default(uuid()) @db.Uuid
800-
tenancyId String @db.Uuid
801-
customerId String
802-
customerType CustomerType
803-
offerId String?
804-
offer Json
805-
quantity Int
801+
id String @default(uuid()) @db.Uuid
802+
tenancyId String @db.Uuid
803+
customerId String
804+
customerType CustomerType
805+
offerId String?
806+
priceId String?
807+
offer Json
808+
quantity Int
806809
stripePaymentIntentId String?
807-
createdAt DateTime @default(now())
808-
creationSource PurchaseCreationSource
810+
createdAt DateTime @default(now())
811+
creationSource PurchaseCreationSource
809812
810813
@@id([tenancyId, id])
811814
@@unique([tenancyId, stripePaymentIntentId])

apps/backend/src/app/api/latest/integrations/stripe/webhooks/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
4242
if (!accountId) {
4343
throw new StackAssertionError("Stripe webhook account id missing", { event });
4444
}
45-
console.log("Processing1", mockData);
4645
const stripe = getStackStripe(mockData);
4746
const account = await stripe.accounts.retrieve(accountId);
4847
const tenancyId = account.metadata?.tenancyId;
@@ -75,13 +74,15 @@ async function processStripeWebhookEvent(event: Stripe.Event): Promise<void> {
7574
customerId: metadata.customerId,
7675
customerType: typedToUppercase(metadata.customerType),
7776
offerId: metadata.offerId || null,
77+
priceId: metadata.priceId || null,
7878
stripePaymentIntentId,
7979
offer,
8080
quantity: qty,
8181
creationSource: "PURCHASE_PAGE",
8282
},
8383
update: {
8484
offerId: metadata.offerId || null,
85+
priceId: metadata.priceId || null,
8586
offer,
8687
quantity: qty,
8788
}

apps/backend/src/app/api/latest/internal/payments/test-mode-purchase-session/route.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const POST = createSmartRouteHandler({
5555
customerId: data.customerId,
5656
customerType: typedToUppercase(data.offer.customerType),
5757
offerId: data.offerId,
58+
priceId: price_id,
5859
offer: data.offer,
5960
quantity,
6061
creationSource: "TEST_MODE",
@@ -87,6 +88,7 @@ export const POST = createSmartRouteHandler({
8788
customerType: typedToUppercase(data.offer.customerType),
8889
status: "active",
8990
offerId: data.offerId,
91+
priceId: price_id,
9092
offer: data.offer,
9193
quantity,
9294
currentPeriodStart: new Date(),
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { getPrismaClientForTenancy } from "@/prisma-client";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { Prisma } from "@prisma/client";
4+
import { AdminTransaction, adminTransaction } from "@stackframe/stack-shared/dist/interface/crud/transactions";
5+
import { adaptSchema, adminAuthTypeSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
6+
import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects";
7+
import { typedToLowercase, typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
8+
9+
10+
type SelectedPrice = NonNullable<AdminTransaction['price']>;
11+
type OfferWithPrices = {
12+
displayName?: string,
13+
prices?: Record<string, SelectedPrice & { serverOnly?: unknown, freeTrial?: unknown }> | "include-by-default",
14+
} | null | undefined;
15+
16+
function resolveSelectedPriceFromOffer(offer: OfferWithPrices, priceId?: string | null): SelectedPrice | null {
17+
if (!offer) return null;
18+
if (!priceId) return null;
19+
const prices = offer.prices;
20+
if (!prices || prices === "include-by-default") return null;
21+
const selected = prices[priceId as keyof typeof prices] as (SelectedPrice & { serverOnly?: unknown, freeTrial?: unknown }) | undefined;
22+
if (!selected) return null;
23+
const { serverOnly: _serverOnly, freeTrial: _freeTrial, ...rest } = selected as any;
24+
return rest as SelectedPrice;
25+
}
26+
27+
function getOfferDisplayName(offer: OfferWithPrices): string | null {
28+
return offer?.displayName ?? null;
29+
}
30+
31+
32+
export const GET = createSmartRouteHandler({
33+
metadata: {
34+
hidden: true,
35+
},
36+
request: yupObject({
37+
auth: yupObject({
38+
type: adminAuthTypeSchema.defined(),
39+
project: adaptSchema.defined(),
40+
tenancy: adaptSchema.defined(),
41+
}).defined(),
42+
query: yupObject({
43+
cursor: yupString().optional(),
44+
limit: yupString().optional(),
45+
type: yupString().oneOf(['subscription', 'one_time', 'item_quantity_change']).optional(),
46+
customer_type: yupString().oneOf(['user', 'team', 'custom']).optional(),
47+
}).optional(),
48+
}),
49+
response: yupObject({
50+
statusCode: yupNumber().oneOf([200]).defined(),
51+
bodyType: yupString().oneOf(["json"]).defined(),
52+
body: yupObject({
53+
transactions: yupArray(adminTransaction).defined(),
54+
next_cursor: yupString().nullable().defined(),
55+
}).defined(),
56+
}),
57+
handler: async ({ auth, query }) => {
58+
const prisma = await getPrismaClientForTenancy(auth.tenancy);
59+
60+
const rawLimit = query.limit ?? "50";
61+
const parsedLimit = Number.parseInt(rawLimit, 10);
62+
const limit = Math.max(1, Math.min(200, Number.isFinite(parsedLimit) ? parsedLimit : 50));
63+
const cursorStr = query.cursor ?? "";
64+
const [subCursor, iqcCursor, otpCursor] = (cursorStr.split("|") as [string?, string?, string?]);
65+
66+
const paginateWhere = async <T extends "subscription" | "itemQuantityChange" | "oneTimePurchase">(
67+
table: T,
68+
cursorId?: string
69+
): Promise<
70+
T extends "subscription"
71+
? Prisma.SubscriptionWhereInput | undefined
72+
: T extends "itemQuantityChange"
73+
? Prisma.ItemQuantityChangeWhereInput | undefined
74+
: Prisma.OneTimePurchaseWhereInput | undefined
75+
> => {
76+
if (!cursorId) return undefined as any;
77+
let pivot: { createdAt: Date } | null = null;
78+
if (table === "subscription") {
79+
pivot = await prisma.subscription.findUnique({
80+
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: cursorId } },
81+
select: { createdAt: true },
82+
});
83+
} else if (table === "itemQuantityChange") {
84+
pivot = await prisma.itemQuantityChange.findUnique({
85+
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: cursorId } },
86+
select: { createdAt: true },
87+
});
88+
} else {
89+
pivot = await prisma.oneTimePurchase.findUnique({
90+
where: { tenancyId_id: { tenancyId: auth.tenancy.id, id: cursorId } },
91+
select: { createdAt: true },
92+
});
93+
}
94+
if (!pivot) return undefined as any;
95+
return {
96+
OR: [
97+
{ createdAt: { lt: pivot.createdAt } },
98+
{ AND: [{ createdAt: { equals: pivot.createdAt } }, { id: { lt: cursorId } }] },
99+
],
100+
} as any;
101+
};
102+
103+
const [subWhere, iqcWhere, otpWhere] = await Promise.all([
104+
paginateWhere("subscription", subCursor),
105+
paginateWhere("itemQuantityChange", iqcCursor),
106+
paginateWhere("oneTimePurchase", otpCursor),
107+
]);
108+
109+
const baseOrder = [{ createdAt: "desc" as const }, { id: "desc" as const }];
110+
const customerTypeFilter = query.customer_type ? { customerType: typedToUppercase(query.customer_type) } : {};
111+
112+
let merged: AdminTransaction[] = [];
113+
114+
const [subs, iqcs, otps] = await Promise.all([
115+
(query.type === "subscription" || !query.type) ? prisma.subscription.findMany({
116+
where: { tenancyId: auth.tenancy.id, ...(subWhere ?? {}), ...customerTypeFilter },
117+
orderBy: baseOrder,
118+
take: limit,
119+
}) : [],
120+
(query.type === "item_quantity_change" || !query.type) ? prisma.itemQuantityChange.findMany({
121+
where: { tenancyId: auth.tenancy.id, ...(iqcWhere ?? {}), ...customerTypeFilter },
122+
orderBy: baseOrder,
123+
take: limit,
124+
}) : [],
125+
(query.type === "one_time" || !query.type) ? prisma.oneTimePurchase.findMany({
126+
where: { tenancyId: auth.tenancy.id, ...(otpWhere ?? {}), ...customerTypeFilter },
127+
orderBy: baseOrder,
128+
take: limit,
129+
}) : [],
130+
]);
131+
132+
const subRows: AdminTransaction[] = subs.map((s) => ({
133+
id: s.id,
134+
type: 'subscription',
135+
created_at_millis: s.createdAt.getTime(),
136+
customer_type: typedToLowercase(s.customerType),
137+
customer_id: s.customerId,
138+
quantity: s.quantity,
139+
test_mode: s.creationSource === 'TEST_MODE',
140+
offer_display_name: getOfferDisplayName(s.offer as OfferWithPrices),
141+
price: resolveSelectedPriceFromOffer(s.offer as OfferWithPrices, s.priceId ?? null),
142+
status: s.status,
143+
}));
144+
145+
const iqcRows: AdminTransaction[] = iqcs.map((i) => {
146+
const itemCfg = getOrUndefined(auth.tenancy.config.payments.items, i.itemId) as { customerType?: 'user' | 'team' | 'custom' } | undefined;
147+
const customerType = (itemCfg && itemCfg.customerType) ? itemCfg.customerType : 'custom';
148+
return {
149+
id: i.id,
150+
type: 'item_quantity_change',
151+
created_at_millis: i.createdAt.getTime(),
152+
customer_type: customerType,
153+
customer_id: i.customerId,
154+
quantity: i.quantity,
155+
test_mode: false,
156+
offer_display_name: null,
157+
price: null,
158+
status: null,
159+
item_id: i.itemId,
160+
description: i.description ?? null,
161+
expires_at_millis: i.expiresAt ? i.expiresAt.getTime() : null,
162+
} as const;
163+
});
164+
165+
const otpRows: AdminTransaction[] = otps.map((o) => ({
166+
id: o.id,
167+
type: 'one_time',
168+
created_at_millis: o.createdAt.getTime(),
169+
customer_type: typedToLowercase(o.customerType),
170+
customer_id: o.customerId,
171+
quantity: o.quantity,
172+
test_mode: o.creationSource === 'TEST_MODE',
173+
offer_display_name: getOfferDisplayName(o.offer as OfferWithPrices),
174+
price: resolveSelectedPriceFromOffer(o.offer as OfferWithPrices, o.priceId ?? null),
175+
status: null,
176+
}));
177+
178+
merged = [...subRows, ...iqcRows, ...otpRows]
179+
.sort((a, b) => (a.created_at_millis === b.created_at_millis ? (a.id < b.id ? 1 : -1) : (a.created_at_millis < b.created_at_millis ? 1 : -1)));
180+
181+
const page = merged.slice(0, limit);
182+
let lastSubId = "";
183+
let lastIqcId = "";
184+
let lastOtpId = "";
185+
for (const r of page) {
186+
if (r.type === 'subscription') lastSubId = r.id;
187+
if (r.type === 'item_quantity_change') lastIqcId = r.id;
188+
if (r.type === 'one_time') lastOtpId = r.id;
189+
}
190+
191+
const nextCursor = page.length === limit
192+
? [lastSubId, lastIqcId, lastOtpId].join('|')
193+
: null;
194+
195+
return {
196+
statusCode: 200,
197+
bodyType: "json",
198+
body: {
199+
transactions: page,
200+
next_cursor: nextCursor,
201+
},
202+
};
203+
},
204+
});
205+
206+

apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const POST = createSmartRouteHandler({
7272
data: {
7373
tenancyId: tenancy.id,
7474
customerId: req.params.customer_id,
75+
customerType: typedToUppercase(req.params.customer_type),
7576
itemId: req.params.item_id,
7677
quantity: req.body.delta,
7778
description: req.body.description,

apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const POST = createSmartRouteHandler({
7373
metadata: {
7474
offerId: data.offerId ?? null,
7575
offer: JSON.stringify(data.offer),
76+
priceId: price_id,
7677
},
7778
});
7879
const clientSecretUpdated = getClientSecretFromStripeSubscription(updated);
@@ -114,6 +115,7 @@ export const POST = createSmartRouteHandler({
114115
purchaseQuantity: String(quantity),
115116
purchaseKind: "ONE_TIME",
116117
tenancyId: data.tenancyId,
118+
priceId: price_id,
117119
},
118120
});
119121
const clientSecret = paymentIntent.client_secret;
@@ -147,6 +149,7 @@ export const POST = createSmartRouteHandler({
147149
metadata: {
148150
offerId: data.offerId ?? null,
149151
offer: JSON.stringify(data.offer),
152+
priceId: price_id,
150153
},
151154
});
152155
const clientSecret = getClientSecretFromStripeSubscription(created);

0 commit comments

Comments
 (0)