feat(GRO-607): Stripe Elements payment UI replacing mock flow
* GRO-605: Stripe SDK integration + payment service Co-Authored-By: Paperclip <noreply@paperclip.ing> * GRO-606: Add payment API endpoints (pay invoice, payment methods, refunds) Co-Authored-By: Paperclip <noreply@paperclip.ing> * feat(GRO-597): Stripe payment backend — schema, service, API, webhooks Consolidates GRO-605, GRO-606, GRO-608 into a single clean PR: - GRO-605: Stripe SDK integration + payment service - GRO-606: Payment API endpoints (pay invoice, payment methods, refunds) - GRO-608: Stripe webhook handler Migration consolidation: - Single 0026_stripe_payment.sql migration adds stripeCustomerId to clients and stripe_payment_intent_id, stripe_refund_id, payment_failure_reason to invoices - Removed duplicate 0027_stripe_identifiers.sql Co-Authored-By: Paperclip <noreply@paperclip.ing> * GRO-607: Install Stripe frontend packages Co-Authored-By: Paperclip <noreply@paperclip.ing> * GRO-607: Add /portal/config endpoint + rename date field Co-Authored-By: Paperclip <noreply@paperclip.ing> * GRO-607: Replace mock payment flow with real Stripe Elements Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-607): Stripe Elements payment UI - lint/type fixes Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-607): remove unused eslint-disable directive in CustomerPortal Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(GRO-607): CTO review fixes — payment security and correctness - Fix multi-invoice total calculation: use inArray() instead of eq() on single ID, sum all invoices not just first - Add ownership check to payment method deletion: verify the payment method belongs to the authenticated Stripe customer before detaching - Remove duplicate /config endpoint in portal.ts - Fix webhook Stripe client: use getStripeClient() from payment service instead of constructing with WEBHOOK_SECRET - Remove unnecessary body validator on /invoices/:id/pay route - Export getStripeClient() for use by stripe-webhooks.ts - Add inArray import to payment.ts Co-Authored-By: Paperclip <noreply@paperclip.ing> --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #275.
This commit is contained in:
committed by
GitHub
parent
4f6a1e8149
commit
c438f5772c
@@ -13,8 +13,9 @@ import {
|
||||
clients,
|
||||
sql,
|
||||
} from "@groombook/db";
|
||||
import type { AppEnv } from "../middleware/rbac.js";
|
||||
|
||||
export const invoicesRouter = new Hono();
|
||||
export const invoicesRouter = new Hono<AppEnv>();
|
||||
|
||||
const createInvoiceSchema = z.object({
|
||||
appointmentId: z.string().uuid().optional(),
|
||||
@@ -338,3 +339,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