Replaces the two `pricing_*_tiers_json` blobs and five `first_session_discount_*`
keys in app_config with dedicated `pricing_tiers` and `pricing_promotions`
tables plus matching `_history` audit tables. UUID PKs, UNIQUE(mode, minutes)
natural-key constraint, optimistic-lock via `updated_at` token returning 409
STALE_WRITE on conflicts. Every mutation writes a history row capturing the
operator (changed_by from request.auth.userId) and change_kind.
CC SettingsPage replaces the JSON-textarea editors with per-row tables —
add / edit / soft-delete / reactivate / reorder, plus a buffered first-session
discount form with the same optimistic-lock contract. `minutes` and `mode` are
read-only on edit since they form the natural key; operators soft-delete and
recreate to change duration.
Stage 5 fixes a latent leak: `client.payment.routes.js` had its own local
`readDiscountConfig` that still read from app_config — would have silently
fallen to hardcoded defaults once the legacy rows were deleted. Now reads from
pricing_promotions via the shared service helper, so CC edits to the first-
session discount affect actual payment pricing on the next request.
Customer-facing GET /api/client/chat/pricing shape unchanged (id values are
now UUIDs instead of "5"/"12"/"60" but lookups happen by (mode, minutes), so
no app changes needed). 27 new backend tests, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chat-screen performance (customer + mitra):
- Parent screens have zero `ref.watch` — only `ref.listen` for side effects
- Body extracted into its own `ConsumerStatefulWidget`; AppBar parts split
into narrow `.select` consumers (mode, sensitivity, timer)
- Per-second timer ticks routed to dedicated providers
(`chatRemainingSecondsProvider` + new `mitraChatRemainingSecondsProvider`)
so WS `session_tick` frames don't invalidate the rest of the chat state
Dispose-in-ref bug fix:
- `home_screen.dart`, `payment_screen.dart`, `mitra_chat_screen.dart` —
ref-using cleanup moved from `dispose()` to `deactivate()`. Modern
Riverpod invalidates `ref` the moment `dispose()` runs; the resulting
silent error corrupts the widget-tree finalize and the next screen
appears frozen
- `halo_lints` package added at repo root with `no_ref_in_dispose` rule
to catch this pattern in CI / IDE analysis
- `custom_lint` activated in both apps' `analysis_options.yaml`
(was installed but never wired in — also brings `riverpod_lint`'s
`avoid_ref_inside_state_dispose` online)
- CLAUDE.md Pitfalls section added to client_app + mitra_app
Phase 4 §3 retryable blast-failure (Option A):
- Backend `expirePairingRequest` + all-rejected use
`recordIntermediateFailure` instead of `failPaymentSession` so the
payment session stays `confirmed` for re-blast
- WS `pairing_failed` payload carries `is_terminal: false` on the
retryable paths; client parses the flag and exposes `retryBlast()`
- "Coba cari lagi" CTA on S7 Timeout now re-blasts on the same payment
- Pairing service test updated to reflect the new semantics
Customer waiting-payment screen navigation patch:
- `_navigateTerminal` uses `Future.microtask` + `addPostFrameCallback`
redundancy after a release-mode bug where polling stopped but
`context.go` never fired, leaving the screen visually stuck on
"menunggu pembayaran"
See requirement/resume-2026-05-15.md for next-day pickup checklist
(mitra release rebuild + S21 Ultra install + retest is the gating item).
Bundles unrelated in-flight Phase 4 §2.x work that was already on disk
(ESP screen removal, USP one-time gate scaffolding, bestie-availability
public route, OTP service edits, Maestro flow tweaks) — kept together
to avoid a partial-rebase mess.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `customers.account_belongs_to UUID NULL` and refactors customer
sign-in (phone/Google/Apple) so an anon row that re-verifies into an
existing customer no longer 409s. Instead the anon row stays intact
with a breadcrumb pointing at the real customer; tokens are issued
for the existing user. Actual data reconciliation onto the existing
row (chat_sessions, customer_transactions, payment_sessions,
pairing_failures) is deferred.
Backend
- migrate.js: ADD COLUMN account_belongs_to UUID REFERENCES customers(id)
ON DELETE SET NULL.
- customer.service.js: stampAccountBelongsTo helper; account_belongs_to
exposed in CUSTOMER_SELECT.
- auth.service.js: new shared resolveCustomerForIdentity (4-case logic);
normalizeIdentityConflict + IDENTITY_ALREADY_LINKED 409 deleted;
completeCustomerPhoneSignIn / signInWithGoogle / signInWithApple all
route through the shared helper.
- client.auth.routes.js: new resolveAnonymousCustomerId picks the anon
prefix ONLY from a verified Bearer JWT — closes the UUID-leak attack
where a tamper-able body field could mis-route someone else's
transactions. /otp/verify, /google, /apple all use it; the body field
`anonymous_customer_id` is no longer accepted on any of them.
- test/services/auth.service.test.js: 9 Vitest cases covering phone +
Google + Apple, all 4 logic cases + multi-merge accumulation.
Customer app
- auth_notifier.dart::verifyOtp: drop `skipAuth: true` and the dead
body field so ApiClient auto-attaches the anon's Bearer from
AuthBridge. Survives the AuthOtpSentData state transition (the
earlier `_currentAnonymousCustomerId()` state-drop bug is bypassed by
sourcing the id from the bridge instead of state).
- Google + Apple client paths remain unchanged (gated on provider
creds; mirror this fix when wiring lands).
Docs
- flow_customer.mermaid.md: new §2.1 sub-section with the merge
diagram, schema note, replaces-current-behaviour paragraph, and
Bearer-only security callout.
- phase3.4-testing.md: §1.5 line 76 simplified (no more per-path
split); new §1.5.1 with the 5-step operator scenario + DB invariants
+ curl recipe + Vitest pointer; new §1.5.2 covering Google/Apple
parity (deferred client work flagged).
Verification (against live dev backend, before this commit):
- Vitest: 9/9 in auth.service.test.js; 49/51 overall (2 unrelated
pre-existing failures in session-timer.service.test.js).
- Operator Node smoke: 14/14 in the §1.5.1 scenario; 11/11 in the
Bearer-precedence cases.
- Real-device UI walkthrough on SM-A530F still pending — see resume
memory `project_phase4_2_1_resume_test`.
Sister WIP bundled in migrate.js + customer.service.js: `usp_seen`
column + `markCustomerUspSeen` helper (Phase 4 USP one-time gate, was
already uncommitted in the working tree).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend half of Stage 10 — the new Chat tab in the customer app that
replaces /chat/history with a 3-sub-tab list (Aktif / Pembayaran /
Selesai).
- New GET /api/client/payment-sessions/pending — returns the customer's
pending initial + extension payment sessions. Filter is status='pending'
AND expires_at > NOW(). Mitra info comes from session_extensions →
chat_sessions for extension rows, payment_sessions.targeted_mitra_id
for targeted-curhat-lagi initial rows. TTL reuses the existing
payment_session_timeout_minutes app_config row (default 20m) — no new
config row needed since payment is still mocked.
- getCustomerHistory migrated from offset (page/limit) to cursor
pagination. Cursor is base64url(`<endedAtIso>|<id>`) with id-tiebreak
in ORDER BY so rows with identical timestamps don't duplicate or skip
across pages. SELECT now JOINs payment_sessions to surface `mode`
(chat/call) for the Selesai-row voice-call pill.
- requirement/flow_customer.mermaid.md: new §7 Chat Tab subgraph + Figma
cross-ref entry for SChatList.
- requirement/phase4-customer-flow-plan.md: Stage 10 plan section. Also
carries forward earlier uncommitted "Post-Stage-8 corrections" notes
from the Stage 9 sweep (boot path / SHome1st / onboarding fixes).
Tests: +7 for getCustomerPendingPayments (initial null mitra,
targeted-mitra fill, extension-via-session JOIN, mixed-newest-first,
expired excluded, non-pending excluded, customer scoping). +10 for
cursor history (empty, exact-fit, multi-page walk, same-timestamp
tiebreak, limit clamp, customer scoping, CLOSING+COMPLETED only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>