@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const base = 'http://127.0.0.1:8005';
|
||||
const cookies = new Map();
|
||||
async function request(path, method = 'GET', body) {
|
||||
const response = await fetch(base + path, {method, redirect: 'manual', headers: {
|
||||
Accept: 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '),
|
||||
...(body ? {'Content-Type': 'application/x-www-form-urlencoded'} : {}),
|
||||
}, ...(body ? {body: new URLSearchParams(body)} : {})});
|
||||
for (const cookie of response.headers.getSetCookie()) {
|
||||
const pair = cookie.split(';', 1)[0], index = pair.indexOf('=');
|
||||
cookies.set(pair.slice(0,index),pair.slice(index+1));
|
||||
}
|
||||
return {status: response.status, text: await response.text(), location: response.headers.get('location')};
|
||||
}
|
||||
const token = text => text.match(/name="_token" value="([^"]+)"/)[1];
|
||||
async function enter(role) {
|
||||
const page = await request('/avaliar');
|
||||
assert.equal(page.status, 200);
|
||||
const entered = await request('/avaliar/entrar', 'POST', {role, _token: token(page.text)});
|
||||
assert.equal(entered.status, 302, role);
|
||||
assert.equal(new URL(entered.location).pathname, '/dashboard');
|
||||
assert.equal((await request('/dashboard')).status, 200);
|
||||
}
|
||||
let page = await request('/avaliar');
|
||||
assert.equal((await request('/avaliar/entrar','POST',{role:'admin'})).status,419);
|
||||
assert.equal((await request('/avaliar/entrar','POST',{role:'root',_token:token(page.text)})).status,422);
|
||||
assert.equal((await request('/avaliar/entrar','POST',{role:'admin',user_id:'1',_token:token(page.text)})).status,422);
|
||||
await enter('admin');
|
||||
const modules = ['/dashboard','/empresa','/requisicoes','/planeamento','/fornecedores','/cotacoes','/ordens',
|
||||
'/recepcoes','/stock','/stock/operacoes','/equipamentos','/orcamentos','/regras-investimento','/facturas',
|
||||
'/pagamentos','/obrigacoes-fornecedores','/conflitos','/revisao-compras','/politica-admissao','/politica-recomendacao'];
|
||||
for (const path of modules) assert.equal((await request(path)).status,200,path);
|
||||
const roles = ['requester','department_head','procurement','finance','warehouse','admin'];
|
||||
for (const role of roles) await enter(role);
|
||||
page = await request('/avaliar');
|
||||
assert.equal((await request('/logout','POST',{_token:token(page.text)})).status,204);
|
||||
assert.equal((await request('/dashboard')).status,401);
|
||||
assert.equal((await request('/empresa')).status,401);
|
||||
const operational = await fetch('http://127.0.0.1:8015/avaliar', {redirect:'manual'}).catch(() => null);
|
||||
if (operational) assert.equal(operational.status,404,'Evaluation entry must not exist on the normal installation');
|
||||
await fs.mkdir('docs/proofs',{recursive:true});
|
||||
await fs.writeFile('docs/proofs/evaluation-access.json',JSON.stringify({checked_at:new Date().toISOString(),base,
|
||||
owner_modules:modules,roles,csrf_enforced:true,arbitrary_role_refused:true,arbitrary_user_refused:true,logout:true,
|
||||
business_commands:0,real_accounts_changed:false,operational_entry_status:operational?.status??'not running',
|
||||
visual_review:false},null,2)+'\n');
|
||||
console.log('Owner entry, 20 module pages, six roles, CSRF, rejected identifiers and logout passed.');
|
||||
@@ -0,0 +1,39 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
const cookies = new Map(), base = 'http://127.0.0.1:8005';
|
||||
async function request(path, method = 'GET', body) {
|
||||
const r = await fetch(base + path, {method, redirect:'manual', headers:{Accept:'application/json',
|
||||
Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),
|
||||
...(body?{'Content-Type':'application/x-www-form-urlencoded'}:{})},
|
||||
...(body?{body:new URLSearchParams(body)}:{})});
|
||||
for(const c of r.headers.getSetCookie()) {const p=c.split(';',1)[0],i=p.indexOf('=');cookies.set(p.slice(0,i),p.slice(i+1));}
|
||||
return {status:r.status,text:await r.text()};
|
||||
}
|
||||
const landing=await request('/avaliar');
|
||||
const csrf=landing.text.match(/name="_token" value="([^"]+)"/)[1];
|
||||
assert.equal((await request('/avaliar/entrar','POST',{role:'admin',_token:csrf})).status,302);
|
||||
const before=JSON.parse((await request('/api/supplier-payables')).text);
|
||||
assert(before.documents.length);
|
||||
for(const doc of before.documents) {
|
||||
const filtered=await request('/api/supplier-payables?invoice='+doc.local_id);
|
||||
assert.equal(filtered.status,200);
|
||||
const data=JSON.parse(filtered.text);
|
||||
assert.deepEqual(data.documents,[doc]);
|
||||
const page=await request('/obrigacoes-fornecedores?invoice='+doc.local_id);
|
||||
assert.equal(page.status,200);
|
||||
assert(page.text.includes('href="/facturas"'));
|
||||
assert(page.text.includes('Voltar às facturas'));
|
||||
assert(page.text.includes(doc.number));
|
||||
}
|
||||
for(const query of ['0','-1','abc','1.5']) {
|
||||
assert.equal((await request('/api/supplier-payables?invoice='+query)).status,422);
|
||||
assert.equal((await request('/obrigacoes-fornecedores?invoice='+query)).status,422);
|
||||
}
|
||||
assert.equal((await request('/api/supplier-payables?invoice=2147483647')).status,404);
|
||||
const after=JSON.parse((await request('/api/supplier-payables')).text);
|
||||
assert.deepEqual(after,before,'Read-only navigation must preserve all documents and histories');
|
||||
await fs.writeFile('docs/proofs/invoice-payable-link.json',JSON.stringify({checked_at:new Date().toISOString(),
|
||||
documents_checked:before.documents.length,per_invoice_entry:true,return_to_invoices:true,
|
||||
invalid_identifiers_refused:true,unknown_invoice_refused:true,full_listing_unchanged:true,business_commands:0,
|
||||
visual_review:false},null,2)+'\n');
|
||||
console.log('Per-invoice payable navigation, invalid/unknown identifiers and unchanged history passed.');
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import {procurementNavigation, activeProcurementEntry} from '../resources/js/lib/procurement-navigation.ts';
|
||||
|
||||
const base = 'http://127.0.0.1:8005';
|
||||
const cookies = new Map();
|
||||
let version = '';
|
||||
async function request(path, {method = 'GET', body, inertia = false} = {}) {
|
||||
const response = await fetch(base + path, {method, redirect: 'manual', headers: {
|
||||
Accept: 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '),
|
||||
...(body ? {'Content-Type': 'application/x-www-form-urlencoded'} : {}),
|
||||
...(inertia ? {'X-Inertia': 'true', 'X-Inertia-Version': version} : {}),
|
||||
}, ...(body ? {body: new URLSearchParams(body)} : {})});
|
||||
for (const cookie of response.headers.getSetCookie()) {
|
||||
const pair = cookie.split(';', 1)[0], index = pair.indexOf('=');
|
||||
cookies.set(pair.slice(0,index),pair.slice(index+1));
|
||||
}
|
||||
const text = await response.text();
|
||||
let data;
|
||||
try {data = JSON.parse(text);} catch {}
|
||||
if (!data) {
|
||||
const page = text.match(/<script data-page="app" type="application\/json">([\s\S]*?)<\/script>/);
|
||||
if (page) data = JSON.parse(page[1]);
|
||||
}
|
||||
if (data?.version) version = data.version;
|
||||
return {status:response.status, text, data};
|
||||
}
|
||||
async function enter(role) {
|
||||
const landing = await request('/avaliar');
|
||||
assert.equal(landing.status, 200);
|
||||
const csrf = landing.text.match(/name="_token" value="([^"]+)"/)[1];
|
||||
assert.equal((await request('/avaliar/entrar', {method:'POST',body:{role,_token:csrf}})).status,302);
|
||||
}
|
||||
await enter('admin');
|
||||
await request('/dashboard');
|
||||
const baseline = (await request('/api/supplier-payables')).data;
|
||||
const results = [];
|
||||
for (const role of ['admin','procurement','finance','department_head','warehouse','requester']) {
|
||||
await enter(role);
|
||||
const dashboard = await request('/dashboard', {inertia:true});
|
||||
assert.equal(dashboard.status,200);
|
||||
assert.equal(dashboard.data.props.auth.user.purchasing_role,role);
|
||||
const groups = procurementNavigation(role,dashboard.data.props.payablesAvailable === true);
|
||||
const entries = groups.flatMap(group=>group.entries);
|
||||
for (const entry of entries) {
|
||||
const response = await request(entry.path,{inertia:!entry.fullPage});
|
||||
assert.equal(response.status,200,role+' '+entry.path);
|
||||
if (!entry.fullPage) assert(response.data?.component,role+' Inertia '+entry.path);
|
||||
assert.equal(activeProcurementEntry(groups,entry.path),entry.id,role+' active '+entry.path);
|
||||
if (entry.id === 'my_drafts') {
|
||||
assert.equal(response.data.props.filters.view,'mine');
|
||||
assert.equal(response.data.props.filters.status,'draft');
|
||||
}
|
||||
}
|
||||
for (const path of ['/settings/profile','/settings/appearance']) assert.equal((await request(path,{inertia:true})).status,200,role+' '+path);
|
||||
// Hidden administration and finance pages remain enforced on the server.
|
||||
if (role !== 'admin') assert.equal((await request('/empresa')).status,403,role+' company isolation');
|
||||
if (!['admin','finance'].includes(role)) assert.equal((await request('/facturas')).status,403,role+' finance isolation');
|
||||
results.push({role,checked_entries:entries.length,paths:entries.map(entry=>entry.path)});
|
||||
}
|
||||
await enter('admin');
|
||||
const all = await request('/requisicoes?view=all',{inertia:true});
|
||||
const requestId = all.data.props.requisitions.data[0]?.id;
|
||||
const processLinks = [];
|
||||
if (requestId) {
|
||||
assert.equal((await request('/requisicoes/'+requestId,{inertia:true})).status,200);
|
||||
const process = await request('/requisicoes/'+requestId+'/processo',{inertia:true});
|
||||
assert.equal(process.status,200);
|
||||
function collectLinks(value) {
|
||||
if (!value || typeof value !== 'object') return;
|
||||
if (typeof value.href === 'string' && value.href.startsWith('/') && !value.href.startsWith('//')) processLinks.push(value.href);
|
||||
for (const child of Object.values(value)) if (typeof child === 'object') collectLinks(child);
|
||||
}
|
||||
collectLinks(process.data.props);
|
||||
for (const href of new Set(processLinks)) assert.equal((await request(href)).status,200,'Process link '+href);
|
||||
}
|
||||
const invoices = await request('/facturas',{inertia:true});
|
||||
for (const invoice of invoices.data.props.invoices.data) {
|
||||
const page = await request('/obrigacoes-fornecedores?invoice='+invoice.id);
|
||||
assert.equal(page.status,200);
|
||||
assert(page.text.includes('href="/facturas"'));
|
||||
}
|
||||
assert.deepEqual((await request('/api/supplier-payables')).data,baseline,'Business history must remain unchanged');
|
||||
const groups = procurementNavigation('admin',true);
|
||||
assert.equal(activeProcurementEntry(groups,'/requisicoes?status=draft&view=mine'),'my_drafts');
|
||||
assert.equal(activeProcurementEntry(groups,'/ordens/1/recepcoes'),'receipts');
|
||||
assert.equal(activeProcurementEntry(groups,'/requisicoes/1/cotacoes'),'quotations');
|
||||
assert.equal(activeProcurementEntry(groups,'/requisicoes/1/relatorio'),'quotations');
|
||||
assert.equal(activeProcurementEntry(groups,'/requisicoes/1/adjudicacao'),'orders');
|
||||
assert.equal(activeProcurementEntry(groups,'/stock/operacoes'),'stock_operations');
|
||||
await fs.writeFile('docs/proofs/menu-links.json',JSON.stringify({checked_at:new Date().toISOString(),results,
|
||||
request_detail_checked:!!requestId,invoice_count:invoices.data.props.invoices.data.length,
|
||||
process_links_checked:[...new Set(processLinks)],
|
||||
canonical_query_order:true,active_subroutes:true,business_history_unchanged:true,business_commands:0},null,2)+'\n');
|
||||
console.log('All visible menu links passed for six roles, with active entries, query filters, detail links and unchanged business history.');
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
// Every job is confined to the fresh disposable payables proof database.
|
||||
$settingsFile = 'C:/laragon/data/sgf-tempo/secrets/payables-proof-compras.json';
|
||||
$settings = json_decode(file_get_contents($settingsFile), true, 64, JSON_THROW_ON_ERROR);
|
||||
if (! preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $settings['DB_DATABASE'] ?? '') || ($settings['DB_HOST'] ?? '') !== '127.0.0.1') {
|
||||
throw new LogicException('Disposable proof only.');
|
||||
}
|
||||
foreach ($settings as $key => $value) {
|
||||
putenv($key.'='.$value);
|
||||
$_ENV[$key] = $_SERVER[$key] = $value;
|
||||
}
|
||||
require getcwd().'/vendor/autoload.php';
|
||||
$app = require getcwd().'/bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use App\Services\ExternalPaymentWorkflow;
|
||||
use App\Services\SupplierBankWorkflow;
|
||||
use App\Services\SupplierPayables;
|
||||
use Illuminate\Database\Events\QueryExecuted;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tempo\Finance\Payables\Payables;
|
||||
use Tempo\Finance\Payables\Workspace;
|
||||
|
||||
$identity = DB::selectOne('select current_database() as db, current_user as role');
|
||||
if ($identity->db !== $settings['DB_DATABASE'] || $identity->role !== 'payable_qa_'.substr($identity->db, -12)) {
|
||||
throw new LogicException('Proof identity mismatch.');
|
||||
}
|
||||
config(['common-payables.history_enabled' => true]);
|
||||
$preparer = User::where('email', 'preparacao-obrigacoes@example.test')->firstOrFail();
|
||||
$validator = User::where('email', 'validacao-obrigacoes@example.test')->firstOrFail();
|
||||
auth()->guard()->setUser($preparer);
|
||||
$invoice = Invoice::query()->sole();
|
||||
$service = app(SupplierPayables::class);
|
||||
$payments = app(ExternalPaymentWorkflow::class);
|
||||
$job = $argv[1] ?? '';
|
||||
function historyProofFingerprint(): string
|
||||
{
|
||||
$all = [];
|
||||
foreach (['invoices', 'invoice_items', 'external_payments', 'budget_entries', 'budget_consumptions', 'stock_movements'] as $table) {
|
||||
$all[$table] = DB::table($table)->orderBy('id')->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
return hash('sha256', Payables::canonical($all));
|
||||
}
|
||||
if ($job === 'setup') {
|
||||
if (DB::table('external_payments')->exists() || DB::table('pa_proposals')->exists()) {
|
||||
throw new LogicException('Fresh scenario required.');
|
||||
}
|
||||
$order = PurchaseOrder::query()->findOrFail($invoice->purchase_order_id);
|
||||
$issuer = User::findOrFail($order->issued_by);
|
||||
$supplier = Supplier::query()->findOrFail($invoice->supplier_id);
|
||||
auth()->guard()->setUser($issuer);
|
||||
$bank = app(SupplierBankWorkflow::class)->propose($issuer, $supplier, ['previous_id' => 0, 'bank_name' => 'Banco sintético', 'country' => 'MZ', 'currency' => 'MZN', 'beneficiary' => 'Titular sintético', 'account_identifier' => 'HISTORY-PROOF-ONLY', 'evidence_reference' => 'Documento sintético', 'reason' => 'Preparar histórico exclusivamente na base descartável']);
|
||||
foreach ([$preparer, $validator] as $reviewer) {
|
||||
auth()->guard()->setUser($reviewer);
|
||||
app(SupplierBankWorkflow::class)->review($reviewer, $bank, 'approved', 'Conferência sintética independente');
|
||||
}
|
||||
foreach ([['3000.00', 'confirmed'], ['1000.00', 'pending'], ['500.00', 'rejected']] as [$amount,$status]) {
|
||||
auth()->guard()->setUser($preparer);
|
||||
$payment = $payments->register($preparer, $invoice, ['supplier_bank_detail_id' => $bank->id, 'amount' => $amount, 'paid_on' => now()->format('Y-m-d'), 'external_reference' => 'HISTORY-'.$status, 'evidence_reference' => 'Prova sintética '.$status]);
|
||||
if ($status !== 'pending') {
|
||||
auth()->guard()->setUser($validator);
|
||||
$payments->review($validator, $payment, $status, 'Decisão original sintética');
|
||||
}
|
||||
}
|
||||
auth()->guard()->setUser($preparer);
|
||||
$proposal = $service->engine($preparer)->command('prepare_history', $invoice->id, 'Preparar passagem com histórico existente', (string) Str::uuid());
|
||||
if ($proposal['state'] !== 'prepared') {
|
||||
throw new RuntimeException('History proposal not ready.');
|
||||
}
|
||||
$result = ['invoice_id' => $invoice->id, 'proposal_id' => $proposal['proposal_id'], 'business_fingerprint' => historyProofFingerprint(), 'balance_before' => $payments->outstanding($invoice)];
|
||||
} elseif ($job === 'validate' || $job === 'fault') {
|
||||
auth()->guard()->setUser($validator);
|
||||
$proposal = DB::table('pa_proposals')->orderByDesc('id')->first();
|
||||
if ($job === 'fault') {
|
||||
$fail = true;
|
||||
DB::listen(function (QueryExecuted $event) use (&$fail) {
|
||||
if ($fail && str_contains($event->sql, 'insert into "pa_imported_payments"')) {
|
||||
$fail = false;
|
||||
throw new RuntimeException('HISTORY_PROOF_INJECTED_FAULT');
|
||||
}
|
||||
});
|
||||
try {
|
||||
$service->engine($validator)->command('validate_history', $proposal->id, 'Prova de recuperação atómica', (string) Str::uuid());
|
||||
throw new LogicException('Fault not injected.');
|
||||
} catch (RuntimeException $e) {
|
||||
if ($e->getMessage() !== 'HISTORY_PROOF_INJECTED_FAULT') {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$clean = DB::table('pa_obligations')->count() === 0 && DB::table('pa_history_imports')->count() === 0 && DB::table('pa_imported_payments')->count() === 0;
|
||||
if (! $clean) {
|
||||
throw new RuntimeException('Partial history import remained.');
|
||||
}
|
||||
$result = ['fault_rolled_back' => true, 'business_fingerprint' => historyProofFingerprint()];
|
||||
} else {
|
||||
$result = $service->engine($validator)->command('validate_history', $proposal->id, 'Validação concorrente independente', (string) Str::uuid());
|
||||
}
|
||||
} elseif ($job === 'inspect') {
|
||||
auth()->guard()->setUser($validator);
|
||||
$listing = $service->listing($validator);
|
||||
$position = $listing['obligations'][0];
|
||||
$html = Workspace::render($listing, ['host' => 'Prova PostgreSQL', 'actor' => 'Validador sintético', 'organisation' => 'Empresa sintética', 'api' => '/api/supplier-payables', 'csrf' => 'proof-only']);
|
||||
$result = ['principal_minor' => $service->principal($invoice), 'balance_minor' => $payments->outstanding($invoice),
|
||||
'confirmed_minor' => $position['confirmed_payments_minor'], 'pending_minor' => $position['pending_payments_minor'], 'rejected_minor' => $position['rejected_payments_minor'],
|
||||
'obligations' => DB::table('pa_obligations')->count(), 'imports' => DB::table('pa_history_imports')->count(), 'payment_links' => DB::table('pa_imported_payments')->count(),
|
||||
'business_fingerprint' => historyProofFingerprint(), 'ui_balance_correct' => str_contains($html, 'Saldo por liquidar: 8 600.00 MZN'),
|
||||
'due_on' => $position['due_on'], 'days_overdue' => $position['days_overdue']];
|
||||
$protected = [];
|
||||
foreach (['pa_history_imports', 'pa_imported_payments'] as $table) {
|
||||
try {
|
||||
DB::transaction(fn () => DB::table($table)->delete());
|
||||
throw new LogicException('History was deletable.');
|
||||
} catch (QueryException $e) {
|
||||
if (! str_contains($e->getMessage(), 'PA_APPEND_ONLY')) {
|
||||
throw $e;
|
||||
}$protected[] = $table;
|
||||
}
|
||||
}
|
||||
$result['append_only_tables'] = $protected;
|
||||
$db = DB::connection();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
DB::table('external_payments')->where('status', 'pending')->increment('amount_minor');
|
||||
try {
|
||||
$service->balance($invoice);
|
||||
throw new LogicException('Changed original history was accepted.');
|
||||
} catch (HttpException $e) {
|
||||
if ($e->getStatusCode() !== 409) {
|
||||
throw $e;
|
||||
}$result['changed_history_refused'] = true;
|
||||
}
|
||||
} finally {
|
||||
$db->rollBack();
|
||||
}
|
||||
if (historyProofFingerprint() !== $result['business_fingerprint']) {
|
||||
throw new RuntimeException('Original history not restored.');
|
||||
}
|
||||
} elseif (in_array($job, ['decision_fault', 'decision_confirm', 'decision_inspect'], true)) {
|
||||
config(['common-payables.history_decisions_enabled' => true]);
|
||||
auth()->guard()->setUser($validator);
|
||||
$payment = \App\Models\ExternalPayment::query()->where('amount_minor', 100000)->sole();
|
||||
$frozen = fn () => hash('sha256', Payables::canonical(['imports' => DB::table('pa_history_imports')->get()->toArray(), 'links' => DB::table('pa_imported_payments')->get()->toArray()]));
|
||||
$original = $frozen();
|
||||
if ($job === 'decision_fault') {
|
||||
$fingerprint = historyProofFingerprint();
|
||||
$auditCount = DB::table('audit_logs')->count();
|
||||
$fail = true;
|
||||
DB::listen(function (QueryExecuted $event) use (&$fail) {
|
||||
if ($fail && str_contains($event->sql, 'insert into "pa_payment_decisions"')) {
|
||||
$fail = false;
|
||||
throw new RuntimeException('DECISION_PROOF_FAULT');
|
||||
}
|
||||
});
|
||||
try {
|
||||
$payments->review($validator, $payment, 'confirmed', 'Prova de reversão atómica');
|
||||
throw new LogicException('Expected injected decision failure.');
|
||||
} catch (RuntimeException $e) {
|
||||
if ($e->getMessage() !== 'DECISION_PROOF_FAULT') {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
if ($payment->fresh()->status !== 'pending' || DB::table('pa_payment_decisions')->count() !== 0 || $auditCount !== DB::table('audit_logs')->count() || $fingerprint !== historyProofFingerprint()) {
|
||||
throw new LogicException('Partial decision remained.');
|
||||
}
|
||||
$result = ['rolled_back' => true, 'balance_minor' => $service->balance($invoice)];
|
||||
} elseif ($job === 'decision_confirm') {
|
||||
$payments->review($validator, $payment, 'confirmed', 'Conferência concorrente sintética');
|
||||
$result = ['balance_minor' => $service->balance($invoice), 'frozen' => $frozen(), 'original' => $original];
|
||||
} else {
|
||||
$position = $service->listing($validator)['obligations'][0];
|
||||
try {
|
||||
DB::transaction(fn () => DB::table('pa_payment_decisions')->delete());
|
||||
throw new LogicException('Decision was deletable.');
|
||||
} catch (QueryException $e) {
|
||||
if (! str_contains($e->getMessage(), 'PA_APPEND_ONLY')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$result = ['balance_minor' => $service->balance($invoice), 'confirmed_minor' => $position['confirmed_payments_minor'], 'pending_minor' => $position['pending_payments_minor'],
|
||||
'decisions' => DB::table('pa_payment_decisions')->count(), 'events' => DB::table('pa_events')->where('kind', 'payable.imported_payment_decided')->count(),
|
||||
'original_status' => $payment->fresh()->status, 'append_only' => true, 'frozen' => $original];
|
||||
}
|
||||
} else {
|
||||
throw new LogicException('Unknown proof job.');
|
||||
}
|
||||
echo json_encode($result,JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE).PHP_EOL;
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import {execFile} from 'node:child_process';
|
||||
import {promisify} from 'node:util';
|
||||
const run=promisify(execFile);
|
||||
const php='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe';
|
||||
const sgf='C:/laragon/www/sgf-tempo';
|
||||
for(const host of ['compras','sgf']) {
|
||||
const exists=await fs.access(`C:/laragon/data/sgf-tempo/secrets/payables-proof-${host}.json`).then(()=>true,()=>false);
|
||||
assert(!exists,'An existing proof must be reconciled before creating a new one.');
|
||||
}
|
||||
async function job(name) {
|
||||
const {stdout}=await run(php,['scripts/payable-history-pg.php',name],{cwd:process.cwd(),windowsHide:true,timeout:90000,maxBuffer:100000});
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
try {
|
||||
await run(php,[`${sgf}/scripts/payables-provision.php`,'--proof'],{cwd:sgf,windowsHide:true,timeout:180000,maxBuffer:100000});
|
||||
const before=await job('setup');
|
||||
const fault=await job('fault');
|
||||
assert(fault.fault_rolled_back);assert.equal(fault.business_fingerprint,before.business_fingerprint);
|
||||
const recognised=await Promise.all([job('validate'),job('validate')]);
|
||||
assert(recognised.every(result=>result.state==='recognised'));
|
||||
assert.equal(recognised[0].obligation_id,recognised[1].obligation_id);
|
||||
const after=await job('inspect');
|
||||
assert.equal(after.principal_minor,1160000);assert.equal(after.balance_minor,860000);
|
||||
assert.equal(after.confirmed_minor,300000);assert.equal(after.pending_minor,100000);assert.equal(after.rejected_minor,50000);
|
||||
assert.equal(after.obligations,1);assert.equal(after.imports,1);assert.equal(after.payment_links,3);
|
||||
assert.equal(after.business_fingerprint,before.business_fingerprint);
|
||||
assert(after.ui_balance_correct);assert(after.changed_history_refused);
|
||||
assert.equal(after.due_on,null);assert.equal(after.days_overdue,null);assert.equal(after.append_only_tables.length,2);
|
||||
if(process.argv.includes('--decisions')) {
|
||||
const fault=await job('decision_fault');
|
||||
assert(fault.rolled_back);assert.equal(fault.balance_minor,860000);
|
||||
const concurrent=await Promise.all([job('decision_confirm'),job('decision_confirm')]);
|
||||
assert(concurrent.every(r=>r.balance_minor===760000 && r.frozen===r.original));
|
||||
const final=await job('decision_inspect');
|
||||
assert.equal(final.balance_minor,760000);assert.equal(final.confirmed_minor,400000);assert.equal(final.pending_minor,0);
|
||||
assert.equal(final.decisions,1);assert.equal(final.events,1);assert.equal(final.original_status,'confirmed');assert(final.append_only);
|
||||
assert(concurrent.every(r=>r.frozen===final.frozen));
|
||||
await fs.writeFile('docs/proofs/payable-payment-decisions-pg.json',JSON.stringify({checked_at:new Date().toISOString(),passed:true,atomic_rollback:true,
|
||||
concurrent_decision_once:true,original_import_preserved:true,new_transfers_executed:false,scenario:final,visual_review:false},null,2)+'\n');
|
||||
}
|
||||
await fs.writeFile('docs/proofs/payable-history-pg.json',JSON.stringify({checked_at:new Date().toISOString(),passed:true,
|
||||
fault_recovery:true,concurrent_recognition:true,original_business_history_unchanged:true,new_transfers_executed:false,
|
||||
scenario:after,visual_review:false},null,2)+'\n');
|
||||
console.log('PostgreSQL historical import passed: gross principal, single deduction, pending/rejected preservation, concurrency, rollback and immutable history.');
|
||||
} finally {
|
||||
await run(php,[`${sgf}/scripts/payables-provision.php`,'--cleanup-proof'],{cwd:sgf,windowsHide:true,timeout:90000,maxBuffer:100000});
|
||||
}
|
||||
Reference in New Issue
Block a user