V3 Vision¶
V3 ideas captured during the requirements session. None of these block V2; the V2 data model is shaped to accommodate them when they land. Cross-cuts: V2 scope (Could list).
C1. Client-facing magic-link portal¶
A separate, light client-side surface where a player — using only the email captured on their Player record — can:
- Receive a magic-link login (no password, no signup form).
- View their own stringing history: list of past orders, dates, racket, strings, total.
- Re-download any receipt PDF for an order they own.
- See a "your racket is ready" status when the stringer has marked it Strung.
- Update their notification preferences (email channel opt-in/out, future SMS).
Read-only on order data. The stringer remains the single writer.
C2. Notification dispatch (auto pickup-ready)¶
The data model for notifications is captured in V2 (see data model — Player notification_prefs, Stringer notification template, per-client overrides). What V3 adds is the actual send path:
- Trigger: auto-fire on Strung AND manual "notify" button — both available.
- Channels: email default; SMS only if free / near-free integration exists.
- Content: stringer-level template (editable), per-client override, includes the receipt.
- Per-client preferences: opt-in/out per channel, stored on the Player.
V2 already exposes the manual flow via the Strung event + emailed receipt (M14). V3 turns that into a configurable, automatable notification pipeline.
C3. Bulk historical-data import¶
Today (V2): a new stringer onboarded by Stefan starts with an empty slate. There is no import path on signup.
V3 adds bulk import for stringers arriving with their own history (XLSX / CSV / JSON). The data shape Stefan migrates from his own XLSX in V2 (M15) becomes the reference template — once the V2 migration tooling is stable, generalising it for arbitrary stringers is the V3 step.
C4. PWA / offline order entry¶
Online-only at the stringing table is acceptable for V2. V3 candidate: a Progressive Web App shell with offline order entry + deferred sync, useful for courtside data capture in spotty-connectivity venues.
Out of V2 and V3 — see V2 scope, Won't¶
Online payments, native mobile apps, and a partial-return / warranty workflow are explicitly out of both V2 and V3 scope.
V3 sign-off workflow & channel exploration — scoped, answers captured¶
Captured 2026-05-01 from Stefan; open questions answered the same day. Still deferred to the V3 milestone — not part of the 2026-04-27 V2 sign-off, and no V2 schema/UX ships from this section. But the design is no longer hand-wavy: the decisions below are the acceptance criteria V3 issues will reference.
V2 hooks to consider landing now¶
To avoid a V3 migration, the V2 schema/UX should reserve a few fields even if behavior is V3. Theo / Atlas to react:
- Player.
notification_channel_pref— already implied bynotification_prefs(email/SMS); explicitly name and document the channel set as extensible (email | sms | whatsapp | telegram | …) so adding a channel in V3 is data-only, not schema-change. - Player.
signoff_pref— client-side preference:require|skip(defaultrequireper Stefan's locked answer Q4 — "default every order, client can opt out". V2 ships the column withrequireeven though no V2 UI surfaces it; the OR-logic in V3 will then trigger sign-off for every player by default once C1 lights up). - Stringer.
signoff_default— stringer-level default:require|skip. Defaultrequirein V2 (matches Q4). - Stringer per-client
signoff_override— per-Player override of the stringer default. Could live as a column on Player owned by the stringer side (e.g.stringer_signoff_overridenullable). Distinct fromsignoff_prefwhich is the client's own preference. - Order.
signed_off_at+ Order.signoff_required(snapshot at order creation) — even if V2 never setssigned_off_at, having the columns reserved means V3 can light up the workflow without touching existing rows.
If these don't land in V2, V3 forces a schema migration on a live multi-stringer dataset. Cheap to add now, expensive to add later.
B1. Sign-off workflow (extends C1 + C2)¶
When a stringer enters an order, the client receives a notification (via their preferred channel — see B2) and signs off on the order: confirms racket, strings, tension, and price. Either party can opt out of the workflow.
Default = sign-off required for every order (Stefan, Q4). Either side can opt out; mutual opt-out is what suppresses sign-off. Because Player records exist per-stringer, the client opt-out is per-stringer by construction (the "trust this stringer" toggle is the same control).
Two settings, OR-logic:
| Setting | Owner | Where | Default | Meaning |
|---|---|---|---|---|
Player.signoff_pref |
Client | V3 client portal (C1) — "Require sign-off on my orders" toggle | require |
Client-expressed preference. Per-stringer (Player rows are per-stringer). |
Player.stringer_signoff_override |
Stringer | Stringer's per-client view in main app | nullable (= use stringer default) | Stringer's per-client override. |
Stringer.signoff_default |
Stringer | Stringer settings | require |
Falls through when override is null. |
Decision rule (load-bearing — keep this exact):
Sign-off is required iff
client_pref == require OR stringer_effective == require. Mutual opt-out is the only state that skips it.
Either side opting in wins. This lets cautious clients force sign-off even if their stringer wouldn't bother, and lets cautious stringers force sign-off even on clients who don't care.
Effective stringer setting = stringer_signoff_override if set on the Player row, else Stringer.signoff_default.
Sign-off artifact: client confirms in the portal (C1) → Order.signed_off_at is stamped.
Decisions locked 2026-05-01 (Stefan):
| # | Decision | Resolution |
|---|---|---|
| Q1 | Block effect | Advisory only. Sign-off does not block the Strung or Paid transitions. UI must clearly highlight unsigned-off orders so the stringer sees the state. |
| Q2 | Timeout | Never expires. If the stringer marks the order Strung anyway, status is Strung but an indicator on the order shows it was never signed off. |
| Q3 | Mutability after sign-off | Editable. Once the stringer confirms a new setup, the prior sign-off is revoked and re-triggered — client must sign off again on the changed order. |
| Q4 | Default scope | Sign-off required for every order by default. Either side can opt out (per-stringer for clients, since Player rows are per-stringer). |
| Q5 | What client sees | Racket, Strings, Tension, Price — same fields as the receipt. Client action is approve / reject (read-only on the line items). Reject path: notifies stringer, order parks pending stringer edit (which re-triggers sign-off per Q3). |
| Q6 | Default direction | Resolved by Q4 (default = require). The original Q6 phrasing (opt-in vs opt-out asymmetry) is dropped. |
| Q11 | Sign-off action surface | C1 portal only for the action itself. Notifications go via the client's preferred channel, but the sign-off click happens in the portal (deep-link). "Reply YES via SMS/WhatsApp" is nice-to-have within V3 if not too complex (Q10) — not a must. |
| Q12 | V3 launch backfill | Existing V2 orders are implicitly approved at V3 launch: signed_off_at = strung_at for all already-Strung orders. Forward-only sign-off from launch onwards for new/in-flight orders. |
B2. Channel exploration (extends C2)¶
Currently captured for V3:
- Email — primary, NFR-2 covers it (Resend free tier — 100/day, 3,000/month per keystone ADR-0005; DKIM/SPF/DMARC handled by Atlas).
- SMS — S1 in V2 scope; "only if cheap" — cost and provider still pending pricing call. S1 disposition (V2 if cheap, else V3) unchanged by this update.
V3 channel decisions (Stefan, 2026-05-01):
| Channel | Status | Notes |
|---|---|---|
| In V3 (baseline). | Resend per NFR-2 / keystone ADR-0005. Universal fallback target — see Q9 below. | |
| SMS | Still pending pricing call. | S1 from V2 should-list stays "V2 if cheap, else V3". Twilio / Swiss gateway cost per message at our volume not yet known. |
| Out of V3 scope. May revisit. | Stefan, Q7: cost of WhatsApp Business API not yet decided, "probably won't go that route for now". Struck from V3 channel candidates. | |
| Telegram | In V3. | Architecture (Q8): one application-wide bot by default; stringer can override and set their own bot for branding. Free Bot API; client must /start the bot once (deep-link from C1 portal). |
| Signal | Out. | No public Bot API; no programmatic send path. |
| Push notifications (PWA) | Tied to C4. | Defer until PWA path is decided. |
| In-app inbox (in C1 portal) | In V3 (alongside C1). | Always-available baseline; no external dependency. |
| iMessage / RCS | Out. | No open business API in CH. |
Cross-channel decisions:
| # | Decision | Resolution |
|---|---|---|
| Q9 | Channel fallback | On delivery failure of the preferred channel, fall back to email. Email is the universal target; failure of email itself is surfaced as an operational alert (Resend bounce/complaint webhooks per ADR-0005). |
| Q10 | Reply YES via SMS / WhatsApp for sign-off | Nice-to-have within V3, not a must. Build only if cheap; otherwise the C1 portal deep-link is the sole sign-off action surface (per B1 / Q11). |
| Q12-arch | Inbound replies / webhook receiver | Outbound only — no inbound webhook receiver in V3. Significantly simplifies architecture (no per-channel inbound parsing, no provider webhook surface to operate). Theo flagged this; recorded here as a locked decision. Q10's "reply YES" — if pursued — must be implementable without a general inbound receiver, or it falls out of scope. |
Open questions¶
All 12 open questions from the 2026-05-01 brainstorm are resolved (see B1 and B2 above). No outstanding asks for Stefan in this scope. New asks that surface during V3 design go through normal change-control.