$v) { putenv($k.'='.$v); $_ENV[$k] = $_SERVER[$k] = $v; } require $s['HOST_ROOT'].'/vendor/autoload.php'; $app = require $s['HOST_ROOT'].'/bootstrap/app.php'; $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); use Illuminate\Support\Facades\DB; use Symfony\Component\Process\Process; use Tempo\Finance\Payables\{Payables, SupplierSettlements, SettlementSchema}; $db = DB::connection(); if (! preg_match('/^(compras_receipts_test_|payable_sgf_)[a-f0-9]{12}$/D', $db->getDatabaseName()) || $db->selectOne('select current_database() as d')->d !== $s['DB_DATABASE']) throw new LogicException('QA target refused.'); $f = json_decode(file_get_contents($s['PAYABLE_ACCESS_FILE']), true, 32, JSON_THROW_ON_ERROR); $input = json_decode(stream_get_contents(STDIN) ?: '{}', true, 64, JSON_THROW_ON_ERROR); $class = $mode === 'compras' ? \App\Services\SupplierPayables::class : \App\Modules\Purchasing\SupplierPayables::class; $select = function (string $role) use ($f, $mode) { $u = \App\Models\User::findOrFail($f['accounts'][$role]['id']); auth()->guard()->setUser($u); if ($mode === 'sgf') app(\App\Shared\Context\CompanyContext::class)->selectFor($u->id, $f['scope']['tenant_id'], $f['scope']['company_id'], $f['scope']['unit_id']); return $u; }; if ($job === 'inspect') { $counts = []; foreach (array_merge(SettlementSchema::TABLES, \Tempo\Finance\Payables\ReversalSchema::TABLES) as $table) if ($db->getSchemaBuilder()->hasTable($table)) $counts[$table] = $db->table($table)->count(); $out = ['counts' => $counts, 'allocation_events' => $db->table('ps_events')->where('kind', 'supplier_allocation.confirmed')->count(), 'core_sha256' => hash_file('sha256', __DIR__.'/../packages/payables/src/SupplierSettlements.php'), 'original_hash' => hash('sha256', Payables::canonical([$db->table('ps_payments')->orderBy('id')->get()->all(), $db->table('ps_payment_decisions')->orderBy('id')->get()->all(), $db->table('ps_allocations')->orderBy('id')->first(), $db->table('pa_obligations')->orderBy('id')->get()->all()]))]; } elseif ($job === 'worker') { $u = $select($input['role']); $db->select('select set_config(?, ?, false)', ['application_name', $input['label']]); try { $out = ['status' => 200, 'result' => app($class)->engine($u)->settlements()->command($input['action'], $input['body'], $input['key'])]; } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $out = ['status' => $e->getStatusCode()]; } catch (\DomainException $e) { $out = ['status' => 409, 'message' => $e->getMessage()]; } catch (\InvalidArgumentException $e) { $out = ['status' => 422]; } } elseif ($job === 'race') { $db->beginTransaction(); $db->table($mode === 'compras' ? 'tenants' : 'companies')->where('id', $f['scope'][$mode === 'compras' ? 'tenant_id' : 'company_id'])->lockForUpdate()->firstOrFail(); $processes = []; try { foreach ([0, 1] as $i) { $command = $input['commands'][$i] ?? $input; $p = new Process([PHP_BINARY, __FILE__, $mode, 'worker'], $s['HOST_ROOT']); $p->setInput(json_encode($command + ['key' => \Illuminate\Support\Str::uuid()->toString(), 'label' => 'ps_proof_'.$i], JSON_THROW_ON_ERROR)); $p->setTimeout(35); $p->start(); $processes[] = $p; } config(['database.connections.ps_monitor' => $db->getConfig()]); $monitor = DB::connection('ps_monitor'); $deadline = microtime(true) + 18; $waits = 0; do { $waits = (int) $monitor->selectOne("select count(*) as n from pg_stat_activity where datname=? and application_name in ('ps_proof_0','ps_proof_1') and wait_event_type='Lock'", [$db->getDatabaseName()])->n; if ($waits === 2) break; foreach ($processes as $p) if (! $p->isRunning()) throw new RuntimeException('Worker ended before overlap: '.$p->getOutput().$p->getErrorOutput()); usleep(20000); } while (microtime(true) < $deadline); if ($waits !== 2) throw new RuntimeException('Two lock waits required.'); if (isset($input['revoke'])) $db->table('pa_access')->where($f['scope'])->where(['user_id' => $f['accounts']['validacao']['id'], 'action' => $input['revoke']])->delete(); $db->commit(); $results = []; foreach ($processes as $p) { $p->wait(); if (! $p->isSuccessful()) throw new RuntimeException($p->getOutput().$p->getErrorOutput()); $results[] = json_decode($p->getOutput(), true, 32, JSON_THROW_ON_ERROR); } $out = ['waits' => $waits, 'results' => $results]; } finally { if ($db->transactionLevel()) $db->rollBack(); foreach ($processes as $p) if ($p->isRunning()) $p->stop(); } } elseif ($job === 'fault') { $kind = $input['kind'] ?? 'supplier_allocation.confirmed'; if (! in_array($kind, ['supplier_allocation.confirmed', 'supplier_allocation.reversed'], true)) throw new LogicException('Invalid proof fault kind.'); if ($input['enabled']) $db->unprepared("CREATE FUNCTION ps_qa_fault() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.kind='{$kind}' THEN RAISE EXCEPTION 'PS_QA_FAULT'; END IF; RETURN NEW; END $$; CREATE TRIGGER ps_qa_fault BEFORE INSERT ON ps_events FOR EACH ROW EXECUTE FUNCTION ps_qa_fault()"); else $db->unprepared('DROP TRIGGER ps_qa_fault ON ps_events; DROP FUNCTION ps_qa_fault()'); $out = ['configured' => true]; } elseif ($job === 'grant') { if (! in_array($input['action'], ['payments_view', 'payment_prepare', 'payment_confirm', 'allocation_prepare', 'allocation_confirm', 'reversal_prepare', 'reversal_confirm'], true)) throw new LogicException('Invalid QA action.'); $role = $input['role']; $match = $f['scope'] + ['user_id' => $f['accounts'][$role]['id'], 'action' => $input['action']]; if ($input['enabled']) $db->table('pa_access')->insertOrIgnore($match + ['person' => 'synthetic-person-'.$role]); else $db->table('pa_access')->where($match)->delete(); $out = ['changed' => true]; } elseif ($job === 'person') { $db->table('pa_access')->where($f['scope'])->where(['user_id' => $f['accounts']['validacao']['id'], 'action' => 'view'])->update(['person' => $input['same'] ? 'synthetic-person-preparacao' : 'synthetic-person-validacao']); $out = ['changed' => true]; } elseif ($job === 'reversal-immutable') { $refused = 0; foreach (\Tempo\Finance\Payables\ReversalSchema::TABLES as $table) { foreach (['update', 'delete'] as $operation) { try { DB::transaction(function () use ($db, $table, $operation): void { $q = $db->table($table)->where('id', $db->table($table)->min('id')); if ($operation === 'delete') $q->delete(); else $q->update(['reason' => 'Forbidden alteration']); }); } catch (\Illuminate\Database\QueryException) { $refused++; } } } $row = (array) $db->table('ps_reversals')->first(); unset($row['id']); $proposal = $db->table('ps_reversal_proposals')->where('id', $row['proposal_id'])->first(); $invalid = 0; foreach ([['amount_minor' => (int) $row['amount_minor'] - 1], ['actor_id' => $proposal->prepared_by, 'person' => $proposal->prepared_person], ['unit_id' => 999999]] as $override) { try { DB::transaction(fn () => $db->table('ps_reversals')->insert(array_replace($row, $override))); } catch (\Illuminate\Database\QueryException $e) { if (! str_contains($e->getMessage(), 'Invalid full allocation compensation')) throw $e; $invalid++; } } $out = ['immutable_operations_refused' => $refused, 'invalid_compensations_refused' => $invalid]; } elseif ($job === 'immutable') { $refused = 0; foreach (['ps_payments', 'ps_payment_decisions', 'ps_allocations'] as $table) { try { DB::transaction(fn () => $db->table($table)->where('id', $db->table($table)->min('id'))->delete()); } catch (\Illuminate\Database\QueryException) { $refused++; } } $out = ['refused' => $refused]; } elseif ($job === 'foreign') { $now = now(); $tenant = $db->table('tenants')->insertGetId(['name' => 'Outro contexto sintético', 'created_at' => $now, 'updated_at' => $now]); if ($mode === 'compras') { $u = \App\Models\User::factory()->create(['tenant_id' => $tenant, 'purchasing_role' => 'finance']); $scope = ['tenant_id' => $tenant, 'company_id' => 0, 'unit_id' => 0]; } else { $company = $db->table('companies')->insertGetId(['tenant_id' => $tenant, 'name' => 'Outra empresa sintética', 'created_at' => $now, 'updated_at' => $now]); $unit = $db->table('units')->insertGetId(['tenant_id' => $tenant, 'company_id' => $company, 'name' => 'Outra unidade', 'created_at' => $now, 'updated_at' => $now]); $scope = ['tenant_id' => $tenant, 'company_id' => $company, 'unit_id' => $unit]; $u = \App\Models\User::create(['name' => 'Outro actor sintético', 'email' => 'foreign-settlement@example.test', 'password' => bin2hex(random_bytes(20))]); $db->table('company_memberships')->insert(['user_id' => $u->id, 'tenant_id' => $tenant, 'company_id' => $company]); foreach (['company.view' => null, 'unit.view' => $unit, 'supplier_payables.view' => $unit] as $permission => $which) $db->table('permission_grants')->insert(['tenant_id' => $tenant, 'company_id' => $company, 'unit_id' => $which, 'permission' => $permission, 'user_id' => $u->id, 'created_at' => $now, 'updated_at' => $now]); } foreach (['view', 'payments_view', 'payment_confirm', 'allocation_confirm', 'reversal_prepare', 'reversal_confirm'] as $action) $db->table('pa_access')->insert($scope + ['user_id' => $u->id, 'action' => $action, 'person' => 'foreign-synthetic-person']); auth()->guard()->setUser($u); if ($mode === 'sgf') app(\App\Shared\Context\CompanyContext::class)->selectFor($u->id, $tenant, $company, $unit); try { app($class)->engine($u)->settlements()->command($input['action'] ?? 'payment-decide', $input['body'], \Illuminate\Support\Str::uuid()->toString()); $out = ['refused' => false]; } catch (\DomainException) { $out = ['refused' => true]; } } elseif ($job === 'multi') { // Core contract test with explicit synthetic source adapter, distinct from the real host source proof. $base = json_decode(file_get_contents($s['PAYABLE_BUNDLE']), true, 64, JSON_THROW_ON_ERROR); $scope = ['tenant_id' => 900001, 'company_id' => 900002, 'unit_id' => 900003]; $sources = []; foreach ([1 => 1160000, 2 => 500000, 3 => 100000] as $id => $minor) { $sources[$id] = array_replace($base, ['instance' => 'synthetic-core-multi', 'company' => 900002, 'document' => $id, 'local_id' => $id, 'number' => 'SIM-CORE-'.$id, 'subtotal_minor' => $minor, 'tax_minor' => 0, 'total_minor' => $minor, 'supplier_id' => $id === 3 ? 999 : $base['supplier_id'], 'lines' => [['id' => $id, 'order_line' => $id, 'quantity_milli' => 1000, 'subtotal_minor' => $minor, 'tax_minor' => 0, 'snapshot' => ['synthetic_core_fixture' => true]]]]); } $sources[4] = array_replace($sources[1], ['document' => 4, 'local_id' => 4, 'number' => 'SIM-CORE-UNIT-4']); $engine = static function ($actor, ?array $otherScope = null) use ($db, $scope, $sources): Payables { return new Payables($db, $actor, 'core-person-'.$actor, $otherScope ?? $scope, static function ($action) use ($actor) { $allowed = $actor === 1 ? ['view', 'prepare', 'payments_view', 'payment_prepare', 'allocation_prepare'] : ['view', 'validate', 'payments_view', 'payment_confirm', 'allocation_confirm']; if (! in_array($action, $allowed, true)) throw new DomainException('Core role refused.'); }, static fn ($id) => $sources[$id] ?? throw new DomainException('Unknown core source.')); }; $p = $engine(1); $v = $engine(2); $uuid = static fn () => \Illuminate\Support\Str::uuid()->toString(); $obs = []; foreach ([1, 2, 3] as $id) { $proposal = $p->command('prepare', $id, 'Core synthetic source', $uuid()); $obs[$id] = $v->command('validate', $proposal['proposal_id'], 'Independent core recognition', $uuid())['obligation_id']; } $pay = static function ($ref, $amount) use ($p, $v, $obs, $sources, $uuid) { $r = $p->settlements()->command('payment-prepare', ['obligation_id' => $obs[1], 'source_revision' => Payables::revision($sources[1]), 'amount' => $amount, 'currency' => 'MZN', 'paid_on' => date('Y-m-d'), 'fact_reference' => $ref, 'evidence_reference' => $ref.'-EVID', 'reason' => 'Core synthetic payment'], $uuid()); $v->settlements()->command('payment-decide', ['payment_id' => $r['payment_id'], 'decision' => 'confirmed', 'evidence_reference' => $ref.'-CONF', 'reason' => 'Independent core confirmation'], $uuid()); return $r['payment_id']; }; $payment = $pay('SIM-MULTI-001', '14000.00'); $plan = ['payment_id' => $payment, 'reference' => 'SIM-MULTI-ALLOC-001', 'reason' => 'Explicit multi-document allocation', 'allocations' => [ ['obligation_id' => $obs[1], 'source_revision' => Payables::revision($sources[1]), 'amount' => '11600.00'], ['obligation_id' => $obs[2], 'source_revision' => Payables::revision($sources[2]), 'amount' => '2400.00']]]; $proposal = $p->settlements()->command('allocation-prepare', $plan, $uuid()); $v->settlements()->command('allocation-decide', ['proposal_id' => $proposal['proposal_id'], 'decision' => 'confirmed', 'reason' => 'Independent multi allocation'], $uuid()); $payment2 = $pay('SIM-MULTI-002', '3000.00'); $proposal2 = $p->settlements()->command('allocation-prepare', ['payment_id' => $payment2, 'reference' => 'SIM-MULTI-ALLOC-002', 'reason' => 'Second payment allocation', 'allocations' => [['obligation_id' => $obs[2], 'source_revision' => Payables::revision($sources[2]), 'amount' => '2600.00']]], $uuid()); $v->settlements()->command('allocation-decide', ['proposal_id' => $proposal2['proposal_id'], 'decision' => 'confirmed', 'reason' => 'Independent second allocation'], $uuid()); $foreign = false; try { $p->settlements()->command('allocation-prepare', ['payment_id' => $payment2, 'reference' => 'SIM-MULTI-FOREIGN', 'reason' => 'Foreign supplier refusal', 'allocations' => [['obligation_id' => $obs[3], 'source_revision' => Payables::revision($sources[3]), 'amount' => '100.00']]], $uuid()); } catch (DomainException) { $foreign = true; } $positions = $p->listing([1, 2, 3])['obligations']; $payments = $p->settlements()->listing()['payments']; $otherScope = array_replace($scope, ['unit_id' => 900004]); $op = $engine(1, $otherScope); $ov = $engine(2, $otherScope); $otherProposal = $op->command('prepare', 4, 'Other unit source', $uuid()); $otherObligation = $ov->command('validate', $otherProposal['proposal_id'], 'Independent other unit recognition', $uuid())['obligation_id']; $otherUnitRefused = false; try { $op->settlements()->command('payment-prepare', ['obligation_id' => $otherObligation, 'source_revision' => Payables::revision($sources[4]), 'amount' => '14000.00', 'currency' => 'MZN', 'paid_on' => date('Y-m-d'), 'fact_reference' => 'SIM-MULTI-001', 'evidence_reference' => 'SIM-OTHER-UNIT-EVID', 'reason' => 'Duplicate through another unit'], $uuid()); } catch (DomainException) { $otherUnitRefused = true; } $out = ['balances' => array_column($positions, 'balance_minor'), 'unallocated_minor' => $payments[1]['unallocated_minor'], 'foreign_supplier_refused' => $foreign, 'other_unit_reference_refused' => $otherUnitRefused, 'kind' => 'core explicit synthetic source adapter; not another host invoice workflow']; } else throw new LogicException('Unknown probe job.'); echo json_encode($out, JSON_THROW_ON_ERROR).PHP_EOL;