Compare commits

..

12 Commits

Author SHA1 Message Date
Test User 628ed34d73 fix(GRO-876): wire up refund button in invoice detail modal
- Add stripePaymentIntentId and stripeRefundId to Invoice interface (types)
- Include both fields in the invoice list endpoint select (api)
- Show Refund button only when: status=paid, stripePaymentIntentId exists,
  stripeRefundId is null, and current user is manager
- Render "Refunded" badge on already-refunded invoices
- Full/partial refund dialog with amount input for partial

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 22:53:06 +00:00
Test User 9da0520eb6 fix(GRO-866): update infra submodule to include UAT persona env vars
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 22:51:36 +00:00
Test User c38d647c48 fix(GRO-866): add UAT persona env vars to reset-demo-data CronJob
Update infra submodule to include SEED_ADMIN_EMAIL, SEED_ADMIN_NAME,
SEED_UAT_GROOMER_EMAILS, and SEED_UAT_GROOMER_NAMES env vars in the
UAT reset-demo-data CronJob so Jordan, Sam, and Sarah SSO personas get
proper staff records after each hourly data reset.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 22:51:36 +00:00
the-dogfather-cto[bot] 7bf9cf9734 Merge pull request #359 from groombook/fix/gro-890-seed-stripe-payment-intent
fix(GRO-890): populate stripePaymentIntentId on paid seed invoices
2026-04-23 22:36:27 +00:00
groombook-engineer[bot] bf159f8b1f fix(GRO-890): populate stripePaymentIntentId on all paid seed invoices
All paid invoices created by the seed script now get a deterministic
stripePaymentIntentId of the form pi_test_seed_NNNNNN, unblocking the
refund button conditional in Invoices.tsx:514 during UAT.

Pending/draft invoices retain null as before.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 19:29:45 +00:00
the-dogfather-cto[bot] 2f3d4d8d01 fix(gro-609): refund button, stats 5xx, dashboard payment stats (#357)
fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
2026-04-23 14:01:41 +00:00
Test User db9bb31702 fix(gro-609): add payment stats to admin dashboard (AppointmentsPage)
- Fetch /api/invoices/stats/summary on mount and display Revenue/Outstanding/Refunds
  summary cards above the calendar view on /admin
- Mirrors the same stats section already on /admin/invoices
- Gracefully handles errors via try/catch on the stats endpoint

Parent: GRO-882
Grandparent: GRO-816

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 13:51:15 +00:00
Test User b38db65dde fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
- Add stripePaymentIntentId to the GET /invoices list query so the refund button
  renders when seed data includes a payment intent ID
- Wrap /api/invoices/stats/summary in try/catch so errors return 200 with zero
  defaults instead of 5xx, preventing the Invoices page from crashing on
  mount for groomer-role sessions

Parent: GRO-882
Grandparent: GRO-816

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 13:47:27 +00:00
scrubs-mcbarkley-ceo[bot] 3178f81b99 promote: uat → main (GRO-865 logo proxy mixed content fix)
All SDLC gates cleared. Logo proxy fix ships to production. cc @cpfarhood
2026-04-22 03:50:15 +00:00
scrubs-mcbarkley-ceo[bot] 544d65959d promote: dev → uat (GRO-867 + GRO-870 logo proxy fixes)
Promoting logo proxy fixes to UAT. All SDLC gates passed. cc @cpfarhood
2026-04-22 03:49:30 +00:00
lint-roller-qa[bot] f38bb244a4 Merge pull request #339 from groombook/dev
Promote dev → uat
2026-04-20 14:06:22 +00:00
the-dogfather-cto[bot] abee344ca4 Promote dev → uat: ARIA modal fix + tip split atomicity (#335)
* feat(GRO-785): validate tip split totals before marking invoice paid

- PATCH /invoices/:id returns 400 when tipCents > 0 but no tip splits
  exist or splits don't sum to 100%
- POST /invoices/:id/tip-splits now returns 400 (not 422) on validation
  failure via router-level ZodError handler

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(GRO-786): add ARIA label attributes to Modal dialog component

- Update Modal component to accept title and titleStyle props
- Add role="dialog", aria-modal="true", and aria-labelledby attributes
- Use useId() to generate stable ID for title heading association
- Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet,
  Log Grooming Visit, Permanently Delete Client) with title props
- Delete modal passes titleStyle for red color on warning

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-786): remove duplicate dialog role and restore focus trap

- Remove role="dialog" and aria-modal="true" from outer backdrop div
- Keep ARIA attributes only on inner dialog div (the actual modal)
- Restore useEffect focus management: auto-focus first element,
  Tab cycle wrapping, Escape key handler, focus restore on close

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): restore atomic tip split save in PATCH and fix error message

- When body.tipSplits is provided in PATCH /invoices/:id, validate sum
  first then atomically replace existing splits (delete + insert)
- When no incoming splits, validate existing DB splits with corrected
  message: "Tip splits are required when tip amount is greater than zero"
  (previously misleading "must sum to 100%" when no splits existed)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): address invoice tip split regression

- Use body.tipCents ?? current.tipCents for validation condition
  so that simultaneous status=paid + tipCents=0 skip split validation
- Use body.tipCents (now aliased as tipCents) instead of current.tipCents
  inside the atomic transaction for shareCents calculation
- Add explicit check for empty tipSplits array with appropriate error
  message ("Tip splits are required when tip amount is greater than zero")
  before the sum-to-100% check
- Destructure tipSplits out of body before spreading into update object
  to prevent it from leaking into the invoices table SET clause

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): wrap tip split save + invoice update in single transaction

Both tip split persistence (delete + insert) and the invoice PATCH update
are now inside one db.transaction() block. If the invoice update fails
after splits are written, the entire operation rolls back.

Also removed unnecessary eslint-disable comment on _tipSplits.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: the-dogfather-cto[bot] <269737991+the-dogfather-cto[bot]@users.noreply.github.com>
2026-04-17 22:58:00 +00:00
3 changed files with 40 additions and 2 deletions
+29
View File
@@ -291,6 +291,35 @@ const [showRefundDialog, setShowRefundDialog] = useState(false);
}
}
async function issueRefund() {
const amountCents = refundType === "partial"
? Math.round(parseFloat(refundAmount) * 100)
: undefined;
if (refundType === "partial" && (!amountCents || amountCents <= 0)) {
setError("Enter a valid refund amount");
return;
}
setSaving(true);
setError(null);
try {
const res = await fetch(`/api/invoices/${invoice.id}/refund`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(amountCents ? { amountCents } : {}),
});
if (!res.ok) {
const err = (await res.json()) as { error?: string };
throw new Error(err.error ?? `HTTP ${res.status}`);
}
setShowRefundDialog(false);
onUpdated();
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to issue refund");
} finally {
setSaving(false);
}
}
if (loading) return <Modal onClose={onClose}><p style={{ padding: "1rem" }}>Loading</p></Modal>;
const tipCentsCalc = Math.round(parseFloat(tipStr) * 100) || 0;
+1 -1
Submodule infra updated: b667a3f005...d9486dbfb1
+10 -1
View File
@@ -883,6 +883,7 @@ async function seed() {
let appointmentCount = 0;
let invoiceCount = 0;
let visitLogCount = 0;
let paidInvoiceCounter = 0;
// Process in batches per client to keep memory manageable
const apptBatchSize = 100;
@@ -977,6 +978,10 @@ 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;
paidInvoiceCounter++;
const stripePaymentIntentId = invoiceStatus === "paid"
? `pi_test_seed_${String(paidInvoiceCounter).padStart(6, "0")}`
: null;
invoiceBatch.push({
id: invoiceId,
@@ -989,6 +994,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 +1098,16 @@ 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);
paidInvoiceCounter++;
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: `pi_test_seed_${String(paidInvoiceCounter).padStart(6, "0")}`,
notes: null,
});
lineItemBatch.push({
id: uuid(), invoiceId, description: svc.name, quantity: 1,