Backend
- payment_sessions → payment_requests rename across DB schema + 29 files
- payment.service.js becomes product-agnostic owner: EventEmitter +
Xendit wrapper + requestPayment / confirmPayment public API; legacy
aliases retained for existing chat callers
- Webhook handler at POST /api/shared/payment/webhooks/xendit, with
constant-time token verification (8 vitest cases)
- Server-driven pairing: payment.service emits
payment_request.confirmed → pairing subscriber starts the blast.
Legacy POST /chat/request still works during the cutover.
- Reconciliation sweeper extended (re-emits events for confirmed rows
with no chat session)
- SIGTERM drain + startup reconciliation pass in server.js
Customer app
- waiting_payment_screen opens xendit_invoice_url via
LaunchMode.inAppBrowserView
- searching / no-bestie / targeted-waiting / pairing-notifier updated
to consume the new payment_request_id contract
- pending_payments_provider + bestie-unavailable dialog migrated
Dev / testing
- XENDIT_ENABLED=false is the safe default; .env.example documents the
four new vars
- backend/.dev/xendit-fake-webhook.sh exercises the handler without
ngrok
- 90/92 backend tests pass (two pre-existing session-timer flakes,
unrelated); client_app analyzer clean
- requirement/phase5-xendit-plan.md is the canonical reference
Stage 8 (live E2E) blocked on Xendit test-mode keys. The dashboard's
single-webhook-URL constraint will be worked around via a self-poll
script next session.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>