diff --git a/apps/web/app/(ee)/api/commissions/route.ts b/apps/web/app/(ee)/api/commissions/route.ts index 3e130c3a795..d23d65fe924 100644 --- a/apps/web/app/(ee)/api/commissions/route.ts +++ b/apps/web/app/(ee)/api/commissions/route.ts @@ -90,15 +90,18 @@ export const POST = withWorkspace( console.timeEnd("createManualCommissions"); - return NextResponse.json( - createCommissionResponseSchema.parse({ - success: true, - message: "Your commissions are being created and will appear shortly.", - }), - { - status: 202, - }, - ); + const isClawback = body.type === "custom" && body.amount < 0; + + const response = createCommissionResponseSchema.parse({ + success: true, + message: isClawback + ? "A clawback has been queued for the partner!" + : "Your commissions are being created and will appear shortly.", + }); + + return NextResponse.json(response, { + status: 202, + }); }, { requiredPlan: ["business", "advanced", "enterprise"], diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-clawback-sheet.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-clawback-sheet.tsx index 2e1b8e80107..89355406456 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-clawback-sheet.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/create-clawback-sheet.tsx @@ -1,14 +1,13 @@ -import { createClawbackAction } from "@/lib/actions/partners/create-clawback"; import { mutatePrefix } from "@/lib/swr/mutate"; +import { useApiMutation } from "@/lib/swr/use-api-mutation"; import useWorkspace from "@/lib/swr/use-workspace"; import { CLAWBACK_REASONS, - createClawbackSchema, + createCommissionResponseSchema, } from "@/lib/zod/schemas/commissions"; import { PartnerSelector } from "@/ui/partners/partner-selector"; import { X } from "@/ui/shared/icons"; import { Button, Sheet } from "@dub/ui"; -import { useAction } from "next-safe-action/hooks"; import { useParams } from "next/navigation"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -21,7 +20,11 @@ interface CreateClawbackSheetProps { nested?: boolean; } -type FormData = z.infer; +type FormData = { + partnerId?: string; + amount?: number; + reason?: (typeof CLAWBACK_REASONS)[number]["value"]; +}; function CreateClawbackSheetContent( props: Omit, @@ -37,47 +40,42 @@ function CreateClawbackSheetContent( reset, watch, getValues, - formState: { errors, isSubmitting, isSubmitSuccessful }, + formState: { errors, isSubmitting }, } = useForm({ defaultValues: { partnerId: params.partnerId, - description: "", + reason: undefined, }, }); - const [partnerId, amount, description] = watch([ - "partnerId", - "amount", - "description", - ]); + const [partnerId, amount, reason] = watch(["partnerId", "amount", "reason"]); - const { executeAsync, isPending } = useAction(createClawbackAction, { - onSuccess: () => { - toast.success("A clawback has been created for the partner!"); - setIsOpen(false); - mutatePrefix(`/api/commissions?workspaceId=${workspaceId}`); - const currentValues = getValues(); - reset(currentValues); - }, - onError({ error }) { - toast.error(error.serverError || "Failed to create clawback."); - }, - }); + const { makeRequest, isSubmitting: isCreating } = + useApiMutation>(); const onSubmit = async (data: FormData) => { if (!workspaceId || !defaultProgramId) { return; } - await executeAsync({ - ...data, - amount: data.amount * 100, - workspaceId, + await makeRequest("/api/commissions", { + method: "POST", + body: { + type: "custom", + partnerId: data.partnerId, + amount: data.amount ? -Math.round(data.amount * 100) : 0, + description: data.reason, + }, + onSuccess: async ({ message }) => { + toast.success(message); + setIsOpen(false); + await mutatePrefix("/api/commissions"); + const currentValues = getValues(); + reset(currentValues); + }, }); }; - const disableSubmitButton = !partnerId || !amount || !description; - return (
@@ -110,7 +108,7 @@ function CreateClawbackSheetContent( rules={{ required: true }} render={({ field }) => ( )} @@ -171,21 +169,21 @@ function CreateClawbackSheetContent(
( )} /> - {errors.description && ( + {errors.reason && ( - {errors.description.message} + {errors.reason.message} )}
@@ -216,15 +214,15 @@ function CreateClawbackSheetContent( onClick={() => setIsOpen(false)} text="Cancel" className="w-fit" - disabled={isPending || isSubmitting || isSubmitSuccessful} + disabled={isCreating || isSubmitting} />
diff --git a/apps/web/lib/actions/partners/create-clawback.ts b/apps/web/lib/actions/partners/create-clawback.ts deleted file mode 100644 index 5372d8d10f1..00000000000 --- a/apps/web/lib/actions/partners/create-clawback.ts +++ /dev/null @@ -1,38 +0,0 @@ -"use server"; - -import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; -import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw"; -import { queuePartnerCommissionCreation } from "@/lib/partners/queue-partner-commission-creation"; -import { createClawbackSchema } from "@/lib/zod/schemas/commissions"; -import { authActionClient } from "../safe-action"; -import { throwIfNoPermission } from "../throw-if-no-permission"; - -export const createClawbackAction = authActionClient - .inputSchema(createClawbackSchema) - .action(async ({ parsedInput, ctx }) => { - const { workspace, user } = ctx; - const programId = getDefaultProgramIdOrThrow(workspace); - - throwIfNoPermission({ - role: workspace.role, - requiredRoles: ["owner", "member"], - }); - - const { partnerId, amount, description } = parsedInput; - - await getProgramEnrollmentOrThrow({ - programId, - partnerId, - include: {}, - }); - - await queuePartnerCommissionCreation({ - event: "custom", - partnerId, - programId, - description, - amount: -amount, - quantity: 1, - userId: user.id, - }); - }); diff --git a/apps/web/lib/api/commissions/create-manual-commissions.ts b/apps/web/lib/api/commissions/create-manual-commissions.ts index ffbd3f7ebe3..06809015177 100644 --- a/apps/web/lib/api/commissions/create-manual-commissions.ts +++ b/apps/web/lib/api/commissions/create-manual-commissions.ts @@ -121,13 +121,6 @@ export async function createManualCommissions(args: CreateCommissionsArgs) { productId, } = args; - if (!importStripeInvoices && !saleAmount) { - throw new DubApiError({ - code: "bad_request", - message: "Either saleAmount or importStripeInvoices must be provided.", - }); - } - const hasManualSaleFields = saleAmount || saleEventDate || invoiceId || productId; diff --git a/apps/web/lib/openapi/commissions/create-commission.ts b/apps/web/lib/openapi/commissions/create-commission.ts index 9c6c9edf0cd..9ccee783d93 100644 --- a/apps/web/lib/openapi/commissions/create-commission.ts +++ b/apps/web/lib/openapi/commissions/create-commission.ts @@ -10,7 +10,7 @@ export const createCommission: ZodOpenApiOperationObject = { "x-speakeasy-name-override": "create", summary: "Create commission", description: - "Create one or more commissions (custom, lead or sale) for a partner. Commission creation is processed asynchronously. Use the List Commissions endpoint or webhooks to be notified when the commission is created.", + "Create one or more commissions (custom, lead or sale) for a partner. Custom commissions accept a negative `amount` to create a clawback; in that case `description` is required and may be a known clawback reason or any other string. Commission creation is processed asynchronously. Use the List Commissions endpoint or webhooks to be notified when the commission is created.", requestBody: { content: { "application/json": { diff --git a/apps/web/lib/zod/schemas/commissions.ts b/apps/web/lib/zod/schemas/commissions.ts index a940a4dcfc0..8b515f49186 100644 --- a/apps/web/lib/zod/schemas/commissions.ts +++ b/apps/web/lib/zod/schemas/commissions.ts @@ -318,15 +318,6 @@ export const CLAWBACK_REASONS_MAP = Object.fromEntries( CLAWBACK_REASONS.map((r) => [r.value, r]), ); -export const createClawbackSchema = z.object({ - workspaceId: z.string(), - partnerId: z.string(), - amount: z.number().gt(0, "Amount must be greater than 0."), - description: z.enum( - CLAWBACK_REASONS.map((r) => r.value) as [string, ...string[]], - ), -}); - export const COMMISSION_EXPORT_COLUMNS = [ { id: "id", label: "ID", type: "string", default: true }, { id: "type", label: "Type", type: "string", default: true }, @@ -461,15 +452,21 @@ export const createPartnerCommissionSchema = z.object({ export const createManualCommissionBodySchema = z .discriminatedUnion("type", [ - // Custom commission + // Custom commission (negative amount = clawback) z.object({ type: z.literal("custom"), partnerId: z .string() .describe("The ID of the partner to create the commission for."), amount: centsSchema - .pipe(z.number().min(1)) - .describe("The commission amount in cents."), + .pipe( + z.number().refine((n) => n !== 0, { + message: "Amount cannot be 0.", + }), + ) + .describe( + "The commission amount in cents. Use a negative amount to create a clawback.", + ), date: parseDateSchema .nullish() .describe("If not provided, the current date will be used."), @@ -477,7 +474,9 @@ export const createManualCommissionBodySchema = z .string() .max(190) .nullish() - .describe("The description of the commission."), + .describe( + "The description of the commission. Required for clawbacks (negative `amount`). May be a known clawback reason (`order_canceled`, `fraud`, `terms_violation`, `tracking_error`, `payment_failed`, `ineligible_partner`, `duplicate_commission`, `other`) or any other string.", + ), }), // Lead commission @@ -573,15 +572,40 @@ export const createManualCommissionBodySchema = z }), ]) .superRefine((data, ctx) => { - if (data.type !== "sale") return; + if (data.type === "custom") { + if (data.amount < 0 && !data.description?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "`description` is required when creating a clawback (negative amount).", + path: ["description"], + }); + } + return; + } + + if (data.type === "sale") { + if (data.importStripeInvoices) { + return; + } + + if (data.saleAmount == null) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "`saleAmount` is required when `importStripeInvoices` is false.", + path: ["saleAmount"], + }); + return; + } - if (!data.importStripeInvoices && data.saleAmount == null) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: - "`saleAmount` is required when `importStripeInvoices` is false.", - path: ["saleAmount"], - }); + if (data.saleAmount === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Sale amount cannot be 0.", + path: ["saleAmount"], + }); + } } }); diff --git a/apps/web/playwright/api/clawbacks/clawbacks.spec.ts b/apps/web/playwright/api/clawbacks/clawbacks.spec.ts new file mode 100644 index 00000000000..69282245276 --- /dev/null +++ b/apps/web/playwright/api/clawbacks/clawbacks.spec.ts @@ -0,0 +1,185 @@ +import { prisma } from "@/lib/prisma"; +import { nanoid } from "@dub/utils"; +import { expect } from "@playwright/test"; +import { apiError } from "../../utils"; +import { test } from "../fixtures"; +import { createPartner, deletePartner } from "../partners/helpers"; + +const expectedQueuedResponse = { + success: true, + message: "A clawback has been queued for the partner!", +}; + +async function expectClawbackCreated({ + partnerId, + programId, + amount, + description, +}: { + partnerId: string; + programId: string; + amount: number; + description: string; +}) { + await expect + .poll(async () => { + const commission = await prisma.commission.findFirst({ + where: { + partnerId, + programId, + type: "custom", + description, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (!commission) { + return null; + } + + return { + partnerId: commission.partnerId, + programId: commission.programId, + type: commission.type, + amount: Number(commission.amount), + earnings: Number(commission.earnings), + quantity: commission.quantity, + description: commission.description, + }; + }) + .toEqual({ + partnerId, + programId, + type: "custom", + amount: 0, + earnings: -amount, + quantity: 1, + description, + }); +} + +test("POST /commissions – clawback by partnerId", async ({ api, program }) => { + let partnerId: string | undefined; + + try { + const { status: createStatus, data: created } = await createPartner(api, { + groupId: program.defaultGroupId, + }); + partnerId = created.id; + expect(createStatus).toEqual(201); + + const { status, data } = await api.post("/api/commissions", { + type: "custom", + partnerId, + amount: -500, + description: "fraud", + }); + + expect(status).toEqual(202); + expect(data).toStrictEqual(expectedQueuedResponse); + + await expectClawbackCreated({ + partnerId: created.id, + programId: program.id, + amount: 500, + description: "fraud", + }); + } finally { + await deletePartner(partnerId); + } +}); + +test("POST /commissions – clawback with arbitrary description", async ({ + api, + program, +}) => { + let partnerId: string | undefined; + const description = `chargeback-${nanoid()}`; + + try { + const { status: createStatus, data: created } = await createPartner(api, { + groupId: program.defaultGroupId, + }); + partnerId = created.id; + expect(createStatus).toEqual(201); + + const { status, data } = await api.post("/api/commissions", { + type: "custom", + partnerId, + amount: -100, + description, + }); + + expect(status).toEqual(202); + expect(data).toStrictEqual(expectedQueuedResponse); + + await expectClawbackCreated({ + partnerId: created.id, + programId: program.id, + amount: 100, + description, + }); + } finally { + await deletePartner(partnerId); + } +}); + +const missingPartnerId = `pn_${nanoid()}`; + +const clawbackErrorCases = [ + { + name: "POST /commissions – clawback partner not found", + body: { + type: "custom", + partnerId: missingPartnerId, + amount: -500, + description: "fraud", + }, + expected: ({ program }: { program: { id: string } }) => + apiError({ + code: "not_found", + message: `Partner ${missingPartnerId} is not enrolled in program ${program.id}.`, + }), + }, + { + name: "POST /commissions – clawback missing partnerId", + body: { type: "custom", amount: -500, description: "fraud" }, + expected: apiError({ + code: "unprocessable_entity", + message: + "invalid_type: partnerId: Invalid input: expected string, received undefined", + }), + }, + { + name: "POST /commissions – clawback amount 0", + body: { + type: "custom", + partnerId: "pn_test", + amount: 0, + description: "fraud", + }, + expected: apiError({ + code: "unprocessable_entity", + message: "custom: amount: Amount cannot be 0.", + }), + }, + { + name: "POST /commissions – clawback missing description", + body: { type: "custom", partnerId: "pn_test", amount: -500 }, + expected: apiError({ + code: "unprocessable_entity", + message: + "custom: description: `description` is required when creating a clawback (negative amount).", + }), + }, +]; + +for (const { name, body, expected } of clawbackErrorCases) { + test(name, async ({ api, program }) => { + expect(await api.post("/api/commissions", body)).toEqual( + typeof expected === "function" ? expected({ program }) : expected, + ); + }); +} diff --git a/apps/web/playwright/api/discount-codes/discount-codes.spec.ts b/apps/web/playwright/api/discount-codes/discount-codes.spec.ts index 6def2c2381c..df7b2d04911 100644 --- a/apps/web/playwright/api/discount-codes/discount-codes.spec.ts +++ b/apps/web/playwright/api/discount-codes/discount-codes.spec.ts @@ -1,16 +1,17 @@ import { createId } from "@/lib/api/create-id"; import { constructDiscountCode } from "@/lib/discounts/construct-discount-code"; -import { conn } from "@/lib/planetscale"; import { prisma } from "@/lib/prisma"; -import type { EnrolledPartnerProps } from "@/lib/types"; import { DiscountCodeSchema } from "@/lib/zod/schemas/discount"; import { DEFAULT_ADDITIONAL_PARTNER_LINKS } from "@/lib/zod/schemas/groups"; import { nanoid } from "@dub/utils"; import { expect } from "@playwright/test"; import { DiscountProvider, RewardStructure } from "@prisma/client"; import * as z from "zod/v4"; -import { randomName, randomPartnerEmail } from "../../utils"; import { test, type ApiClient } from "../fixtures"; +import { + createPartner as createPartnerApi, + deletePartner, +} from "../partners/helpers"; import { TEST_WORKSPACE } from "../setup-test-workspace"; type DiscountCode = z.infer; @@ -123,38 +124,12 @@ async function createPartner( throw new Error("Custom discount group was not seeded."); } - return api.post("/api/partners", { - name: randomName(), - email: randomPartnerEmail(), + return createPartnerApi(api, { groupId: partnerGroupId, ...overrides, }); } -async function deletePartner(partnerId: string | undefined) { - if (!partnerId) return; - - await prisma.discountCode.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.link.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.programEnrollment.deleteMany({ - where: { - partnerId, - }, - }); - - await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]); -} - async function createDiscountCode( api: ApiClient, overrides: Record = {}, diff --git a/apps/web/playwright/api/discounts/discounts.spec.ts b/apps/web/playwright/api/discounts/discounts.spec.ts index 502caf8ef08..31104f59700 100644 --- a/apps/web/playwright/api/discounts/discounts.spec.ts +++ b/apps/web/playwright/api/discounts/discounts.spec.ts @@ -1,5 +1,4 @@ import { createId } from "@/lib/api/create-id"; -import { conn } from "@/lib/planetscale"; import { prisma } from "@/lib/prisma"; import type { Customer, @@ -12,8 +11,12 @@ import { DEFAULT_ADDITIONAL_PARTNER_LINKS } from "@/lib/zod/schemas/groups"; import { nanoid } from "@dub/utils"; import { expect } from "@playwright/test"; import { DiscountProvider, RewardStructure } from "@prisma/client"; -import { randomCustomer, randomName, randomPartnerEmail } from "../../utils"; +import { randomCustomer, randomName } from "../../utils"; import { test, type ApiClient } from "../fixtures"; +import { + createPartner as createPartnerApi, + deletePartner, +} from "../partners/helpers"; import { TEST_WORKSPACE } from "../setup-test-workspace"; test.describe.configure({ @@ -139,37 +142,11 @@ async function createPartner(api: ApiClient) { throw new Error("Custom discount group was not seeded."); } - return api.post("/api/partners", { - name: randomName(), - email: randomPartnerEmail(), + return createPartnerApi(api, { groupId: partnerGroupId, }); } -async function deletePartner(partnerId: string | undefined) { - if (!partnerId) return; - - await prisma.discountCode.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.link.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.programEnrollment.deleteMany({ - where: { - partnerId, - }, - }); - - await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]); -} - async function createCustomerWithCustomDiscount({ api, program, diff --git a/apps/web/playwright/api/partners/ban-partner.spec.ts b/apps/web/playwright/api/partners/ban-partner.spec.ts index 14db60401e1..d0f71fc4083 100644 --- a/apps/web/playwright/api/partners/ban-partner.spec.ts +++ b/apps/web/playwright/api/partners/ban-partner.spec.ts @@ -1,41 +1,9 @@ -import { conn } from "@/lib/planetscale"; -import { prisma } from "@/lib/prisma"; import type { EnrolledPartnerProps } from "@/lib/types"; import { nanoid } from "@dub/utils"; import { expect } from "@playwright/test"; -import { apiError, randomName, randomPartnerEmail } from "../../utils"; +import { apiError } from "../../utils"; import { test, type ApiClient } from "../fixtures"; - -async function createPartner( - api: ApiClient, - overrides: Record = {}, -) { - return api.post("/api/partners", { - name: randomName(), - email: randomPartnerEmail(), - ...overrides, - }); -} - -async function deletePartner(partnerId: string | undefined) { - if (!partnerId) return; - - await prisma.link.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.programEnrollment.deleteMany({ - where: { - partnerId, - }, - }); - - // Prisma partner.delete hits a PlanetScale relation quirk; raw SQL matches - // bulkDeletePartners cleanup used by e2e cron. - await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]); -} +import { createPartner, deletePartner } from "./helpers"; async function expectPartnerBanned( api: ApiClient, diff --git a/apps/web/playwright/api/partners/helpers.ts b/apps/web/playwright/api/partners/helpers.ts new file mode 100644 index 00000000000..472aa73c3fe --- /dev/null +++ b/apps/web/playwright/api/partners/helpers.ts @@ -0,0 +1,54 @@ +import { conn } from "@/lib/planetscale"; +import { prisma } from "@/lib/prisma"; +import type { EnrolledPartnerProps } from "@/lib/types"; +import { randomName, randomPartnerEmail } from "../../utils"; +import type { ApiClient } from "../fixtures"; + +export async function createPartner( + api: ApiClient, + overrides: Record = {}, +) { + return api.post("/api/partners", { + name: randomName(), + email: randomPartnerEmail(), + ...overrides, + }); +} + +export async function deletePartner(partnerId: string | undefined) { + if (!partnerId) return; + + await prisma.commission.deleteMany({ + where: { + partnerId, + }, + }); + + await prisma.payout.deleteMany({ + where: { + partnerId, + }, + }); + + await prisma.discountCode.deleteMany({ + where: { + partnerId, + }, + }); + + await prisma.link.deleteMany({ + where: { + partnerId, + }, + }); + + await prisma.programEnrollment.deleteMany({ + where: { + partnerId, + }, + }); + + // Prisma partner.delete hits a PlanetScale relation quirk; raw SQL matches + // bulkDeletePartners cleanup used by e2e cron. + await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]); +} diff --git a/apps/web/playwright/api/partners/partners.spec.ts b/apps/web/playwright/api/partners/partners.spec.ts index d7fbc62a0bf..cbbc82e0bd2 100644 --- a/apps/web/playwright/api/partners/partners.spec.ts +++ b/apps/web/playwright/api/partners/partners.spec.ts @@ -1,5 +1,3 @@ -import { conn } from "@/lib/planetscale"; -import { prisma } from "@/lib/prisma"; import type { EnrolledPartnerProps } from "@/lib/types"; import { EnrolledPartnerSchema as EnrolledPartnerSchemaDate } from "@/lib/zod/schemas/partners"; import { nanoid } from "@dub/utils"; @@ -7,8 +5,9 @@ import { expect } from "@playwright/test"; import slugify from "@sindresorhus/slugify"; import * as z from "zod/v4"; import { apiError, randomName, randomPartnerEmail } from "../../utils"; -import { test, type ApiClient } from "../fixtures"; +import { test } from "../fixtures"; import { TEST_WORKSPACE } from "../setup-test-workspace"; +import { createPartner, deletePartner } from "./helpers"; const EnrolledPartnerSchema = EnrolledPartnerSchemaDate.extend({ createdAt: z.string(), @@ -22,37 +21,6 @@ function reEscape(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -async function createPartner( - api: ApiClient, - overrides: Record = {}, -) { - return api.post("/api/partners", { - name: randomName(), - email: randomPartnerEmail(), - ...overrides, - }); -} - -async function deletePartner(partnerId: string | undefined) { - if (!partnerId) return; - - await prisma.link.deleteMany({ - where: { - partnerId, - }, - }); - - await prisma.programEnrollment.deleteMany({ - where: { - partnerId, - }, - }); - - // Prisma partner.delete hits a PlanetScale relation quirk; raw SQL matches - // bulkDeletePartners cleanup used by e2e cron. - await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]); -} - test("POST /partners", async ({ api, program }) => { let partnerId: string | undefined; diff --git a/apps/web/tests/commissions/create-commission.test.ts b/apps/web/tests/commissions/create-commission.test.ts index 04aae324f07..941c92120eb 100644 --- a/apps/web/tests/commissions/create-commission.test.ts +++ b/apps/web/tests/commissions/create-commission.test.ts @@ -31,7 +31,7 @@ const validationCases = [ name: "custom commission with amount 0", body: { type: "custom", partnerId: E2E_PARTNER.id, amount: 0 }, expectedStatus: 422, - expectedMessage: "too_small: amount: Too small: expected number to be >=1", + expectedMessage: "custom: amount: Amount cannot be 0.", }, { name: "sale commission missing saleAmount", @@ -45,6 +45,18 @@ const validationCases = [ expectedMessage: "custom: saleAmount: `saleAmount` is required when `importStripeInvoices` is false.", }, + { + name: "sale commission with saleAmount 0", + body: { + type: "sale", + partnerId: E2E_PARTNER.id, + customerId: E2E_CUSTOMER_ID, + importStripeInvoices: false, + saleAmount: 0, + }, + expectedStatus: 422, + expectedMessage: "custom: saleAmount: Sale amount cannot be 0.", + }, ]; validationCases.forEach(({ name, body, expectedStatus, expectedMessage }) => {