From aaa27d3de99c86da8cedf21bc46290d602baecad Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Wed, 8 Jul 2026 15:41:56 +0700 Subject: [PATCH] Block rejected invoice payments --- .../[invoiceId]/payment-request/route.test.ts | 24 +++++++++++++++++++ .../[invoiceId]/payment-request/route.ts | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.test.ts b/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.test.ts index 5d4770f8..6f665e27 100644 --- a/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.test.ts +++ b/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.test.ts @@ -166,6 +166,30 @@ describe("POST /api/gigs/[id]/invoice/[invoiceId]/payment-request", () => { expect(createPayment).not.toHaveBeenCalled(); }); + it("does not create a payment request for a rejected invoice", async () => { + const invoice = { + ...payableInvoice({ + receiver_payment_currency: "sol", + merchant_wallet_address: WALLET_ADDRESS, + }), + status: "rejected", + }; + + (getAuthContext as any).mockResolvedValue({ + user: { id: POSTER_ID }, + supabase: { from: vi.fn(() => invoiceQuery(invoice)) }, + }); + (createServiceClient as any).mockReturnValue(serviceClient({ id: INVOICE_ID })); + + const res = await POST({} as any, params); + + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toBe("Invoice is not payable"); + expect(createPayment).not.toHaveBeenCalled(); + expect(createServiceClient).not.toHaveBeenCalled(); + }); + it("returns the invoice receiving wallet metadata", async () => { const invoice = payableInvoice({ receiver_payment_currency: "sol", diff --git a/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.ts b/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.ts index 970a0ace..8781ee73 100644 --- a/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.ts +++ b/src/app/api/gigs/[id]/invoice/[invoiceId]/payment-request/route.ts @@ -6,6 +6,7 @@ import { createServiceClient } from "@/lib/supabase/service"; export const dynamic = "force-dynamic"; const PAYMENT_REQUEST_SECONDS = 15 * 60; +const PAYABLE_INVOICE_STATUSES = new Set(["sent", "expired"]); type InvoiceContext = | { response: NextResponse } | { @@ -76,7 +77,7 @@ async function loadInvoiceContext( if (invoice.status === "paid") { return { response: NextResponse.json({ error: "Invoice is already paid" }, { status: 400 }) }; } - if (invoice.status === "cancelled" || invoice.status === "draft") { + if (!PAYABLE_INVOICE_STATUSES.has(invoice.status)) { return { response: NextResponse.json({ error: "Invoice is not payable" }, { status: 400 }) }; }