import { randomUUID } from 'node:crypto'; import type { DestinationStream } from 'pino'; import type pg from 'pg'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { buildApp } from '../build-app.js'; import { createPool } from '../../infrastructure/db/pool.js'; import { createLogger } from '../../infrastructure/logging/logger.js'; import { getTestDbUrl, recreateDatabase, runMigrations, } from '../../infrastructure/db/tests/db-test-support.js'; import { SESSION_COOKIE_NAME } from '../../modules/identity/index.js'; const hasDb = Boolean(process.env.TEST_DATABASE_URL); function silentLogger() { const destination: DestinationStream = { write: () => undefined }; return createLogger({ level: 'info', destination }); } function cookieValue(setCookieHeader: string | string[] | undefined): string { const raw = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader; expect(raw).toBeDefined(); const pair = (raw as string).split(';')[0] as string; return pair.slice(pair.indexOf('=') + 1); } describe.skipIf(!hasDb)('pricing flows (real PostgreSQL)', () => { const url = hasDb ? getTestDbUrl() : ''; let pool: pg.Pool; let app: Awaited>; let adminCookie = ''; beforeAll(async () => { await recreateDatabase(url); await runMigrations(url, 'up'); pool = createPool(url); app = await buildApp({ logger: silentLogger(), pool, cookieSecure: true }); const user = { email: 'pricing-admin@example.com', password: 'correct horse battery staple' }; const registered = await app.inject({ method: 'POST', url: '/auth/register', headers: { 'content-type': 'application/json' }, payload: user, }); expect(registered.statusCode).toBe(201); const id = (registered.json() as { id: string }).id; await pool.query('UPDATE identity_users SET role = $1 WHERE id = $2', ['admin', id]); const login = await app.inject({ method: 'POST', url: '/auth/login', headers: { 'content-type': 'application/json' }, payload: user, }); expect(login.statusCode).toBe(200); adminCookie = cookieValue(login.headers['set-cookie']); }); afterAll(async () => { await app.close(); await pool.end(); }); it('calculates totals with VAT from server-side price (AC1)', async () => { const variantId = randomUUID(); const setPrice = await app.inject({ method: 'PUT', url: `/pricing/variants/${variantId}`, headers: { 'content-type': 'application/json' }, cookies: { [SESSION_COOKIE_NAME]: adminCookie }, payload: { netUnitAmountCents: 1000, vatRate: 'general' }, }); expect(setPrice.statusCode).toBe(200); const calculation = await app.inject({ method: 'POST', url: '/pricing/calculate', headers: { 'content-type': 'application/json' }, payload: { variantId, quantity: 2 }, }); expect(calculation.statusCode).toBe(200); expect(calculation.json()).toMatchObject({ variantId, quantity: 2, currency: 'EUR', vatRate: 'general', vatBasisPoints: 2100, netUnitAmountCents: 1000, netSubtotalCents: 2000, vatAmountCents: 420, totalCents: 2420, }); }); it('ignores client-supplied prices and recalculates from the server (AC2)', async () => { const variantId = randomUUID(); await app.inject({ method: 'PUT', url: `/pricing/variants/${variantId}`, headers: { 'content-type': 'application/json' }, cookies: { [SESSION_COOKIE_NAME]: adminCookie }, payload: { netUnitAmountCents: 500, vatRate: 'reduced' }, }); const calculation = await app.inject({ method: 'POST', url: '/pricing/calculate', headers: { 'content-type': 'application/json' }, payload: { variantId, quantity: 3, netUnitAmountCents: 1, totalCents: 1, vatAmountCents: 0, }, }); expect(calculation.statusCode).toBe(200); expect(calculation.json()).toMatchObject({ netUnitAmountCents: 500, netSubtotalCents: 1500, vatAmountCents: 150, totalCents: 1650, }); }); it('writes a history row for creation and every price change (AC3)', async () => { const variantId = randomUUID(); await app.inject({ method: 'PUT', url: `/pricing/variants/${variantId}`, headers: { 'content-type': 'application/json' }, cookies: { [SESSION_COOKIE_NAME]: adminCookie }, payload: { netUnitAmountCents: 100, vatRate: 'general' }, }); await app.inject({ method: 'PUT', url: `/pricing/variants/${variantId}`, headers: { 'content-type': 'application/json' }, cookies: { [SESSION_COOKIE_NAME]: adminCookie }, payload: { netUnitAmountCents: 200, vatRate: 'reduced' }, }); const history = await pool.query( `SELECT previous_net_unit_amount_cents, previous_vat_rate, new_net_unit_amount_cents, new_vat_rate FROM pricing_price_history WHERE variant_id = $1 ORDER BY created_at, id`, [variantId], ); expect(history.rows).toEqual([ { previous_net_unit_amount_cents: null, previous_vat_rate: null, new_net_unit_amount_cents: 100, new_vat_rate: 'general', }, { previous_net_unit_amount_cents: 100, previous_vat_rate: 'general', new_net_unit_amount_cents: 200, new_vat_rate: 'reduced', }, ]); }); });