setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if ($pdo instanceof \Pdo\Sqlite) { $pdo->createFunction('NOW', static fn() => gmdate('Y-m-d H:i:s'), 0); } else { $pdo->sqliteCreateFunction('NOW', static fn() => gmdate('Y-m-d H:i:s'), 0); } $property = new ReflectionProperty(Database::class, 'instance'); $property->setValue(null, $pdo); $pdo->exec('CREATE TABLE ledger_accounts (id INTEGER PRIMARY KEY, code TEXT, owner_type TEXT, owner_id INTEGER, currency TEXT)'); $pdo->exec('CREATE TABLE ledger_entries (id INTEGER PRIMARY KEY, debit_account_id INTEGER, credit_account_id INTEGER, amount_fils INTEGER)'); $pdo->exec('CREATE TABLE teacher_withdrawal_holds (id INTEGER PRIMARY KEY, uuid TEXT, teacher_id INTEGER, amount_fils INTEGER, status TEXT, idempotency_key TEXT, released_at TEXT, release_reason TEXT)'); function checkLedger(bool $condition, string $message): void { if (!$condition) throw new RuntimeException($message); } $empty = TeacherLedgerService::balance(42); checkLedger($empty['account_id'] === null && $empty['available_fils'] === 0, 'Missing account must have zero available'); checkLedger((int)$pdo->query('SELECT COUNT(*) FROM ledger_accounts')->fetchColumn() === 0, 'Balance read created an account'); $pdo->exec("INSERT INTO ledger_accounts VALUES (1, 'teacher_available', 'teacher', 42, 'JOD')"); $pdo->exec('INSERT INTO ledger_entries VALUES (1, 2, 1, 2000), (2, 1, 2, 500)'); $pdo->exec("INSERT INTO teacher_withdrawal_holds VALUES (1, '5de1e10c-a2ee-47c4-a9ba-5db6e894a94f', 42, 400, 'held', 'fixture-key', NULL, NULL)"); $balance = TeacherLedgerService::balance(42); checkLedger($balance['gross_fils'] === 1500 && $balance['held_fils'] === 400 && $balance['available_fils'] === 1100, 'Signed net balance or hold subtraction wrong'); $wrongTeacher = TeacherLedgerService::releaseHold(43, '5de1e10c-a2ee-47c4-a9ba-5db6e894a94f', 'سبب مراجعة'); checkLedger($wrongTeacher['status'] === 'hold_not_releasable', 'Cross-teacher release was allowed'); $released = TeacherLedgerService::releaseHold(42, '5de1e10c-a2ee-47c4-a9ba-5db6e894a94f', 'فشل نهائي موثق'); checkLedger($released['status'] === 'released', 'First release failed'); $again = TeacherLedgerService::releaseHold(42, '5de1e10c-a2ee-47c4-a9ba-5db6e894a94f', 'إعادة'); checkLedger($again['status'] === 'hold_not_releasable', 'Second release changed state'); $row = $pdo->query('SELECT status, release_reason FROM teacher_withdrawal_holds WHERE id=1')->fetch(PDO::FETCH_ASSOC); checkLedger($row['status'] === 'released' && $row['release_reason'] === 'فشل نهائي موثق', 'Release reason not persisted'); checkLedger(TeacherLedgerService::balance(42)['available_fils'] === 1500, 'Released hold still reduced availability'); $invalid = TeacherLedgerService::placeWithdrawalHold(42, 0, 'valid-key-1234567'); checkLedger($invalid['http_status'] === 400, 'Zero hold should be rejected before database access'); echo "PASS 9 local ledger checks (not a MySQL concurrency test)\n";