Files
saqel/backend/scripts/test_teacher_ledger_local.php
T

53 lines
3.4 KiB
PHP

<?php
declare(strict_types=1);
// Isolated SQLite memory fixture. No server, production database, or money movement.
// FOR UPDATE concurrency semantics remain a separate MySQL staging test.
require_once dirname(__DIR__) . '/app/Core/Database.php';
require_once dirname(__DIR__) . '/app/Services/TeacherLedgerService.php';
use App\Core\Database;
use App\Services\TeacherLedgerService;
$pdo = class_exists('Pdo\\Sqlite') ? new \Pdo\Sqlite('sqlite::memory:') : new PDO('sqlite::memory:');
$pdo->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";