Phase 4 Stage 10 backend: Chat-tab feeds (pending payments + cursor history)

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>
This commit is contained in:
2026-05-12 20:04:58 +08:00
parent 770f61074c
commit 350b92f1f3
8 changed files with 924 additions and 81 deletions

View File

@@ -201,12 +201,13 @@ export const clientChatRoutes = async (app) => {
return reply.send({ success: true, data: extension })
})
// Chat history
// Phase 4 Stage 10 — Chat Tab Selesai feed. Cursor-paginated; old `page`
// param removed. Response shape: { items, next_cursor, has_more }.
app.get('/history', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
const { page, limit } = request.query
const { cursor, limit } = request.query
const history = await getCustomerHistory(request.customer.id, {
page: page ? parseInt(page) : 1,
limit: limit ? parseInt(limit) : 20,
cursor: cursor ?? null,
limit: limit ? parseInt(limit, 10) : 20,
})
return reply.send({ success: true, data: history })
})

View File

@@ -5,6 +5,7 @@ import {
confirmPaymentSession,
abandonPaymentSession,
getPaymentSession,
getCustomerPendingPayments,
} from '../../services/payment.service.js'
import {
isCustomerEligibleForFirstSessionDiscount,
@@ -172,6 +173,13 @@ export const clientPaymentRoutes = async (app) => {
})
})
// Phase 4 Stage 10 — Chat Tab Pembayaran feed. Static path; registered
// before `/:id` so find-my-way matches this and not the wildcard.
app.get('/pending', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
const data = await getCustomerPendingPayments(request.customer.id)
return reply.send({ success: true, data })
})
app.get('/:id', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
const session = await getPaymentSession(request.params.id)
if (!session) {