import { describe, it, expect, beforeAll, beforeEach } from 'vitest' import { getCustomerHistory } from '../../src/services/session.service.js' import { SessionStatus } from '../../src/constants.js' import { resetDb, resetAppConfig, db } from '../helpers/db.js' import { createCustomer, createMitra } from '../helpers/fixtures.js' // Phase 4 Stage 10 — Chat Tab Selesai feed uses cursor pagination. // Cursor is base64url(`|`); response is { items, next_cursor, has_more }. describe('session.service.getCustomerHistory (cursor paginated)', () => { let customer let mitra beforeAll(async () => { await resetAppConfig() }) beforeEach(async () => { await resetDb() customer = await createCustomer({ callName: 'Alice' }) mitra = await createMitra({ callName: 'kak Sari' }) }) // Seed N completed sessions ended at deterministically spaced times. // i=0 is the OLDEST, i=N-1 is the NEWEST. const seedCompleted = async (count) => { const sql = db() const now = Date.now() const ids = [] for (let i = 0; i < count; i++) { const endedAt = new Date(now - (count - i) * 60_000) // 1 min apart const [row] = await sql` INSERT INTO chat_sessions ( customer_id, mitra_id, status, duration_minutes, price, ended_at, created_at ) VALUES ( ${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 12, 5000, ${endedAt}, ${endedAt} ) RETURNING id ` ids.push(row.id) } return ids } it('returns empty + has_more=false when there is no history', async () => { const result = await getCustomerHistory(customer.id, { limit: 10 }) expect(result.items).toEqual([]) expect(result.has_more).toBe(false) expect(result.next_cursor).toBeNull() }) it('returns all items + has_more=false when count <= limit', async () => { await seedCompleted(5) const result = await getCustomerHistory(customer.id, { limit: 10 }) expect(result.items).toHaveLength(5) expect(result.has_more).toBe(false) expect(result.next_cursor).toBeNull() }) it('first page returns `limit` items, has_more=true, and a usable cursor', async () => { await seedCompleted(7) const page1 = await getCustomerHistory(customer.id, { limit: 3 }) expect(page1.items).toHaveLength(3) expect(page1.has_more).toBe(true) expect(page1.next_cursor).toBeTruthy() }) it('walks across pages without duplicates or skips', async () => { const seeded = await seedCompleted(7) // ids[0]=oldest .. ids[6]=newest const collected = [] let cursor = null do { const page = await getCustomerHistory(customer.id, { cursor, limit: 3 }) collected.push(...page.items.map((r) => r.id)) cursor = page.next_cursor if (!page.has_more) break } while (cursor) // Newest → oldest = reverse of seeded expect(collected).toEqual([...seeded].reverse()) // No duplicates expect(new Set(collected).size).toBe(collected.length) }) it('orders by ended_at DESC with id DESC tiebreak (no gaps for same-timestamp rows)', async () => { const sql = db() const sameTime = new Date() // Insert 3 rows with the EXACT same ended_at — only id distinguishes them. const ids = [] for (let i = 0; i < 3; i++) { const [row] = await sql` INSERT INTO chat_sessions ( customer_id, mitra_id, status, duration_minutes, price, ended_at, created_at ) VALUES ( ${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 12, 5000, ${sameTime}, ${sameTime} ) RETURNING id ` ids.push(row.id) } // Page through them 1 at a time. const collected = [] let cursor = null for (let i = 0; i < 5; i++) { const page = await getCustomerHistory(customer.id, { cursor, limit: 1 }) collected.push(...page.items.map((r) => r.id)) cursor = page.next_cursor if (!page.has_more) break } expect(collected).toHaveLength(3) expect(new Set(collected).size).toBe(3) }) it('clamps limit to 50 max', async () => { await seedCompleted(60) const result = await getCustomerHistory(customer.id, { limit: 999 }) expect(result.items.length).toBeLessThanOrEqual(50) }) it('scopes by customer — does not leak other customers history', async () => { const other = await createCustomer({ callName: 'Bob' }) const sql = db() await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, duration_minutes, price, ended_at) VALUES (${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 12, 5000, NOW()) ` await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, duration_minutes, price, ended_at) VALUES (${other.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 12, 5000, NOW()) ` const result = await getCustomerHistory(customer.id, { limit: 20 }) expect(result.items).toHaveLength(1) }) it('includes CLOSING status (grace period) alongside COMPLETED', async () => { const sql = db() await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, duration_minutes, price, ended_at) VALUES (${customer.id}, ${mitra.id}, ${SessionStatus.CLOSING}, 12, 5000, NOW()) ` await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, duration_minutes, price, ended_at) VALUES (${customer.id}, ${mitra.id}, ${SessionStatus.COMPLETED}, 12, 5000, NOW()) ` const result = await getCustomerHistory(customer.id, { limit: 20 }) expect(result.items).toHaveLength(2) }) it('excludes ACTIVE / PENDING_PAYMENT / EXTENDING', async () => { const sql = db() for (const status of [SessionStatus.ACTIVE, SessionStatus.PENDING_PAYMENT, SessionStatus.EXTENDING]) { await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, duration_minutes, price) VALUES (${customer.id}, ${mitra.id}, ${status}, 12, 5000) ` } const result = await getCustomerHistory(customer.id, { limit: 20 }) expect(result.items).toHaveLength(0) }) })