TransactionType.FIRST_SESSION_DISCOUNT ('first_session_discount', 22 chars) overflowed the VARCHAR(20) column, throwing in acceptPairingRequest AFTER the session was flipped to ACTIVE but before startSessionTimer/startSessionListener/PAIRED-notify ran. Every first-session-discount pairing thus half-completed: lost transaction row, no server-side timer, and a 500 to the mitra so its app never opened the chat. Widen the column (CREATE TABLE + idempotent ALTER). Deferred hardening (bookkeeping INSERT in the critical path) logged in TECH_DEBT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
6.0 KiB
Markdown
130 lines
6.0 KiB
Markdown
# Tech Debt
|
|
|
|
Running list of known shortcuts, deferred hardening, and "good enough for now"
|
|
decisions that need follow-up before they bite us in production.
|
|
|
|
Format: `[date]` short title, then enough context for someone (or future-you)
|
|
to act on it without re-deriving the discussion.
|
|
|
|
---
|
|
|
|
## Backend
|
|
|
|
### `[2026-06-01]` Bookkeeping INSERT sits in the pairing critical path
|
|
|
|
**File:** `backend/src/services/pairing.service.js` (`acceptPairingRequest`, ~line 506)
|
|
|
|
**What happened:** the `INSERT INTO customer_transactions` runs *after* the session
|
|
is flipped to `ACTIVE` but *before* `startSessionTimer`, `startSessionListener`,
|
|
the customer `PAIRED` WS notify, and the other-mitra dismiss fan-out. A
|
|
`varchar(20)` overflow on `type = 'first_session_discount'` (22 chars) threw
|
|
there, so every first-session-discount pairing half-completed: no transaction
|
|
row, no server-side timer, no PAIRED push (customer recovered via polling), and a
|
|
500 returned to the mitra so its app never opened the chat.
|
|
|
|
**Fixed now:** column widened to `VARCHAR(128)` (migrate.js), so the INSERT no
|
|
longer throws.
|
|
|
|
**Why it's still debt:** a *bookkeeping* write can still abort *critical* pairing
|
|
steps if it ever fails again (constraint change, DB hiccup, future longer enum).
|
|
Hardening: either move the `customer_transactions` INSERT to the end of
|
|
`acceptPairingRequest`, or wrap it in a `try/catch` that logs-but-doesn't-throw,
|
|
so transaction recording can never again half-complete a pairing. Same applies to
|
|
the equivalent INSERT in `extension.service.js`.
|
|
|
|
---
|
|
|
|
### `[2026-05-11]` Public `GET /api/public/bestie/available` needs rate limiting before prod
|
|
|
|
**File:** `backend/src/routes/public/public.bestie-availability.routes.js`
|
|
|
|
**Decision:** The endpoint was made unauthenticated by business requirement —
|
|
SHome1st renders before any JWT exists, and the CTA must reflect global mitra
|
|
availability so users see whether bestie is online before committing to
|
|
onboarding. Response is intentionally a single boolean (no count, no IDs).
|
|
|
|
**Why it's debt:** No auth + no rate limit. The 10s in-memory cache bounds DB
|
|
load, but a single attacker can still hammer the endpoint to:
|
|
- run sustained traffic against the public listener (DoS surface)
|
|
- scrape `available` over time to infer mitra online/offline patterns (weak
|
|
information leak — only "is anyone online", but still a signal)
|
|
|
|
**Mitigation before prod:**
|
|
- Per-IP rate limit (suggested: ~30 req/min/IP, headroom over the legitimate
|
|
5s client poll cadence = 12 req/min/IP).
|
|
- Implement via `@fastify/rate-limit` plugin so other public endpoints can
|
|
share the policy as we add them under `/api/public/*`.
|
|
- Verify Cloud Run / NLB preserves real client IP and that
|
|
`request.ip` reflects it (Fastify already has `trustProxy: true`).
|
|
|
|
**Not required:** auth, captcha, or removing the count from
|
|
`/api/client/mitra-availability` (that route stays authed for CC/debug).
|
|
|
|
---
|
|
|
|
## Client app
|
|
|
|
### `[2026-05-11]` Social-login (Google / Apple) has no entry point after S3a rewrite
|
|
|
|
**Files:** `client_app/lib/features/auth/screens/register_screen.dart` (no longer renders them); `client_app/lib/core/auth/auth_providers_provider.dart` (still wired).
|
|
|
|
**Decision:** `RegisterScreen` was rewritten to match Figma `S3Phone` 1:1
|
|
(step-dots + name greeting + privacy card + tanpa-verif ghost link). Figma
|
|
S3a shows no Google/Apple buttons, so they were removed from this screen.
|
|
|
|
**Why it's debt:** Google/Apple buttons used to render here when the
|
|
`authProvidersProvider` flags were enabled. Today both flags are `false`
|
|
(creds pending — see `Phase 3.4 Status` memory), so nothing visible is
|
|
missing. But the moment `/api/shared/auth-providers` flips either flag,
|
|
the buttons have nowhere to live.
|
|
|
|
**Fix-when-creds-arrive:**
|
|
- Decide where Google/Apple buttons belong (likely a dedicated login screen
|
|
reachable from the SHome1st "masuk →" banner), or whether to bring them
|
|
back to S3a as Figma-friendly tiles above the phone input.
|
|
- `loginGoogle` / `loginApple` on `authProvider` are still intact, so the
|
|
wiring is one button widget away.
|
|
|
|
### `[2026-05-12]` Stage 10 — Bestie Offline Popup variant not wired on BestieHistoryList
|
|
|
|
**File:** `client_app/lib/features/home/screens/bestie_history_list_screen.dart`
|
|
|
|
**Decision:** Stage 10 follow-up restored `BestieHistoryList` as a separate
|
|
picker screen (per mermaid §4) and made offline rows un-tappable (dimmed).
|
|
Mermaid §4 actually calls for a **Bestie Offline Popup (returning variant)**
|
|
to surface when the user picks an offline bestie — with options "cari bestie
|
|
lain" and "tanya admin".
|
|
|
|
**Why it's debt:** Today the offline row is just disabled. The user gets no
|
|
explicit prompt to redirect them into the blast flow or to contact admin.
|
|
|
|
**Fix:** wire `BestieOfflinePopup` with `variant='returning'` on offline-row
|
|
tap. The popup widget already exists from Stage 8 (Tanya Admin sheet ships
|
|
with the wiring); just needs to be triggered here.
|
|
|
|
### `[2026-05-12]` S5 ESP screen retired from spec — code still ships it
|
|
|
|
**Files:** `client_app/lib/features/onboarding/` (S5ESP screen + nav wiring);
|
|
`screens/onboarding.jsx::S5ESP` (Figma reference still in handoff); any
|
|
`espSelectionProvider` / `espSkippedProvider` Riverpod state.
|
|
|
|
**Decision:** Business removed the ESP multi-select step from the customer
|
|
flow on 2026-05-12. Both verified and anonymous branches now go from
|
|
`VerifChoiceSheet` straight to the `usp_seen?` gate. See
|
|
`requirement/flow_customer.mermaid.md` §2.
|
|
|
|
**Why it's debt:** Stage 2 (commit `2645bcd`) shipped the ESP screen and its
|
|
state providers. The screen is still reachable in the current build. The
|
|
mermaid spec is the source of truth — the code has drifted behind by one
|
|
business decision.
|
|
|
|
**Fix:**
|
|
- Delete the ESP screen widget and its route registration.
|
|
- Remove `espSelectionProvider` / `espSkippedProvider` and any nav step that
|
|
routes through ESP.
|
|
- Wire `VerifChoiceSheet → USPGate → (USP screen | skip → next)` directly.
|
|
- Drop the "ESP is decorative only" memory (it's now superseded by removal).
|
|
- Keep `screens/onboarding.jsx::S5ESP` in the Figma handoff folder — it's
|
|
history, not active design.
|
|
|