Phase 5 Xendit: Stages 1-7 (XENDIT_ENABLED=false; Stage 8 pending creds)
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>
This commit is contained in:
@@ -48,7 +48,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
await sql`DELETE FROM chat_request_notifications WHERE session_id IN (SELECT id FROM chat_sessions WHERE customer_id = ${id})`
|
||||
await sql`DELETE FROM customer_transactions WHERE customer_id = ${id}`
|
||||
await sql`DELETE FROM chat_sessions WHERE customer_id = ${id}`
|
||||
await sql`DELETE FROM payment_sessions WHERE customer_id = ${id}`
|
||||
await sql`DELETE FROM payment_requests WHERE customer_id = ${id}`
|
||||
await sql`DELETE FROM auth_sessions WHERE user_id = ${id} AND user_type = 'customer'`
|
||||
}
|
||||
await sql`DELETE FROM customers WHERE phone = ${phone}`
|
||||
@@ -65,7 +65,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
let target
|
||||
if (latest === true) {
|
||||
const [row] = await sql`
|
||||
SELECT id FROM payment_sessions
|
||||
SELECT id FROM payment_requests
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
@@ -78,7 +78,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return reply.code(400).send({ error: 'latest:true required in body' })
|
||||
}
|
||||
const [updated] = await sql`
|
||||
UPDATE payment_sessions
|
||||
UPDATE payment_requests
|
||||
SET status = 'confirmed', confirmed_at = NOW()
|
||||
WHERE id = ${target} AND status = 'pending'
|
||||
RETURNING id, customer_id, status, mode, duration_minutes, is_first_session_discount, targeted_mitra_id
|
||||
@@ -105,7 +105,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
let target
|
||||
if (latest === true) {
|
||||
const [row] = await sql`
|
||||
SELECT id FROM payment_sessions
|
||||
SELECT id FROM payment_requests
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
@@ -120,7 +120,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return reply.code(400).send({ error: 'payment_id or latest:true required in body' })
|
||||
}
|
||||
const [updated] = await sql`
|
||||
UPDATE payment_sessions
|
||||
UPDATE payment_requests
|
||||
SET status = 'expired', expires_at = NOW() - INTERVAL '1 minute'
|
||||
WHERE id = ${target} AND status = 'pending'
|
||||
RETURNING id, status
|
||||
@@ -168,7 +168,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
const [linked] = await sql`
|
||||
SELECT ps.targeted_mitra_id
|
||||
FROM chat_sessions cs
|
||||
LEFT JOIN payment_sessions ps ON ps.id = cs.payment_session_id
|
||||
LEFT JOIN payment_requests ps ON ps.id = cs.payment_request_id
|
||||
WHERE cs.id = ${target}
|
||||
LIMIT 1
|
||||
`
|
||||
@@ -268,7 +268,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return { ok: true, session_id: session.id, mitra_id: mitra.id, mitra_name: mitra.display_name }
|
||||
})
|
||||
|
||||
// Seed a payment_sessions row in `pending` status for the customer linked
|
||||
// Seed a payment_requests row in `pending` status for the customer linked
|
||||
// to `phone`, with expires_at safely in the future. Used by Maestro Stage
|
||||
// 10 flow (09_chat_tab.yaml) to populate the Pembayaran sub-tab without
|
||||
// walking the multi-screen S6 paywall → method → duration → method flow.
|
||||
@@ -297,7 +297,7 @@ export const internalTestRoutes = async (fastify) => {
|
||||
return reply.code(404).send({ error: 'no_customer_for_phone', phone })
|
||||
}
|
||||
const [row] = await sql`
|
||||
INSERT INTO payment_sessions (
|
||||
INSERT INTO payment_requests (
|
||||
customer_id, amount, duration_minutes, is_first_session_discount,
|
||||
is_extension, status, mode, expires_at
|
||||
) VALUES (
|
||||
|
||||
Reference in New Issue
Block a user