Billing & Payments
Tables: ledger_entries, stripe_webhook_events
ledger_entries
Light double-entry-style accounting so balances are explainable. A student’s balance is simply:
SUM(debits) - SUM(credits) -- per for_student_user_id
Key columns:
for_student_user_id— money is tracked per student, not per parent. A parent’s balance is the sum over their children.accounting_typeENUM(debit,credit) — charges are debits; payments, scholarships, and downward adjustments are credits.entry_typeENUM(registration,lessons,recital_fee,payment,scholarship_application,other) — what kind of line this is.amount_centsINT UNSIGNED— integer cents: exact math, and the same unit Stripe uses.semester_id— ties money to a term. It is what the parent portal groups a family’s balance by, and what “are they on schedule?” is judged against (half the term’s charges before it starts, the rest by its half-way lesson). Only entries that genuinely belong to no term — an old book fee, say — leave itNULL.ON DELETE SET NULL.stripe_checkout_session_id/stripe_payment_intent_id— set on entries recorded from Stripe.created_by_user_id—NULLfor webhook-recorded Stripe payments.
Idempotency
Recording money must survive being attempted twice — the Stripe webhook and the browser’s return trip can both try to record the same payment. Two unique keys guarantee only one wins:
- (
stripe_checkout_session_id,for_student_user_id) — for Checkout payments - (
stripe_payment_intent_id,for_student_user_id) — for embedded card-form payments, where there is a PaymentIntent but no Checkout Session
Charge posting is idempotent too: confirming a reservation posts the
registration / lessons / recital-fee debits at most once per student +
semester + entry type (Billing::postSemesterConfirmationCharges), and
un-confirming reverses them (reverseSemesterConfirmationCharges).
How entries are created
- Confirming a reservation posts the semester’s registration, lessons, and
recital-fee debits, priced from the
semestersrow. - Online payment — Stripe (Checkout for registration; Elements on the
parent Balance & Payments page). Recorded by
Billing::recordStripePaymentorrecordStripeIntentPayment. - Manual payment — an admin accepting a check or cash
(
Billing::recordManualPayment). - Scholarship — an admin-specified credit
(
Billing::applyScholarship). - Custom entry — an admin-specified debit or credit for anything else
(
Billing::addCustomEntry). - CSV import — opening balances when migrating an existing roster into the portal (semester wizard step 7); re-uploading the same file is a no-op.
stripe_webhook_events
A dedup ledger for incoming Stripe webhook deliveries: stripe_event_id is
unique, so an event id is processed at most once and webhook retries (and the
success-redirect fallback) are harmless. Also stores event_type, the raw
payload_json, and processed_at for debugging.
