diff --git a/apps/api/src/routes/invoices.ts b/apps/api/src/routes/invoices.ts index a5c0d95..91ac4ee 100644 --- a/apps/api/src/routes/invoices.ts +++ b/apps/api/src/routes/invoices.ts @@ -130,7 +130,17 @@ invoicesRouter.get("/:id", async (c) => { db.select().from(invoiceTipSplits).where(eq(invoiceTipSplits.invoiceId, id)), ]); - return c.json({ ...invoice, lineItems, tipSplits }); + let cardLast4: string | null = null; + let paymentStatus: string | null = null; + if (invoice.stripePaymentIntentId) { + const details = await getPaymentIntentDetails(invoice.stripePaymentIntentId); + if (details) { + cardLast4 = details.cardLast4; + paymentStatus = details.paymentStatus; + } + } + + return c.json({ ...invoice, lineItems, tipSplits, cardLast4, paymentStatus }); }); // Save tip splits for an invoice (replaces existing splits) @@ -450,9 +460,6 @@ invoicesRouter.post( if (invoice.status !== "paid") { return c.json({ error: "Refund only allowed on paid invoices" }, 422); } - if (!invoice.stripePaymentIntentId) { - return c.json({ error: "No Stripe payment intent found for this invoice" }, 422); - } return await db.transaction(async (tx) => { if (body.idempotencyKey) { @@ -465,17 +472,25 @@ invoicesRouter.post( } } - const result = await processRefund(id, body.amountCents); - if (!result) return c.json({ error: "Refund failed" }, 500); + let refundId: string; + + if (invoice.stripePaymentIntentId) { + const result = await processRefund(id, body.amountCents); + if (!result) return c.json({ error: "Refund failed" }, 500); + refundId = result.refundId; + } else { + // Manual refund — no Stripe call needed + refundId = `manual_${id}_${Date.now()}`; + } await tx.insert(refunds).values({ invoiceId: id, - stripeRefundId: result.refundId, + stripeRefundId: refundId, idempotencyKey: body.idempotencyKey ?? null, amountCents: body.amountCents ?? null, }); - return c.json({ refundId: result.refundId }); + return c.json({ refundId }); }); } ); diff --git a/apps/web/src/pages/Invoices.tsx b/apps/web/src/pages/Invoices.tsx index 246a9be..0eceb4c 100644 --- a/apps/web/src/pages/Invoices.tsx +++ b/apps/web/src/pages/Invoices.tsx @@ -487,7 +487,7 @@ const [showRefundDialog, setShowRefundDialog] = useState(false); )}
- {invoice.status === "paid" && invoice.stripePaymentIntentId && !invoice.stripeRefundId && isManager && ( + {invoice.status === "paid" && !invoice.stripeRefundId && isManager && ( diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index dca21d4..4563ce7 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -978,6 +978,7 @@ async function seed() { const invoiceStatus = rand() < 0.95 ? "paid" as const : "pending" as const; const paidAt = invoiceStatus === "paid" ? new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000) : null; + const stripePaymentIntentId = invoiceStatus === "paid" && rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null; invoiceBatch.push({ id: invoiceId, appointmentId: apptId, @@ -989,6 +990,7 @@ async function seed() { status: invoiceStatus, paymentMethod: invoiceStatus === "paid" ? pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check" : null, paidAt, + stripePaymentIntentId, notes: rand() < 0.05 ? "Added extra service at checkout" : null, }); @@ -1092,13 +1094,14 @@ async function seed() { const taxCents = Math.round(effectivePrice * 0.08); const totalCents = effectivePrice + taxCents + tipCents; const paidAt = new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000); + const stripePaymentIntentId = rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null; invoiceBatch.push({ id: invoiceId, appointmentId: apptId, clientId, subtotalCents: effectivePrice, taxCents, tipCents, totalCents, status: "paid" as const, paymentMethod: pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check", - paidAt, notes: null, + paidAt, stripePaymentIntentId, notes: null, }); lineItemBatch.push({ id: uuid(), invoiceId, description: svc.name, quantity: 1,