62 lines
14 KiB
PHP
62 lines
14 KiB
PHP
<?php
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use App\Modules\Services\ServiceReceivables;
|
|
use App\Modules\Purchasing\SupplierPayables;
|
|
use Tempo\Finance\Payables\Payables;
|
|
$costG=$g;$cg=json_decode(file_get_contents($s['root'].'/cost-correction-fixture.json'),true,64,JSON_THROW_ON_ERROR);
|
|
$preserved=[];foreach(['wo_orders','wo_entries','wo_decisions','dc_rates','dc_calculations','dc_decisions','dc_outbox','permission_grants'] as $table)$preserved[$table]=hash('sha256',DB::table($table)->orderBy($table==='dc_outbox'?'event_id':'id')->get()->toJson());
|
|
$g['invoice_id']=$cf['invoice_id'];$g['accounts']=$cf['sales_accounts'];$sr=app(ServiceReceivables::class);
|
|
|
|
if(file_exists($s['root'].'/composition-workflow.json')||DB::table('sr_effects')->exists()||DB::table('gl_journals')->count()!==(config('industrial_ledger.correction_enabled')===true?4:(config('industrial_ledger.synthetic_enabled')===true?2:1)))throw new LogicException('Fresh unconsumed sales workflow required');
|
|
$checks=0;$ok=function($v,$why)use(&$checks){$checks++;if(!$v)throw new LogicException($why);};
|
|
$originHash=hash('sha256',DB::table('si_invoices')->where('id',$g['invoice_id'])->value('snapshot'));
|
|
$p=$sr->propose($enter('prepare')->id,['kind'=>'obligation','invoice_id'=>$g['invoice_id'],'currency'=>'MZN','source'=>'TEST-MANUAL','external_id'=>'SALES-QA-OPEN','revision'=>0,'reason'=>'Explicit financial opening of the actual test invoice.'],(string)Str::uuid());$ob=$p['id'];
|
|
$sr->decide($ob,$enter('validate')->id,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Independent documentary opening review.'],(string)Str::uuid());
|
|
$post=function($kind,$id,$rule)use($command){$p=$command('prepare','effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>$rule,'reason'=>'Capture exact synthetic commercial financial effect.']);$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent exact commercial accounting review.']);$body=['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Apply reviewed commercial effect once.'];$key=(string)Str::uuid();$j=$command('prepare','post',$body,$key);return [$j,$command('prepare','post',$body,$key)];};
|
|
[$invoiceJournal,$repeat]=$post('sales.invoice',$g['invoice_id'],'QA-SALES');$ok($repeat['replayed']&&DB::table('gl_lines')->count()===(config('industrial_ledger.correction_enabled')===true?11:(config('industrial_ledger.synthetic_enabled')===true?7:5)),'Three-line invoice posted once');
|
|
$allocations=[];$receipts=[];$rev=fn()=>DB::table('sr_effects')->where('obligation_id',$ob)->count();
|
|
foreach(['400.00','600.00'] as $n=>$amount){
|
|
$p=$sr->propose($enter('prepare')->id,['kind'=>'receipt','invoice_id'=>$g['invoice_id'],'obligation_id'=>$ob,'currency'=>'MZN','amount'=>$amount,'date'=>'2026-10-29','evidence'=>'Explicit synthetic receipt; no money movement.','source'=>'TEST-MANUAL','external_id'=>'SALES-QA-RECEIPT-'.$n,'revision'=>$rev(),'reason'=>'Individual receipt preparation.'],(string)Str::uuid());$receipts[]=$p['id'];
|
|
$sr->decide($p['id'],$enter('validate')->id,['version'=>1,'revision'=>$rev(),'decision'=>'approved','reason'=>'Independent receipt confirmation.'],(string)Str::uuid());
|
|
$a=$sr->allocate($p['id'],$enter('prepare')->id,['version'=>1,'revision'=>$rev(),'amount'=>$amount],(string)Str::uuid());$allocations[]=$a['id'];
|
|
$position=$sr->position($ob,$enter('read')->id);$ok($position['balance']===($n===0?'600.00':'0.00'),'Authoritative partial/full balance');
|
|
$post('receivable.allocated',$a['id'],'QA-RECEIVABLE');$ok($sr->position($ob,$enter('read')->id)['balance']===$position['balance'],'Accounting never deducts financial balance again');
|
|
}
|
|
$p=$sr->propose($enter('prepare')->id,['kind'=>'correction','invoice_id'=>$g['invoice_id'],'obligation_id'=>$ob,'currency'=>'MZN','source'=>'TEST-MANUAL','external_id'=>'SALES-QA-REVERSE-400','revision'=>$rev(),'action'=>'reverse_allocation','target_effect_id'=>$allocations[0],'reason'=>'Reverse first synthetic allocation with original preserved.'],(string)Str::uuid());
|
|
$reversal=$sr->decide($p['id'],$enter('validate')->id,['version'=>1,'revision'=>$rev(),'decision'=>'approved','reason'=>'Independent integral allocation reversal review.'],(string)Str::uuid());
|
|
$ok($sr->position($ob,$enter('read')->id)['balance']==='400.00','Financial reversal restores400');$post('receivable.reversed',$reversal['id'],'QA-RECEIVABLE-REVERSE');
|
|
$ok($sr->position($ob,$enter('read')->id)['balance']==='400.00','Reversal journal does not affect financial balance');
|
|
$p=$command('prepare','prepare',['obligation_id'=>$cf['supplier_obligation_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>'QA-PAYABLE-JOURNAL','reason'=>'Capture original approved supplier60 in combined QA book.']);
|
|
$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Distinct supplier journal review.']);
|
|
$command('prepare','post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Apply original supplier60 once.']);
|
|
$p=$command('prepare','correct-prepare',['journal_id'=>$cg['original_journal_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','reason'=>'Previously authorised indivisible cost275 to295 in combined book.']);
|
|
$command('validate','correct-validate',['correction_id'=>$p['correction_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Distinct exact original275 and successor295 pair review.']);
|
|
$pairBody=['correction_id'=>$p['correction_id'],'revision'=>$p['revision'],'reason'=>'Apply both cost correction parts atomically.'];$pairKey=(string)Str::uuid();$pair=$command('prepare','correct-post',$pairBody,$pairKey);$again=$command('prepare','correct-post',$pairBody,$pairKey);$other=$command('prepare','correct-post',$pairBody);
|
|
$ok($again['replayed']&&$pair['result_id']===$other['result_id']&&DB::table('gl_correction_results')->count()===(config('industrial_ledger.correction_enabled')===true?2:1),'Cost correction replay never duplicates a pair');
|
|
$p=$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Capture full commercial cycle and pending evidence.']);$command('validate','period-validate',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent reconciled sales period review.']);$closeBody=['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Close artificial commercial period once.'];$closeKey=(string)Str::uuid();
|
|
foreach(array_merge(['service_receivables.view','direct_costs.view','supplier_payables.view'],config('industrial_ledger.synthetic_enabled')===true?['industrial_costs.view']:[]) as $right){DB::beginTransaction();try{DB::table('permission_grants')->where($scope)->where('user_id',$costG['actors']['prepare'])->where('permission',$right)->delete();$caught=false;try{$command('prepare','period-close',$closeBody,$closeKey);}catch(DomainException|Symfony\Component\HttpKernel\Exception\HttpException|Illuminate\Auth\Access\AuthorizationException){$caught=true;}$ok($caught&&!DB::table('gl_period_closures')->exists(),'Joint close revalidates provider permission: '.$right);}finally{DB::rollBack();}}
|
|
DB::beginTransaction();try{
|
|
$u=$enter('prepare');$r=$dc->rate($costG['order_id'],$u->id,['entry_id'=>$costG['material_id'],'entry_version'=>1,'identifier'=>'QA-COST-MATERIAL','version'=>3,'previous_id'=>$cg['replacement_rate_id'],'currency'=>'MZN','unit'=>'UN','amount'=>'60.00','evidence'=>'Rollback-only same-value successor to test joint version conference.'],(string)Str::uuid());
|
|
$dc->decide($costG['order_id'],$enter('validate')->id,['kind'=>'rate','id'=>$r['id'],'version'=>3,'decision'=>'approved','reason'=>'Rollback-only distinct successor rate review.'],(string)Str::uuid());
|
|
$calc=$dc->calculate($costG['order_id'],$enter('prepare')->id,['version'=>3,'previous_id'=>$cg['replacement_calculation_id'],'currency'=>'MZN','lines'=>[['entry_id'=>$costG['material_id'],'entry_version'=>1,'rate_id'=>$r['id'],'rate_version'=>3],['entry_id'=>$costG['time_id'],'entry_version'=>1,'rate_id'=>$costG['rate_ids']['time'],'rate_version'=>1]]],(string)Str::uuid());
|
|
$dc->decide($costG['order_id'],$enter('validate')->id,['kind'=>'calculation','id'=>$calc['id'],'version'=>3,'decision'=>'approved','reason'=>'Rollback-only same amount295 with new authoritative version.'],(string)Str::uuid());
|
|
$caught=false;try{$command('prepare','period-close',$closeBody,$closeKey);}catch(DomainException){$caught=true;}$ok($caught&&!DB::table('gl_period_closures')->exists(),'Joint close refuses changed cost version even at same amount295');
|
|
}finally{DB::rollBack();}
|
|
if(config('industrial_ledger.synthetic_enabled')===true){DB::beginTransaction();try{$industrial=app(App\Modules\Industry\IndustrialCosts::class);$fixture=json_decode(file_get_contents($s['root'].'/industrial-fixture.json'),true);$last=DB::table('ic_inputs')->where('lot_id',$fixture['lot_id'])->where('kind','material_rate')->orderByDesc('version')->first();$r=$industrial->input($fixture['lot_id'],$enter('prepare')->id,['kind'=>'material_rate','source_ref'=>$last->source_ref,'version'=>$last->version+1,'previous_id'=>$last->id,'data'=>json_decode($last->data,true),'evidence'=>'Rollback-only same-value industrial version before joint close.'],(string)Str::uuid());$industrial->decide($fixture['lot_id'],$enter('validate')->id,['type'=>'input','id'=>$r['id'],'version'=>$last->version+1,'decision'=>'approved','reason'=>'Distinct same-value industrial successor; new conference required.'],(string)Str::uuid());$caught=false;try{$command('prepare','period-close',$closeBody,$closeKey);}catch(DomainException){$caught=true;}$ok($caught&&!DB::table('gl_period_closures')->exists(),'Joint close revalidates industrial dependency version at same amount');}finally{DB::rollBack();}}
|
|
if(isset($compositionCloseRace))$compositionCloseRace($closeBody);
|
|
$closed=$command('prepare','period-close',$closeBody,$closeKey);$closedRepeat=$command('prepare','period-close',$closeBody,$closeKey);$closedOther=$command('prepare','period-close',$closeBody);$ok($closedRepeat['replayed']&&$closedOther['closure_id']===$closed['closure_id']&&DB::table('gl_period_closures')->count()===1,'Recover after rejected stale/missing-right close without duplication');
|
|
$book=$ledger->listing($enter('read'));$ok($book['totals']===['debit_minor'=>(config('industrial_ledger.correction_enabled')===true?872500:(config('industrial_ledger.synthetic_enabled')===true?510500:330500)),'credit_minor'=>(config('industrial_ledger.correction_enabled')===true?872500:(config('industrial_ledger.synthetic_enabled')===true?510500:330500)),'net_minor'=>0]&&count($book['journals'])===(config('industrial_ledger.correction_enabled')===true?11:(config('industrial_ledger.synthetic_enabled')===true?9:8))&&count($book['lines'])===(config('industrial_ledger.correction_enabled')===true?23:(config('industrial_ledger.synthetic_enabled')===true?19:17)),'Eight journals/seventeen lines reconcile to gross3305 each side');
|
|
$client=$ledger->listing($enter('read'),['account_id'=>$g['accounts']['QA-CLIENTES']]);$ok($client['totals']['net_minor']===40000,'Client account agrees with financial balance400');
|
|
$ok($originHash===hash('sha256',DB::table('si_invoices')->where('id',$g['invoice_id'])->value('snapshot')),'Original issued invoice remains immutable');DB::select('SELECT gl_assert_balanced(id) FROM gl_journals');
|
|
$proof=(['passed'=>true,'checks'=>$checks,'invoice_id'=>$g['invoice_id'],'obligation_id'=>$ob,'receipts'=>$receipts,'allocations'=>$allocations,'reversal_effect_id'=>$reversal['id'],'invoice_journal_id'=>$invoiceJournal['journal_id'],'journals'=>(config('industrial_ledger.correction_enabled')===true?11:(config('industrial_ledger.synthetic_enabled')===true?9:8)),'lines'=>(config('industrial_ledger.correction_enabled')===true?23:(config('industrial_ledger.synthetic_enabled')===true?19:17)),'gross_debit_minor'=>(config('industrial_ledger.correction_enabled')===true?872500:(config('industrial_ledger.synthetic_enabled')===true?510500:330500)),'gross_credit_minor'=>(config('industrial_ledger.correction_enabled')===true?872500:(config('industrial_ledger.synthetic_enabled')===true?510500:330500)),'client_balance_minor'=>40000,'financial_balance_minor'=>40000,'period_closed'=>true,'origin_unchanged'=>true]);
|
|
|
|
foreach($preserved as $table=>$hash)$ok(hash('sha256',DB::table($table)->orderBy($table==='dc_outbox'?'event_id':'id')->get()->toJson())===$hash,'Source/outbox/rights unchanged: '.$table);
|
|
$ok(hash('sha256',Payables::canonical((array)DB::table('gl_journals')->where('id',$cg['original_journal_id'])->first()))===$cg['original_journal_sha256'],'Original cost journal immutable');
|
|
$ok($ledger->report($enter('read'),'account-ledger',['from_on'=>'2026-10-01','to_on'=>'2026-10-31','account_id'=>$costG['accounts']['QA-CUSTO-DIRECTO']])['closing_net_minor']===29500,'Cost account reconciles295');
|
|
$ok(app(SupplierPayables::class)->engine($enter('read'))->positionForSource($cf['supplier_source_id'])['balance_minor']===6000,'Supplier debt unchanged60');
|
|
foreach(['sales_invoice_id'=>[$cf['invoice_id'],4,240000],'cost_order_id'=>[$costG['order_id'],3,84500],'obligation_id'=>[$cf['supplier_obligation_id'],1,6000]] as $filter=>[$id,$count,$amount]){$book=$ledger->listing($enter('read'),[$filter=>$id]);$ok(count($book['journals'])===$count&&$book['totals']['debit_minor']===$amount,'Typed source filter: '.$filter);}
|
|
$workspace=app(App\Modules\Accounting\LedgerComposition::class)->workspace($enter('read'));$ok($workspace['all_current_source_views']&&$workspace['book']['accounts']===[]&&$workspace['period']->status==='closed','Unified read workspace and joint closed period');
|
|
$proof+=['combined_checks'=>$checks,'source_outbox_rights_preserved'=>true,'supplier_balance_minor'=>6000,'cost_balance_minor'=>29500,'typed_source_filters_separate'=>true,'joint_period_closed'=>true];file_put_contents($s['root'].'/composition-workflow.json',json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
|
|
|
echo 'Combined composition: '.$checks." checks, journal totals reconciled and joint QA close.\n"; |