You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
6.7 KiB
TypeScript
130 lines
6.7 KiB
TypeScript
// 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)
|
|
})
|
|
})
|