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.
sims-hq/apps/hq/test/import-routes.test.ts

79 lines
4.4 KiB
TypeScript

// apps/hq/test/import-routes.test.ts — go-live cluster WS-B: APEX import over HTTP (D18)
import express from 'express'
import { describe, it, expect, afterAll } from 'vitest'
import { openDb } from '../src/db'
import { seedIfEmpty } from '../src/seed'
import { createStaff } from '../src/auth'
import { apiRouter } from '../src/api'
const CLIENTS_CSV = 'code,name,gstin,state_code,address,phone,email,status\nAPX001,Acme Bank,,32,Kochi,,r@acme.in,active\n'
const BAD_CLIENTS_CSV = 'code,name,gstin,state_code,address,phone,email,status\n,No Code Bank,,32,,,,active\n'
// Amounts are RUPEES in the APEX export; staging converts via fromRupees.
// Current-FY doc date so the INVOICE series seeds past the legacy number.
const INVOICES_CSV = 'client_code,doc_no,doc_date,taxable,tax,total,paid\nAPX001,INV/26-27-0042,2026-07-01,1000,180,1180,1\n'
function appWith() {
const db = openDb(':memory:'); seedIfEmpty(db)
createStaff(db, { email: 'owner@test.in', displayName: 'O', role: 'owner', password: 'owner-password' })
createStaff(db, { email: 'staff@test.in', displayName: 'S', role: 'staff', password: 'staff-password' })
const app = express(); app.use(express.json({ limit: '5mb' })); app.locals['db'] = db
app.use('/api', apiRouter(db))
const server = app.listen(0)
const baseUrl = `http://localhost:${(server.address() as { port: number }).port}/api`
return { db, server, baseUrl }
}
const login = async (baseUrl: string, email: string, password: string) =>
((await (await fetch(`${baseUrl}/auth/login`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email, password }),
})).json()) as { token: string }).token
const H = (t: string) => ({ 'content-type': 'application/json', authorization: `Bearer ${t}` })
describe('APEX import routes (owner-only)', () => {
const servers: { close: () => void }[] = []
afterAll(() => { for (const s of servers) s.close() })
it('staff get 403 on every import route', async () => {
const ctx = appWith(); servers.push(ctx.server)
const t = await login(ctx.baseUrl, 'staff@test.in', 'staff-password')
for (const [method, path] of [['POST', '/import/stage'], ['GET', '/import/status'], ['POST', '/import/commit']] as const) {
const res = await fetch(`${ctx.baseUrl}${path}`, { method, headers: H(t), ...(method === 'POST' ? { body: '{}' } : {}) })
expect(res.status).toBe(403)
}
})
it('stage → status → commit round-trip; problems lock the commit until re-staged clean', async () => {
const ctx = appWith(); servers.push(ctx.server)
const t = await login(ctx.baseUrl, 'owner@test.in', 'owner-password')
// Stage a BAD clients file → problems reported, commit refused.
const bad = await fetch(`${ctx.baseUrl}/import/stage`, {
method: 'POST', headers: H(t), body: JSON.stringify({ clientsCsv: BAD_CLIENTS_CSV }),
})
const badJson = await bad.json() as { report: { clients: { problems: number } } }
expect(bad.status).toBe(200)
expect(badJson.report.clients.problems).toBeGreaterThan(0)
const refused = await fetch(`${ctx.baseUrl}/import/commit`, { method: 'POST', headers: H(t), body: '{}' })
expect(refused.status).toBe(400)
// Re-stage clean + invoices → status shows zero problems → commit succeeds.
const ok = await fetch(`${ctx.baseUrl}/import/stage`, {
method: 'POST', headers: H(t), body: JSON.stringify({ clientsCsv: CLIENTS_CSV, invoicesCsv: INVOICES_CSV }),
})
expect(ok.status).toBe(200)
const status = await (await fetch(`${ctx.baseUrl}/import/status`, { headers: H(t) })).json() as {
report: { clients: { problems: number }; invoices: { problems: number } }
problemClients: unknown[]
}
expect(status.report.clients.problems).toBe(0)
expect(status.report.invoices.problems).toBe(0)
const commit = await fetch(`${ctx.baseUrl}/import/commit`, { method: 'POST', headers: H(t), body: '{}' })
const cj = await commit.json() as { result: { clients: number; invoices: number; seeded: { fy: string; lastSeq: number } | null } }
expect(commit.status).toBe(200)
expect(cj.result.clients).toBe(1)
expect(cj.result.invoices).toBe(1)
expect(cj.result.seeded).not.toBeNull() // INVOICE series seeded past the legacy number
// The imported rows are real now.
const imported = ctx.db.prepare(`SELECT source, status FROM document WHERE doc_no='INV/26-27-0042'`).get()
expect(imported).toMatchObject({ source: 'apex' })
})
})