Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js and pairing-failure.service.js (new); rewritten pairing.service.js (payment-gated blast + targeted "Curhat lagi" + cancel + fallback); rewritten extension.service.js (data-driven auto-approve with offline safeguard, charge-at-approval); pricing.service.js (extension tiers without free trial); mitra-status.service.js (countAvailableMitras cached path); 60s sweeper for stale payment sessions - Backend routes: client.payment.routes, client.mitra-availability.routes, internal/failed-pairings.routes; client.chat.routes rewritten for payment-gated start + /returning + /cancel + /fallback-to-blast; internal/config.routes adds 4 new keys with Valkey invalidate publish - client_app: mitra-availability poll, payment screen + notifier, pairing notifier rewrite (PairingTargetedWaiting + PairingFailed states), targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi" CTA, failed-pairing terminal, extension via payment-session - mitra_app: PairingRequestType enum, returning-chat 20s countdown auto-dismiss, extension card "otomatis disetujui" copy - control_center: 4 new config rows in Settings, Failed Pairings page (filter + paginate + action menu), sidebar + route registered - Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4 pass), Maestro mobile scaffold (CLI install pending) - Bugs found via Playwright + fixed: LoginPage labels not associated with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE in allow-methods (silent settings breakage in browsers since Stage 4) - Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A), phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md (today's run results) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,13 @@ const migrate = async () => {
|
||||
ON chat_sessions (status)
|
||||
`
|
||||
|
||||
// Composite index for the per-mitra active-session count subquery used by the
|
||||
// 5s availability poll and the per-blast capacity filter.
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_mitra_status
|
||||
ON chat_sessions (mitra_id, status)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS chat_request_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -413,6 +420,135 @@ const migrate = async () => {
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
// --- Phase 3.7: Paid Pairing Flow + Returning-Chat + Extension Flip ---
|
||||
|
||||
// payment_sessions: customer-initiated payment intents (mocked) that gate pairing
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_id UUID NOT NULL REFERENCES customers(id),
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
duration_minutes INTEGER NOT NULL,
|
||||
is_free_trial BOOLEAN NOT NULL DEFAULT false,
|
||||
is_extension BOOLEAN NOT NULL DEFAULT false,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending','confirmed','consumed','failed_pairing','abandoned','expired')),
|
||||
targeted_mitra_id UUID REFERENCES mitras(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
confirmed_at TIMESTAMPTZ,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_sessions_customer
|
||||
ON payment_sessions (customer_id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_sessions_status_expires
|
||||
ON payment_sessions (status, expires_at)
|
||||
`
|
||||
|
||||
// pairing_failures: cause-tagged audit rows for confirmed payments that did not yield a chat
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS pairing_failures (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
payment_session_id UUID NOT NULL REFERENCES payment_sessions(id) ON DELETE CASCADE,
|
||||
customer_id UUID NOT NULL REFERENCES customers(id),
|
||||
targeted_mitra_id UUID REFERENCES mitras(id),
|
||||
cause_tag TEXT NOT NULL
|
||||
CHECK (cause_tag IN (
|
||||
'no_mitra_available',
|
||||
'all_mitras_rejected',
|
||||
'targeted_mitra_offline',
|
||||
'targeted_mitra_rejected',
|
||||
'targeted_mitra_timeout',
|
||||
'payment_session_expired',
|
||||
'customer_cancelled'
|
||||
)),
|
||||
amount INTEGER NOT NULL,
|
||||
operator_action TEXT
|
||||
CHECK (operator_action IS NULL OR operator_action IN ('refunded','credited','no_action')),
|
||||
actioned_by UUID REFERENCES control_center_users(id),
|
||||
actioned_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`
|
||||
|
||||
// Phase 3.7 follow-up: extend the pairing_failures.cause_tag CHECK to include the two
|
||||
// extension-specific tags. Idempotent: drop the existing check (whatever its exact name) and
|
||||
// re-add the expanded list. Postgres auto-names CHECK constraints `<table>_<column>_check`
|
||||
// unless we name them explicitly; the original DDL above relies on that default.
|
||||
await sql`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conrelid = 'pairing_failures'::regclass
|
||||
AND conname = 'pairing_failures_cause_tag_check'
|
||||
) THEN
|
||||
ALTER TABLE pairing_failures DROP CONSTRAINT pairing_failures_cause_tag_check;
|
||||
END IF;
|
||||
ALTER TABLE pairing_failures ADD CONSTRAINT pairing_failures_cause_tag_check
|
||||
CHECK (cause_tag IN (
|
||||
'no_mitra_available',
|
||||
'all_mitras_rejected',
|
||||
'targeted_mitra_offline',
|
||||
'targeted_mitra_rejected',
|
||||
'targeted_mitra_timeout',
|
||||
'payment_session_expired',
|
||||
'customer_cancelled',
|
||||
'extension_rejected',
|
||||
'extension_safeguard_tripped'
|
||||
));
|
||||
END
|
||||
$$
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pairing_failures_created_at
|
||||
ON pairing_failures (created_at DESC)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pairing_failures_cause
|
||||
ON pairing_failures (cause_tag)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_pairing_failures_unactioned
|
||||
ON pairing_failures (created_at DESC) WHERE operator_action IS NULL
|
||||
`
|
||||
|
||||
// chat_sessions FK to payment_sessions (nullable for backward compat with pre-3.7 rows)
|
||||
await sql`
|
||||
ALTER TABLE chat_sessions
|
||||
ADD COLUMN IF NOT EXISTS payment_session_id UUID REFERENCES payment_sessions(id)
|
||||
`
|
||||
|
||||
await sql`
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_payment
|
||||
ON chat_sessions (payment_session_id)
|
||||
`
|
||||
|
||||
// session_extensions FK to payment_sessions (extensions also have their own payment session)
|
||||
await sql`
|
||||
ALTER TABLE session_extensions
|
||||
ADD COLUMN IF NOT EXISTS payment_session_id UUID REFERENCES payment_sessions(id)
|
||||
`
|
||||
|
||||
// Phase 3.7 config keys (idempotent — existing dev DBs need a manual update for extension_timeout_seconds → 10)
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value) VALUES
|
||||
('payment_session_timeout_minutes', '{"value": 20}'),
|
||||
('returning_chat_confirmation_timeout_seconds', '{"value": 20}'),
|
||||
('extension_default_action_on_timeout', '{"value": "auto_approve"}'),
|
||||
('pairing_blast_timeout_seconds', '{"value": 60}')
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`
|
||||
|
||||
console.log('Migration complete.')
|
||||
await sql.end()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user