import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest' vi.mock('../../src/plugins/websocket.js', () => ({ sendToUser: vi.fn(() => false), sendToSessionParticipant: vi.fn(() => false), registerWebSocketPlugin: vi.fn(async () => {}), registerWebSocketRoute: vi.fn(), isUserOnlineWs: vi.fn(() => false), getSessionConnections: vi.fn(() => ({})), })) vi.mock('../../src/services/notification.service.js', () => ({ sendPushNotification: vi.fn(async () => true), registerDeviceToken: vi.fn(async () => {}), })) const { buildPublic } = await import('../helpers/server.js') const { resetDb, resetAppConfig, db } = await import('../helpers/db.js') const { createCustomer } = await import('../helpers/fixtures.js') const { customerJwt, authHeader } = await import('../helpers/jwt.js') describe('GET /api/client/chat/pricing (Phase 4)', () => { let app let customer let token beforeAll(async () => { await resetAppConfig() app = await buildPublic() }) beforeEach(async () => { await resetDb() const phone = `+628${Math.floor(Math.random() * 1e10).toString().padStart(10, '0')}` customer = await createCustomer({ callName: 'PricingTester', phone }) token = customerJwt(customer.id) }) afterAll(async () => { await app?.close() }) it('returns chat + call tier groups and a discount block; eligibility flips when the customer has a completed session', async () => { const res = await app.inject({ method: 'GET', url: '/api/client/chat/pricing', headers: authHeader(token), }) expect(res.statusCode).toBe(200) const body = res.json() expect(body.success).toBe(true) const data = body.data // Two tier groups, both non-empty expect(Array.isArray(data.chat?.tiers)).toBe(true) expect(Array.isArray(data.call?.tiers)).toBe(true) expect(data.chat.tiers.length).toBeGreaterThan(0) expect(data.call.tiers.length).toBeGreaterThan(0) // Tier shape (chat 12-min should match the seed config) const chat12 = data.chat.tiers.find((t) => t.minutes === 12) expect(chat12).toBeDefined() expect(chat12.price_idr).toBe(12000) // Discount block — eligible (phone-verified + no completed sessions) expect(data.first_session_discount.eligible).toBe(true) expect(data.first_session_discount.actual_price_idr).toBe(2000) expect(data.first_session_discount.gimmick_price_idr).toBe(12000) expect(data.first_session_discount.duration_minutes).toBe(12) expect(data.first_session_discount.modes).toEqual(['chat']) // Insert a completed session — eligibility must flip. const sql = db() await sql` INSERT INTO chat_sessions (customer_id, status, duration_minutes, price) VALUES (${customer.id}, 'completed', 12, 12000) ` const after = await app.inject({ method: 'GET', url: '/api/client/chat/pricing', headers: authHeader(token), }) expect(after.statusCode).toBe(200) expect(after.json().data.first_session_discount.eligible).toBe(false) }) it('eligibility is false when phone is not set (anonymous customer)', async () => { const anon = await createCustomer({ callName: 'AnonCust', phone: null }) const anonToken = customerJwt(anon.id) const res = await app.inject({ method: 'GET', url: '/api/client/chat/pricing', headers: authHeader(anonToken), }) expect(res.statusCode).toBe(200) expect(res.json().data.first_session_discount.eligible).toBe(false) }) })