GRO-606: Add payment API endpoints (pay invoice, payment methods, refunds)
Portal routes (client-facing): - POST /api/portal/invoices/:id/pay - create PaymentIntent for single invoice - POST /api/portal/invoices/pay-multiple - create PaymentIntent for multiple invoices - GET /api/portal/payment-methods - list saved payment methods - POST /api/portal/payment-methods - create SetupIntent for saving new card - DELETE /api/portal/payment-methods/:id - detach payment method - GET /api/portal/config - return Stripe publishable key Admin routes: - POST /api/invoices/:id/refund - manager-only refund endpoint Validation: - Cannot pay draft, void, or already-paid invoices - Multi-invoice: all must belong to same client and be pending - Refund requires invoice to be paid with stripePaymentIntentId Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -338,3 +338,41 @@ invoicesRouter.patch(
|
||||
return c.json({ ...updated, lineItems });
|
||||
}
|
||||
);
|
||||
|
||||
// ─── Refund ───────────────────────────────────────────────────────────────────
|
||||
|
||||
import { processRefund } from "../services/payment.js";
|
||||
|
||||
const refundSchema = z.object({
|
||||
amountCents: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
invoicesRouter.post(
|
||||
"/:id/refund",
|
||||
zValidator("json", refundSchema),
|
||||
async (c) => {
|
||||
const db = getDb();
|
||||
const staff = c.get("staff");
|
||||
if (!staff) return c.json({ error: "Forbidden" }, 403);
|
||||
if (staff.role !== "manager" && !staff.isSuperUser) {
|
||||
return c.json({ error: "Manager role required" }, 403);
|
||||
}
|
||||
|
||||
const id = c.req.param("id");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const [invoice] = await db.select().from(invoices).where(eq(invoices.id, id));
|
||||
if (!invoice) return c.json({ error: "Not found" }, 404);
|
||||
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);
|
||||
}
|
||||
|
||||
const result = await processRefund(id, body.amountCents);
|
||||
if (!result) return c.json({ error: "Refund failed" }, 500);
|
||||
|
||||
return c.json({ refundId: result.refundId });
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user