{"id":394,"title":"Data Flow: Westside Basketball","slug":"arch-dataflow-westside-basketball","html_content":"<h3 id=\"data-flow\">Data Flow</h3>\n<p>How information moves through the system at runtime. Answers: <strong>what happens when?</strong></p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n    participant Parent\n    participant Stripe\n    participant API as basketball-api\n    participant DB as Postgres\n    participant Outbox as Outbox Table\n    participant Worker as Outbox Worker\n    participant Email as Gmail SDK\n    participant GM as GroupMe\n    participant Admin as Admin / Cron\n    participant Coach as Coach View\n\n    Note over Parent,Coach: Registration Flow\n    Parent-&gt;&gt;Stripe: Pay $30 (QR code or link)\n    Stripe-&gt;&gt;API: checkout.session.completed\n    API-&gt;&gt;DB: Create Parent + Player + Registration (paid)\n    API-&gt;&gt;Email: Send confirmation email with token link\n    Email-&gt;&gt;Parent: \"Complete your registration\"\n\n    Note over Parent,Coach: Waiver Flow\n    Parent-&gt;&gt;API: GET /register?token=xxx\n    API-&gt;&gt;Parent: Registration form (pre-filled)\n    Parent-&gt;&gt;API: POST /register (waiver + details)\n    API-&gt;&gt;DB: Update parent.waiver_signed, player details\n\n    Note over Parent,Coach: Team Placement Flow\n    Admin-&gt;&gt;API: POST /api/teams/{id}/players (assign player)\n    API-&gt;&gt;DB: Set player.team_id\n\n    Note over Parent,Coach: Contract Signing → Outbox → Welcome Email\n    Parent-&gt;&gt;API: POST /api/players/{id}/contract (sign)\n    API-&gt;&gt;DB: SET contract_status='signed' (same transaction)\n    API-&gt;&gt;Outbox: INSERT event_type='contract_signed' status='pending' (same transaction)\n    API-&gt;&gt;Parent: 200 OK (contract recorded)\n\n    Note over Admin,Email: Outbox Worker (cron every 30s or admin trigger)\n    Admin-&gt;&gt;API: POST /api/admin/process-outbox\n    API-&gt;&gt;Outbox: SELECT * WHERE status='pending'\n    Outbox-&gt;&gt;Worker: pending events\n    Worker-&gt;&gt;DB: Lookup team.groupme_share_url + parent.email\n    Worker-&gt;&gt;Email: Send welcome email with GroupMe share link\n    Email-&gt;&gt;Parent: \"Welcome to [team]! Join our GroupMe: [link]\"\n    Worker-&gt;&gt;Outbox: UPDATE status='processed'\n    Parent-&gt;&gt;GM: Clicks share link → self-joins team group\n\n    Note over Parent,Coach: Tryout Day Flow\n    Admin-&gt;&gt;API: GET /api/roster/{tenant}\n    API-&gt;&gt;DB: Query players + parents + registrations\n    DB-&gt;&gt;API: Player roster with status\n    API-&gt;&gt;Admin: JSON roster\n</code></pre>\n<h4 id=\"key-flows\">Key flows</h4>\n<ul>\n<li><strong>Registration</strong>: Stripe payment → webhook → DB record → email with token → parent fills form → waiver signed. Two entry points to parent creation (Stripe webhook vs form submission) — must converge on same record.</li>\n<li><strong>Tryout day</strong>: Admin checks players in via dashboard → coaches see live roster on phones. All mutations go through basketball-api JSON endpoints.</li>\n<li><strong>Team placement</strong>: Admin assigns players to teams via draft board. Sets <code>player.team_id</code>.</li>\n<li><strong>Contract signing → GroupMe join</strong>: Parent signs contract → <code>contract_status</code> flips to <code>signed</code> → basketball-api looks up <code>team.groupme_share_url</code> → sends welcome email via pal-e-mail with GroupMe share link → parent clicks link and self-joins the team group. No API member-add — parent self-joins. GroupMe API is source of truth for membership.</li>\n<li><strong>Email</strong>: Sent via pal-e-mail service (MJML templates, Gmail OAuth). Tenant account: westsidebasketball@gmail.com.</li>\n<li><strong>GroupMe admin ops</strong>: Group creation and announcements handled via groupme-mcp tools in agent sessions. Membership audits query GroupMe API on demand — no local replication.</li>\n</ul>\n<h4 id=\"known-bug-2026-03-13\">Known bug (2026-03-13)</h4>\n<p>The registration form looks up parents by email (line 796 of register.py). If a parent uses a different email than Stripe checkout, a <strong>new parent record</strong> is created. The waiver lands on the new parent, not the paid one. Fix: match by token, not email.</p>\n<h4 id=\"related\">Related</h4>\n<ul>\n<li><a href=\"/notes/project-westside-basketball\">Project: Westside Basketball</a></li>\n<li><a href=\"/notes/arch-domain-westside-basketball\">Domain Model</a></li>\n<li><a href=\"/notes/arch-deployment-westside-basketball\">Deployment</a></li>\n</ul>","is_public":true,"note_type":"architecture","status":null,"parent_slug":null,"position":null,"created_by_sub":null,"created_by_name":null,"updated_by_sub":null,"updated_by_name":null,"project":{"id":5,"name":"Westside Basketball","slug":"westside-basketball","platform":"github","repo_url":"https://github.com/ldraney/west-side-basketball","is_public":true,"page_note":null,"created_at":"2026-02-26T02:00:39","updated_at":"2026-03-15T18:39:02.426531"},"tags":[{"id":1,"name":"architecture"}],"created_at":"2026-03-14T03:59:35.033246","updated_at":"2026-03-24T07:58:50.134077"}