Architecture: Payment Delinquency Report (#50)

arch-payment-report Architecture

architecture active sprint:2

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.
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_links is stale — created at checkout time but never updated?
  • Does StripeClient ever reconcile local records against Stripe's actual payment status?
  • Should the report cross-check Stripe API (Stripe::PaymentIntent or Stripe::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.
  • arch-domain-westside-basketball — domain model
  • arch-dataflow-westside-basketball — data flow showing Stripe integration
  • Forgejo: westside-basketball #50
  • Forgejo: westside-basketball #19 (Admin payment dashboard — related long-term)