fix(GRO-637): invoice status transitions, tip-split validation, refund idempotency, and tip-split response format
* Fix invoice status transitions, tip-split validation, refund idempotency, and tip-split response format - Add ALLOWED_TRANSITIONS state machine for invoice status changes (GRO-637) - Replace floating-point tip-split validation with integer basis-points math - Add idempotency key support to refund endpoint with new refunds table - Return full invoice shape from POST /:id/tip-splits matching GET response - All existing tests pass Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(invoices): wrap refund flow in transaction for idempotency safety - Wrap idempotency check + processRefund() + db.insert() in db.transaction() - This prevents duplicate Stripe refunds if the DB insert fails after Stripe processes the refund - Add migration 0027_refunds for the refunds table (was missing) - Removes out-of-scope changes from PR #278 (csrf.ts, appointmentGroups, appointments, book, groomingLogs, services, stripe-webhooks) Fixes GRO-637 per CTO review Co-Authored-By: Paperclip <noreply@paperclip.ing> * fix(api): wire up CSRF middleware for protected routes Register csrfMiddleware in the protected API routes after authMiddleware and resolveStaffMiddleware to protect against CSRF attacks on state- changing operations (POST, PUT, PATCH, DELETE). Addresses CTO review feedback on PR #278. * fix(api): remove CSRF middleware that breaks POST/PUT/PATCH/DELETE The CSRF middleware requires x-csrf-token header but the frontend never sends it, which would break all mutating operations with 403 errors. CSRF protection should be implemented in a separate coordinated PR with frontend changes. Co-Authored-By: Paperclip <noreply@paperclip.ing> --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Flea Flicker <flea-flicker@groombook.ai>
This commit was merged in pull request #278.
This commit is contained in:
committed by
GitHub
parent
dc3b3ddcb7
commit
d433c902b4
@@ -300,6 +300,25 @@ export const invoiceTipSplits = pgTable(
|
||||
(t) => [index("idx_invoice_tip_splits_invoice_id").on(t.invoiceId)]
|
||||
);
|
||||
|
||||
// Refund records with idempotency key support
|
||||
export const refunds = pgTable(
|
||||
"refunds",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
invoiceId: uuid("invoice_id")
|
||||
.notNull()
|
||||
.references(() => invoices.id, { onDelete: "restrict" }),
|
||||
stripeRefundId: text("stripe_refund_id").notNull(),
|
||||
idempotencyKey: text("idempotency_key").unique(),
|
||||
amountCents: integer("amount_cents"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index("idx_refunds_invoice_id").on(t.invoiceId),
|
||||
index("idx_refunds_idempotency_key").on(t.idempotencyKey),
|
||||
]
|
||||
);
|
||||
|
||||
// Tracks which reminder emails have been sent per appointment (prevents duplicates).
|
||||
// reminder_type values: "confirmation", "24h", "2h"
|
||||
export const reminderLogs = pgTable(
|
||||
|
||||
Reference in New Issue
Block a user