// apps/hq/test/tickets.test.ts — D20 P1: ticket desk + branches + module service data. import { describe, it, expect } from 'vitest' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createClient } from '../src/repos-clients' import { createModule, assignModule, updateClientModule, setModulePassword, revealModulePassword, getClientModule } from '../src/repos-modules' import { createBranch, listBranches, updateBranch } from '../src/repos-branches' import { createTicket, updateTicket, listTickets, ticketCounts, ticketKinds } from '../src/repos-tickets' import { createStaff } from '../src/auth' const KEY = '11'.repeat(32) async function world() { const db = openDb(':memory:') await seedIfEmpty(db) const staff = await createStaff(db, { email: 't@t.in', displayName: 'Tibin', role: 'staff', password: 'password-1' }) const c = await createClient(db, 'u1', { name: 'Kothavara SCB', stateCode: '32' }) return { db, c, staffId: staff.id } } describe('client branches (D20)', () => { it('creates, lists (active first), edits and deactivates — audited', async () => { const { db, c } = await world() const b1 = await createBranch(db, 'u1', { clientId: c.id, name: 'HEAD OFFICE', code: '1' }) await createBranch(db, 'u1', { clientId: c.id, name: 'PALA BRANCH', code: '2' }) expect((await listBranches(db, c.id)).map((b) => b.name)).toEqual(['HEAD OFFICE', 'PALA BRANCH']) await updateBranch(db, 'u1', b1.id, { active: false }) const after = await listBranches(db, c.id) expect(after.find((b) => b.id === b1.id)!.active).toBe(false) expect(after[0]!.name).toBe('PALA BRANCH') // active sorts first const audits = (await db.get<{ n: number }>( `SELECT COUNT(*) AS n FROM audit_log WHERE entity='client_branch'`, ))!.n expect(audits).toBe(3) await expect(createBranch(db, 'u1', { clientId: c.id, name: ' ' })).rejects.toThrow(/name/i) }) }) describe('ticket desk (D20)', () => { it('walks open → assign → in_progress → closed (stamps closed_on), reopen clears it', async () => { const { db, c, staffId } = await world() const t = await createTicket(db, 'u1', { clientId: c.id, moduleCode: 'SMS', kind: 'MODIFICATION', description: 'Loan form change', }) expect(t.status).toBe('open') expect(t.closedOn).toBeNull() const assigned = await updateTicket(db, 'u1', t.id, { assignedTo: staffId, status: 'in_progress' }) expect(assigned.assignedName).toBe('Tibin') const closed = await updateTicket(db, 'u1', t.id, { status: 'closed' }) expect(closed.closedOn).not.toBeNull() const reopened = await updateTicket(db, 'u1', t.id, { status: 'open' }) expect(reopened.closedOn).toBeNull() await expect(updateTicket(db, 'u1', t.id, { status: 'bogus' as never })).rejects.toThrow(/status/i) }) it('validates client, branch ownership and active assignee', async () => { const { db, c, staffId } = await world() const other = await createClient(db, 'u1', { name: 'Other Bank', stateCode: '32' }) const foreign = await createBranch(db, 'u1', { clientId: other.id, name: 'HO' }) await expect(createTicket(db, 'u1', { clientId: 'nope' })).rejects.toThrow(/client/i) await expect(createTicket(db, 'u1', { clientId: c.id, branchId: foreign.id })).rejects.toThrow(/branch/i) await db.run(`UPDATE staff_user SET active=0 WHERE id=?`, staffId) await expect(createTicket(db, 'u1', { clientId: c.id, assignedTo: staffId })).rejects.toThrow(/inactive/i) }) it('lists with chips filters, mine filter, search and honest pagination; counts + kind suggestions', async () => { const { db, c, staffId } = await world() for (let i = 0; i < 55; i++) { await createTicket(db, 'u1', { clientId: c.id, kind: 'MODIFICATION', description: `bulk ${i}` }) } const mine = await createTicket(db, 'u1', { clientId: c.id, kind: 'SOFTWARE ADDONS', description: 'RTGS cert', assignedTo: staffId, }) await updateTicket(db, 'u1', mine.id, { status: 'waiting' }) const page1 = await listTickets(db, { page: 1, pageSize: 50 }) expect(page1.total).toBe(56) expect(page1.tickets).toHaveLength(50) const waiting = await listTickets(db, { status: 'waiting' }) expect(waiting.total).toBe(1) expect(waiting.tickets[0]!.id).toBe(mine.id) const assigned = await listTickets(db, { assignedTo: staffId }) expect(assigned.total).toBe(1) const search = await listTickets(db, { q: 'rtgs cert' }) expect(search.total).toBe(1) const counts = await ticketCounts(db) expect(counts['open']).toBe(55) expect(counts['waiting']).toBe(1) expect(await ticketKinds(db)).toEqual(['MODIFICATION', 'SOFTWARE ADDONS']) }) }) describe('client_module service data (D20)', () => { it('patches provider/username/details/remark; portal password encrypts, reveals audited, never in payloads', async () => { const { db, c } = await world() const m = await createModule(db, 'u1', { code: 'SMS', name: 'Bulk SMS' }) const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' }) const upd = await updateClientModule(db, 'u1', cm.id, { provider: 'BSNL', username: '20250043', remark: 'DEPO CR DR active', details: [{ label: 'SMS balance', value: '82,000' }, { label: 'Reseller', value: 'SiMS' }], }) expect(upd.provider).toBe('BSNL') expect(upd.details).toHaveLength(2) expect(upd.hasPassword).toBe(false) expect(JSON.stringify(upd)).not.toContain('password_enc') await expect(setModulePassword(db, 'u1', cm.id, 'rfUb9kW0', '')).rejects.toThrow(/HQ_SECRET_KEY/) const withPw = await setModulePassword(db, 'u1', cm.id, 'rfUb9kW0', KEY) expect(withPw.hasPassword).toBe(true) const stored = (await db.get<{ password_enc: string }>( `SELECT password_enc FROM client_module WHERE id=?`, cm.id, ))! expect(stored.password_enc).not.toContain('rfUb9kW0') // encrypted at rest expect(await revealModulePassword(db, 'u1', cm.id, KEY)).toBe('rfUb9kW0') const reveals = (await db.get<{ n: number }>( `SELECT COUNT(*) AS n FROM audit_log WHERE action='reveal_module_password'`, ))!.n expect(reveals).toBe(1) // Clearing with '' works and the audit payload never carried the plaintext. const cleared = await setModulePassword(db, 'u1', cm.id, '', KEY) expect(cleared.hasPassword).toBe(false) const auditBlob = (await db.all<{ after_json: string | null }>( `SELECT after_json FROM audit_log WHERE action='set_module_password'`, )).map((r) => r.after_json ?? '').join('') expect(auditBlob).not.toContain('rfUb9kW0') await expect(updateClientModule(db, 'u1', cm.id, { details: [{ label: '', value: 'x' }], })).rejects.toThrow(/label/i) }) })