import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest' // Mock the WS plugin (we assert on what extension.service tried to broadcast) // and the FCM notification service so tests don't try to reach external APIs. vi.mock('../../src/plugins/websocket.js', () => ({ sendToUser: vi.fn(() => false), sendToSessionParticipant: vi.fn(() => false), registerWebSocketPlugin: vi.fn(), registerWebSocketRoute: vi.fn(), isUserOnlineWs: vi.fn(() => true), getSessionConnections: vi.fn(() => ({})), })) vi.mock('../../src/services/notification.service.js', () => ({ sendPushNotification: vi.fn(async () => true), registerDeviceToken: vi.fn(async () => {}), })) const { sendToSessionParticipant } = await import('../../src/plugins/websocket.js') const { respondToExtension } = await import('../../src/services/extension.service.js') const { createPaymentSession, confirmPaymentSession } = await import('../../src/services/payment.service.js') const { WsMessage, SessionStatus, ExtensionStatus, } = await import('../../src/constants.js') const { db, resetDb, resetAppConfig } = await import('../helpers/db.js') const { createCustomer, createMitra } = await import('../helpers/fixtures.js') describe('extension.service — EXTENSION_RESPONSE payload', () => { let customer let mitra beforeAll(async () => { await resetAppConfig() }) beforeEach(async () => { await resetDb() customer = await createCustomer({ callName: 'ExtCust' }) mitra = await createMitra({ callName: 'ExtMitra', isOnline: true }) sendToSessionParticipant.mockClear() }) afterEach(() => { vi.clearAllMocks() }) it('accepted extension broadcasts EXTENSION_RESPONSE with the new expires_at', async () => { const sql = db() // Seed an active chat_sessions row whose timer is about to run out so the // extension push has a meaningful baseline to advance. const baseExpiresAt = new Date(Date.now() + 30_000) // 30s left const [session] = await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, expires_at, duration_minutes) VALUES (${customer.id}, ${mitra.id}, ${SessionStatus.ACTIVE}, ${baseExpiresAt}, 12) RETURNING id ` // A confirmed extension payment session (is_extension=true). const extPay = await createPaymentSession({ customerId: customer.id, durationMinutes: 10, amount: 9000, isExtension: true, }) await confirmPaymentSession(extPay.id, customer.id) // Pending extension row tied to that payment. const [extension] = await sql` INSERT INTO session_extensions ( session_id, requested_duration_minutes, requested_price, status, payment_request_id ) VALUES (${session.id}, 10, 9000, ${ExtensionStatus.PENDING}, ${extPay.id}) RETURNING id ` // Act await respondToExtension(extension.id, session.id, mitra.id, true) // Find the EXTENSION_RESPONSE call to the customer const respCalls = sendToSessionParticipant.mock.calls.filter( ([, , payload]) => payload?.type === WsMessage.EXTENSION_RESPONSE, ) expect(respCalls).toHaveLength(1) const payload = respCalls[0][2] expect(payload.accepted).toBe(true) expect(payload.duration_minutes).toBe(10) expect(payload.expires_at).toBeTruthy() // The new expires_at must be ahead of the seeded baseExpiresAt by ~10 min. const newExp = new Date(payload.expires_at).getTime() const baseMs = baseExpiresAt.getTime() const deltaMin = (newExp - baseMs) / 60_000 expect(deltaMin).toBeGreaterThan(9.5) expect(deltaMin).toBeLessThan(10.5) // DB should reflect the same shift. const [refreshed] = await sql`SELECT expires_at FROM chat_sessions WHERE id = ${session.id}` expect(new Date(refreshed.expires_at).getTime()).toBe(newExp) }) it('rejected extension broadcasts EXTENSION_RESPONSE without expires_at', async () => { const sql = db() const baseExpiresAt = new Date(Date.now() + 30_000) const [session] = await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status, expires_at, duration_minutes) VALUES (${customer.id}, ${mitra.id}, ${SessionStatus.ACTIVE}, ${baseExpiresAt}, 12) RETURNING id ` const extPay = await createPaymentSession({ customerId: customer.id, durationMinutes: 10, amount: 9000, isExtension: true, }) await confirmPaymentSession(extPay.id, customer.id) const [extension] = await sql` INSERT INTO session_extensions ( session_id, requested_duration_minutes, requested_price, status, payment_request_id ) VALUES (${session.id}, 10, 9000, ${ExtensionStatus.PENDING}, ${extPay.id}) RETURNING id ` await respondToExtension(extension.id, session.id, mitra.id, false) const respCalls = sendToSessionParticipant.mock.calls.filter( ([, , payload]) => payload?.type === WsMessage.EXTENSION_RESPONSE, ) expect(respCalls).toHaveLength(1) const payload = respCalls[0][2] expect(payload.accepted).toBe(false) // Rejected path does not extend the timer, so no expires_at is sent. expect(payload.expires_at).toBeUndefined() }) })