const assert = require('node:assert/strict'); const { execFileSync, spawnSync } = require('node:child_process'); const { randomUUID } = require('node:crypto'); const { mkdtempSync, mkdirSync, chmodSync, readFileSync, readdirSync, rmSync } = require('node:fs'); const { tmpdir } = require('node:os'); const path = require('node:path'); const { createRequire } = require('node:module'); const { setTimeout: delay } = require('node:timers/promises'); const root = path.resolve(__dirname, '../..'); const serverRequire = createRequire(path.join(root, 'server/package.json')); const { Pool } = serverRequire('pg'); async function main() { assert.equal(process.env.RUN_ISOLATED_LOAN_DB_TEST, '1'); const diagnostic = process.env.RUN_ISOLATED_LOAN_BROWSER_DIAGNOSTIC; if (diagnostic !== undefined) { assert.ok(['authorization-pagination', 'refund'].includes(diagnostic), 'unknown loan browser diagnostic'); assert.equal(process.env.RUN_ISOLATED_LOAN_BROWSER, '1', 'browser diagnostic requires the browser switch'); assert.ok(!['RUN_ISOLATED_LOAN_RELATED_ONLY', 'RUN_ISOLATED_LOAN_ACK_ONLY', 'RUN_ISOLATED_LOAN_INCOMING_ONLY', 'RUN_ISOLATED_LOAN_EMPLOYEE_ONLY'] .some(key => process.env[key] === '1'), 'diagnostic must not be shadowed by an ONLY switch'); } const scratch = mkdtempSync(path.join(tmpdir(), 'hyb-loan-synthetic-')); const socket = path.join(scratch, 'socket'); const dockerConfig = path.join(scratch, 'docker'); mkdirSync(socket); chmodSync(scratch, 0o755); chmodSync(socket, 0o777); mkdirSync(dockerConfig); const owner = randomUUID(); const docker = (args, input) => execFileSync('docker', ['--config', dockerConfig, ...args], { input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000, env: { PATH: process.env.PATH, LANG: 'C.UTF-8' }, }).trim(); let container; let pool; let failure; const passed = []; try { container = docker(['run', '-d', '--pull=never', '--network', 'none', '--read-only', '--label', `hyb.qa.owner=${owner}`, '--tmpfs', '/var/lib/postgresql/data', '--tmpfs', '/var/run/postgresql', '--tmpfs', '/tmp', '--mount', `type=bind,src=${socket},dst=/qa-socket`, '-e', 'POSTGRES_HOST_AUTH_METHOD=trust', '-e', 'POSTGRES_USER=synthetic', '-e', 'POSTGRES_DB=loan_synthetic', 'postgres:16-alpine', '-c', 'listen_addresses=', '-c', 'unix_socket_directories=/qa-socket,/var/run/postgresql']); assert.equal(docker(['inspect', '--format', '{{.HostConfig.NetworkMode}}', container]), 'none'); pool = new Pool({ host: socket, user: 'synthetic', database: 'loan_synthetic', password: '', max: 4, connectionTimeoutMillis: 1500, statement_timeout: 10000 }); let ready = false; for (let i = 0; i < 40; i++) { try { await pool.query('SELECT 1'); ready = true; break; } catch { await delay(250); } } if (!ready) { const logs = spawnSync('docker', ['--config', dockerConfig, 'logs', container], { encoding: 'utf8', env: { PATH: process.env.PATH }, timeout: 10000, }); console.error(logs.stdout, logs.stderr); throw new Error('isolated database did not become ready'); } const migrations = path.join(root, 'server/prisma/migrations'); let count = 0; for (const name of readdirSync(migrations).sort()) { if (name === 'migration_lock.toml') continue; // psql autocommit preserves PostgreSQL's new-enum-value transaction boundary. docker(['exec', '-i', container, 'psql', '-X', '-q', '-v', 'ON_ERROR_STOP=1', '-h', '/qa-socket', '-U', 'synthetic', '-d', 'loan_synthetic'], readFileSync(path.join(migrations, name, 'migration.sql'))); count++; } console.log(JSON.stringify({ stage: 'migrations', count, network: 'none', persistentVolumes: false })); passed.push('clean_database_all_migrations'); const tables = new Set(['legal_entities','bank_institutions','loan_lenders','company_loans','loan_repayments', 'loan_repayment_allocations','payment_batches','payment_batch_items']); async function insert(client, table, data) { assert.ok(tables.has(table)); const keys = Object.keys(data); await client.query(`INSERT INTO ${table} (${keys.join(',')}) VALUES (${keys.map((_, i) => '$'+(i+1)).join(',')})`, Object.values(data)); } async function transaction(fn) { const client = await pool.connect(); try { await client.query('BEGIN'); const result = await fn(client); await client.query('COMMIT'); return result; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } const companyId = randomUUID(); const bankId = randomUUID(); const actorId = randomUUID(); await insert(pool,'legal_entities',{id:companyId,code:'SYNTHETIC-LOAN',name:'synthetic company'}); await insert(pool,'bank_institutions',{id:bankId,code:'SYNTHETIC-LOAN',name:'synthetic bank'}); async function fixture(options={}) { const lenderId = randomUUID(); const loanId = randomUUID(); await insert(pool,'loan_lenders',{id:lenderId,legal_entity_id:companyId,name:'synthetic lender',identity_status:'verified', identity_confirmed_by:actorId,identity_confirmed_at:new Date(),identity_confirmation:'synthetic external identity checked', bank_institution_id:bankId,bank_name:'synthetic',bank_account_encrypted:'synthetic ciphertext',account_name_encrypted:'synthetic ciphertext', created_by:actorId,updated_by:actorId,updated_at:new Date(),request_key:randomUUID()}); const loan={id:loanId,loan_no:'SYN-'+loanId,origin_reference:'SYN-'+loanId,legal_entity_id:companyId,lender_id:lenderId, original_principal:'100.00',opening_outstanding_principal:'100.00',received_date:'2025-01-01',balance_as_of:'2026-09-17', historical_entry:true,confirmation_status:'confirmed',confirmed_by:actorId,confirmed_at:new Date(),accounting_confirmation_ref:'synthetic check', created_by:actorId,updated_by:actorId,updated_at:new Date(),request_key:randomUUID(),...options}; await insert(pool,'company_loans',loan); return {lenderId,loanId,loan}; } async function repayment(client, f, options={}) { const id=randomUUID(); const batchId=randomUUID(); const itemId=randomUUID(); const amount=options.amount ?? '60.00'; await insert(client,'loan_repayments',{id,repayment_no:'SYN-'+id,legal_entity_id:companyId,lender_id:f.lenderId, reason:'synthetic principal repayment',amount,submitted_by:actorId,lender_identity_version:1,lender_account_version:1, payee_snapshot_json:'{}',request_key:randomUUID()}); await insert(client,'payment_batches',{id:batchId,batch_no:'SYN-'+batchId,entity_type:'loan_repayment',loan_repayment_id:id, legal_entity_id:companyId,name:'synthetic batch',default_payment_channel:'bank',created_by:actorId,updated_at:new Date()}); await insert(client,'payment_batch_items',{id:itemId,payment_batch_id:batchId,entity_id:id,entity_code:'SYN-'+id, entity_title:'synthetic principal',amount:options.itemAmount??amount,snapshot_json:'{}'}); await insert(client,'loan_repayment_allocations',{id:randomUUID(),repayment_id:id,loan_id:f.loanId, amount:options.allocationAmount??amount,loan_version:1}); return {id,batchId,itemId}; } const rejectsCheck = (fn) => assert.rejects(fn, (error) => error.code === '23514'); await rejectsCheck(() => fixture({original_principal:'0.00'})); await rejectsCheck(() => fixture({opening_outstanding_principal:'100.01'})); await rejectsCheck(() => fixture({accounting_confirmation_ref:null})); passed.push('money_and_confirmation_checks'); const f=await fixture(); await assert.rejects(() => insert(pool,'company_loans',{...f.loan,id:randomUUID(),loan_no:'SYN-DUP',request_key:randomUUID()}), e=>e.code==='23505'); passed.push('duplicate_incoming_reference'); await rejectsCheck(()=>transaction(c=>repayment(c,f,{itemAmount:'59.00'}))); await rejectsCheck(()=>transaction(c=>repayment(c,f,{allocationAmount:'59.00'}))); const unconfirmed=await fixture({confirmation_status:'unconfirmed'}); await rejectsCheck(()=>transaction(c=>repayment(c,unconfirmed))); const other=await fixture(); await rejectsCheck(()=>transaction(c=>repayment(c,{...f,loanId:other.loanId}))); passed.push('allocation_total_source_and_batch_identity'); const first=await transaction(c=>repayment(c,f)); const balances=await pool.query(`SELECT opening_outstanding_principal::text AS outstanding, (SELECT sum(amount)::text FROM loan_repayment_allocations WHERE loan_id=$1 AND reservation_state='reserved') AS reserved FROM company_loans WHERE id=$1`,[f.loanId]); assert.deepEqual(balances.rows[0],{outstanding:'100.00',reserved:'60.00'}); await pool.query("UPDATE payment_batches SET status='reviewed' WHERE id=$1",[first.batchId]); assert.equal((await pool.query('SELECT opening_outstanding_principal::text AS value FROM company_loans WHERE id=$1',[f.loanId])).rows[0].value,'100.00'); await rejectsCheck(()=>transaction(c=>repayment(c,f))); passed.push('approval_reserves_without_settlement_and_total_capacity'); await rejectsCheck(()=>pool.query('UPDATE company_loans SET opening_outstanding_principal=50 WHERE id=$1',[f.loanId])); passed.push('confirmed_opening_balance_immutable'); await pool.query("UPDATE loan_repayments SET bank_result_state='unknown' WHERE id=$1",[first.id]); await rejectsCheck(()=>transaction(async c=>{ await c.query("UPDATE payment_batches SET status='cancelled' WHERE id=$1",[first.batchId]); await c.query("UPDATE loan_repayment_allocations SET reservation_state='released',released_at=now(),release_reason='synthetic' WHERE repayment_id=$1",[first.id]); })); assert.equal((await pool.query('SELECT status FROM payment_batches WHERE id=$1',[first.batchId])).rows[0].status,'reviewed'); passed.push('unknown_bank_state_cannot_release_and_transaction_rolls_back'); await transaction(async c=>{ await c.query("UPDATE loan_repayments SET bank_result_state='confirmed_failed',bank_result_ref='synthetic confirmed failure' WHERE id=$1",[first.id]); await c.query("UPDATE payment_batches SET status='cancelled' WHERE id=$1",[first.batchId]); await c.query("UPDATE loan_repayment_allocations SET reservation_state='released',released_at=now(),release_reason='synthetic confirmed failure' WHERE repayment_id=$1",[first.id]); }); passed.push('confirmed_failure_atomic_release'); const concurrent=await fixture(); const left=await pool.connect();const right=await pool.connect(); try { await left.query('BEGIN');await right.query('BEGIN'); await left.query('SELECT id FROM company_loans WHERE id=$1 FOR UPDATE',[concurrent.loanId]); const pid=(await right.query('SELECT pg_backend_pid() AS pid')).rows[0].pid; const waiting=right.query('SELECT id FROM company_loans WHERE id=$1 FOR UPDATE',[concurrent.loanId]); let blocked=false; for(let i=0;i<30;i++){ const state=(await pool.query('SELECT wait_event_type FROM pg_stat_activity WHERE pid=$1',[pid])).rows[0]; if(state?.wait_event_type==='Lock'){blocked=true;break;} await delay(10); } assert.ok(blocked,'second connection must actually wait for the same loan lock'); await repayment(left,concurrent);await left.query('COMMIT');await waiting; await repayment(right,concurrent);await rejectsCheck(()=>right.query('COMMIT'));await right.query('ROLLBACK'); assert.equal((await pool.query("SELECT sum(amount)::text AS value FROM loan_repayment_allocations WHERE loan_id=$1",[concurrent.loanId])).rows[0].value,'60.00'); } finally {await left.query('ROLLBACK');await right.query('ROLLBACK');left.release();right.release();} passed.push('two_connections_serialize_and_reject_over_reservation'); await require('./company-loan-http-fixture.cjs')(pool, scratch); console.log(JSON.stringify({status:'PASS',checks:passed,scope:'schema and real PostgreSQL constraints; not HTTP/auth/payment acceptance'})); } catch (error) { failure = error; throw error; } finally { try { if(pool) await pool.end(); if(container){ assert.equal(docker(['inspect','--format','{{ index .Config.Labels "hyb.qa.owner" }}',container]),owner); docker(['rm','-f',container]); } rmSync(scratch,{recursive:true,force:true}); console.log(JSON.stringify({cleanup:'PASS',onlyRunOwnedResources:true})); } catch (cleanupError) { console.error(JSON.stringify({cleanup:'FAIL',code:cleanupError.code??null})); if (!failure) throw cleanupError; } } } main().catch(error=>{console.error(JSON.stringify({status:'FAIL',name:error.name,code:error.code??null,message:String(error.message).replace(/\/home\/[^\s]+/g,'')}));process.exitCode=1;});