fix(GRO-818): refund button for all paid invoices, inline cardLast4, manual refund for non-Stripe
- Backend refund endpoint: allow refunds on paid invoices without stripePaymentIntentId (manual refund path) - Backend GET /invoices/🆔 inline fetch cardLast4 + paymentStatus from Stripe when stripePaymentIntentId present - Frontend: show Refund button on all paid invoices for managers (not just Stripe-backed ones) - Seed: add stripePaymentIntentId (pi_test_*) to ~20% of paid invoices for Stripe-path testing cc @cpfarhood
This commit is contained in:
@@ -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 });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user