Architecture: Payment Delinquency Report (#50)
Architecture: Payment Delinquency Report (#50)
Purpose
Give Marcus an actionable list of players behind on payment — player name, parent contact info (email + phone), amount owed — so he can follow up directly. Time-sensitive: Marcus needs this for invoice sends.
What Already Exists
| Component | What it does | Location |
|---|---|---|
| <code>/admin/payments</code> dashboard | Shows payment summary grouped by team — total_owed vs total_paid per player | <code>app/controllers/admin/payments_controller.rb</code> |
| <code>StripeClient#db_payment_summary</code> | Queries <code>payment_links</code> table, aggregates by team | <code>app/services/stripe_client.rb</code> |
| <code>payment_links</code> table | Local DB records: <code>status</code> (active/paid/canceled), <code>paid_at</code>, <code>amount_cents</code>, player FK | Database |
Open Questions
1. Is the existing dashboard enough?
The
/admin/payments page already shows total_owed vs total_paid per player by team. If the gap between what Marcus needs and what exists is just "filter to delinquent only + add parent contact info + CSV export," that's a small delta on the existing page — not a new report. The implementation should check what the current dashboard actually renders before building something new.2. Is payment_links in sync with Stripe?
This is the critical data integrity question. The ticket queries
payment_links where status = 'active' AND paid_at IS NULL. But:- What if a parent paid via Stripe and the webhook didn't fire (or failed)?
- What if
payment_linksis stale — created at checkout time but never updated? - Does
StripeClientever reconcile local records against Stripe's actual payment status? - Should the report cross-check Stripe API (
Stripe::PaymentIntentorStripe::Checkout::Session) before declaring someone delinquent?
If the local DB is authoritative (webhooks reliably update
paid_at), querying locally is fine. If not, the report could produce false positives — telling Marcus someone hasn't paid when they actually have. That's worse than no report.3. What does Marcus actually need?
The ticket assumes a one-time CSV. But Marcus's real need might be:
- Contact info — parent name, email, phone (for text/call follow-up)
- Monthly status — not just "owes money" but which months are behind, payment history
- Per-player breakdown — which payment_links are unpaid, what they're for
- Recurring view — if he needs this regularly, a dashboard filter beats a rake task
Worth asking Marcus directly: "What do you open when you want to follow up on payments, and what's missing?"
Data Model
Ticket's Definition of "Behind"
payment_links where status = 'active' AND paid_at IS NULL. Amount owed = SUM(amount_cents) on those rows. This is a local-DB-only query — no Stripe API call.Implementation Approach
Two paths depending on what the existing dashboard shows:
- If dashboard already has the data: Add a "delinquent only" filter + parent contact columns + CSV export button. Small delta.
- If dashboard is too different: Rake task (
lib/tasks/payments.rake) that queries payment_links + players + parents, outputs CSV to stdout. Fast to build, Marcus runs it or Lucas runs it for him.
Key Decisions
- Report, not automated collection — generates a list for Marcus to act on manually. No automated reminders or Stripe actions.
- Phone number required — Marcus follows up via text/call, not just email.
- No dependency on #47 or #48 — pure query + formatting, independent of email infrastructure.
- Stripe sync question must be answered before trusting results — false positives (saying someone owes when they've paid) erode trust.
Related
arch-domain-westside-basketball— domain modelarch-dataflow-westside-basketball— data flow showing Stripe integration- Forgejo: westside-basketball #50
- Forgejo: westside-basketball #19 (Admin payment dashboard — related long-term)