import { randomUUID } from "node:crypto"; import process from "node:process"; import { createInterface } from "node:readline"; import pg from "pg"; const { Pool } = pg; async function readStandardInput() { const lines = createInterface({ input: process.stdin, terminal: false }); for await (const line of lines) { lines.close(); return line.trim(); } throw Object.assign(new Error("Missing connection input"), { code: "MISSING_CONNECTION_INPUT" }); } function databaseMessage(error) { return typeof error?.message === "string" ? error.message : ""; } function safeError(error) { return { ok: false, errorCode: typeof error?.code === "string" ? error.code : "DATABASE_TEST_FAILED", errorMessage: databaseMessage(error).startsWith("CONDON_") ? databaseMessage(error) : "Database integration test failed" }; } async function expectDatabaseError( client, checks, label, text, values, expectedMessage ) { await client.query("SAVEPOINT expected_error"); let matched = false; try { await client.query(text, values); } catch (error) { matched = databaseMessage(error) === expectedMessage; } finally { await client.query("ROLLBACK TO SAVEPOINT expected_error"); await client.query("RELEASE SAVEPOINT expected_error"); } checks[label] = matched; } function usageFunctionSql() { return ` SELECT record.* FROM condon.create_usage_record( $1::uuid, $2::uuid, $3::uuid, $4::varchar, $5::date, $6::date, $7::varchar, $8::smallint, $9::text, $10::uuid ) AS record `; } const checks = {}; let pool; let rollbackClient; let concurrencyOwnerId; let concurrencyPeriodId; try { const input = JSON.parse(await readStandardInput()); pool = new Pool({ host: input.host, port: input.port, user: input.user, password: input.password, database: input.database, ssl: input.ssl ? { rejectUnauthorized: false } : undefined, max: 4, application_name: "condon_database_integration_test", connectionTimeoutMillis: 10_000, options: "-c statement_timeout=30000 -c lock_timeout=5000" }); const identityResult = await pool.query(` SELECT current_database() AS database_name, pg_catalog.to_regnamespace('condon') IS NOT NULL AS schema_exists `); checks.databaseTarget = identityResult.rows[0].database_name === "booking_test" && identityResult.rows[0].schema_exists === true; const initialCountResult = await pool.query(` SELECT (SELECT count(*)::integer FROM condon.owner_accounts) AS owners, (SELECT count(*)::integer FROM condon.entitlement_periods) AS periods, (SELECT count(*)::integer FROM condon.usage_records) AS usage, (SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger `); if (Object.values(initialCountResult.rows[0]).some(value => value !== 0)) { throw Object.assign(new Error("CONDO business tables must be empty"), { code: "TEST_REQUIRES_EMPTY_CONDON" }); } checks.initiallyEmpty = true; rollbackClient = await pool.connect(); await rollbackClient.query("BEGIN"); const matrixResult = await rollbackClient.query(` SELECT count(*)::integer AS case_count, bool_and( condon.calculate_multiplier( purchased.code, used.code, NULL ) = GREATEST( 1, used.entitlement_tier - purchased.entitlement_tier + 1 ) ) AS all_match FROM condon.room_types AS purchased CROSS JOIN condon.room_types AS used WHERE NOT purchased.requires_manual_multiplier AND NOT used.requires_manual_multiplier `); checks.fullAutomaticMatrix = matrixResult.rows[0].case_count === 100 && matrixResult.rows[0].all_match === true; const manualResult = await rollbackClient.query(` SELECT condon.calculate_multiplier('AC2', 'RM1', 3::smallint) AS purchased_ac2, condon.calculate_multiplier('RM1', 'AC2', 2::smallint) AS used_ac2 `); checks.ac2ManualValues = manualResult.rows[0].purchased_ac2 === 3 && manualResult.rows[0].used_ac2 === 2; await expectDatabaseError( rollbackClient, checks, "ac2RequiresManual", "SELECT condon.calculate_multiplier('AC2', 'RM1', NULL::smallint)", [], "CONDON_MANUAL_MULTIPLIER_REQUIRED" ); await expectDatabaseError( rollbackClient, checks, "automaticRejectsManual", "SELECT condon.calculate_multiplier('RM1', 'SU1', 2::smallint)", [], "CONDON_MANUAL_MULTIPLIER_NOT_ALLOWED" ); const ownerId = randomUUID(); const periodId = randomUUID(); const grantLedgerId = randomUUID(); await rollbackClient.query( ` INSERT INTO condon.owner_accounts ( id, account_no, transfer_date, owner_name, room_no, purchased_room_type_code, unit_no, member_no ) VALUES ($1, NULL, NULL, 'Rollback Test Owner', $2, 'RM1', 'TEST/1', 'TEST-1') `, [ownerId, `R${ownerId.replaceAll("-", "").slice(0, 12)}`] ); const periodResult = await rollbackClient.query( ` SELECT period.* FROM condon.open_entitlement_period( $1::uuid, $2::uuid, 2026::smallint, $3::uuid, NULL::uuid ) AS period `, [periodId, ownerId, grantLedgerId] ); checks.openingPeriod = periodResult.rows[0].annual_grant === 15 && periodResult.rows[0].carry_forward === 0 && periodResult.rows[0].current_balance === 15; const usageId = randomUUID(); const usageLedgerId = randomUUID(); const idempotencyKey = randomUUID(); const usageResult = await rollbackClient.query( usageFunctionSql(), [ usageId, usageLedgerId, ownerId, "990000001", "2026-09-01", "2026-09-04", "SU1", null, "matrix test", idempotencyKey ] ); checks.derivedUsage = usageResult.rows[0].night_count === 3 && usageResult.rows[0].applied_multiplier === 2 && usageResult.rows[0].use_nights === 6 && usageResult.rows[0].balance_before === 15 && usageResult.rows[0].balance_after === 9; const retryResult = await rollbackClient.query( usageFunctionSql(), [ randomUUID(), randomUUID(), ownerId, "990000001", "2026-09-01", "2026-09-04", "SU1", null, "matrix test", idempotencyKey ] ); const idempotencyCountResult = await rollbackClient.query( ` SELECT (SELECT count(*)::integer FROM condon.usage_records WHERE owner_account_id = $1) AS usage_count, (SELECT current_balance FROM condon.entitlement_periods WHERE id = $2) AS balance `, [ownerId, periodId] ); checks.idempotency = retryResult.rows[0].id === usageId && idempotencyCountResult.rows[0].usage_count === 1 && idempotencyCountResult.rows[0].balance === 9; await expectDatabaseError( rollbackClient, checks, "duplicateConfirmation", usageFunctionSql(), [ randomUUID(), randomUUID(), ownerId, "990000001", "2026-10-01", "2026-10-02", "RM1", null, "", randomUUID() ], "CONDON_CONFIRMATION_NO_EXISTS" ); await expectDatabaseError( rollbackClient, checks, "crossYearRejected", usageFunctionSql(), [ randomUUID(), randomUUID(), ownerId, "990000002", "2026-12-30", "2027-01-02", "RM1", null, "", randomUUID() ], "CONDON_CROSS_YEAR_STAY_NOT_SUPPORTED" ); await expectDatabaseError( rollbackClient, checks, "insufficientBalance", usageFunctionSql(), [ randomUUID(), randomUUID(), ownerId, "990000003", "2026-10-01", "2026-10-05", "SU3", null, "", randomUUID() ], "CONDON_INSUFFICIENT_BALANCE" ); const carryPeriodResult = await rollbackClient.query( ` SELECT period.* FROM condon.open_entitlement_period( $1::uuid, $2::uuid, 2027::smallint, $3::uuid, $4::uuid ) AS period `, [randomUUID(), ownerId, randomUUID(), randomUUID()] ); checks.annualCarryForward = carryPeriodResult.rows[0].annual_grant === 15 && carryPeriodResult.rows[0].carry_forward === 9 && carryPeriodResult.rows[0].current_balance === 24; await rollbackClient.query("ROLLBACK"); rollbackClient.release(); rollbackClient = undefined; const postRollbackCountResult = await pool.query(` SELECT (SELECT count(*)::integer FROM condon.owner_accounts) AS owners, (SELECT count(*)::integer FROM condon.entitlement_periods) AS periods, (SELECT count(*)::integer FROM condon.usage_records) AS usage, (SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger `); checks.rollbackLeavesNoData = Object.values(postRollbackCountResult.rows[0]).every(value => value === 0); concurrencyOwnerId = randomUUID(); concurrencyPeriodId = randomUUID(); const concurrencyRoomNo = `C${concurrencyOwnerId.replaceAll("-", "").slice(0, 12)}`; const setupClient = await pool.connect(); try { await setupClient.query("BEGIN"); await setupClient.query( ` INSERT INTO condon.owner_accounts ( id, account_no, transfer_date, owner_name, room_no, purchased_room_type_code, unit_no, member_no ) VALUES ($1, NULL, NULL, 'Concurrency Test Owner', $2, 'RM1', 'TEST/2', 'TEST-2') `, [concurrencyOwnerId, concurrencyRoomNo] ); await setupClient.query( ` SELECT condon.open_entitlement_period( $1::uuid, $2::uuid, 2098::smallint, $3::uuid, NULL::uuid ) `, [concurrencyPeriodId, concurrencyOwnerId, randomUUID()] ); await setupClient.query("COMMIT"); } catch (error) { await setupClient.query("ROLLBACK").catch(() => undefined); throw error; } finally { setupClient.release(); } const concurrentCalls = [ [ randomUUID(), randomUUID(), concurrencyOwnerId, "998000001", "2098-01-01", "2098-01-11", "RM1", null, "concurrency A", randomUUID() ], [ randomUUID(), randomUUID(), concurrencyOwnerId, "998000002", "2098-02-01", "2098-02-11", "RM1", null, "concurrency B", randomUUID() ] ]; const concurrentResults = await Promise.allSettled( concurrentCalls.map(values => pool.query(usageFunctionSql(), values)) ); const fulfilled = concurrentResults.filter(result => result.status === "fulfilled"); const rejected = concurrentResults.filter(result => result.status === "rejected"); checks.concurrentOverspendBlocked = fulfilled.length === 1 && rejected.length === 1 && databaseMessage(rejected[0]?.reason) === "CONDON_INSUFFICIENT_BALANCE"; const concurrencyStateResult = await pool.query( ` SELECT ep.current_balance, (SELECT count(*)::integer FROM condon.usage_records WHERE owner_account_id = $1) AS usage_count, ( SELECT count(*)::integer FROM condon.entitlement_ledger WHERE entitlement_period_id = $2 AND entry_type = 'usage' ) AS usage_ledger_count FROM condon.entitlement_periods AS ep WHERE ep.id = $2 `, [concurrencyOwnerId, concurrencyPeriodId] ); checks.concurrentStateConsistent = concurrencyStateResult.rows[0].current_balance === 5 && concurrencyStateResult.rows[0].usage_count === 1 && concurrencyStateResult.rows[0].usage_ledger_count === 1; } catch (error) { process.stdout.write(`${JSON.stringify(safeError(error), null, 2)}\n`); process.exitCode = 1; } finally { if (rollbackClient) { await rollbackClient.query("ROLLBACK").catch(() => undefined); rollbackClient.release(); } if (pool && concurrencyOwnerId && concurrencyPeriodId) { const cleanupClient = await pool.connect().catch(() => undefined); if (cleanupClient) { try { await cleanupClient.query("BEGIN"); await cleanupClient.query( "DELETE FROM condon.entitlement_ledger WHERE entitlement_period_id = $1", [concurrencyPeriodId] ); await cleanupClient.query( "DELETE FROM condon.usage_records WHERE owner_account_id = $1", [concurrencyOwnerId] ); await cleanupClient.query( "DELETE FROM condon.entitlement_periods WHERE id = $1", [concurrencyPeriodId] ); await cleanupClient.query( "DELETE FROM condon.owner_accounts WHERE id = $1", [concurrencyOwnerId] ); await cleanupClient.query("COMMIT"); checks.concurrencyCleanup = true; } catch { await cleanupClient.query("ROLLBACK").catch(() => undefined); checks.concurrencyCleanup = false; process.exitCode = 1; } finally { cleanupClient.release(); } } } if (pool) { try { const finalCountResult = await pool.query(` SELECT (SELECT count(*)::integer FROM condon.owner_accounts) AS owners, (SELECT count(*)::integer FROM condon.entitlement_periods) AS periods, (SELECT count(*)::integer FROM condon.usage_records) AS usage, (SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger `); checks.finalBusinessTablesEmpty = Object.values(finalCountResult.rows[0]).every(value => value === 0); if (!checks.finalBusinessTablesEmpty) process.exitCode = 1; } catch { checks.finalBusinessTablesEmpty = false; process.exitCode = 1; } await pool.end().catch(() => { process.exitCode = 1; }); } if (Object.keys(checks).length > 0) { const ok = Object.values(checks).every(Boolean); process.stdout.write(`${JSON.stringify({ ok, checks }, null, 2)}\n`); if (!ok) process.exitCode = 1; } }