26 lines
9.2 KiB
PHP
26 lines
9.2 KiB
PHP
<?php
|
|
use App\Modules\Finance\CashWorkspace;use Illuminate\Support\Str;
|
|
require __DIR__.'/sgf-cash-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();$originalInvoices=$host->table('si_invoices')->orderBy('id')->get()->toJson();$originalFinancial=$host->table('sr_effects')->orderBy('id')->get()->toJson();
|
|
try{
|
|
$actor=$selectCash('read_only');$state=app(CashWorkspace::class)->listing($actor,[]);$ok($state['basis']['revision']===0&&$state['basis']['balance_minor']===0,'new bound QA cash book starts explicitly empty');
|
|
request()->attributes->set('sgf_module','commercial');$reject(fn()=>app(CashWorkspace::class)->listing($actor,[]),403,'Commercial cannot acquire Finance cash context');request()->attributes->set('sgf_module','finance');$reject(fn()=>app(CashWorkspace::class)->listing($actor,['company_id'=>1]),422,'cash context cannot be overridden by a filter');
|
|
$reject(fn()=>$cashPreview('read_only','prepare','open',['date'=>'2026-11-10','label'=>'Caixa QA','opening'=>'0.00','evidence'=>'Abertura sintética explícita, sem dinheiro real.']),403,'read-only cannot prepare cash');
|
|
[$open]=$cashCommand('finance_prepare','prepare','open',['date'=>'2026-11-10','label'=>'Caixa QA MZN','opening'=>'0.00','evidence'=>'Saldo de abertura QA explicitamente zero.']);$reject(fn()=>$cashCommand('finance_prepare','review','open',['decision'=>'approved'],$open['id']),403,'cash opener cannot self-review');$cashCommand('finance_review','review','open',['decision'=>'approved'],$open['id']);
|
|
$reject(fn()=>$cashPreview('finance_prepare','prepare','expense',['date'=>'2026-11-10','amount'=>'50.00','beneficiary'=>'Beneficiário QA','description'=>'Despesa fictícia sem saldo disponível.','evidence'=>'Comprovativo exclusivamente sintético.']),422,'expense requires available cash before approval');
|
|
$reject(fn()=>$cashPreview('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>1,'medium'=>'cash','evidence'=>'Origem não autorizada para Caixa QA.']),403,'unselected receipt refused without assigning cash automatically');
|
|
[$receipt]=$cashCommand('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>$cashBinding['receipt_id'],'medium'=>'cash','evidence'=>'Declaração explícita de custódia fictícia em numerário1000.']);$stale=$cashPreview('finance_review','review','receipt',['decision'=>'approved'],$receipt['id']);[$accepted]=$cashCommand('finance_review','review','receipt',['decision'=>'approved'],$receipt['id']);$actor=$selectCash('finance_review');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),409,'competing receipt conference refuses old cash version');$ok($accepted['impact']['after_minor']===100000,'distinct cash classification confirms1000 without another receipt');
|
|
$host->beginTransaction();try{$actor=$selectCash('finance_prepare');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),403,'different actor lacks the review authority');$actor=$selectCash('finance_review');request()->session()->put('company_context.token','changed-generation');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$stale['preview']),409,'changed generation cannot reuse a conference');}finally{$host->rollBack();}
|
|
$reject(fn()=>$cashPreview('finance_prepare','prepare','receipt',['date'=>'2026-11-10','receipt_id'=>$cashBinding['receipt_id'],'medium'=>'cash','evidence'=>'Não duplicar o mesmo recebimento QA.']),409,'same confirmed receipt cannot be classified twice');
|
|
[$expense]=$cashCommand('finance_prepare','prepare','expense',['date'=>'2026-11-10','amount'=>'50.00','beneficiary'=>'Beneficiário sintético QA','description'=>'Despesa fictícia de expediente50MZN.','evidence'=>'QA-DESPESA-50, não é comprovativo fiscal real.']);$pending=$cashPreview('finance_review','review','expense',['decision'=>'approved'],$expense['id']);
|
|
$host->unprepared("CREATE FUNCTION cash_test_fail() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''CASH_TEST_FAILURE''; END'");$host->unprepared('REVOKE ALL ON FUNCTION cash_test_fail() FROM PUBLIC');$host->unprepared('CREATE TRIGGER cash_test_fail BEFORE INSERT ON cash_commands FOR EACH ROW EXECUTE FUNCTION cash_test_fail()');$actor=$selectCash('finance_review');$n=$host->table('cash_events')->count();$failed=false;try{app(CashWorkspace::class)->confirm($actor,$pending['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('cash_events')->count()===$n,'failure after cash movement rolls back event and confirmation together');$host->unprepared('DROP TRIGGER cash_test_fail ON cash_commands');$host->unprepared('DROP FUNCTION cash_test_fail()');
|
|
$approved=app(CashWorkspace::class)->confirm($actor,$pending['preview']);$again=app(CashWorkspace::class)->confirm($actor,$pending['preview']);$ok($again['replayed']&&$again['id']===$approved['id']&&$approved['impact']['after_minor']===95000,'expense recovery confirms exactly50 leaving950 once');
|
|
[$badCount]=$cashCommand('finance_prepare','prepare','count',['date'=>'2026-11-10','counted'=>'940.00','evidence'=>'Contagem divergente QA, sem ajuste automático.']);$reject(fn()=>$cashCommand('finance_review','review','count',['decision'=>'approved'],$badCount['id']),409,'count discrepancy cannot be approved as reconciled');$cashCommand('finance_review','review','count',['decision'=>'refused'],$badCount['id']);
|
|
[$count]=$cashCommand('finance_prepare','prepare','count',['date'=>'2026-11-10','counted'=>'950.00','evidence'=>'Contagem QA950 conferida, sem movimentação de dinheiro.']);[$countReview]=$cashCommand('finance_review','review','count',['decision'=>'approved'],$count['id']);$reject(fn()=>$cashPreview('finance_review','close','count',[],$countReview['id']),409,'unposted cash origins block closing');
|
|
$original=$host->table('sd_outputs')->where('id',$cashBinding['source_output_id'])->first();$postCashOrigin('sales.invoice',$cashBinding['invoice_id']);$postCashOrigin('receivable.allocated',$original->effect_id);$cashJournal=$postCashOrigin('cash.receipt_classified',$accepted['id']);$expenseJournal=$postCashOrigin('cash.expense_confirmed',$approved['id']);$ok((int)$native->table('gl_journals')->where('id',$cashJournal['journal_id'])->value('debit_minor')===100000&&(int)$native->table('gl_journals')->where('id',$expenseJournal['journal_id'])->value('debit_minor')===5000,'same writer reclassifies1000 and recognises50 expense without duplicate income');
|
|
$close=$cashPreview('finance_review','close','count',[],$countReview['id']);$actor=$selectCash('finance_review');$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$actor,'permission'=>'cash.close'])->delete();$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$close['preview']),403,'revoked close right refuses an existing conference');}finally{$host->rollBack();}
|
|
$host->beginTransaction();try{$actor=$selectCash('finance_prepare');$origin=$host->table('sr_records')->where('id',$cashBinding['receipt_id'])->first();$revision=(int)$host->table('sr_effects')->where('obligation_id',$origin->obligation_id)->count();$sr=app(App\Modules\Services\ServiceReceivables::class);$request=$sr->propose($actor,['kind'=>'correction','invoice_id'=>$origin->invoice_id,'obligation_id'=>$origin->obligation_id,'currency'=>'MZN','source'=>'SIM-CASH-REVERSAL','external_id'=>'QA-REV-CASH-'.Str::uuid(),'revision'=>$revision,'action'=>'reverse_allocation','target_effect_id'=>$original->effect_id,'reason'=>'Reversão da origem apenas em prova de bloqueio da Caixa.'],(string)Str::uuid());$actor=$selectCash('finance_review');$sr->decide($request['id'],$actor,['version'=>1,'revision'=>$revision,'decision'=>'approved','reason'=>'Conferência distinta da reversão da origem da Caixa.'],(string)Str::uuid());$state=app(CashWorkspace::class)->listing($actor,[]);$ok($state['basis']===null&&$state['blocked']!==null,'reversed source is explicit blockage rather than zero or automatic cash refund');$reject(fn()=>app(CashWorkspace::class)->confirm($actor,$close['preview']),409,'source reversal refuses an already conferred cash closing');}finally{$host->rollBack();}
|
|
$actor=$selectCash('finance_review');$closed=app(CashWorkspace::class)->confirm($actor,$close['preview']);$ok($closed['state']==='closed'&&$closed['impact']['after_minor']===95000,'distinct reviewed count closes950 after both journal entries');$reject(fn()=>$cashPreview('finance_prepare','prepare','open',['date'=>'2026-11-10','label'=>'Reabrir','opening'=>'0.00','evidence'=>'Reabertura não autorizada e recusada.']),409,'closed cash session cannot reopen');
|
|
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$host->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok($originalInvoices===$host->table('si_invoices')->orderBy('id')->get()->toJson()&&$originalFinancial===$host->table('sr_effects')->orderBy('id')->get()->toJson(),'cash does not change original invoices, receipts, allocations or debt');
|
|
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
|
|
file_put_contents($work.'/cash-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'real_money'=>false,'browser_visual_review'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/cash-result.json']).PHP_EOL;
|