diff --git a/apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql b/apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql new file mode 100644 index 0000000000..2e6eb8bfca --- /dev/null +++ b/apps/backend/prisma/migrations/20250820164831_custom_customer_types/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - Added the required column `customerType` to the `ItemQuantityChange` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +ALTER TYPE "CustomerType" ADD VALUE 'CUSTOM'; + +-- AlterTable +ALTER TABLE "ItemQuantityChange" ADD COLUMN "customerType" "CustomerType" NOT NULL, +ALTER COLUMN "customerId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "Subscription" ALTER COLUMN "customerId" SET DATA TYPE TEXT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index c2f8ff2c4f..3d8320aecf 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -19,7 +19,7 @@ model Project { displayName String description String @default("") isProductionMode Boolean - ownerTeamId String? @db.Uuid + ownerTeamId String? @db.Uuid logoUrl String? fullLogoUrl String? @@ -715,6 +715,7 @@ model ThreadMessage { enum CustomerType { USER TEAM + CUSTOM } enum SubscriptionStatus { @@ -731,7 +732,7 @@ enum SubscriptionStatus { model Subscription { id String @default(uuid()) @db.Uuid tenancyId String @db.Uuid - customerId String @db.Uuid + customerId String customerType CustomerType offer Json @@ -749,14 +750,15 @@ model Subscription { } model ItemQuantityChange { - id String @default(uuid()) @db.Uuid - tenancyId String @db.Uuid - customerId String @db.Uuid - itemId String - quantity Int - description String? - expiresAt DateTime? - createdAt DateTime @default(now()) + id String @default(uuid()) @db.Uuid + tenancyId String @db.Uuid + customerType CustomerType + customerId String + itemId String + quantity Int + description String? + expiresAt DateTime? + createdAt DateTime @default(now()) @@id([tenancyId, id]) @@index([tenancyId, customerId, expiresAt]) diff --git a/apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/route.ts b/apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts similarity index 75% rename from apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/route.ts rename to apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts index 2eae2e60b1..9c45afeb59 100644 --- a/apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/route.ts +++ b/apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/route.ts @@ -1,8 +1,8 @@ -import { ensureItemCustomerTypeMatches, getItemQuantityForCustomer } from "@/lib/payments"; import { getPrismaClientForTenancy } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { adaptSchema, clientOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { ensureCustomerExists, getItemQuantityForCustomer } from "@/lib/payments"; import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects"; @@ -17,6 +17,7 @@ export const GET = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ + customer_type: yupString().oneOf(["user", "team", "custom"]).defined(), customer_id: yupString().defined(), item_id: yupString().defined(), }).defined(), @@ -38,16 +39,23 @@ export const GET = createSmartRouteHandler({ if (!itemConfig) { throw new KnownErrors.ItemNotFound(req.params.item_id); } - - await ensureItemCustomerTypeMatches(req.params.item_id, itemConfig.customerType, req.params.customer_id, tenancy); + if (req.params.customer_type !== itemConfig.customerType) { + throw new KnownErrors.ItemCustomerTypeDoesNotMatch(req.params.item_id, req.params.customer_id, itemConfig.customerType, req.params.customer_type); + } const prisma = await getPrismaClientForTenancy(tenancy); + await ensureCustomerExists({ + prisma, + tenancyId: tenancy.id, + customerType: req.params.customer_type, + customerId: req.params.customer_id, + }); const totalQuantity = await getItemQuantityForCustomer({ prisma, tenancy, itemId: req.params.item_id, customerId: req.params.customer_id, + customerType: req.params.customer_type, }); - return { statusCode: 200, bodyType: "json", @@ -59,3 +67,5 @@ export const GET = createSmartRouteHandler({ }; }, }); + + diff --git a/apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/update-quantity/route.ts b/apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts similarity index 78% rename from apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/update-quantity/route.ts rename to apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts index 0d2efe5394..9a44eb02d3 100644 --- a/apps/backend/src/app/api/latest/payments/items/[customer_id]/[item_id]/update-quantity/route.ts +++ b/apps/backend/src/app/api/latest/payments/items/[customer_type]/[customer_id]/[item_id]/update-quantity/route.ts @@ -1,9 +1,10 @@ -import { ensureItemCustomerTypeMatches, getItemQuantityForCustomer } from "@/lib/payments"; +import { adaptSchema, serverOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { ensureCustomerExists, getItemQuantityForCustomer } from "@/lib/payments"; import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared"; -import { adaptSchema, serverOrHigherAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { getOrUndefined } from "@stackframe/stack-shared/dist/utils/objects"; +import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; export const POST = createSmartRouteHandler({ metadata: { @@ -16,6 +17,7 @@ export const POST = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), params: yupObject({ + customer_type: yupString().oneOf(["user", "team", "custom"]).defined(), customer_id: yupString().defined(), item_id: yupString().defined(), }).defined(), @@ -44,8 +46,16 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.ItemNotFound(req.params.item_id); } - await ensureItemCustomerTypeMatches(req.params.item_id, itemConfig.customerType, req.params.customer_id, tenancy); + if (req.params.customer_type !== itemConfig.customerType) { + throw new KnownErrors.ItemCustomerTypeDoesNotMatch(req.params.item_id, req.params.customer_id, itemConfig.customerType, req.params.customer_type); + } const prisma = await getPrismaClientForTenancy(tenancy); + await ensureCustomerExists({ + prisma, + tenancyId: tenancy.id, + customerType: req.params.customer_type, + customerId: req.params.customer_id, + }); const changeId = await retryTransaction(prisma, async (tx) => { const totalQuantity = await getItemQuantityForCustomer({ @@ -53,6 +63,7 @@ export const POST = createSmartRouteHandler({ tenancy, itemId: req.params.item_id, customerId: req.params.customer_id, + customerType: req.params.customer_type, }); if (!allowNegative && (totalQuantity + req.body.delta < 0)) { throw new KnownErrors.ItemQuantityInsufficientAmount(req.params.item_id, req.params.customer_id, req.body.delta); @@ -61,6 +72,7 @@ export const POST = createSmartRouteHandler({ data: { tenancyId: tenancy.id, customerId: req.params.customer_id, + customerType: typedToUppercase(req.params.customer_type), itemId: req.params.item_id, quantity: req.body.delta, description: req.body.description, @@ -77,3 +89,5 @@ export const POST = createSmartRouteHandler({ }; }, }); + + diff --git a/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts b/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts index f8bdd1c377..c84ec6d29d 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts +++ b/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts @@ -1,11 +1,12 @@ -import { ensureOfferCustomerTypeMatches, ensureOfferIdOrInlineOffer } from "@/lib/payments"; +import { adaptSchema, clientOrHigherAuthTypeSchema, inlineOfferSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { ensureOfferIdOrInlineOffer } from "@/lib/payments"; import { getStripeForAccount } from "@/lib/stripe"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; -import { adaptSchema, clientOrHigherAuthTypeSchema, inlineOfferSchema, 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"; import { CustomerType } from "@prisma/client"; +import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; export const POST = createSmartRouteHandler({ metadata: { @@ -18,7 +19,8 @@ export const POST = createSmartRouteHandler({ tenancy: adaptSchema.defined(), }).defined(), body: yupObject({ - customer_id: yupString().uuid().defined(), + customer_type: yupString().oneOf(["user", "team", "custom"]).defined(), + customer_id: yupString().defined(), offer_id: yupString().optional(), offer_inline: inlineOfferSchema.optional(), }), @@ -34,8 +36,10 @@ export const POST = createSmartRouteHandler({ const { tenancy } = req.auth; const stripe = getStripeForAccount({ tenancy }); const offerConfig = await ensureOfferIdOrInlineOffer(tenancy, req.auth.type, req.body.offer_id, req.body.offer_inline); - await ensureOfferCustomerTypeMatches(req.body.offer_id, offerConfig.customerType, req.body.customer_id, tenancy); const customerType = offerConfig.customerType ?? throwErr("Customer type not found"); + if (req.body.customer_type !== customerType) { + throw new KnownErrors.OfferCustomerTypeDoesNotMatch(req.body.offer_id, req.body.customer_id, customerType, req.body.customer_type); + } const stripeCustomerSearch = await stripe.customers.search({ query: `metadata['customerId']:'${req.body.customer_id}'`, diff --git a/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx b/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx index d0c74e2170..2ecceb4bc9 100644 --- a/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx @@ -87,6 +87,7 @@ export const teamInvitationCodeHandler = createVerificationCodeHandler({ tenancy, customerId: data.team_id, itemId: "dashboard_admins", + customerType: "team", }); if (currentMemberCount + 1 > maxDashboardAdmins) { throw new KnownErrors.ItemQuantityInsufficientAmount("dashboard_admins", data.team_id, -1); diff --git a/apps/backend/src/lib/payments.tsx b/apps/backend/src/lib/payments.tsx index 952b201f28..993a50a6f3 100644 --- a/apps/backend/src/lib/payments.tsx +++ b/apps/backend/src/lib/payments.tsx @@ -1,14 +1,14 @@ -import { teamsCrudHandlers } from "@/app/api/latest/teams/crud"; -import { usersCrudHandlers } from "@/app/api/latest/users/crud"; +import { PrismaClientTransaction } from "@/prisma-client"; +import { SubscriptionStatus } from "@prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { inlineOfferSchema, offerSchema } from "@stackframe/stack-shared/dist/schema-fields"; +import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; +import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currencies"; import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { getOrUndefined, typedFromEntries } from "@stackframe/stack-shared/dist/utils/objects"; import * as yup from "yup"; import { Tenancy } from "./tenancies"; -import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currencies"; -import { SubscriptionStatus } from "@prisma/client"; -import { PrismaClientTransaction } from "@/prisma-client"; +import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids"; export async function ensureOfferIdOrInlineOffer( tenancy: Tenancy, @@ -56,80 +56,19 @@ export async function ensureOfferIdOrInlineOffer( } } -export async function ensureItemCustomerTypeMatches(itemId: string, itemCustomerType: "user" | "team" | undefined, customerId: string, tenancy: Tenancy) { - const actualCustomerType = await getCustomerType(tenancy, customerId); - if (itemCustomerType !== actualCustomerType) { - throw new KnownErrors.ItemCustomerTypeDoesNotMatch(itemId, customerId, itemCustomerType, actualCustomerType); - } -} - -export async function ensureOfferCustomerTypeMatches(offerId: string | undefined, offerCustomerType: "user" | "team" | undefined, customerId: string, tenancy: Tenancy) { - const actualCustomerType = await getCustomerType(tenancy, customerId); - if (offerCustomerType !== actualCustomerType) { - throw new KnownErrors.OfferCustomerTypeDoesNotMatch(offerId, customerId, offerCustomerType, actualCustomerType); - } -} - -export async function getCustomerType(tenancy: Tenancy, customerId: string) { - let user; - try { - user = await usersCrudHandlers.adminRead( - { - user_id: customerId, - tenancy, - allowedErrorTypes: [ - KnownErrors.UserNotFound, - ], - } - ); - } catch (e) { - if (KnownErrors.UserNotFound.isInstance(e)) { - user = null; - } else { - throw e; - } - } - let team; - try { - team = await teamsCrudHandlers.adminRead({ - team_id: customerId, - tenancy, - allowedErrorTypes: [ - KnownErrors.TeamNotFound, - ], - }); - } catch (e) { - if (KnownErrors.TeamNotFound.isInstance(e)) { - team = null; - } else { - throw e; - } - } - - if (user && team) { - throw new StackAssertionError("Found a customer that is both user and team at the same time? This should never happen!", { customerId, user, team, tenancy }); - } - - if (user) { - return "user"; - } - if (team) { - return "team"; - } - throw new KnownErrors.CustomerDoesNotExist(customerId); -} - export async function getItemQuantityForCustomer(options: { prisma: PrismaClientTransaction, tenancy: Tenancy, itemId: string, customerId: string, + customerType: "user" | "team" | "custom", }) { const itemConfig = getOrUndefined(options.tenancy.config.payments.items, options.itemId); const defaultQuantity = itemConfig?.default.quantity ?? 0; const subscriptions = await options.prisma.subscription.findMany({ where: { tenancyId: options.tenancy.id, + customerType: typedToUppercase(options.customerType), customerId: options.customerId, status: { in: [SubscriptionStatus.active, SubscriptionStatus.trialing], @@ -146,6 +85,7 @@ export async function getItemQuantityForCustomer(options: { const { _sum } = await options.prisma.itemQuantityChange.aggregate({ where: { tenancyId: options.tenancy.id, + customerType: typedToUppercase(options.customerType), customerId: options.customerId, itemId: options.itemId, OR: [ @@ -159,3 +99,42 @@ export async function getItemQuantityForCustomer(options: { }); return subscriptionQuantity + (_sum.quantity ?? 0) + defaultQuantity; } + +export async function ensureCustomerExists(options: { + prisma: PrismaClientTransaction, + tenancyId: string, + customerType: "user" | "team" | "custom", + customerId: string, +}) { + if (options.customerType === "user") { + if (!isUuid(options.customerId)) { + throw new KnownErrors.UserNotFound(); + } + const user = await options.prisma.projectUser.findUnique({ + where: { + tenancyId_projectUserId: { + tenancyId: options.tenancyId, + projectUserId: options.customerId, + }, + }, + }); + if (!user) { + throw new KnownErrors.UserNotFound(); + } + } else if (options.customerType === "team") { + if (!isUuid(options.customerId)) { + throw new KnownErrors.TeamNotFound(options.customerId); + } + const team = await options.prisma.team.findUnique({ + where: { + tenancyId_teamId: { + tenancyId: options.tenancyId, + teamId: options.customerId, + }, + }, + }); + if (!team) { + throw new KnownErrors.TeamNotFound(options.customerId); + } + } +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx index 3b63ed38c9..ed97f06ee1 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx @@ -1,7 +1,5 @@ "use client"; -import * as yup from "yup"; -import { useState } from "react"; import { PaymentItemTable } from "@/components/data-table/payment-item-table"; import { PaymentOfferTable } from "@/components/data-table/payment-offer-table"; import { FormDialog, SmartFormDialog } from "@/components/form-dialog"; @@ -9,24 +7,22 @@ import { InputField, SelectField, SwitchField } from "@/components/form-fields"; import { IncludedItemEditorField } from "@/components/payments/included-item-editor"; import { PriceEditorField } from "@/components/payments/price-editor"; import { AdminProject } from "@stackframe/stack"; -import { KnownErrors } from "@stackframe/stack-shared"; import { offerPriceSchema, userSpecifiedIdSchema, yupRecord } from "@stackframe/stack-shared/dist/schema-fields"; import { wait } from "@stackframe/stack-shared/dist/utils/promises"; -import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { - ActionDialog, Button, Card, CardContent, - InlineCode, Typography, toast } from "@stackframe/stack-ui"; import { ArrowRight, BarChart3, Repeat, Shield, Wallet, Webhook } from "lucide-react"; +import { useState } from "react"; +import * as yup from "yup"; import { PageLayout } from "../page-layout"; import { useAdminApp } from "../use-admin-app"; @@ -172,7 +168,7 @@ function CreateOfferDialog({ const offerSchema = yup.object({ offerId: yup.string().defined().label("Offer ID"), displayName: yup.string().defined().label("Display Name"), - customerType: yup.string().oneOf(["user", "team"]).defined().label("Customer Type"), + customerType: yup.string().oneOf(["user", "team", "custom"]).defined().label("Customer Type"), prices: yupRecord(userSpecifiedIdSchema("priceId"), offerPriceSchema).defined().label("Prices").test("at-least-one-price", "At least one price is required", (value) => { return Object.keys(value).length > 0; }), @@ -214,6 +210,7 @@ function CreateOfferDialog({ @@ -233,11 +230,12 @@ function CreateItemDialog({ open, onOpenChange, project }: { open: boolean, onOp const itemSchema = yup.object({ itemId: yup.string().defined().label("Item ID"), displayName: yup.string().optional().label("Display Name"), - customerType: yup.string().oneOf(["user", "team"]).defined().label("Customer Type").meta({ + customerType: yup.string().oneOf(["user", "team", "custom"]).defined().label("Customer Type").meta({ stackFormFieldRender: (props) => ( ), }), diff --git a/apps/dashboard/src/components/data-table/payment-item-table.tsx b/apps/dashboard/src/components/data-table/payment-item-table.tsx index 0a120f37ba..5356fccd58 100644 --- a/apps/dashboard/src/components/data-table/payment-item-table.tsx +++ b/apps/dashboard/src/components/data-table/payment-item-table.tsx @@ -8,6 +8,7 @@ import { ActionCell, DataTable, DataTableColumnHeader, TextCell, toast } from "@ import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import * as yup from "yup"; +import { SelectField } from "../form-fields"; type PaymentItem = { id: string, @@ -54,7 +55,7 @@ const columns: ColumnDef[] = [ }, { id: "actions", - cell: ({ row }) => , + cell: ({ row }) => , } ]; @@ -80,7 +81,7 @@ export function PaymentItemTable({ />; } -function ActionsCell({ itemId }: { itemId: string }) { +function ActionsCell({ item }: { item: PaymentItem }) { const [open, setOpen] = useState(false); return ( <> @@ -100,17 +101,25 @@ function ActionsCell({ itemId }: { itemId: string }) { ); } -function CreateItemQuantityChangeDialog({ open, onOpenChange, itemId }: { open: boolean, onOpenChange: (open: boolean) => void, itemId: string }) { +type CreateItemQuantityChangeDialogProps = { + open: boolean, + onOpenChange: (open: boolean) => void, + itemId: string, + customerType: "user" | "team" | "custom" | undefined, +} + +function CreateItemQuantityChangeDialog({ open, onOpenChange, itemId, customerType }: CreateItemQuantityChangeDialogProps) { const stackAdminApp = useAdminApp(); const schema = yup.object({ - customerId: yup.string().uuid().defined().label("Customer ID"), + customerId: yup.string().defined().label("Customer ID"), quantity: yup.number().defined().label("Quantity"), description: yup.string().optional().label("Description"), expiresAt: yup.date().optional().label("Expires At"), @@ -118,7 +127,12 @@ function CreateItemQuantityChangeDialog({ open, onOpenChange, itemId }: { open: const submit = async (values: yup.InferType) => { const result = await Result.fromPromise(stackAdminApp.createItemQuantityChange({ - customerId: values.customerId, + ...(customerType === "user" ? + { userId: values.customerId } : + customerType === "team" ? + { teamId: values.customerId } : + { customId: values.customerId } + ), itemId, quantity: values.quantity, expiresAt: values.expiresAt ? values.expiresAt.toISOString() : undefined, @@ -130,10 +144,10 @@ function CreateItemQuantityChangeDialog({ open, onOpenChange, itemId }: { open: } if (result.error instanceof KnownErrors.ItemNotFound) { toast({ title: "Item not found", variant: "destructive" }); - } else if (result.error instanceof KnownErrors.ItemCustomerTypeDoesNotMatch) { - toast({ title: "Customer type does not match expected type for this item", variant: "destructive" }); - } else if (result.error instanceof KnownErrors.CustomerDoesNotExist) { - toast({ title: "Customer does not exist", variant: "destructive" }); + } else if (result.error instanceof KnownErrors.UserNotFound) { + toast({ title: "No user found with the given ID", variant: "destructive" }); + } else if (result.error instanceof KnownErrors.TeamNotFound) { + toast({ title: "No team found with the given ID", variant: "destructive" }); } else { toast({ title: "An unknown error occurred", variant: "destructive" }); } diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index ca363f89ab..bf16e59fe1 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -1419,6 +1419,7 @@ export namespace Payments { method: "POST", accessType: "client", body: { + customer_type: "user", customer_id: userId, offer_id: "test-offer", }, diff --git a/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts index d854b20d7b..2d8c1d9442 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/payments/create-purchase-url.test.ts @@ -13,6 +13,7 @@ it("should not be able to create purchase URL without offer_id or offer_inline", method: "POST", accessType: "client", body: { + customer_type: "user", customer_id: generateUuid(), }, }); @@ -52,6 +53,7 @@ it("should error for non-existent offer_id", async ({ expect }) => { method: "POST", accessType: "client", body: { + customer_type: "user", customer_id: generateUuid(), offer_id: "non-existent-offer", }, @@ -103,24 +105,30 @@ it("should error for invalid customer_id", async ({ expect }) => { method: "POST", accessType: "client", body: { + customer_type: "team", customer_id: generateUuid(), offer_id: "test-offer", }, }); expect(response).toMatchInlineSnapshot(` - NiceResponse { - "status": 400, - "body": { - "code": "CUSTOMER_DOES_NOT_EXIST", - "details": { "customer_id": "" }, - "error": "Customer with ID \\"\\" does not exist.", - }, - "headers": Headers { - "x-stack-known-error": "CUSTOMER_DOES_NOT_EXIST", -