feat: add cash rounding, POS test suite, and fix test harness port cleanup
- Add Swedish rounding (nearest nickel) for cash payments at locations with cash_rounding enabled - Add rounding_adjustment column to transactions, cash_rounding to locations - Add POS schema to database plugin for relational query support - Complete/void routes now return full transaction with line items via getById - Test harness killPort falls back to fuser when lsof unavailable (fixes stale process bug) - Add 35-test POS API suite covering discounts, drawer, transactions, tax, rounding, e2e flow - Add unit tests for tax service and POS Zod schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,15 +5,18 @@ import { getSuites, runSuite } from './lib/context.js'
|
||||
import { createClient } from './lib/client.js'
|
||||
|
||||
// --- Config ---
|
||||
// Use DATABASE_URL from env if available, otherwise construct from individual vars
|
||||
const DB_HOST = process.env.DB_HOST ?? 'localhost'
|
||||
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
|
||||
const DB_USER = process.env.DB_USER ?? 'lunarfront'
|
||||
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
|
||||
const TEST_DB = 'lunarfront_api_test'
|
||||
const DB_URL = process.env.DATABASE_URL ?? `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`
|
||||
const USE_EXTERNAL_DB = !!process.env.DATABASE_URL
|
||||
const TEST_PORT = 8001
|
||||
const BASE_URL = `http://localhost:${TEST_PORT}`
|
||||
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
|
||||
const LOCATION_ID = 'a0000000-0000-0000-0000-000000000002'
|
||||
const COMPANY_ID = '10000000-1000-4000-8000-000000000001'
|
||||
const LOCATION_ID = '10000000-1000-4000-8000-000000000002'
|
||||
|
||||
// --- Parse CLI args ---
|
||||
const args = process.argv.slice(2)
|
||||
@@ -27,13 +30,16 @@ for (let i = 0; i < args.length; i++) {
|
||||
|
||||
// --- DB setup ---
|
||||
async function setupDatabase() {
|
||||
const adminSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/postgres`)
|
||||
const [exists] = await adminSql`SELECT 1 FROM pg_database WHERE datname = ${TEST_DB}`
|
||||
if (!exists) {
|
||||
await adminSql.unsafe(`CREATE DATABASE ${TEST_DB}`)
|
||||
console.log(` Created database ${TEST_DB}`)
|
||||
if (!USE_EXTERNAL_DB) {
|
||||
// Local: create test DB if needed
|
||||
const adminSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/postgres`)
|
||||
const [exists] = await adminSql`SELECT 1 FROM pg_database WHERE datname = ${TEST_DB}`
|
||||
if (!exists) {
|
||||
await adminSql.unsafe(`CREATE DATABASE ${TEST_DB}`)
|
||||
console.log(` Created database ${TEST_DB}`)
|
||||
}
|
||||
await adminSql.end()
|
||||
}
|
||||
await adminSql.end()
|
||||
|
||||
// Run migrations
|
||||
const { execSync } = await import('child_process')
|
||||
@@ -41,13 +47,13 @@ async function setupDatabase() {
|
||||
cwd: new URL('..', import.meta.url).pathname,
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`,
|
||||
DATABASE_URL: DB_URL,
|
||||
},
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
// Truncate all tables
|
||||
const testSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
|
||||
const testSql = postgres(DB_URL)
|
||||
await testSql.unsafe(`
|
||||
DO $$ DECLARE r RECORD;
|
||||
BEGIN
|
||||
@@ -61,7 +67,8 @@ async function setupDatabase() {
|
||||
|
||||
// Seed company + location (company table stays as store settings)
|
||||
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
|
||||
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
|
||||
await testSql`INSERT INTO location (id, name, tax_rate, service_tax_rate) VALUES (${LOCATION_ID}, 'Test Location', '0.0825', '0.0500')`
|
||||
await testSql`INSERT INTO location (id, name, tax_rate, service_tax_rate, cash_rounding) VALUES ('10000000-1000-4000-8000-000000000003', 'Rounding Location', '0.0825', '0.0500', true)`
|
||||
|
||||
// Seed lookup tables
|
||||
const { SYSTEM_UNIT_STATUSES, SYSTEM_ITEM_CONDITIONS } = await import('../src/db/schema/lookups.js')
|
||||
@@ -115,7 +122,12 @@ async function setupDatabase() {
|
||||
async function killPort(port: number) {
|
||||
try {
|
||||
const { execSync } = await import('child_process')
|
||||
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: 'pipe' })
|
||||
// Try lsof first, fall back to fuser
|
||||
try {
|
||||
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: 'pipe' })
|
||||
} catch {
|
||||
execSync(`fuser -k ${port}/tcp 2>/dev/null || true`, { stdio: 'pipe' })
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
} catch {}
|
||||
}
|
||||
@@ -127,7 +139,7 @@ async function startBackend(): Promise<Subprocess> {
|
||||
cwd: new URL('..', import.meta.url).pathname,
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: `postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`,
|
||||
DATABASE_URL: DB_URL,
|
||||
REDIS_URL: process.env.REDIS_URL ?? 'redis://localhost:6379',
|
||||
JWT_SECRET: 'test-secret-for-api-tests',
|
||||
PORT: String(TEST_PORT),
|
||||
@@ -181,7 +193,7 @@ async function registerTestUser(): Promise<string> {
|
||||
|
||||
// Assign admin role to the user via direct SQL
|
||||
if (registerRes.status === 201 && registerData.user) {
|
||||
const assignSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
|
||||
const assignSql = postgres(DB_URL)
|
||||
const [adminRole] = await assignSql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await assignSql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${registerData.user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
|
||||
Reference in New Issue
Block a user