Skip to content

feat: record completed cash private-lesson sessions (#150) - #155

Open
Berny-ft wants to merge 2 commits into
developfrom
feature/coaching-cash-sessions
Open

Berny-ft wants to merge 2 commits into
developfrom
feature/coaching-cash-sessions

Conversation

@Berny-ft

@Berny-ft Berny-ft commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Closes #150

Coordinators can record a completed, cash-paid private lesson from its service, choosing a registered client and, for children's services, the child who attended. Admins can record sessions for any active private-lesson service; coordinators can only record their own.

  • Stripe is the cash-session record. An invoice paid out of band stores the service, client, child, session date, actual duration, cash collection date, amount, collector, recorder and any price-adjustment reason. New cash recordings do not write or delete local attendance rows.
  • History reads those invoices. Customer, coordinator and admin registration views merge paid cash sessions with existing online bookings. Older database-backed cash sessions remain visible, with duplicates removed by invoice ID.
  • Retries resume the same invoice. The browser preserves the exact submission across closing/reopening the dialog and reloading the tab. Uncertain attempts remain locked for a safe retry; changed or ambiguous receipts require administrator review. Stripe totals and customer balances are checked before confirming the recording.
  • One Stripe customer per client. Customer creation uses a stable request key and conditionally links an unassigned profile, so concurrent submissions use the same canonical customer.
  • Validation and isolation. Dates cannot be in the future, amounts and durations must be valid, adjusted prices require a reason, and children must belong to the selected client. Forms reset between services, and pending results cannot close another service's dialog.

Cash and online bookings remain separate. Phone-only clients, partial payments, a correction/void interface, and cash entries in the admin's card-transaction modal remain outside this PR. Cash invoice webhooks do not alter memberships.

Validation:

  • Jest: 204 passed, with the same two pre-existing badge-color failures in registrations-view.test.tsx. Coverage includes failures at each invoice step, overlapping attempts, lost responses, changed payloads, browser restore, ownership, legacy deduplication and concurrent customer creation.
  • ESLint: no errors; 20 existing warnings.
  • TypeScript: only the three existing errors in children-actions.test.ts, children-admin-actions.test.ts and services-data-table.test.tsx.
  • Recovery tests use mocked Stripe/database operations. No new cash receipt was created against the shared backend while validating this update.

Coordinators (or admins on their behalf) can record a private lesson that
already happened and was paid in cash, from the services table.

- New "Record cash session" dialog: pick a registered client (and child),
  date/time, duration and amount; a reason is required when the price is
  adjusted. Shows a success/error toast.
- Saves a completed session and records the payment in Stripe as an
  invoice paid out of band. The invoice id is stored in stripe_order_id
  (in_... = cash), so no schema change is needed.
- Submission id doubles as the session id and Stripe idempotency key, so
  retries never create a duplicate session or invoice; the session is
  removed if Stripe fails.
- Registrations show "Paid in cash"; webhook ignores cash invoices.
- Stripe: enable SDK retries and cap concurrent service lookups to avoid
  rate limits on /services.
- Fix stale coachingSessions imports in registrations (renamed to
  privateLessonSessions).
Copilot AI lite review requested due to automatic review settings September 26, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved authorization, validation, consistency, and registration-display issues remain.

Review effort: Lite
Findings: 1 High severity · 4 Medium severity

Open (5)
What changed in this PR

Adds support for recording completed private lessons paid in cash, including Stripe out-of-band invoices and registration indicators.

Changes:

  • Added cash-session UI, validation, authorization, and persistence.
  • Added Stripe invoice recording and webhook handling.
  • Updated registration views and test coverage.
File Reviewed changes
lib/​stripe.ts Stripe retries and cash invoice helpers
lib/​private-lessons.ts Cash-session detection
app/​api/​webhooks/​stripe/​route.ts Cash invoice webhook handling
app/​(authenticated)/​services/​services-table.tsx Recording dialog integration
app/​(authenticated)/​services/​services-data-table.tsx Cash-session action
app/​(authenticated)/​services/​registrations-dialog.tsx Cash payment indicator
app/​(authenticated)/​services/​record-cash-session-dialog.tsx Cash-session form
app/​(authenticated)/​services/​queries.ts Client and registration queries
app/​(authenticated)/​services/​cash-session-schema.ts Input validation
app/​(authenticated)/​services/​actions.ts Authorization and recording workflow
app/​(authenticated)/​registrations/​queries.ts Private-session queries
app/​(authenticated)/​registrations/​build-registrations.ts Registration view data
app/​(authenticated)/​registrations/​_components/​registrations-view.tsx Parent-facing cash indicator
__tests__/​registrations-build.test.ts Registration tests
__tests__/​record-cash-session.test.ts Action workflow tests
__tests__/​private-lessons.test.ts Cash detection tests
__tests__/​fixtures/​registrations.ts Test fixture updates
__tests__/​cash-invoice-stripe.test.ts Stripe and webhook tests

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +671 to +672
if (!(await authorizeCashSession(serviceId))) throw new Error("Forbidden");
return listCashSessionClients();
Comment thread app/(authenticated)/services/actions.ts Outdated
Comment on lines +721 to +726
if (service.isForChildren && !input.child_id) {
return { errors: { child_id: ["Select which child attended"] } };
}
if (input.child_id && !client.children.some((c) => c.id === input.child_id)) {
return { errors: { child_id: ["This child doesn't belong to the client"] } };
}
Comment thread app/(authenticated)/services/actions.ts Outdated
Comment on lines +774 to +775
const customerId = await getOrCreateStripeCustomer(client.id, client.email);
const invoiceId = await recordOutOfBandInvoice({
.transform((v) => new Date(v))
.refine((d) => !Number.isNaN(d.getTime()), "Invalid session date")
.refine(
(d) => d.getTime() <= Date.now() + FUTURE_SKEW_MS,
status: r.status,
createdAt: r.createdAt,
answers: r.childId ? (answersByChild.get(r.childId) ?? []) : [],
paidInCash: isCashSession(r.stripeOrderId ?? null),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants