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.
79 lines
4.4 KiB
TypeScript
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' })
|
|
})
|
|
})
|