Phase 4 Stage 8: returning-user shell + Tanya Admin sheet

Bestie Choice Sheet on home Mulai Curhat CTA. When the user has at
least one prior session (bestieHistoryHasItemsProvider hits the chat-
sessions history endpoint), the CTA opens a HaloBottomSheet with two
cards: 'bestie yang udah kenal' -> /chat/history, 'bestie baru' ->
/payment/entry. Empty history -> direct to /payment/entry.

Bestie history list visual upgrade: HaloOrb (mitraId seed) + name +
last-session date + topic pills + sessions count + ONLINE pill.
Backend getCustomerHistory now returns topics, mitra_is_online,
sessions_count in a single payload (no per-row presence round-trip).

BestieOfflinePopup with two variants (returning | new_) replacing the
legacy BestieUnavailableDialog. tanya admin ghost CTA on both variants
opens the new TanyaAdminSheet. Stage 5's targeted-wait declined stub
+ Stage 7's chat-screen 409 stub + searching-screen call site all
migrated to the real component.

TanyaAdminSheet: HaloBottomSheet with WA + Telegram buttons, deeplinks
fetched via supportHandlesProvider (CC-config-driven). url_launcher
added to client_app; ios LSApplicationQueriesSchemes covers
https/http/whatsapp/tg.

Stage 2's OTP-blocked popup hubungi admin SnackBar stub also migrated
to TanyaAdminSheet.

Dev-only POST /internal/_test/seed-history-session lets Maestro 08
flow seed a history row before exercising the choice sheet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 17:47:02 +08:00
parent d454fd39db
commit 862fc35a40
23 changed files with 1122 additions and 215 deletions

View File

@@ -154,4 +154,47 @@ export const internalTestRoutes = async (fastify) => {
_broadcastTimerResyncForTest(updated.id, updated.expires_at)
return { ok: true, session_id: updated.id, expires_at: updated.expires_at }
})
// Seed a completed chat_sessions row for the customer linked to `phone`,
// pairing them with the most-recent online mitra. Used by Maestro Stage 8
// flow (08_returning_targeted.yaml) so the bestie history list isn't empty.
//
// Body shape:
// { phone: '+62...' } — the customer; mitra is auto-picked.
fastify.post('/seed-history-session', async (request, reply) => {
const phone = request.body?.phone
if (!phone) {
return reply.code(400).send({ error: 'phone required in body' })
}
const [customer] = await sql`
SELECT id FROM customers WHERE phone = ${phone} LIMIT 1
`
if (!customer) {
return reply.code(404).send({ error: 'no_customer_for_phone', phone })
}
const [mitra] = await sql`
SELECT m.id, m.display_name FROM mitras m
INNER JOIN mitra_online_status s ON s.mitra_id = m.id
WHERE s.is_online = true
ORDER BY s.last_heartbeat_at DESC NULLS LAST
LIMIT 1
`
if (!mitra) {
return reply.code(404).send({ error: 'no_online_mitra' })
}
const [session] = await sql`
INSERT INTO chat_sessions (
customer_id, mitra_id, status, topic_sensitivity, topics,
created_at, paired_at, ended_at, duration_minutes, price
) VALUES (
${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 'regular',
${sql.array(['hubungan'])},
NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day',
NOW() - INTERVAL '1 day' + INTERVAL '15 minutes',
15, 30000
)
RETURNING id
`
return { ok: true, session_id: session.id, mitra_id: mitra.id, mitra_name: mitra.display_name }
})
}