import type pg from 'pg'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { CreatePosSaleUseCase } from '../../modules/pos/index.js'; import { createPool } from '../../infrastructure/db/pool.js'; import { getTestDbUrl, recreateDatabase, runMigrations, } from '../../infrastructure/db/tests/db-test-support.js'; const hasDb = Boolean(process.env.TEST_DATABASE_URL); const STORE_ID = '00000000-0000-0000-0000-000000000001'; const USER_ID = '10000000-0000-4000-8000-000000000001'; const TERMINAL_ID = '20000000-0000-4000-8000-000000000001'; const SESSION_ID = '30000000-0000-4000-8000-000000000001'; const PRODUCT_ID = '40000000-0000-4000-8000-000000000001'; const VARIANT_ID = '50000000-0000-4000-8000-000000000001'; describe.skipIf(!hasDb)('F-186 POS checkout and receipts (real PostgreSQL)', () => { const url = hasDb ? getTestDbUrl() : ''; let pool: pg.Pool; beforeAll(async () => { await recreateDatabase(url); await runMigrations(url, 'up'); pool = createPool(url); await pool.query( `INSERT INTO backoffice_users (id, email, password_hash, role) VALUES ($1, 'cashier@example.test', 'test-hash', 'pos_cashier')`, [USER_ID], ); await pool.query( `INSERT INTO pos_terminals (id, store_id, name, settings) VALUES ($1, $2, 'Caja test', '{"lineDiscountsEnabled":true}'::jsonb)`, [TERMINAL_ID, STORE_ID], ); await pool.query( `INSERT INTO pos_cash_sessions ( id, terminal_id, store_id, user_id, opening_cash_cents, expected_cash_cents ) VALUES ($1, $2, $3, $4, 2000, NULL)`, [SESSION_ID, TERMINAL_ID, STORE_ID, USER_ID], ); await pool.query( `INSERT INTO catalog_products (id, name, slug, state) VALUES ($1, 'Producto test', 'producto-test', 'active')`, [PRODUCT_ID], ); await pool.query( `INSERT INTO catalog_product_variants (id, product_id, sku, ean) VALUES ($1, $2, 'SKU-TEST', '8412345678901')`, [VARIANT_ID, PRODUCT_ID], ); await pool.query( `INSERT INTO pricing_variant_prices (variant_id, net_unit_amount_cents, vat_rate) VALUES ($1, 700, 'reduced')`, [VARIANT_ID], ); await pool.query( `INSERT INTO inventory_stock (variant_id, store_id, available) VALUES ($1, $2, 3)`, [VARIANT_ID, STORE_ID], ); }); afterAll(async () => { await pool.end(); }); it('atomically records mixed payment, change, free item, reporting and receipt', async () => { const useCase = new CreatePosSaleUseCase(pool); const input = { idempotencyKey: 'f186-sale-1', cashSessionId: SESSION_ID, terminalId: TERMINAL_ID, userId: USER_ID, items: [ { kind: 'stock' as const, variantId: VARIANT_ID, quantity: 1, discountCents: 0 }, { kind: 'free' as const, name: 'Servicio libre', unitPriceCents: 500, quantity: 1 }, ], payments: [ { methodCode: 'cash', amountCents: 500, tenderedCents: 1000 }, { methodCode: 'card', amountCents: 770 }, ], }; const result = await useCase.execute(input); expect(result.totalCents).toBe(1270); expect(result.changeCents).toBe(500); expect(result.receiptNumber).toBe('TPV-000001'); expect(result.receipt.items).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'Producto test', freeItem: false, totalCents: 770, taxCents: 70 }), expect.objectContaining({ name: 'Servicio libre', freeItem: true, totalCents: 500 }), ]), ); expect(result.receipt.payments).toEqual( expect.arrayContaining([ expect.objectContaining({ methodCode: 'cash', amountCents: 500, changeCents: 500 }), expect.objectContaining({ methodCode: 'card', amountCents: 770 }), ]), ); const order = await pool.query<{ state: string; receipt_number: string }>( 'SELECT state, receipt_number FROM orders_orders WHERE id = $1', [result.orderId], ); expect(order.rows[0]).toEqual({ state: 'COMPLETED', receipt_number: 'TPV-000001' }); const freeLine = await pool.query<{ product_id: string | null; variant_id: string | null; is_free_item: boolean; }>( `SELECT product_id, variant_id, is_free_item FROM orders_items WHERE order_id = $1 AND is_free_item = true`, [result.orderId], ); expect(freeLine.rows[0]).toEqual({ product_id: null, variant_id: null, is_free_item: true }); const stock = await pool.query<{ available: number; sold: number }>( 'SELECT available, sold FROM inventory_stock WHERE variant_id = $1 AND store_id = $2', [VARIANT_ID, STORE_ID], ); expect(stock.rows[0]).toEqual({ available: 2, sold: 1 }); const reporting = await pool.query<{ count: number }>( 'SELECT COUNT(*)::int AS count FROM reporting_payment_lines WHERE order_id = $1', [result.orderId], ); expect(reporting.rows[0]?.count).toBe(2); const session = await pool.query<{ expected_cash_cents: number }>( 'SELECT expected_cash_cents FROM pos_cash_sessions WHERE id = $1', [SESSION_ID], ); expect(session.rows[0]?.expected_cash_cents).toBe(2500); const replay = await useCase.execute(input); expect(replay.orderId).toBe(result.orderId); expect(replay.receiptNumber).toBe(result.receiptNumber); const afterReplay = await pool.query<{ available: number }>( 'SELECT available FROM inventory_stock WHERE variant_id = $1 AND store_id = $2', [VARIANT_ID, STORE_ID], ); expect(afterReplay.rows[0]?.available).toBe(2); }); });