diff --git a/apps/hq/build-server.mjs b/apps/hq/build-server.mjs new file mode 100644 index 0000000..227ff31 --- /dev/null +++ b/apps/hq/build-server.mjs @@ -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') diff --git a/apps/hq/src/server.ts b/apps/hq/src/server.ts index a009c68..bfd164a 100644 --- a/apps/hq/src/server.ts +++ b/apps/hq/src/server.ts @@ -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) } diff --git a/apps/hq/test/e2e.test.ts b/apps/hq/test/e2e.test.ts new file mode 100644 index 0000000..c104ae9 --- /dev/null +++ b/apps/hq/test/e2e.test.ts @@ -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) + }) +})