feat(hq): server assembly, build script and e2e smoke (HQ-1 task 14)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 2 weeks ago
parent 4a025786aa
commit 3671c9c40f

@ -0,0 +1,16 @@
import { build } from 'esbuild'
await build({
entryPoints: ['src/server.ts'],
outfile: 'dist/server.cjs',
bundle: true,
platform: 'node',
format: 'cjs',
// Native module (better-sqlite3) and puppeteer (locates its browser relative
// to its own package dir) must stay outside the bundle.
external: ['better-sqlite3', 'puppeteer'],
// server.ts falls back to import.meta.url only when __dirname is missing (ESM
// under vitest/tsx); in this CJS bundle that branch is dead, so the warning is noise.
logOverride: { 'empty-import-meta': 'silent' },
})
console.log('hq built')

@ -1,9 +1,18 @@
import express from 'express'
import fs from 'node:fs'
import type http from 'node:http'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { apiRouter } from './api'
import { openDb } from './db'
import { seedIfEmpty } from './seed'
// dist/server.cjs (esbuild CJS bundle) has __dirname; vitest/tsx run this file as ESM.
// Both src/ and dist/ sit one level under apps/hq, so ../../hq-web/dist works from either.
const moduleDir = typeof __dirname === 'undefined'
? path.dirname(fileURLToPath(import.meta.url))
: __dirname
export function startServer(port: number): http.Server {
const app = express()
const db = openDb(process.env['HQ_DATA_DIR'])
@ -11,6 +20,8 @@ export function startServer(port: number): http.Server {
app.locals['db'] = db
app.use(express.json({ limit: '2mb' }))
app.use('/api', apiRouter(db))
const webDist = path.resolve(moduleDir, '../../hq-web/dist')
if (fs.existsSync(webDist)) app.use('/', express.static(webDist))
return app.listen(port)
}

@ -0,0 +1,41 @@
// apps/hq/test/e2e.test.ts
import { describe, it, expect, afterAll } from 'vitest'
import express from 'express'
import { openDb } from '../src/db'
import { seedIfEmpty } from '../src/seed'
import { createStaff } from '../src/auth'
import { apiRouter } from '../src/api'
const db = openDb(':memory:')
seedIfEmpty(db)
createStaff(db, { email: 'e2e@test.in', displayName: 'E2E', role: 'owner', password: 'e2e-password' })
const app = express(); app.use(express.json()); app.locals['db'] = db; app.use('/api', apiRouter(db))
const server = app.listen(0)
const base = `http://localhost:${(server.address() as { port: number }).port}/api`
let token = ''
const call = async (method: string, path: string, body?: unknown) => {
const res = await fetch(base + path, { method,
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
...(body ? { body: JSON.stringify(body) } : {}) })
return { status: res.status, json: await res.json() as any }
}
describe('hq end to end', () => {
afterAll(() => server.close())
it('walks quotation → invoice → payment → paid', async () => {
token = (await call('POST', '/auth/login', { email: 'e2e@test.in', password: 'e2e-password' })).json.token
const client = (await call('POST', '/clients', { name: 'Flow Mart', stateCode: '32' })).json.client
const mod = (await call('POST', '/modules', { code: 'POS', name: 'POS Billing' })).json.module
await call('POST', `/modules/${mod.id}/prices`, { kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' })
const qt = (await call('POST', '/documents', { docType: 'QUOTATION', clientId: client.id,
lines: [{ moduleId: mod.id, qty: 1, kind: 'yearly' }] })).json.document
await call('POST', `/documents/${qt.id}/issue`)
const inv = (await call('POST', `/documents/${qt.id}/convert`, { to: 'INVOICE' })).json.document
await call('POST', `/documents/${inv.id}/issue`)
await call('POST', '/payments', { clientId: client.id, receivedOn: '2026-07-10', mode: 'bank', amountPaise: 11_800_00 })
const after = (await call('GET', `/documents/${inv.id}`)).json.document
expect(after.status).toBe('paid')
const ledger = (await call('GET', `/clients/${client.id}/ledger`)).json
expect(ledger.advancePaise).toBe(0)
})
})
Loading…
Cancel
Save