From 9962084087626aaad5918794da135eb06f1fdc87 Mon Sep 17 00:00:00 2001 From: nala Date: Tue, 27 Oct 2026 13:57:09 +0200 Subject: [PATCH] commit2 --- AGENTS.md | 4 + README.md | 4 + .../Http/IndustrialCostsController.php | 13 ++ app/Modules/Industry/IndustrialCosts.php | 133 ++++++++++++++++++ app/Modules/Workshop/DirectCosts.php | 4 +- app/Shared/Costs/ExactCostMath.php | 10 ++ ...0000_create_industrial_cost_simulation.php | 16 +++ database/sql/industrial-costs.sql | 106 ++++++++++++++ docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md | 68 +++++++++ docs/INDUSTRIAL_COST_DELIVERY.md | 89 ++++++++++++ docs/WORKSHOP_DIRECT_COST_DELIVERY.md | 12 ++ docs/proofs/industrial-cost-1800-browser.png | Bin 0 -> 66412 bytes docs/proofs/industrial-cost-800-browser.png | Bin 0 -> 71287 bytes .../industrial-cost-browser-results.json | 35 +++++ docs/proofs/industrial-cost-concurrency.json | 14 ++ docs/proofs/industrial-cost-tests.json | 22 +++ resources/js/IndustrialCostsWorkspace.jsx | 33 +++++ resources/js/app.jsx | 4 +- routes/web.php | 5 + scripts/direct-cost-dev-install.php | 100 +++++++++++++ scripts/industrial-cost-concurrency.php | 26 ++++ scripts/industrial-cost-fixture.php | 7 + .../industrial-cost-permissions-preview.php | 12 ++ scripts/industrial-cost-proof.php | 10 ++ scripts/industrial-cost-worker.php | 7 + tests/Feature/IndustrialCostsTest.php | 74 ++++++++++ tests/Support/IndustrialCostFixture.php | 18 +++ 27 files changed, 823 insertions(+), 3 deletions(-) create mode 100644 app/Modules/Industry/Http/IndustrialCostsController.php create mode 100644 app/Modules/Industry/IndustrialCosts.php create mode 100644 app/Shared/Costs/ExactCostMath.php create mode 100644 database/migrations/2026_10_27_100000_create_industrial_cost_simulation.php create mode 100644 database/sql/industrial-costs.sql create mode 100644 docs/INDUSTRIAL_COST_DELIVERY.md create mode 100644 docs/proofs/industrial-cost-1800-browser.png create mode 100644 docs/proofs/industrial-cost-800-browser.png create mode 100644 docs/proofs/industrial-cost-browser-results.json create mode 100644 docs/proofs/industrial-cost-concurrency.json create mode 100644 docs/proofs/industrial-cost-tests.json create mode 100644 resources/js/IndustrialCostsWorkspace.jsx create mode 100644 scripts/direct-cost-dev-install.php create mode 100644 scripts/industrial-cost-concurrency.php create mode 100644 scripts/industrial-cost-fixture.php create mode 100644 scripts/industrial-cost-permissions-preview.php create mode 100644 scripts/industrial-cost-proof.php create mode 100644 scripts/industrial-cost-worker.php create mode 100644 tests/Feature/IndustrialCostsTest.php create mode 100644 tests/Support/IndustrialCostFixture.php diff --git a/AGENTS.md b/AGENTS.md index 0d9f31f..786e22c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,9 @@ # SGF Tempo — instruções locais +- Autorização técnica de 27/10/2026: `INDUSTRIAL-COST-SIM-v1`, secções 15/15.5, só em `sgf_tempo_test`. Ver `docs/INDUSTRIAL_COST_DELIVERY.md`. Um lote/produto sintéticos, material 1000 + trabalho 500 + CIF 300 = 1800, 10 UN / 180 por UN; conjunto 600, capacidade 10 HM, uso 5 HM e residual 300 separado. Sem taxa material, 800 conhecidos, total/unitário indisponíveis. Critério é hipótese sintética, não método real aprovado. Preparação/validação industrial explícitas por identidades distintas; não herdar custo directo, transformar oficina em lote ou duplicar origens. `100000` e quatro concessões reais apenas propostas, NÃO aplicar sem confirmação específica. Preservar checkpoints/alterações/aprovações/bloqueios. Sem aceitação presumida, checkpoint de encerramento, stock, previsto/realizado, margens, lançamentos ou publicação. + +- Autorização específica posterior de 27/10/2026 executada: apenas `090000` aplicada em `sgf_tempo_dev`, por caminho explícito e proprietário separado, e quatro concessões auditadas no tenant #1 / Tempo Lda #1 / SEGI #1: Osmane #1 `direct_costs.view/prepare`; Samira #2 `direct_costs.view/validate`. Ver actualização de `docs/WORKSHOP_DIRECT_COST_DELIVERY.md`. Preservadas 60 tabelas anteriores, dados/acessos e instalações `070000`/`080000`; seis tabelas de custo vazias. Aplicação mantém `sgf_dev_app`; custeio, oficinas, recebimentos e emissão reais bloqueados e guardas SQL verificadas. Sem taxas/cálculos reais, novas concessões técnicas ou publicação. As indicações anteriores de `090000`/quatro concessões pendentes são históricas; não activar uso operacional por consequência desta instalação. + - Encerramento por delegação de 27/10/2026: o utilizador autorizou aceitar a entrega isolada de custos directos após conferência técnica do percurso e preservar checkpoint local. Ver `docs/WORKSHOP_DIRECT_COST_ACCEPTANCE.md`. Não se atribui nova verificação pessoal no navegador ao utilizador. `090000` e quatro concessões continuam NÃO aplicados; custeio real, emissão e restantes operações reais bloqueados. Sem CIF/lote industrial ou publicação. Esta autorização substitui apenas a pendência de encerramento técnico deste ensaio. - Autorização técnica de 27/10/2026: implementar apenas `WORKSHOP-DIRECT-COST-SIM-v1`, secção 14 de `docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md`. Ver `docs/WORKSHOP_DIRECT_COST_DELIVERY.md`. Apenas sintético em `sgf_tempo_test`, origens de oficina verificadas por ID/versão, sem duplicar acontecimentos. 275 MZN conhecidos ou 175 incompletos sem taxa material; exactidão racional, nunca custo completo/margem. Preparação e validação de custos/taxas por identidades distintas, sem editar apontamentos. `090000` e quatro concessões reais apenas propostos, NÃO aplicados; confirmação específica antes de aplicação. Não activar custeio real ou publicar; preservar `7628645`, `ea9dcb3`, alterações posteriores, oficinas/recebimentos/fiscalidade e aprovações. Sem presumir aceitação, CIF ou lote industrial. diff --git a/README.md b/README.md index 249ccfa..6586d6d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # SGF TEMPO +**Ensaio industrial (`INDUSTRIAL-COST-SIM-v1`):** lote/produto sintéticos com composição exacta, imputação por capacidade explícita, residual separado e variante incompleta. Percurso/provas e propostas pendentes em [INDUSTRIAL_COST_DELIVERY.md](docs/INDUSTRIAL_COST_DELIVERY.md). Exclusivamente `sgf_tempo_test`; `100000` e direitos industriais reais não aplicados. Custeio real e restantes operações reais continuam bloqueados; sem stock ou lançamentos. + +**Actualização posterior de custos directos:** `090000` e os quatro direitos de consulta/preparação/validação foram instalados tecnicamente por autorização específica, com tabelas de custo vazias e custeio real bloqueado. Ver a secção de instalação em [WORKSHOP_DIRECT_COST_DELIVERY.md](docs/WORKSHOP_DIRECT_COST_DELIVERY.md); as menções anteriores a essa pendência são históricas. + **Estado posterior em 27/10/2026:** oficinas e recebimentos sintéticos foram aceites; `080000`/quatro concessões de abertura e `070000`/oito concessões foram instaladas tecnicamente por autorizações específicas, sem retirar os bloqueios reais. As indicações de pendência nos parágrafos históricos abaixo foram substituídas pelos relatórios actualizados. O ensaio de custo directo e as suas pendências estão em [WORKSHOP_DIRECT_COST_DELIVERY.md](docs/WORKSHOP_DIRECT_COST_DELIVERY.md); `090000` e quatro concessões de custos não foram aplicadas. **Oficina de teste (`WORKSHOP-RESOURCE-RECORD-v1`):** ordem, responsável, autorização por versão da tarefa, apontamentos de tempos/materiais e verificação distinta do autor. Só funciona em instância isolada ligada a `sgf_tempo_test`. Abrir a ordem não autoriza executar; ocorrência passada pode ser relatada e reconhecida apenas para auditoria, conservando a marca de fora do âmbito. Comandos, percurso e provas em [WORKSHOP_RESOURCE_DELIVERY.md](docs/WORKSHOP_RESOURCE_DELIVERY.md). Migração `080000` e concessões reais não aplicadas; técnico/verificador reais ainda não indicados. A migração `070000` também permanece pendente: não usar migração geral para aplicar ambas sem autorização específica. Sem stock, custos, facturação ou utilização operacional. Não confundir esta entrega técnica com aceitação do utilizador ou de recebimentos. diff --git a/app/Modules/Industry/Http/IndustrialCostsController.php b/app/Modules/Industry/Http/IndustrialCostsController.php new file mode 100644 index 0000000..e0b0427 --- /dev/null +++ b/app/Modules/Industry/Http/IndustrialCostsController.php @@ -0,0 +1,13 @@ +json($d)->header('Cache-Control','no-store, private');} + public function index(Request $r,IndustrialCosts $c){return $this->response($c->listing($r->user()->id));} + public function open(Request $r,IndustrialCosts $c){return $this->response($c->open($r->user()->id,$r->all(),(string)$r->header('Idempotency-Key')));} + public function input(Request $r,int $id,IndustrialCosts $c){return $this->response($c->input($id,$r->user()->id,$r->all(),(string)$r->header('Idempotency-Key')));} + public function calculate(Request $r,int $id,IndustrialCosts $c){return $this->response($c->calculate($id,$r->user()->id,$r->all(),(string)$r->header('Idempotency-Key')));} + public function decide(Request $r,int $id,IndustrialCosts $c){return $this->response($c->decide($id,$r->user()->id,$r->all(),(string)$r->header('Idempotency-Key')));} +} diff --git a/app/Modules/Industry/IndustrialCosts.php b/app/Modules/Industry/IndustrialCosts.php new file mode 100644 index 0000000..2e70bb7 --- /dev/null +++ b/app/Modules/Industry/IndustrialCosts.php @@ -0,0 +1,133 @@ +$this->canonical($x),$v);} + private function exact(array $in,array $rules): array {abort_if(array_diff(array_keys($in),array_keys($rules)),422,'Campo fora do ensaio industrial.');return Validator::make($in,$rules)->validate();} + private function decimal(bool $positive=false): array {return ['bail','required','string','regex:/^(?:0|[1-9][0-9]{0,15})(?:\.[0-9]{1,8})?$/D',function($a,$v,$fail)use($positive){if($positive&&Number::of($v)->isLessThanOrEqualTo(0))$fail('Quantidade/capacidade deve ser positiva.');}];} + private function scope(int $actor,string $right): array { + $s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']!==null,403); + $this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + foreach(['view',$right] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'industrial_costs.'.$r,$s['unit_id']);return $s; + } + private function lot(int $id,array $s): object {return DB::table('ic_lots')->where($s)->where('id',$id)->first()??abort(403,'Lote fora do âmbito autorizado.');} + private function command(?int $lot,int $actor,string $action,array $in,string $key,callable $write): array { + self::guard();$right=$action==='decide'?'validate':'prepare';$s=$this->scope($actor,$right);if($lot)$this->lot($lot,$s); + abort_unless(preg_match('/^[A-Za-z0-9_-]{8,100}$/D',$key),422,'Chave de idempotência necessária.');$hash=hash('sha256',$this->json($this->canonical(['lot_id'=>$lot,'input'=>$in]))); + return DB::transaction(function()use($lot,$actor,$action,$right,$s,$key,$hash,$write){ + DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();$this->scope($actor,$right);if($lot)$this->lot($lot,$s); + $old=DB::table('ic_commands')->where($s)->where(['actor_id'=>$actor,'action'=>$action,'command_key'=>$key])->first(); + if($old){$this->lot($old->lot_id,$s);abort_unless(hash_equals($old->request_hash,$hash),409,'Repetição com conteúdo diferente.');return json_decode($old->result,true,64,JSON_THROW_ON_ERROR)+['replayed'=>true];} + $out=$write($s);$cmd=DB::table('ic_commands')->insertGetId($s+['lot_id'=>$out['lot_id'],'actor_id'=>$actor,'action'=>$action,'command_key'=>$key,'request_hash'=>$hash,'result'=>$this->json($out),'created_at'=>now()]); + Audit::record('industrial_costs.'.$action,$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version']]); + $event=DB::table('ic_events')->insertGetId(['command_id'=>$cmd,'schema_version'=>1,'payload'=>$this->json(['policy'=>self::POLICY,'scope'=>$s,'actor_id'=>$actor,'action'=>$action,'result'=>$out]),'created_at'=>now()]); + DB::table('ic_outbox')->insert(['event_id'=>$event,'state'=>'captured_no_stock_or_accounting']);return $out+['replayed'=>false]; + },3); + } + public function open(int $actor,array $in,string $key): array { + self::guard();$d=$this->exact($in,['reference'=>'required|string|min:3|max:100','product_reference'=>'required|string|min:3|max:100','product_name'=>'required|string|min:3|max:200','centre'=>'required|string|min:3|max:100','period_start'=>'required|date_format:Y-m-d','period_end'=>'required|date_format:Y-m-d|after_or_equal:period_start','currency'=>'required|in:MZN']); + return $this->command(null,$actor,'open',$d,$key,function($s)use($actor,$d){ + abort_if(DB::table('ic_lots')->where('company_id',$s['company_id'])->where('reference',$d['reference'])->exists(),409,'Identidade de lote já usada.'); + abort_if(DB::table('ic_products')->where('company_id',$s['company_id'])->where('reference',$d['product_reference'])->exists(),409,'Este recorte cria um produto próprio por lote; identidade já usada.'); + $product=DB::table('ic_products')->insertGetId($s+['reference'=>$d['product_reference'],'name'=>$d['product_name'],'version'=>1,'unit'=>'UN','actor_id'=>$actor,'created_at'=>now()]); + $id=DB::table('ic_lots')->insertGetId($s+['product_id'=>$product,'reference'=>$d['reference'],'centre'=>$d['centre'],'period_start'=>$d['period_start'],'period_end'=>$d['period_end'],'currency'=>$d['currency'],'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'lot_id'=>$id,'version'=>1,'kind'=>'lot']; + }); + } + private function row(string $type,int $id,int $lot): object {return DB::table($type==='input'?'ic_inputs':'ic_results')->where(['id'=>$id,'lot_id'=>$lot])->first()??abort(403,'Origem/resultado fora do lote.');} + private function current(string $type,object $r): bool {return !DB::table($type==='input'?'ic_inputs':'ic_results')->where('previous_id',$r->id)->exists();} + private function approvedInput(int $id,int $version,int $lot,?string $kind=null): object { + $r=$this->row('input',$id,$lot);abort_unless($r->version===$version&&($kind===null||$r->kind===$kind),409,'Tipo/versão incompatível.'); + abort_unless($this->current('input',$r)&&DB::table('ic_decisions')->where(['input_id'=>$id,'version'=>$version,'decision'=>'approved'])->exists(),409,'Origem/taxa/critério exige versão actual validada.');$r->data=json_decode($r->data,true,64,JSON_THROW_ON_ERROR);return $r; + } + private function validateData(string $kind,array $data,object $lot): array { + if(in_array($kind,['material','labour','pool','machine','production'])){ + $unit=match($kind){'material'=>'KG','labour'=>'MIN','pool'=>'MZN','machine'=>'HM','production'=>'UN'}; + $rules=['quantity'=>$this->decimal($kind!=='pool'),'unit'=>'required|in:'.$unit,'event_date'=>'required|date_format:Y-m-d'];if($kind==='production')$rules['completed']='required|accepted';$d=$this->exact($data,$rules); + abort_unless($d['event_date']>=$lot->period_start&&$d['event_date']<=$lot->period_end,422,'Origem fora do período.');return $d; + } + if(in_array($kind,['material_rate','labour_rate'])){ + $d=$this->exact($data,['source_id'=>'required|integer|min:1','source_version'=>'required|integer|min:1','amount'=>$this->decimal(),'currency'=>'required|in:MZN','unit'=>'required|in:'.($kind==='material_rate'?'KG':'H')]); + $this->approvedInput($d['source_id'],$d['source_version'],$lot->id,str_replace('_rate','',$kind));return $d; + } + $d=$this->exact($data,['pool_id'=>'required|integer|min:1','pool_version'=>'required|integer|min:1','machine_id'=>'required|integer|min:1','machine_version'=>'required|integer|min:1','capacity'=>$this->decimal(true),'driver_unit'=>'required|in:HM','hypothesis'=>'required|in:synthetic_only']); + $this->approvedInput($d['pool_id'],$d['pool_version'],$lot->id,'pool');$this->approvedInput($d['machine_id'],$d['machine_version'],$lot->id,'machine');return $d; + } + public function input(int $lot,int $actor,array $in,string $key): array { + self::guard();$d=$this->exact($in,['kind'=>'required|in:'.implode(',',self::KINDS),'source_ref'=>['required','string','max:100','regex:/^IND-TEST-[A-Za-z0-9_-]+$/D'],'version'=>'required|integer|min:1','previous_id'=>'nullable|integer|min:1','data'=>'required|array','evidence'=>'required|string|min:3|max:1000']); + return $this->command($lot,$actor,'input',$d,$key,function($s)use($lot,$actor,$d){ + $l=$this->lot($lot,$s);$data=$this->validateData($d['kind'],$d['data'],$l);$last=DB::table('ic_inputs')->where(['lot_id'=>$lot,'kind'=>$d['kind']])->orderByDesc('version')->first(); + abort_unless($d['version']===($last?->version??0)+1&&($d['previous_id']??null)===$last?->id,409,'Versão anterior exacta necessária.'); + if($last)abort_unless($last->source_ref===$d['source_ref'],409,'Identidade da origem não pode mudar.'); + else abort_if(DB::table('ic_inputs')->where(['company_id'=>$s['company_id'],'source_ref'=>$d['source_ref']])->exists(),409,'Origem canónica já usada; não duplicar entre lotes, unidades ou directo/CIF.'); + $id=DB::table('ic_inputs')->insertGetId($s+['lot_id'=>$lot,'kind'=>$d['kind'],'source_ref'=>$d['source_ref'],'version'=>$d['version'],'previous_id'=>$last?->id,'data'=>$this->json($data),'evidence'=>$d['evidence'],'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'lot_id'=>$lot,'version'=>$d['version'],'kind'=>'input']; + }); + } + private function compose(int $lot,array $refs): array { + abort_unless(array_diff(array_keys($refs),self::KINDS)===[]&&count($refs)===8,422,'Oito referências explícitas necessárias, incluindo taxa material ausente.');$rows=[];$versions=[]; + foreach(self::KINDS as $kind){ + $ref=$refs[$kind]??null;if($kind==='material_rate'&&$ref===null){$versions[$kind]=null;continue;} + abort_unless(is_array($ref),422,'Referência em falta: '.$kind);$r=$this->exact($ref,['id'=>'required|integer|min:1','version'=>'required|integer|min:1']);$rows[$kind]=$this->approvedInput($r['id'],$r['version'],$lot,$kind);$versions[$kind]=$r; + } + $l=DB::table('ic_lots')->find($lot); + foreach($rows as $r)$this->validateData($r->kind,$r->data,$l); + foreach(['material','labour'] as $kind)if(isset($rows[$kind.'_rate'])){ $rate=$rows[$kind.'_rate']->data;abort_unless($rate['source_id']===$rows[$kind]->id&&$rate['source_version']===$rows[$kind]->version,409,'Taxa não corresponde à origem seleccionada.'); } + $c=$rows['criterion']->data;abort_unless($c['pool_id']===$rows['pool']->id&&$c['pool_version']===$rows['pool']->version&&$c['machine_id']===$rows['machine']->id&&$c['machine_version']===$rows['machine']->version,409,'Critério incompatível com as origens seleccionadas.'); + $n=fn($k)=>Number::of($rows[$k]->data['quantity']);$pool=$n('pool');$machine=$n('machine');$capacity=Number::of($c['capacity']); + abort_if($machine->isGreaterThan($capacity),409,'Utilização superior à capacidade aprovada; residual negativo recusado.'); + $tax=$pool->dividedBy($capacity);$allocated=$tax->multipliedBy($machine);$residual=$pool->minus($allocated);$hours=Exact::hours($rows['labour']->data['quantity']);$labour=$hours->multipliedBy($rows['labour_rate']->data['amount']); + $material=isset($rows['material_rate'])?$n('material')->multipliedBy($rows['material_rate']->data['amount']):null; + $known=$labour->plus($allocated)->plus($material??0);$origins=$labour->plus($pool)->plus($material??0);$pending=$material===null?['Taxa do material em falta; material excluído da reconciliação monetária parcial.']:[]; + $originsDetail=[];foreach($rows as $kind=>$r)$originsDetail[$kind]=['id'=>$r->id,'version'=>$r->version,'source_ref'=>$r->source_ref,'data'=>$r->data,'evidence'=>$r->evidence]; + return ['policy'=>self::POLICY,'notice'=>self::NOTICE,'hypothesis'=>'Hipótese sintética de capacidade/imputação; não é método contabilístico real aprovado.','currency'=>'MZN','refs'=>$versions,'origins'=>$originsDetail, + 'material_exact'=>$material===null?null:Exact::text($material),'labour_hours_exact'=>Exact::text($hours),'labour_exact'=>Exact::text($labour),'pool_exact'=>Exact::text($pool),'capacity_hm'=>Exact::text($capacity),'used_hm'=>Exact::text($machine),'indirect_rate_exact'=>Exact::text($tax),'allocated_exact'=>Exact::text($allocated),'residual_exact'=>Exact::text($residual),'known_cost_exact'=>Exact::text($known),'selected_total_exact'=>$pending?null:Exact::text($known),'good_units'=>Exact::text($n('production')),'unit_cost_exact'=>$pending?null:Exact::text($known->dividedBy($n('production'))),'incomplete'=>$pending!==[],'pending'=>$pending, + 'reconciliation'=>['coverage'=>$pending?'partial_known_only':'scenario_scope','known_origins_exact'=>Exact::text($origins),'assigned_exact'=>Exact::text($known),'unassigned_exact'=>Exact::text($residual),'balanced'=>$origins->isEqualTo($known->plus($residual))]]; + } + private function resultCurrent(object $r): bool { + if(!$this->current('result',$r))return false; + try{$snapshot=json_decode($r->snapshot,true,64,JSON_THROW_ON_ERROR);return hash_equals($r->fingerprint,hash('sha256',$this->json($this->compose($r->lot_id,$snapshot['refs']))));} + catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface){return false;} + } + public function calculate(int $lot,int $actor,array $in,string $key): array { + self::guard();$d=$this->exact($in,['version'=>'required|integer|min:1','previous_id'=>'nullable|integer|min:1','refs'=>'required|array','currency'=>'required|in:MZN']); + return $this->command($lot,$actor,'calculate',$d,$key,function($s)use($lot,$actor,$d){ + $last=DB::table('ic_results')->where('lot_id',$lot)->orderByDesc('version')->first();abort_unless($d['version']===($last?->version??0)+1&&($d['previous_id']??null)===$last?->id,409,'Resultado mudou; rever versão anterior.'); + $snapshot=$this->compose($lot,$d['refs']);$hash=hash('sha256',$this->json($snapshot));if($last&&hash_equals($last->fingerprint,$hash))return ['id'=>$last->id,'lot_id'=>$lot,'version'=>$last->version,'kind'=>'result','unchanged'=>true]; + $id=DB::table('ic_results')->insertGetId($s+['lot_id'=>$lot,'version'=>$d['version'],'previous_id'=>$last?->id,'fingerprint'=>$hash,'snapshot'=>$this->json($snapshot),'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'lot_id'=>$lot,'version'=>$d['version'],'kind'=>'result']; + }); + } + public function decide(int $lot,int $actor,array $in,string $key): array { + self::guard();$d=$this->exact($in,['type'=>'required|in:input,result','id'=>'required|integer|min:1','version'=>'required|integer|min:1','decision'=>'required|in:approved,refused','reason'=>'required|string|min:3|max:1000']); + return $this->command($lot,$actor,'decide',$d,$key,function($s)use($lot,$actor,$d){ + $r=$this->row($d['type'],$d['id'],$lot);abort_if($r->actor_id===$actor,403,'Validador deve ser distinto do preparador.');abort_unless($r->version===$d['version']&&$this->current($d['type'],$r),409,'Versão obsoleta.'); + if($d['type']==='input')$this->validateData($r->kind,json_decode($r->data,true),$this->lot($lot,$s));else abort_unless($this->resultCurrent($r),409,'Dependências alteradas; preparar nova versão.'); + $field=$d['type']==='input'?'input_id':'result_id';abort_if(DB::table('ic_decisions')->where($field,$r->id)->exists(),409,'Versão já decidida.'); + $id=DB::table('ic_decisions')->insertGetId([$field=>$r->id,'version'=>$r->version,'decision'=>$d['decision'],'reason'=>$d['reason'],'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'lot_id'=>$lot,'version'=>$r->version,'kind'=>'decision']; + }); + } + public function listing(int $actor): array { + $s=$this->scope($actor,'view');if(!ServiceCalculation::isolated())return ['blocked'=>true,'lots'=>[],'notice'=>self::NOTICE];self::guard(); + return DB::transaction(function()use($s){ + DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();$lots=[]; + foreach(DB::table('ic_lots')->where($s)->orderBy('id')->get() as $l){ + $inputs=DB::table('ic_inputs')->where('lot_id',$l->id)->orderBy('id')->get();foreach($inputs as $i){$i->current=$this->current('input',$i);$i->data=json_decode($i->data,true);$i->decision=DB::table('ic_decisions')->where('input_id',$i->id)->first();} + $results=DB::table('ic_results')->where('lot_id',$l->id)->orderBy('version')->get();foreach($results as $r){$r->current=$this->resultCurrent($r);$r->snapshot=json_decode($r->snapshot,true,64,JSON_THROW_ON_ERROR);$r->decision=DB::table('ic_decisions')->where('result_id',$r->id)->first();} + $lots[]=(array)$l+['product'=>DB::table('ic_products')->find($l->product_id),'inputs'=>$inputs->all(),'results'=>$results->all()]; + }return ['blocked'=>false,'lots'=>$lots,'notice'=>self::NOTICE,'policy'=>self::POLICY]; + }); + } +} diff --git a/app/Modules/Workshop/DirectCosts.php b/app/Modules/Workshop/DirectCosts.php index 7eba9e2..6afe7e2 100644 --- a/app/Modules/Workshop/DirectCosts.php +++ b/app/Modules/Workshop/DirectCosts.php @@ -64,13 +64,13 @@ final class DirectCosts $id=DB::table('dc_rates')->insertGetId($s+array_merge($d,['order_id'=>$order,'amount'=>(string)BigDecimal::of($d['amount'])->stripTrailingZeros(),'actor_id'=>$actor,'created_at'=>now()]));return ['id'=>$id,'version'=>$d['version'],'kind'=>'rate']; }); } - private function value(BigRational $n): string {try{return (string)$n->toBigDecimal()->stripTrailingZeros();}catch(\Brick\Math\Exception\RoundingNecessaryException){return (string)$n->simplified();}} + private function value(BigRational $n): string {return \App\Shared\Costs\ExactCostMath::text($n);} private function compose(int $order,array $lines): array { abort_unless(count($lines)===2,422,'Este ensaio exige um consumo e um tempo efectivos.');$out=[];$seen=[];$kinds=[];$pending=[];$sum=BigRational::of(0); foreach($lines as $line){ $d=$this->exact($line,['entry_id'=>'required|integer|min:1','entry_version'=>'required|integer|min:1','rate_id'=>'present|nullable|integer|min:1','rate_version'=>'present|nullable|integer|min:1']); $e=$this->source($d['entry_id'],$d['entry_version'],$order);abort_if(isset($seen[$e->id]),422,'Origem duplicada.');$seen[$e->id]=true;$kinds[]=$e->kind; - $quantity=$e->kind==='time'?BigRational::of($e->minutes)->dividedBy(60):BigRational::of($e->quantity); + $quantity=$e->kind==='time'?\App\Shared\Costs\ExactCostMath::hours($e->minutes):BigRational::of($e->quantity); $rate=null;$cost=null; if($d['rate_id']!==null){ $r=$this->row('rate',$d['rate_id'],$order); diff --git a/app/Shared/Costs/ExactCostMath.php b/app/Shared/Costs/ExactCostMath.php new file mode 100644 index 0000000..96729ff --- /dev/null +++ b/app/Shared/Costs/ExactCostMath.php @@ -0,0 +1,10 @@ +dividedBy(60);} + public static function text(BigRational $n): string { + try{return (string)$n->toBigDecimal()->stripTrailingZeros();} + catch(\Brick\Math\Exception\RoundingNecessaryException){return (string)$n->simplified();} + } +} diff --git a/database/migrations/2026_10_27_100000_create_industrial_cost_simulation.php b/database/migrations/2026_10_27_100000_create_industrial_cost_simulation.php new file mode 100644 index 0000000..e006921 --- /dev/null +++ b/database/migrations/2026_10_27_100000_create_industrial_cost_simulation.php @@ -0,0 +1,16 @@ +d,6); + DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check'); + DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (($old) OR (permission IN ('industrial_costs.view','industrial_costs.prepare','industrial_costs.validate') AND unit_id IS NOT NULL))"); + DB::unprepared(file_get_contents(__DIR__.'/../sql/industrial-costs.sql')); + foreach(['ic_products','ic_lots','ic_inputs','ic_results','ic_decisions','ic_commands','ic_events','ic_outbox'] as $t){ + DB::statement("CREATE TRIGGER {$t}_immutable BEFORE UPDATE OR DELETE ON {$t} FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()"); + DB::statement("CREATE TRIGGER {$t}_test_only BEFORE INSERT ON {$t} FOR EACH ROW EXECUTE FUNCTION sgf_ic_test_only()"); + } + } + public function down(): void {throw new RuntimeException('Industrial cost history requires a reviewed preservation procedure.');} +}; diff --git a/database/sql/industrial-costs.sql b/database/sql/industrial-costs.sql new file mode 100644 index 0000000..073579b --- /dev/null +++ b/database/sql/industrial-costs.sql @@ -0,0 +1,106 @@ +CREATE TABLE ic_products ( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + reference text NOT NULL,name text NOT NULL,version integer NOT NULL CHECK(version=1),unit text NOT NULL CHECK(unit='UN'), + actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL, + UNIQUE(company_id,reference),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE ic_lots ( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + product_id bigint NOT NULL REFERENCES ic_products(id),reference text NOT NULL,centre text NOT NULL, + period_start date NOT NULL,period_end date NOT NULL CHECK(period_end>=period_start),currency text NOT NULL CHECK(currency='MZN'), + actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL, + UNIQUE(company_id,reference),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE ic_inputs ( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + lot_id bigint NOT NULL REFERENCES ic_lots(id),kind text NOT NULL CHECK(kind IN ('material','labour','pool','machine','production','material_rate','labour_rate','criterion')), + source_ref text NOT NULL CHECK(source_ref LIKE 'IND-TEST-%'),version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES ic_inputs(id), + data jsonb NOT NULL,evidence text NOT NULL CHECK(length(evidence)>=3),actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL, + UNIQUE(lot_id,kind,version),UNIQUE(company_id,source_ref,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE ic_results ( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + lot_id bigint NOT NULL REFERENCES ic_lots(id),version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES ic_results(id), + fingerprint text NOT NULL,snapshot jsonb NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL, + UNIQUE(lot_id,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE ic_decisions ( + id bigserial PRIMARY KEY,input_id bigint UNIQUE REFERENCES ic_inputs(id),result_id bigint UNIQUE REFERENCES ic_results(id), + version integer NOT NULL CHECK(version>0),decision text NOT NULL CHECK(decision IN ('approved','refused')),reason text NOT NULL CHECK(length(reason)>=3), + actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL,CHECK(num_nonnulls(input_id,result_id)=1) +); +CREATE TABLE ic_commands ( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + lot_id bigint NOT NULL REFERENCES ic_lots(id),actor_id bigint NOT NULL REFERENCES users(id),action text NOT NULL CHECK(action IN ('open','input','calculate','decide')), + command_key text NOT NULL,request_hash text NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL, + UNIQUE(company_id,unit_id,actor_id,action,command_key),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE ic_events(id bigserial PRIMARY KEY,command_id bigint NOT NULL UNIQUE REFERENCES ic_commands(id),schema_version integer NOT NULL CHECK(schema_version=1),payload jsonb NOT NULL,created_at timestamptz NOT NULL); +CREATE TABLE ic_outbox(event_id bigint PRIMARY KEY REFERENCES ic_events(id),state text NOT NULL CHECK(state='captured_no_stock_or_accounting')); +CREATE OR REPLACE FUNCTION sgf_ic_environment() RETURNS void LANGUAGE plpgsql AS $$ +BEGIN IF current_database() IS DISTINCT FROM 'sgf_tempo_test' OR inet_server_port() IS DISTINCT FROM 55432 THEN RAISE EXCEPTION 'SGF_IC_TEST_DATABASE_ONLY'; END IF; END; $$; +CREATE OR REPLACE FUNCTION sgf_ic_test_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN PERFORM sgf_ic_environment(); RETURN NEW; END; $$; +CREATE OR REPLACE FUNCTION sgf_ic_input_current(iid bigint,lid bigint,ver integer) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM ic_inputs i JOIN ic_decisions d ON d.input_id=i.id AND d.version=i.version AND d.decision='approved' WHERE i.id=iid AND i.lot_id=lid AND i.version=ver) + OR EXISTS(SELECT 1 FROM ic_inputs WHERE previous_id=iid) THEN RAISE EXCEPTION 'SGF_IC_INPUT_NOT_CURRENT_APPROVED'; END IF; +END; $$; +CREATE OR REPLACE FUNCTION sgf_ic_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE l ic_lots; p record; target record; i ic_inputs; v integer; lid bigint; ref jsonb; +BEGIN + PERFORM sgf_ic_environment(); + IF TG_TABLE_NAME='ic_products' THEN PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; RETURN NEW; END IF; + IF TG_TABLE_NAME='ic_lots' THEN + PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; + IF NOT EXISTS(SELECT 1 FROM ic_products WHERE id=NEW.product_id AND ROW(tenant_id,company_id,unit_id)=ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id)) THEN RAISE EXCEPTION 'SGF_IC_PRODUCT_SCOPE'; END IF; RETURN NEW; + END IF; + IF TG_TABLE_NAME='ic_decisions' THEN + IF NEW.input_id IS NOT NULL THEN SELECT lot_id INTO STRICT lid FROM ic_inputs WHERE id=NEW.input_id; + ELSE SELECT lot_id INTO STRICT lid FROM ic_results WHERE id=NEW.result_id; END IF; + ELSE lid=NEW.lot_id; END IF; + SELECT * INTO STRICT l FROM ic_lots WHERE id=lid; PERFORM 1 FROM companies WHERE id=l.company_id FOR UPDATE; + IF TG_TABLE_NAME='ic_decisions' THEN + IF NEW.input_id IS NOT NULL THEN + SELECT id,version,actor_id INTO STRICT target FROM ic_inputs WHERE id=NEW.input_id; + IF EXISTS(SELECT 1 FROM ic_inputs WHERE previous_id=target.id) THEN RAISE EXCEPTION 'SGF_IC_OLD_INPUT'; END IF; + ELSE + SELECT id,version,actor_id INTO STRICT target FROM ic_results WHERE id=NEW.result_id; + IF EXISTS(SELECT 1 FROM ic_results WHERE previous_id=target.id) THEN RAISE EXCEPTION 'SGF_IC_OLD_RESULT'; END IF; + FOR ref IN SELECT value FROM ic_results r,jsonb_each(r.snapshot->'refs') WHERE r.id=target.id LOOP + IF ref<>'null'::jsonb THEN PERFORM sgf_ic_input_current((ref->>'id')::bigint,lid,(ref->>'version')::integer); END IF; + END LOOP; + END IF; + IF target.actor_id=NEW.actor_id OR target.version<>NEW.version THEN RAISE EXCEPTION 'SGF_IC_SEPARATION_VERSION'; END IF; RETURN NEW; + END IF; + IF ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id) IS DISTINCT FROM ROW(l.tenant_id,l.company_id,l.unit_id) THEN RAISE EXCEPTION 'SGF_IC_SCOPE'; END IF; + IF TG_TABLE_NAME='ic_commands' THEN RETURN NEW; END IF; + IF TG_TABLE_NAME='ic_inputs' THEN + SELECT coalesce(max(version),0) INTO v FROM ic_inputs WHERE lot_id=lid AND kind=NEW.kind; + IF NEW.version<>v+1 OR (v=0 AND NEW.previous_id IS NOT NULL) THEN RAISE EXCEPTION 'SGF_IC_VERSION'; END IF; + IF v>0 THEN SELECT * INTO STRICT p FROM ic_inputs WHERE id=NEW.previous_id AND lot_id=lid AND kind=NEW.kind AND version=v; + IF p.source_ref<>NEW.source_ref THEN RAISE EXCEPTION 'SGF_IC_ORIGIN_TRANSFER'; END IF; + END IF; + IF NEW.kind IN ('material','labour','machine','production') AND ((NEW.data->>'quantity')::numeric<=0 OR (NEW.data->>'quantity')::numeric<>trunc((NEW.data->>'quantity')::numeric,8)) THEN RAISE EXCEPTION 'SGF_IC_QUANTITY'; END IF; + IF NEW.kind='criterion' AND ((NEW.data->>'capacity')::numeric<=0 OR NEW.data->>'driver_unit' IS DISTINCT FROM 'HM' OR NEW.data->>'hypothesis' IS DISTINCT FROM 'synthetic_only') THEN RAISE EXCEPTION 'SGF_IC_CAPACITY'; END IF; + IF NEW.kind IN ('material_rate','labour_rate') THEN + SELECT * INTO STRICT i FROM ic_inputs WHERE id=(NEW.data->>'source_id')::bigint AND lot_id=lid AND version=(NEW.data->>'source_version')::integer; + IF i.kind<>replace(NEW.kind,'_rate','') OR (NEW.data->>'amount')::numeric<0 OR NEW.data->>'currency' IS DISTINCT FROM 'MZN' OR NEW.data->>'unit' IS DISTINCT FROM (CASE WHEN i.kind='material' THEN 'KG' ELSE 'H' END) THEN RAISE EXCEPTION 'SGF_IC_RATE'; END IF; + END IF; + ELSE + SELECT coalesce(max(version),0) INTO v FROM ic_results WHERE lot_id=lid; + IF NEW.version<>v+1 OR NEW.snapshot->>'policy' IS DISTINCT FROM 'INDUSTRIAL-COST-SIM-v1' OR NEW.snapshot->>'currency' IS DISTINCT FROM 'MZN' THEN RAISE EXCEPTION 'SGF_IC_VERSION_POLICY'; END IF; + IF v=0 THEN IF NEW.previous_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_IC_PREVIOUS'; END IF; + ELSE PERFORM 1 FROM ic_results WHERE id=NEW.previous_id AND lot_id=lid AND version=v; IF NOT FOUND THEN RAISE EXCEPTION 'SGF_IC_PREVIOUS'; END IF; END IF; + FOR ref IN SELECT value FROM jsonb_each(NEW.snapshot->'refs') LOOP + IF ref<>'null'::jsonb THEN PERFORM sgf_ic_input_current((ref->>'id')::bigint,lid,(ref->>'version')::integer); END IF; + END LOOP; + END IF; + RETURN NEW; +EXCEPTION WHEN NO_DATA_FOUND THEN RAISE EXCEPTION 'SGF_IC_REFERENCE'; END; $$; +CREATE TRIGGER ic_product_integrity BEFORE INSERT ON ic_products FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); +CREATE TRIGGER ic_lot_integrity BEFORE INSERT ON ic_lots FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); +CREATE TRIGGER ic_input_integrity BEFORE INSERT ON ic_inputs FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); +CREATE TRIGGER ic_result_integrity BEFORE INSERT ON ic_results FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); +CREATE TRIGGER ic_decision_integrity BEFORE INSERT ON ic_decisions FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); +CREATE TRIGGER ic_command_integrity BEFORE INSERT ON ic_commands FOR EACH ROW EXECUTE FUNCTION sgf_ic_guard(); diff --git a/docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md b/docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md index 8860581..d9ae336 100644 --- a/docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md +++ b/docs/INDUSTRIAL_COST_ACCOUNTING_SCOPE.md @@ -250,3 +250,71 @@ Guardar a origem e versão da taxa, a unidade, moeda, composição e pendências **Osmane prepara; Samira valida a versão dos custos e das taxas.** Samira pode validar/recusar a proposta de custos, sem editar os apontamentos técnicos ou receber supervisão técnica por consequência. Testes futuros com identidades sintéticas distintas entre preparação e validação. Nenhuma nova concessão real é autorizada por estas decisões. Lote industrial e custos indirectos ficam para incremento seguinte. Sem stock, compras, salários reais, facturação, IVA, recebimentos, lançamentos, fecho ou publicação. Manter bloqueios operacionais/fiscais e isolamento em `sgf_tempo_test` nas futuras provas. Esta confirmação fecha as decisões do recorte; não constitui ordem de implementação, migração, concessão ou activação operacional. + +## 15. Retoma do ensaio industrial — recorte para aprovação + +27/10/2026. O utilizador propôs retomar um lote/produto com custos indirectos, primeiro em especificação. O custo directo da secção 14 já foi entregue, encerrado e instalado tecnicamente: ver [entrega](WORKSHOP_DIRECT_COST_DELIVERY.md), [encerramento](WORKSHOP_DIRECT_COST_ACCEPTANCE.md) e checkpoint `d10ba06`. `090000` e quatro direitos foram aplicados por autorização posterior, sem taxas/cálculos em desenvolvimento ou activação real. Oficinas e recebimentos mantêm as suas entregas e instalações; não se reabrem aprovações. + +### 15.1 Proposta mínima INDUSTRIAL-COST-SIM-v1 + +Uma empresa/unidade sintéticas, um período de teste identificado, MZN explícito, um centro sintético, um produto em UN, um lote concluído de etapa única e 10 UN boas verificadas. Sem produção em curso, perdas/retrabalho, vendas ou segundo lote. É prova de gestão; não escolhe indústria, método de custeio, referencial ou política de inventário para empresa real. + +Recomenda-se reutilizar os valores já especificados na secção 8, sem recalibrar o ensaio aceite de 275 MZN: + +| Componente / origem própria identificada | Dados sintéticos explícitos | Resultado esperado | +| --- | --- | ---: | +| Material directo | Consumo líquido 20 KG × 50 MZN/KG | 1 000 MZN | +| Trabalho directo | 300 MIN efectivos ÷ 60 × 100 MZN/H | 500 MZN | +| Conjunto CIF do período | Uma origem de 600 MZN, declarada elegível apenas no ensaio | 600 MZN de origem | +| Critério CIF v1 | Capacidade sintética do período 10 HM; lote usa 5 HM; taxa exacta 600/10 | 60 MZN/HM | +| CIF imputados ao lote | 5 HM × 60 MZN/HM | 300 MZN | +| Custo atribuído ao lote no perímetro do ensaio | 1 000 + 500 + 300 | **1 800 MZN** | +| Custo por UN boa no mesmo perímetro | 1 800 ÷ 10 UN | **180 MZN/UN** | +| Residual CIF | 600 − 300 | **300 MZN por imputar** | + +As 5 HM são uma medição sintética própria de utilização da máquina, não inferidas das 5 H de trabalho humano. Capacidade normal de 10 HM é pressuposto de teste explícito, sem alegar validação legal ou empresarial. O residual decorre da base não utilizada sob esse pressuposto; não é falha de arredondamento e não é convertido automaticamente em custo do produto ou despesa contabilística. + +Reconciliação do conjunto: **600 = 300 imputados + 300 por imputar**. Reconciliação global: **2 100 de origens = 1 800 atribuídos + 300 por imputar**. Os 300 imputados não são uma nova origem a somar aos 600. Não elevar a taxa para absorver o residual nem alterar capacidade para fechar a soma. + +Variante obrigatória: taxa do material ausente, quantidade 20 KG preservada. Mostrar **800 MZN atribuídos conhecidos** (500 + 300), material pendente e custo total/unitário completo indisponíveis. Reconciliação parcial identificada: **1 100 conhecidos = 800 atribuídos conhecidos + 300 por imputar**; componente material excluída dessa igualdade por falta de valor. Zero indicado e validado é distinto de ausência. + +Previsto versus realizado da secção 8 fica para evolução posterior, para manter este primeiro incremento pequeno. Não se acrescentam preços, margens, depreciações reais ou documentos de compra/salário. Produção concluída aqui é estado físico de teste, não fecho contabilístico. + +### 15.2 Reutilização concreta e delta necessário + +| Base já entregue | Reutilização delimitada | Trabalho novo mínimo | +| --- | --- | --- | +| Contexto, pertenças, concessões e guardas | Mesmo isolamento por empresa/unidade e bloqueio real | Direitos industriais explícitos, sem herança dos direitos `direct_costs.*` | +| Exactidão racional do custo directo, MIN/H e estados de completude | Mesmo comportamento sem float, truncagem de IVA ou divisão arredondada silenciosamente | Composição industrial, CIF/HM e custo por UN; fracção exacta se não finita | +| Origem/versão, taxa/versão, decisões distintas e fotografia de composição | Referências e dependências preservadas; mudança sinaliza resultado antigo, não o reescreve | Produto/lote, produção boa, conjunto CIF, critério e execução de imputação versionados | +| Comandos transaccionais, auditoria, eventos/outbox, replay e concorrência | Reutilizar padrões e verificações, sem activar consumidores contabilísticos | Impedir dupla elegibilidade/directo-CIF e dupla imputação, controlar residual | +| Apontamentos técnicos de oficina | Se forem efectivamente a origem de uma parcela autorizada, referenciar o mesmo ID/versão, sem recaptura | Vínculo de destino explícito; nenhuma conversão automática de oficina em lote | + +O código actual `DirectCosts` é delimitado a duas origens verificadas de oficina e não é já um motor industrial genérico. A futura implementação deve extrair/reutilizar apenas a aritmética pura necessária e preservar o contrato/testes aceites, acrescentando um adaptador de origens industriais. Não alargar silenciosamente as regras ou tabelas `wo_*`/`dc_*` para fingir produção. + +As novas quantidades industriais acima representam acontecimentos sintéticos diferentes dos apontamentos de 2 UN/105 MIN da oficina. Não copiar esses apontamentos como se fossem os consumos de 20 KG/300 MIN do lote. Cada origem tem tipo, ID, versão, unidade, âmbito e propósito. Se uma mesma parcela real/sintética for usada por ligação autorizada, é uma só origem, com saldo elegível controlado entre destinos, sem nova captura. Não somar o resultado agregado de 275 e as suas componentes como origens independentes. + +Primeiro recorte: origens, valorização, conjunto e critério preparados → submetidos → validados/recusados por identidade distinta; cálculo sob versões validadas → resultado com composição e residual → validação da versão exacta. Dados em falta permanecem condicionados; uma validação não inventa a informação ausente. Alterações exigem nova versão/revisão, preservando resultados aprovados anteriores e sinalizando desactualização. + +### 15.3 Provas mínimas para futura execução + +- Demonstrar 1 800 atribuídos, 180/UN e residual 300, com as duas reconciliações acima e a variante incompleta de 800. +- Seleccionar explicitamente versões de origens, taxas, conjunto e critério. Sem critério/taxa/unidade/moeda/capacidade demonstrados, recusar imputação integral; não assumir zero ou configuração mais recente. +- Quantidade boa zero/desconhecida não produz custo unitário. Base do lote superior ao denominador/capacidade aprovada é conflito, não autorização de residual negativo. Denominador zero/ausente impede dividir. +- Impedir que a mesma parcela seja directa e CIF, imputada duas vezes ou usada além do elegível. Não criar dois eventos de consumo para oficina/indústria. Distinguir quantidade física de parcela monetária de custo. +- Correcção de origem/taxa/capacidade/critério exige nova revisão; aprovação antiga e repetição/concorrência não duplicam efeitos ou reescrevem história. Falha de evento/outbox desfaz o comando; resposta perdida recupera resultado idempotente. +- Login real e separação de preparador/validador com identidades sintéticas distintas, sem concessões reais; negar acesso entre empresas/unidades e custos a quem só consulta apontamentos técnicos. Revogação tem efeito no pedido seguinte. +- Testes e navegador só em `sgf_tempo_test`, com aplicação normal inalterada. Qualquer migração em desenvolvimento e novas concessões reais terão delta/proposta separados e confirmação específica. Manter bloqueios de custos, emissão, recebimentos, stock e lançamentos reais. + +### 15.4 Decisões novas para fechar o recorte + +1. **Aceitar os números e limites do ensaio acima**, incluindo 10 UN, uma etapa, CIF sintéticos de 600 MZN, capacidade 10 HM, utilização 5 HM e residual 300, sem adoptar estes pressupostos como política real. Previsto/realizado fica fora deste primeiro recorte. +2. **Responsabilidade industrial proposta:** Osmane prepara origens/valorizações, conjunto, critério e cálculo; Samira valida/recusa as versões de custos, critério e resultado, sem editar execução técnica. É extensão a confirmar para este ensaio, não consequência automática do papel já aprovado no custo directo. Provas apenas com equivalentes sintéticos; nenhuma concessão real por esta decisão. + +Exactidão, ausência diferente de zero, origem única, histórico, isolamento e bloqueios já estão definidos e não são pedidos novamente. Não é preciso identificar uma indústria real ou reabrir o cotejo fiscal para provar estes pressupostos sintéticos. A futura activação real exige âmbito e suporte próprios. **Esta secção continua documental: não autoriza implementação ou execução do ensaio.** + +### 15.5 Confirmação do recorte pelo utilizador + +Em 27/10/2026, após a confirmação dos números e limites, o utilizador respondeu «sim». Ficam aprovadas as duas decisões da secção 15.4: números e perímetro sintéticos da secção 15.1, com residual separado e variante incompleta; Osmane prepara e Samira valida/recusa versões do critério e do resultado, apenas com identidades sintéticas distintas nas provas e sem concessões reais. + +O critério de capacidade/imputação continua hipótese de ensaio, não método contabilístico real aprovado. Previsto/realizado fica para depois. Não se reabrem estas decisões; a autorização para implementar código/testes, futuras migrações em desenvolvimento, concessões ou activação real continua separada. Este registo não executa o ensaio. diff --git a/docs/INDUSTRIAL_COST_DELIVERY.md b/docs/INDUSTRIAL_COST_DELIVERY.md new file mode 100644 index 0000000..f3ebe82 --- /dev/null +++ b/docs/INDUSTRIAL_COST_DELIVERY.md @@ -0,0 +1,89 @@ +# INDUSTRIAL-COST-SIM-v1 — entrega técnica isolada + +27/10/2026, Africa/Johannesburg. Implementação delimitada pelas secções 15 e 15.5 de [INDUSTRIAL_COST_ACCOUNTING_SCOPE.md](INDUSTRIAL_COST_ACCOUNTING_SCOPE.md). **Sem aceitação presumida ou checkpoint de encerramento.** Preservados `d10ba06`, `7628645`, alterações posteriores, oficinas, recebimentos, cálculo fiscal e aprovações existentes. Não se acedeu ao InvesteMoz nem se repetiu investigação fiscal. + +## O que funciona + +Lote com identidade própria, produto sintético, centro, empresa/unidade, período e MZN explícitos. Origens industriais identificadas e versionadas para consumo de material, trabalho efectivo, conjunto indirecto, utilização da máquina e produção boa concluída; taxas de material/trabalho e critério com versões e fontes explícitas. As origens são novas ocorrências sintéticas do lote, não cópias dos apontamentos da oficina nem transformação de uma ordem técnica em produção. + +Preparador propõe versões; validador distinto aprova/recusa dados/taxas/critério e resultado exactos. A validação dos dados sintéticos de custo não é verificação técnica de execução real. Alterações conservam a identidade da origem e exigem sucessora explícita; dados, taxas e critério desactualizados não são escolhidos automaticamente. Calcular exige referências exactas a versões actuais aprovadas; taxa de material pode ficar explicitamente ausente. + +Resultado guarda composição, referências e versões, fórmula/base, moeda, residual, cobertura da reconciliação e pendências. Alteração de origem/taxa/critério invalida a actualidade dos dependentes, sem reescrever fotografias ou decisões anteriores. Novo cálculo cria versão com anterior explícita; composição idêntica à última pode recuperar o mesmo resultado. Só a última versão elegível representa a imputação actual; resultados históricos não se acumulam como novas utilizações do conjunto. + +## Números demonstrados + +| Componente | Composição exacta | Custo atribuído | +| --- | --- | ---: | +| Material | 20 KG × 50 MZN/KG | 1 000 MZN | +| Trabalho | 300 MIN ÷ 60 × 100 MZN/H | 500 MZN | +| Indirectos | Conjunto 600 MZN ÷ capacidade 10 HM = 60 MZN/HM; 5 HM × 60 | 300 MZN | +| **Lote / perímetro sintético** | 10 UN boas | **1 800 MZN; 180 MZN/UN** | +| **Residual separado** | 600 − 300 | **300 MZN por imputar** | + +Conjunto: **600 = 300 imputados + 300 por imputar**. Perímetro: **2 100 origens = 1 800 atribuídos + 300 por imputar**. As 5 HM são medição própria da máquina, não deduzidas das 5 H humanas. Capacidade e elegibilidade são pressupostos identificados **apenas do ensaio**, não método contabilístico real aprovado. O residual não é redistribuído, convertido em despesa ou lançado. + +Sem taxa do material: **800 MZN conhecidos** (500 + 300), total e custo unitário definitivo indisponíveis; quantidade 20 KG preservada, com pendência. Reconciliação **parcial**: **1 100 conhecidos = 800 atribuídos conhecidos + 300 por imputar**, excluindo o material sem valor conhecido. Uma validação desse resultado valida só o perímetro conhecido e não elimina a pendência. Taxa zero indicada/aprovada é distinta de ausência, provada por teste. + +## Reutilização e segurança + +Extraída `app/Shared/Costs/ExactCostMath.php` da representação racional/conversão MIN/H do custo directo, usada agora por ambos os percursos. Preservado o contrato da entrega de custo directo e executada a sua regressão. Não há float, redução silenciosa de precisão ou reutilização da política de truncagem do IVA. Decimais não negativos até 16 algarismos inteiros/8 decimais nas entradas; quantidades/capacidade positivas quando necessárias. Divisão não finita conserva fracção exacta (teste `800/3`), sem escolher regra fiscal ou arredondamento implícito. + +Direitos novos de unidade `industrial_costs.view/prepare/validate`, sem herança dos direitos de custo directo/contabilidade/gestão. Contexto, empresa/unidade, pertenças e acção revalidados no servidor, incluindo replay e dentro da transacção. Auto-aprovação recusada mesmo com ambos os poderes. Não há nomes/emails especiais no código funcional. + +Bloqueio por empresa serializa comandos e revisões; idempotência inclui âmbito, autor, acção e conteúdo. Chave repetida com outro conteúdo é conflito. Comando, auditoria, evento e outbox atómicos: falha de outbox desfaz efeitos; resposta perdida recupera o ID. Outbox `captured_no_stock_or_accounting`, sem consumidor contabilístico ou integração externa. + +Origem canónica única por empresa/referência/versão, com cadeia imutável ligada ao lote e tipo. Não pode mudar de material directo para conjunto indirecto ou reaparecer noutro lote/unidade com a mesma identidade. Cada lote tem no máximo uma cadeia por tipo neste recorte; o conjunto pertence exclusivamente ao lote, sem partilha entre lotes. Isso evita uma segunda imputação activa da mesma parcela sem construir um motor geral de repartições. Identidades diferentes não são deduplicadas por coincidência de descrição/valor. + +FKs, versões únicas e triggers reforçam contexto, referências, decisões separadas, fontes aprovadas e imutabilidade contra operações em massa. A composição exacta e autorização autenticada são verificadas pela aplicação; os triggers não são RLS nem uma implementação independente de todo o motor. SQL administrativo é uma fronteira de confiança distinta. + +`IndustrialCosts::guard()` exige ambiente isolado e `TestingDatabaseGuard` confirma `sgf_tempo_test`, porta 55432 e papel `sgf_test_runner`. INSERT nas oito tabelas industriais também exige a base de testes no SQL. Não existe opção de activação real. Guardas de oficinas, recebimentos, custo directo e emissão permanecem preservadas. + +## Testes e percurso no navegador + +Resultados em [industrial-cost-tests.json](proofs/industrial-cost-tests.json); [seis provas concorrentes](proofs/industrial-cost-concurrency.json), [resultados do navegador](proofs/industrial-cost-browser-results.json), [cenário de 1 800](proofs/industrial-cost-1800-browser.png) e [cenário incompleto](proofs/industrial-cost-800-browser.png). + +Resultado final: **58 testes / 1 185 asserções, zero erros/falhas**, incluindo nove casos industriais e regressão do custo directo/oficinas/recebimentos/B1/guardas. **Seis provas de concorrência aprovadas**, com processos realmente sobrepostos. Compilação aprovada com Vite 8.3.5, sem alterações de dependências. Instância do navegador parada antes da regressão final. `migrate:status` normal confirmou `100000` pendente; não houve migração ou concessão em desenvolvimento. + +Prova efectivamente realizada no Edge, `http://127.0.0.1:8001`, apenas `sgf_tempo_test`, sessões/cache isolados e CSRF activo. Login real, sem usar `actingAs` como demonstração: + +1. Fixture cria um lote/produto e sete dados/taxas pelas operações do módulo, com validação sintética distinta; não preenche critério ou cálculos e não cria `wo_*`/`dc_*`. +2. Preparador `gestor@example.test` entra em Empresa Horizonte — Demonstração / Unidade Central — Demonstração → **Custos industriais de teste**. Prepara `IND-TEST-CRITERIO-BROWSER` v1, seleccionando conjunto/utilização exactos, capacidade 10 HM e evidência da hipótese. +3. Validador `consulta@example.test` entra separadamente e valida a versão do critério; não recebe formulário de preparação nem poderes técnicos por consequência. +4. Preparador selecciona explicitamente as oito referências validadas e calcula 1 800 MZN / 180 por UN. Validador revê composição, as duas reconciliações e residual 300, e aprova a versão exacta. +5. Preparador cria nova versão com taxa material ausente; validador aprova apenas os 800 conhecidos, total/unitário indisponíveis e reconciliação parcial, sem esconder pendências. Resultado anterior preservado como histórico. + +IDs desta execução: lote #10, origens/taxas #71–77 e critério #78; não são IDs fixos numa repetição. Credenciais aleatórias no manifesto local protegido, nunca mostradas/versionadas. A prova foi conferida por script guardado; zero eventos técnicos de oficina e zero cálculos de custo directo criados pelo ensaio industrial. + +Seis cenários com dois processos sobrepostos: mesma revisão de critério; mesma decisão; critérios sucessores incompatíveis; mesmo cálculo; cálculos concorrentes com uma só revisão vencedora; resultado novo versus aprovação antiga. A recuperação após falha de outbox e perda de resposta está coberta por testes transaccionais. Outras provas: moedas/unidades/quantidades, denominador zero/ausente, utilização acima da capacidade, produção não concluída/zero, origem duplicada entre tipos/lotes, versão recusada/obsoleta, correcção de origem que exige taxa nova, revogação no pedido seguinte, empresa irmã/unidade sem acesso e imutabilidade SQL. + +Repetição local, com a prova do navegador parada antes dos testes destrutivos: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sgf.ps1 test '--filter=IndustrialCostsTest|DirectCostsTest|WorkshopTest|ServiceReceivablesTest|InternalConfirmationTest|DatabaseSafetyTest' +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sgf.ps1 npm run build +``` + +Para navegador, preparar somente a base de testes vazia sob as guardas existentes; executar `scripts/browser-proof.ps1 start` e depois `sgf.ps1 php scripts/industrial-cost-fixture.php --env=testing`. Seguir os passos acima com a credencial do manifesto protegido. Os preparadores recusam estado preenchido, sem apagar dados para continuar. Após o percurso, `sgf.ps1 php scripts/industrial-cost-proof.php --env=testing`, depois `sgf.ps1 php scripts/industrial-cost-concurrency.php --env=testing`. Parar com `scripts/browser-proof.ps1 stop`. Não parar processos de outros projectos para libertar portas. As provas persistem em ficheiros; a regressão final recria apenas a base de testes. + +## Migração proposta — NÃO aplicada em desenvolvimento + +**`2026_10_27_100000_create_industrial_cost_simulation.php`**, SQL `database/sql/industrial-costs.sql`, destino futuro proposto **`sgf_tempo_dev`**, exigindo confirmação específica antes de qualquer aplicação. + +Cria oito tabelas: `ic_products`, `ic_lots`, `ic_inputs`, `ic_results`, `ic_decisions`, `ic_commands`, `ic_events`, `ic_outbox`, mais índices, FKs e funções/triggers próprios de integridade, ambiente e imutabilidade. Amplia o CHECK de concessões com três nomes industriais de unidade, preservando os anteriores. Não atribui direitos ou altera registos de oficinas/recebimentos/custos/fiscalidade existentes. `down()` recusa destruição de história. Sem reset/fresh em desenvolvimento; mesmo instalada futuramente, mantém custeio real bloqueado na aplicação/base. + +## Quatro concessões propostas — separadas, NÃO aplicadas + +Conferência read-only por `scripts/industrial-cost-permissions-preview.php`, sem modo de aplicação. Tenant #1 / **Tempo Lda #1 / SEGI #1**. As quatro estavam ausentes; `100000` não aplicada e guarda industrial real recusada. + +| Conta existente | Direitos propostos | +| --- | --- | +| Osmane Nala #1 — nala@tempo.co.mz | `industrial_costs.view`, `industrial_costs.prepare` | +| Samira Nal #2 — onala@me.com | `industrial_costs.view`, `industrial_costs.validate` | + +Preparação abrange dados/valorizações, critério e cálculo; validação abrange versões de dados de custo, critério e resultado, sem edição das fontes técnicas. Não inclui gestão de acessos, margens, stock, emissão ou activação real. Só equivalentes sintéticos receberam estes direitos nos testes. Não foram aplicados ou derivados dos quatro direitos de custo directo. + +## Ficheiros e limites + +Novos: `app/Modules/Industry/IndustrialCosts.php`, `Http/IndustrialCostsController.php`, `app/Shared/Costs/ExactCostMath.php`, `resources/js/IndustrialCostsWorkspace.jsx`, migração/SQL acima, `tests/Feature/IndustrialCostsTest.php`, `tests/Support/IndustrialCostFixture.php`, scripts `industrial-cost-fixture.php`, `industrial-cost-worker.php`, `industrial-cost-concurrency.php`, `industrial-cost-proof.php`, `industrial-cost-permissions-preview.php` e documentação/provas. Integrações em `resources/js/app.jsx`, `routes/web.php`; `DirectCosts.php` passa a usar o pequeno auxiliar puro de exactidão/conversão, sem mudança de política ou âmbito. + +Sem novo software, substituição de versões ou dependências. Sem dados de demonstração na aplicação normal ou alterações de ligação. Este recorte cria produto próprio por lote, uma cadeia por componente, unidade KG para material e uma etapa; não implementa catálogo industrial geral, integração de fontes de oficina, partilha de conjuntos entre lotes, stock/compras/salários/activos, previsto/realizado, WIP, perdas, preços/margens, facturação, recebimentos, reconhecimento, lançamentos ou fecho. Dados de origem e evidências são declarações sintéticas, sem comprovação externa de produção ou custo real. Conformidade/activação reais permanecem fora do âmbito. Nenhuma aceitação ou checkpoint de encerramento é declarado. diff --git a/docs/WORKSHOP_DIRECT_COST_DELIVERY.md b/docs/WORKSHOP_DIRECT_COST_DELIVERY.md index 152aec6..901d1f8 100644 --- a/docs/WORKSHOP_DIRECT_COST_DELIVERY.md +++ b/docs/WORKSHOP_DIRECT_COST_DELIVERY.md @@ -78,6 +78,18 @@ São **quatro concessões propostas**, não autorização de aplicação ou acti Em 27/10/2026, o utilizador autorizou o encerramento após conferência técnica do percurso e preservação de checkpoint local. Ver [registo de encerramento e limites](WORKSHOP_DIRECT_COST_ACCEPTANCE.md). As referências anteriores a ausência de aceitação presumida descrevem a entrega antes desta autorização; `090000`, quatro concessões e uso operacional real continuam por autorizar/aplicar. +## Instalação técnica especificamente autorizada — 27/10/2026 + +Após autorização expressa, aplicada **apenas** `2026_10_27_090000_create_workshop_direct_costs` em `sgf_tempo_dev`, por caminho explícito com `sgf_dev_owner`, separado da aplicação `sgf_dev_app`. Acrescentou-se exactamente essa entrada no registo de migrações; instalações anteriores preservadas, sem reaplicar `070000` ou `080000`. + +Aplicadas e auditadas exactamente quatro concessões, todas tenant #1 / Tempo Lda #1 / SEGI #1: Osmane #1 `direct_costs.view` e `direct_costs.prepare`; Samira #2 `direct_costs.view` e `direct_costs.validate`. Nenhuma outra conta/unidade ou função técnica recebeu direitos. + +O procedimento `scripts/direct-cost-dev-install.php` comparou em memória os registos das **60 tabelas preexistentes**, incluindo acessos, auditoria, oficinas, recebimentos e cálculo fiscal. Dados/acessos anteriores preservados. Os únicos novos registos são uma entrada de migração, quatro concessões e quatro eventos de auditoria. As **seis tabelas de custo continuam vazias**: nenhuma taxa, cálculo, decisão de custos ou demonstração na base normal. + +As listagens de custos das duas contas continuam bloqueadas/vazias. Guarda, proposta de taxa, cálculo e decisão recusados com 403; guardas de oficinas, recebimentos e emissão real também recusadas. As funções SQL de ambiente de custos, recebimentos e oficinas recusaram `sgf_tempo_dev`. Fontes funcionais e guardas não foram alteradas. Resumo local em `storage/app/private/direct-cost-dev-install-proof.json`, excluído do Git, sem exportar conteúdos dos registos ou credenciais. + +As referências anteriores a migração/concessões pendentes descrevem o estado anterior a esta autorização. Esta instalação não activa custeio operacional, não resolve pendências de conformidade e não autoriza publicação, custos indirectos ou lote industrial. O checkpoint `d10ba06` permanece preservado. Sem alteração funcional, não foram repetidos testes destrutivos, compilação ou a demonstração aceite. + ## Ficheiros e limites Núcleo `app/Modules/Workshop/DirectCosts.php`, controlador `Http/DirectCostsController.php`, interface `resources/js/DirectCostsWorkspace.jsx`, integrações em `app.jsx` e `routes/web.php`; migração/SQL acima; `tests/Feature/DirectCostsTest.php`, `tests/Support/DirectCostFixture.php`; scripts `direct-cost-fixture.php`, `direct-cost-worker.php`, `direct-cost-concurrency.php`, `direct-cost-proof.php`, `direct-cost-permissions-preview.php`; esta documentação e provas. Não se alteraram ferramentas/dependências ou o motor fiscal. diff --git a/docs/proofs/industrial-cost-1800-browser.png b/docs/proofs/industrial-cost-1800-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..b650a710e89174185c871dcdb41ed1867688b64b GIT binary patch literal 66412 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

X^lFGJ~7nJ#Zf9bz-6aMSDf55y^p}Mll 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 aJ{-~3&b~2y#Ipc$;~p|Ua+A-efxiOkrX;EW literal 0 HcmV?d00001 diff --git a/docs/proofs/industrial-cost-800-browser.png b/docs/proofs/industrial-cost-800-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..de38f92d00659424298a884e1cf060133a0f6a6e GIT binary patch literal 71287 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 Wnu!{-vQ|{N)JVMH2SDNRz`p^yH#CL- literal 0 HcmV?d00001 diff --git a/docs/proofs/industrial-cost-browser-results.json b/docs/proofs/industrial-cost-browser-results.json new file mode 100644 index 0000000..139b942 --- /dev/null +++ b/docs/proofs/industrial-cost-browser-results.json @@ -0,0 +1,35 @@ +{ + "policy": "INDUSTRIAL-COST-SIM-v1", + "database": "sgf_tempo_test", + "synthetic_only": true, + "real_browser_login": true, + "csrf_enabled": true, + "distinct_roles": true, + "full": { + "material": "1000", + "labour": "500", + "pool": "600", + "capacity_hm": "10", + "used_hm": "5", + "rate": "60", + "allocated": "300", + "residual": "300", + "cost": "1800", + "good_units": "10", + "unit_cost": "180", + "origins": "2100" + }, + "partial": { + "known": "800", + "total": null, + "unit_cost": null, + "incomplete": true, + "known_origins": "1100", + "residual": "300" + }, + "workshop_events_created": 0, + "hypothesis_only": true, + "development_migration_applied": false, + "real_grants_applied": false, + "user_acceptance": false +} \ No newline at end of file diff --git a/docs/proofs/industrial-cost-concurrency.json b/docs/proofs/industrial-cost-concurrency.json new file mode 100644 index 0000000..ba0027a --- /dev/null +++ b/docs/proofs/industrial-cost-concurrency.json @@ -0,0 +1,14 @@ +{ + "policy": "INDUSTRIAL-COST-SIM-v1", + "database": "sgf_tempo_test", + "overlapping_processes_per_case": 2, + "checks": { + "criterion_repetition_one_version": true, + "decision_repetition_one_effect": true, + "competing_criteria_one_successor": true, + "calculation_repetition_one_snapshot": true, + "competing_calculations_one_current_allocation": true, + "new_result_refuses_old_approval": true + }, + "synthetic_only": true +} \ No newline at end of file diff --git a/docs/proofs/industrial-cost-tests.json b/docs/proofs/industrial-cost-tests.json new file mode 100644 index 0000000..c48ab79 --- /dev/null +++ b/docs/proofs/industrial-cost-tests.json @@ -0,0 +1,22 @@ +{ + "policy": "INDUSTRIAL-COST-SIM-v1", + "date": "2026-10-27", + "database": "sgf_tempo_test", + "filter": "IndustrialCostsTest|DirectCostsTest|WorkshopTest|ServiceReceivablesTest|InternalConfirmationTest|DatabaseSafetyTest", + "tests": 58, + "assertions": 1185, + "failures": 0, + "errors": 0, + "industrial_test_cases": 9, + "overlapping_process_cases": 6, + "junit_local_ignored": "storage/app/industrial-cost-tests.xml", + "build": {"command": "scripts/sgf.ps1 npm run build", "vite": "8.3.5", "passed": true}, + "shared_exact_cost_math_regression_passed": true, + "real_browser_login": true, + "browser_server_stopped_before_final_tests": true, + "development_migration_100000_applied": false, + "real_industrial_grants_applied": false, + "industrial_and_other_real_operational_blocks_preserved": true, + "user_acceptance": false, + "closing_checkpoint_created": false +} diff --git a/resources/js/IndustrialCostsWorkspace.jsx b/resources/js/IndustrialCostsWorkspace.jsx new file mode 100644 index 0000000..fdbb822 --- /dev/null +++ b/resources/js/IndustrialCostsWorkspace.jsx @@ -0,0 +1,33 @@ +import React,{useEffect,useRef,useState} from 'react'; +const names={material:'Consumo material',labour:'Trabalho efectivo',pool:'Origem do conjunto indirecto',machine:'Utilização de máquina',production:'Produção boa concluída',material_rate:'Taxa de material',labour_rate:'Taxa de trabalho',criterion:'Critério de imputação'}; +const units={material:'KG',labour:'MIN',pool:'MZN',machine:'HM',production:'UN',material_rate:'KG',labour_rate:'H'}; +const money=v=>v===null?'Indisponível':`${v} MZN`; +export default function IndustrialCostsWorkspace({context,can,api,busy}){ + const [data,setData]=useState(null),[error,setError]=useState(''),[lotId,setLotId]=useState(''),[kind,setKind]=useState('criterion'),[selection,setSelection]=useState({}),[working,setWorking]=useState(false);const retry=useRef(null); + const allowed=r=>can(`industrial_costs.${r}`,context.unit?.id??null);const disabled=busy||working; + const load=async()=>{const r=await api('/api/industrial-costs');setData(r);};useEffect(()=>{setSelection({});load().catch(e=>setError(e.message));},[context.token]); + const lot=data?.lots.find(l=>String(l.id)===lotId)??data?.lots[0];const latest=lot?.results.at(-1);const lastInput=lot?.inputs.filter(i=>i.kind===kind).at(-1); + const options=k=>lot?.inputs.filter(i=>i.kind===k&&i.current&&i.decision?.decision==='approved')??[]; + const reference=v=>{const r=lot.inputs.find(i=>String(i.id)===String(v));return r?{id:r.id,version:r.version}:null;}; + async function send(path,input){setWorking(true);setError('');const signature=JSON.stringify([path,input]);if(retry.current?.signature!==signature)retry.current={signature,key:crypto.randomUUID()};try{await api(path,'POST',input,context.token,retry.current.key);retry.current=null;await load();}catch(e){setError(e.message);}finally{setWorking(false);}} + const decide=(type,row,decision,reason)=>send(`/api/industrial-costs/lots/${lot.id}/decisions`,{type,id:row.id,version:row.version,decision,reason}); + const inputForm=e=>{e.preventDefault();const f=new FormData(e.currentTarget);let d; + if(kind==='criterion'){const p=reference(f.get('pool')),m=reference(f.get('machine'));if(!p||!m){setError('Escolher versões explícitas do conjunto e utilização.');return;}d={pool_id:p.id,pool_version:p.version,machine_id:m.id,machine_version:m.version,capacity:f.get('capacity'),driver_unit:'HM',hypothesis:'synthetic_only'};} + else if(kind.endsWith('_rate')){const source=reference(f.get('source'));if(!source){setError('Escolher origem exacta.');return;}d={source_id:source.id,source_version:source.version,amount:f.get('amount'),currency:'MZN',unit:units[kind]};} + else {d={quantity:f.get('quantity'),unit:units[kind],event_date:f.get('event_date')};if(kind==='production')d.completed=f.get('completed')==='on';} + send(`/api/industrial-costs/lots/${lot.id}/inputs`,{kind,source_ref:lastInput?.source_ref??f.get('source_ref'),version:(lastInput?.version??0)+1,previous_id:lastInput?.id??null,data:d,evidence:f.get('evidence')});}; + return

INDUSTRIAL-COST-SIM-v1

Ensaio de custos industriais

ENSAIO INDUSTRIAL — SEM UTILIZAÇÃO OPERACIONAL. Critério sintético; não configura método contabilístico real. Sem stock, facturação, margens ou lançamentos.
{error&&

{error}

} + {!data?

A carregar…

:data.blocked?

Custeio industrial real bloqueado nesta instância.

:<> + {allowed('prepare')&&
Abrir lote sintético e produto próprios
{e.preventDefault();const f=new FormData(e.currentTarget);send('/api/industrial-costs/lots',Object.fromEntries([...f.entries(),['currency','MZN']]));}}>{[['reference','Referência do lote'],['product_reference','Referência do produto'],['product_name','Nome do produto'],['centre','Centro sintético']].map(([n,l])=>)}

MZN explícito; produto em UN. Não é ordem de oficina.

} + + {!lot?

Sem lotes no âmbito autorizado.

:<>

{lot.product.name} · {lot.centre} · {lot.period_start} a {lot.period_end} · {lot.currency}

+ {allowed('prepare')&&

Preparar origem, taxa ou critério versionado

{lastInput?`Nova versão ${(lastInput.version)+1}, anterior #${lastInput.id}; origem ${lastInput.source_ref}.`:'Nova origem sintética identificada, sem duplicar outra origem.'}

{!lastInput&&} + {kind==='criterion'?<>

Hipótese apenas do ensaio; denominador explícito. Não redistribuir residual.

:kind.endsWith('_rate')?<>:<>{kind==='production'&&}} + } +

Origens, taxas e critério

{lot.inputs.map(i=>
#{i.id} · {names[i.kind]} v{i.version}

{i.source_ref} · {i.current?'Última versão':'Histórico'} · {i.decision?`${i.decision.decision==='approved'?'Validado':'Recusado'} — ${i.decision.reason}`:'Aguarda validação'}

{Object.entries(i.data).map(([k,v])=>
{({quantity:'Quantidade',unit:'Unidade',event_date:'Data',amount:'Taxa',currency:'Moeda',capacity:'Capacidade',driver_unit:'Unidade da base',hypothesis:'Hipótese',source_id:'Origem #',source_version:'Versão da origem',pool_id:'Conjunto #',pool_version:'Versão do conjunto',machine_id:'Utilização #',machine_version:'Versão da utilização',completed:'Concluído'})[k]??k}
{v===true?'Sim':v==='synthetic_only'?'Apenas ensaio':String(v)}
)}

{i.evidence}

{allowed('validate')&&i.current&&!i.decision&&decide('input',i,d,r)}/>}
)}
+ {allowed('prepare')&&
{e.preventDefault();const refs=Object.fromEntries(Object.keys(names).map(k=>[k,reference(selection[k]??'')]));send(`/api/industrial-costs/lots/${lot.id}/calculations`,{currency:'MZN',version:(latest?.version??0)+1,previous_id:latest?.id??null,refs});}}>

Seleccionar versões para cálculo

Selecção explícita; taxa material ausente mantém pendência. As outras referências são necessárias.

{Object.entries(names).map(([k,n])=>)}
} +

Resultados e residual

{lot.results.map(r=>

Resultado #{r.id} · v{r.version}

{r.current?'Dependências actuais':'DESACTUALIZADO / HISTÓRICO — não usar como actual'} · {r.decision?`${r.decision.decision==='approved'?'Validado':'Recusado'} — ${r.decision.reason}`:'Aguarda validação'}

{r.snapshot.hypothesis}

ComponenteComposição exactaCusto no lote
Material{r.snapshot.origins.material.data.quantity} KG × {r.snapshot.origins.material_rate?.data.amount??'taxa em falta'}{money(r.snapshot.material_exact)}
Trabalho{r.snapshot.origins.labour.data.quantity} MIN ÷ 60 = {r.snapshot.labour_hours_exact} H × {r.snapshot.origins.labour_rate.data.amount}{money(r.snapshot.labour_exact)}
Indirectos imputados{r.snapshot.pool_exact} ÷ {r.snapshot.capacity_hm} HM = {r.snapshot.indirect_rate_exact} MZN/HM; × {r.snapshot.used_hm} HM{money(r.snapshot.allocated_exact)}

Custo atribuído conhecido: {money(r.snapshot.known_cost_exact)}

{r.snapshot.incomplete?'TOTAL INCOMPLETO — material pendente':'Total do perímetro sintético: '+money(r.snapshot.selected_total_exact)}

{r.snapshot.good_units} UN boas · Custo unitário do cenário: {r.snapshot.unit_cost_exact===null?'Indisponível':money(r.snapshot.unit_cost_exact)+'/UN'}

Residual separado: {money(r.snapshot.residual_exact)} por imputar

Conjunto: {r.snapshot.pool_exact} = {r.snapshot.allocated_exact} imputados + {r.snapshot.residual_exact} por imputar.

Reconciliação {r.snapshot.reconciliation.coverage==='partial_known_only'?'PARCIAL — só valores conhecidos':'do perímetro'}: {r.snapshot.reconciliation.known_origins_exact} = {r.snapshot.reconciliation.assigned_exact} atribuídos + {r.snapshot.reconciliation.unassigned_exact} por imputar.

    {r.snapshot.pending.map(p=>
  • {p}
  • )}
Referências exactas utilizadas
    {Object.entries(r.snapshot.refs).map(([k,v])=>
  • {names[k]}: {v?`#${v.id} v${v.version}`:'Ausente'}
  • )}
{allowed('validate')&&r.current&&!r.decision&&decide('result',r,d,reason)}/>}
)}
+ }}
; +} +function SourceSelect({label,name,rows}){return ;} +function DecisionForm({disabled,onDecision}){return
{e.preventDefault();const f=new FormData(e.currentTarget);onDecision(f.get('decision'),f.get('reason'));}}>
;} diff --git a/resources/js/app.jsx b/resources/js/app.jsx index a85d3e8..5906735 100644 --- a/resources/js/app.jsx +++ b/resources/js/app.jsx @@ -10,6 +10,7 @@ import ServiceInvoiceWorkspace from './ServiceInvoiceWorkspace.jsx'; import ServiceReceivablesWorkspace from './ServiceReceivablesWorkspace.jsx'; import WorkshopWorkspace from './WorkshopWorkspace.jsx'; import DirectCostsWorkspace from './DirectCostsWorkspace.jsx'; +import IndustrialCostsWorkspace from './IndustrialCostsWorkspace.jsx'; const labels = { 'catalogue_accounting.view':'Consultar classificação contabilística', 'catalogue_accounting.propose':'Preparar classificação contabilística', 'catalogue_accounting.approve':'Validar ou recusar classificação contabilística', 'company.view': 'Consultar empresa', 'company.edit': 'Editar nome da empresa', 'unit.create': 'Criar unidades', 'access.manage': 'Gerir acessos', 'unit.view': 'Consultar unidade', 'unit.edit': 'Editar nome da unidade' }; @@ -54,7 +55,7 @@ function App() { if (!state) return

SGF TEMPO

{error || 'A carregar…'}

; if (state.setup_pending) return

SGF TEMPO · PREPARAÇÃO LOCAL

Falta concluir a primeira conta

A aplicação está pronta, mas ainda não existe um utilizador. O login ficará disponível depois de definires a palavra-passe no procedimento local já autorizado.

Concluir no terminal do VS Code

Executa este comando, confere o âmbito apresentado e define a palavra-passe nos dois pedidos de entrada oculta:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\laragon\data\sgf-tempo\run\complete-first-admin.ps1

Não envies a palavra-passe pelo chat. Nenhuma conta é criada por este ecrã.

{error &&

{error}

}
; if (!state.user) return

BEM-VINDO AO SGF TEMPO

Entrar na aplicação

Use a conta preparada pelo procedimento local autorizado.

{error &&
{error}
} perform(() => api('/api/login', 'POST', { email, password }), 'Sessão iniciada. Seleccione o contexto de trabalho.')} />

Precisa de recuperar o acesso? Solicite recuperação local assistida. Não existe recuperação por email nesta etapa.

; - return
CONTEXTO DE TRABALHO{context?.company.name || 'Seleccione uma empresa'}{context?.unit?.name || 'Sem unidade seleccionada'}
Ambiente local
+ return
CONTEXTO DE TRABALHO{context?.company.name || 'Seleccione uma empresa'}{context?.unit?.name || 'Sem unidade seleccionada'}
Ambiente local
{error &&
{error}
}{notice &&
{notice}
}

Apenas os contextos com acesso explícito são apresentados.

{!state.companies.length ?

Sem empresas autorizadas

A conta existe, mas ainda não tem acesso a uma empresa. Peça ao responsável uma concessão explícita.

: !context ?

Escolha onde vai trabalhar

Seleccione uma empresa. A pertença a um cliente contratante não dá acesso automático às suas empresas ou unidades.

:
@@ -66,6 +67,7 @@ function App() { {page === 'documents' && } {page === 'workshop' && } {page === 'direct_costs' && } + {page === 'industrial_costs' && } {page === 'service_receivables' && } {page === 'service_invoices' && } {page === 'service_fiscal' && } diff --git a/routes/web.php b/routes/web.php index 8331411..1570bdc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,6 +17,11 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::middleware(RequireCompanyContext::class)->group(function (): void { Route::get('/api/workshop', [\App\Modules\Workshop\Http\WorkshopController::class,'index']); Route::get('/api/direct-costs', [\App\Modules\Workshop\Http\DirectCostsController::class,'index']); + Route::get('/api/industrial-costs', [\App\Modules\Industry\Http\IndustrialCostsController::class,'index']); + Route::post('/api/industrial-costs/lots', [\App\Modules\Industry\Http\IndustrialCostsController::class,'open']); + Route::post('/api/industrial-costs/lots/{id}/inputs', [\App\Modules\Industry\Http\IndustrialCostsController::class,'input'])->whereNumber('id'); + Route::post('/api/industrial-costs/lots/{id}/calculations', [\App\Modules\Industry\Http\IndustrialCostsController::class,'calculate'])->whereNumber('id'); + Route::post('/api/industrial-costs/lots/{id}/decisions', [\App\Modules\Industry\Http\IndustrialCostsController::class,'decide'])->whereNumber('id'); Route::post('/api/direct-costs/orders/{id}/rates', [\App\Modules\Workshop\Http\DirectCostsController::class,'rate'])->whereNumber('id'); Route::post('/api/direct-costs/orders/{id}/calculations', [\App\Modules\Workshop\Http\DirectCostsController::class,'calculate'])->whereNumber('id'); Route::post('/api/direct-costs/orders/{id}/decisions', [\App\Modules\Workshop\Http\DirectCostsController::class,'decide'])->whereNumber('id'); diff --git a/scripts/direct-cost-dev-install.php b/scripts/direct-cost-dev-install.php new file mode 100644 index 0000000..4f2f005 --- /dev/null +++ b/scripts/direct-cost-dev-install.php @@ -0,0 +1,100 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Artisan; +use App\Modules\Identity\Services\LocalDatabaseGuard; +use App\Modules\Identity\Services\Permissions; +use App\Modules\Identity\Services\Audit; +use App\Modules\Workshop\DirectCosts; +use App\Modules\Workshop\WorkshopWorkflow; +use App\Shared\Context\CompanyContext; + +function ensureDirectCost(bool $ok,string $message): void { if(!$ok)throw new RuntimeException($message); } +function directCostFingerprint(string $table,?int $max=null): array { + ensureDirectCost((bool)preg_match('/^[a-z_]+$/D',$table),'Unexpected table identifier.'); + $where=$max===null?'':' WHERE id <= '.$max; + $r=DB::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text, E\'\\n\' ORDER BY row_to_json(t)::text),\'\')) AS h FROM (SELECT * FROM "'.$table.'"'.$where.') t'); + return [(int)$r->n,$r->h]; // In memory only; no record contents exported. +} +ensureDirectCost(LocalDatabaseGuard::allows(),'Own SGF development application connection required.'); +ensureDirectCost($argv===[$argv[0],'--apply-confirmed-090000-and-four-grants'],'Exact confirmed operation argument required.'); +$migration='2026_10_27_090000_create_workshop_direct_costs'; +ensureDirectCost(!DB::table('migrations')->where('migration',$migration)->exists(),'090000 already installed; review changed state.'); +ensureDirectCost(DB::table('migrations')->where('migration','2026_10_27_080000_create_workshop_resource_records')->exists(),'Expected workshop installation missing.'); +ensureDirectCost(DB::table('migrations')->where('migration','2026_10_27_070000_create_simulated_receivables')->exists(),'Expected receivables installation missing.'); +$newTables=['dc_rates','dc_calculations','dc_decisions','dc_commands','dc_events','dc_outbox']; +$tables=array_map(fn($r)=>$r->tablename,DB::select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename")); +ensureDirectCost(!array_intersect($tables,$newTables),'Receivables tables already exist; review first.'); +$rights=[1=>['view','prepare'],2=>['view','validate']]; +$checkScope=function(): void { + foreach([1=>['Osmane Nala','nala@tempo.co.mz'],2=>['Samira Nal','onala@me.com']] as $id=>[$name,$email]){ + ensureDirectCost(DB::table('users')->where(compact('id','name','email'))->exists(),'Confirmed account identity differs.'); + ensureDirectCost(DB::table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>1])->exists(),'Existing membership required.'); + $p=app(Permissions::class);$p->require($id,1,1,'company.view');$p->require($id,1,1,'unit.view',1); + } + ensureDirectCost(DB::table('companies')->where(['id'=>1,'tenant_id'=>1,'name'=>'Tempo Lda'])->exists(),'Confirmed company differs.'); + ensureDirectCost(DB::table('units')->where(['id'=>1,'tenant_id'=>1,'company_id'=>1,'name'=>'SEGI'])->exists(),'Confirmed unit differs.'); + ensureDirectCost(!DB::table('permission_grants')->where('permission','like','direct_costs.%')->exists(),'Unexpected existing direct cost rights; review first.'); +}; +$checkScope(); +$baseline=[];foreach($tables as $t)$baseline[$t]=directCostFingerprint($t); +$max=[];foreach(['permission_grants','identity_audit'] as $t)$max[$t]=(int)DB::table($t)->max('id'); +$ledger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all(); +$sourceHashes=[];foreach(['app/Modules/Workshop/DirectCosts.php','app/Modules/Services/ServiceReceivables.php','app/Modules/Services/ServiceInvoicing.php','app/Modules/Workshop/WorkshopWorkflow.php'] as $p)$sourceHashes[$p]=hash_file('sha256',base_path($p)); +$secrets=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/database.json'),true,32,JSON_THROW_ON_ERROR); +$owner=config('database.connections.'.config('database.default')); +$owner['username']='sgf_dev_owner';$owner['password']=$secrets['dev_owner'];$owner['url']=null; +config(['database.connections.sgf_direct_cost_owner'=>$owner]);unset($owner,$secrets); +try { + $connection=DB::connection('sgf_direct_cost_owner'); + $identity=$connection->selectOne('SELECT current_database() AS db,current_user AS role,inet_server_port() AS port,rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user'); + ensureDirectCost($identity->db==='sgf_tempo_dev'&&$identity->role==='sgf_dev_owner'&&(int)$identity->port===55432&&!$identity->rolsuper&&!$identity->rolcreatedb&&!$identity->rolcreaterole&&!$identity->rolbypassrls,'Unexpected migration owner/destination.'); + $connection->transaction(function()use($migration,$baseline,$newTables): void { + $applicationConnection=DB::getDefaultConnection();DB::setDefaultConnection('sgf_direct_cost_owner'); + try { + $exit=Artisan::call('migrate',['--database'=>'sgf_direct_cost_owner','--path'=>'database/migrations/'.$migration.'.php','--force'=>true,'--no-interaction'=>true]); + ensureDirectCost($exit===0,'Targeted migration failed.'); + foreach($baseline as $t=>$fp)if($t!=='migrations')ensureDirectCost(directCostFingerprint($t)===$fp,'Existing records changed during migration: '.$t); + foreach($newTables as $t)ensureDirectCost(DB::table($t)->count()===0,'New tables must be empty.'); + } finally { DB::setDefaultConnection($applicationConnection); } + }); +} finally { DB::purge('sgf_direct_cost_owner');config(['database.connections.sgf_direct_cost_owner'=>null]); } +ensureDirectCost(LocalDatabaseGuard::allows(),'Application must remain sgf_dev_app.'); +$afterLedger=DB::table('migrations')->orderBy('id')->get()->map(fn($r)=>(array)$r)->all(); +ensureDirectCost(count($afterLedger)===count($ledger)+1&&array_slice($afterLedger,0,count($ledger))===$ledger&&end($afterLedger)['migration']===$migration,'Unexpected migration ledger delta.'); +$denials=[]; +$grants=DB::transaction(function()use($rights,$checkScope,$baseline,$max,&$denials): array { + DB::table('companies')->where('id',1)->lockForUpdate()->firstOrFail();$checkScope();$grants=[]; + foreach($rights as $user=>$actions)foreach($actions as $action){ + $g=['user_id'=>$user,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>'direct_costs.'.$action]; + DB::table('permission_grants')->insert($g+['created_at'=>now(),'updated_at'=>now()]); + Audit::record('direct_costs.initial_grant',null,1,1,1,subject:$user,details:['permission'=>$g['permission'],'reason'=>'Four unit grants specifically authorised; real operations remain blocked.']);$grants[]=$g; + } + foreach($baseline as $t=>$fp)if($t!=='migrations')ensureDirectCost(directCostFingerprint($t,$max[$t]??null)===$fp,'Prior records changed: '.$t); + ensureDirectCost(DB::table('permission_grants')->where('id','>',$max['permission_grants'])->count()===4,'Exactly four new grants required.'); + ensureDirectCost(DB::table('identity_audit')->where('id','>',$max['identity_audit'])->count()===4&&DB::table('identity_audit')->where('id','>',$max['identity_audit'])->where('action','direct_costs.initial_grant')->count()===4,'Exactly four grant audit entries required.'); + $actual=DB::table('permission_grants')->where('permission','like','direct_costs.%')->get(['user_id','tenant_id','company_id','unit_id','permission'])->map(fn($r)=>(array)$r)->all(); + $sort=fn($a,$b)=>[$a['user_id'],$a['permission']]<=>[$b['user_id'],$b['permission']];usort($actual,$sort);$expected=$grants;usort($expected,$sort);ensureDirectCost($actual===$expected,'Granted accounts/rights/scope differ.'); + foreach([1,2] as $actor){app(CompanyContext::class)->selectFor($actor,1,1,1);$listing=app(DirectCosts::class)->listing($actor);ensureDirectCost($listing['blocked']&&$listing['orders']===[],'Normal direct cost listing must remain blocked.');} + foreach(['guard','rate','calculate','decide','workshop','receivables','issuance'] as $entry){ + try { + $r=app(DirectCosts::class); + match($entry){'guard'=>DirectCosts::guard(),'rate'=>$r->rate(0,1,[],'blocked-probe'),'calculate'=>$r->calculate(0,1,[],'blocked-probe'),'decide'=>$r->decide(0,2,[],'blocked-probe'),'workshop'=>WorkshopWorkflow::guard(),'receivables'=>\App\Modules\Services\ServiceReceivables::guard(),'issuance'=>\App\Modules\Services\ServiceInvoicing::guard()}; + throw new RuntimeException('Operational entry did not refuse.'); + }catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){ensureDirectCost($e->getStatusCode()===403,'Expected operational refusal.');$denials[$entry]=403;} + } + app(CompanyContext::class)->selectFor(1,1,1,1);$w=app(WorkshopWorkflow::class)->listing(1);ensureDirectCost($w['blocked']&&$w['orders']===[],'Workshop operational block must remain.'); + return $grants; +}); +foreach(['dc','sr','wo'] as $prefix){ + try {DB::select('SELECT sgf_'.$prefix.'_environment()');throw new RuntimeException('SQL environment guard did not refuse.');} + catch(\Illuminate\Database\QueryException $e){ensureDirectCost(str_contains($e->getMessage(),'SGF_'.strtoupper($prefix).'_TEST_DATABASE_ONLY'),'Unexpected SQL guard response.');} +} +foreach($sourceHashes as $p=>$hash)ensureDirectCost(hash_file('sha256',base_path($p))===$hash,'Functional source or guard changed.'); +foreach($newTables as $t)ensureDirectCost(DB::table($t)->count()===0,'New direct cost tables must remain empty.'); +$proof=['date'=>'2026-10-27','database'=>'sgf_tempo_dev','migration_applied'=>$migration,'only_one_migration_added'=>true,'workshop_080000_preserved'=>true,'preexisting_tables_checked'=>count($baseline),'prior_records_and_accesses_preserved'=>true,'new_empty_tables'=>$newTables,'new_grants'=>$grants,'audited_new_grants'=>4,'application_role'=>'sgf_dev_app','both_normal_account_listings_blocked'=>true,'entry_denials'=>$denials,'cost_receivables_and_workshop_sql_guards_refused'=>true,'functional_and_guard_sources_unchanged'=>true,'real_operations_created'=>false]; +file_put_contents(storage_path('app/private/direct-cost-dev-install-proof.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo 'Applied only 090000 and four audited grants; '.count($baseline)." prior tables preserved, six new tables empty, costing, workshop and operational/fiscal guards verified.\n"; diff --git a/scripts/industrial-cost-concurrency.php b/scripts/industrial-cost-concurrency.php new file mode 100644 index 0000000..e7aa156 --- /dev/null +++ b/scripts/industrial-cost-concurrency.php @@ -0,0 +1,26 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +use App\Modules\Industry\IndustrialCosts; +use App\Shared\Context\CompanyContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; +use Symfony\Component\Process\Process; +IndustrialCosts::guard();$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$fx=$f['industrial'];$proof=[];$key=fn()=>(string)Str::uuid(); +$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit'],'lot'=>$fx['lot']]; +$pair=function($one,$two)use($base){ + $a=new Process([PHP_BINARY,'scripts/industrial-cost-worker.php','--env=testing'],base_path(),null,json_encode($one+$base+['pause'=>true]));$b=new Process([PHP_BINARY,'scripts/industrial-cost-worker.php','--env=testing'],base_path(),null,json_encode($two+$base)); + $a->start();if(!$a->waitUntil(fn($t,$v)=>str_contains($v,'LOCK_HELD')))throw new RuntimeException('Industrial concurrent lock not observed.');$b->start();$a->wait();$b->wait();$out=[]; + foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Industrial worker failed.');$lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($v)=>str_starts_with($v,'{')));$out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR);}return $out; +}; +$check=function($out,bool $replay){if($out[0]['status']!==200||$out[1]['status']!==($replay?200:409)||($replay&&(!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])))throw new RuntimeException('Unexpected concurrent industrial result.');}; +$old=DB::table('ic_inputs')->where(['lot_id'=>$fx['lot'],'kind'=>'criterion'])->orderByDesc('version')->first(); +$input=['method'=>'input','actor'=>$f['manager_id'],'key'=>$key(),'input'=>['kind'=>'criterion','source_ref'=>$old->source_ref,'version'=>$old->version+1,'previous_id'=>$old->id,'data'=>json_decode($old->data,true),'evidence'=>'Revisão concorrente sintética.']];$out=$pair($input,$input);$check($out,true);$proof['criterion_repetition_one_version']=true;$id=$out[0]['result']['id'];$ver=$out[0]['result']['version']; +$decision=['method'=>'decide','actor'=>$f['reader_id'],'key'=>$key(),'input'=>['type'=>'input','id'=>$id,'version'=>$ver,'decision'=>'approved','reason'=>'Validação concorrente de hipótese sintética.']];$check($pair($decision,$decision),true);$proof['decision_repetition_one_effect']=true; +$one=$input;$one['key']=$key();$one['input']['version']=$ver+1;$one['input']['previous_id']=$id;$two=$one;$two['key']=$key();$two['input']['data']['capacity']='20';$out=$pair($one,$two);$check($out,false);$proof['competing_criteria_one_successor']=true;$cid=$out[0]['result']['id'];$cv=$out[0]['result']['version']; +app(CompanyContext::class)->selectFor($f['reader_id'],$f['tenant'],$f['company'],$f['unit']);app(IndustrialCosts::class)->decide($fx['lot'],$f['reader_id'],['type'=>'input','id'=>$cid,'version'=>$cv,'decision'=>'approved','reason'=>'Validado critério sintético vencedor.'],$key()); +$refs=$fx['refs']+['criterion'=>['id'=>$cid,'version'=>$cv]];$last=DB::table('ic_results')->where('lot_id',$fx['lot'])->orderByDesc('version')->first(); +$calc=['method'=>'calculate','actor'=>$f['manager_id'],'key'=>$key(),'input'=>['currency'=>'MZN','version'=>$last->version+1,'previous_id'=>$last->id,'refs'=>$refs]];$out=$pair($calc,$calc);$check($out,true);$proof['calculation_repetition_one_snapshot']=true; +$last=DB::table('ic_results')->where('lot_id',$fx['lot'])->orderByDesc('version')->first();$calc['key']=$key();$calc['input']['version']=$last->version+1;$calc['input']['previous_id']=$last->id;$calc['input']['refs']['material_rate']=null;$two=$calc;$two['key']=$key();$two['input']['refs']=$refs;$check($pair($calc,$two),false);$proof['competing_calculations_one_current_allocation']=true; +$last=DB::table('ic_results')->where('lot_id',$fx['lot'])->orderByDesc('version')->first();$calc['key']=$key();$calc['input']['version']=$last->version+1;$calc['input']['previous_id']=$last->id;$calc['input']['refs']=$refs; +$stale=['method'=>'decide','actor'=>$f['reader_id'],'key'=>$key(),'input'=>['type'=>'result','id'=>$last->id,'version'=>$last->version,'decision'=>'approved','reason'=>'Aprovação agora obsoleta.']];$check($pair($calc,$stale),false);$proof['new_result_refuses_old_approval']=true; +file_put_contents(base_path('docs/proofs/industrial-cost-concurrency.json'),json_encode(['policy'=>IndustrialCosts::POLICY,'database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'synthetic_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Six overlapping industrial concurrency cases passed; historical allocations not accumulated.\n"; diff --git a/scripts/industrial-cost-fixture.php b/scripts/industrial-cost-fixture.php new file mode 100644 index 0000000..a7b5524 --- /dev/null +++ b/scripts/industrial-cost-fixture.php @@ -0,0 +1,7 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +\App\Modules\Industry\IndustrialCosts::guard();$file='C:/laragon/data/sgf-tempo/secrets/browser-fixture.json';$base=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR); +if(\Illuminate\Support\Facades\DB::table('ic_lots')->exists())throw new RuntimeException('Fresh synthetic proof required; nothing deleted.'); +$f=\Tests\Support\IndustrialCostFixture::create($base+['manager'=>\App\Models\User::findOrFail($base['manager_id']),'reader'=>\App\Models\User::findOrFail($base['reader_id'])],false); +file_put_contents($file,json_encode($base+['industrial'=>array_intersect_key($f,array_flip(['lot','refs','criterionData']))],JSON_THROW_ON_ERROR)); +echo "Industrial synthetic lot and seven validated origins/rates prepared; criterion and calculations remain for browser demonstration. No workshop events created.\n"; diff --git a/scripts/industrial-cost-permissions-preview.php b/scripts/industrial-cost-permissions-preview.php new file mode 100644 index 0000000..609ed21 --- /dev/null +++ b/scripts/industrial-cost-permissions-preview.php @@ -0,0 +1,12 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +if(!\App\Modules\Identity\Services\LocalDatabaseGuard::allows())throw new RuntimeException('Own SGF development application database required.'); +$db=\Illuminate\Support\Facades\DB::class;$grants=[]; +foreach([1=>['view','prepare'],2=>['view','validate']] as $id=>$rights){ + $u=$db::table('users')->where('id',$id)->first(['id','name','email']);if(!$u||!$db::table('company_memberships')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>1])->exists())throw new RuntimeException('Existing account/member required.'); + foreach($rights as $r){$g=['user_id'=>$id,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>'industrial_costs.'.$r];$grants[]=$g+['account'=>$u,'already_granted'=>$db::table('permission_grants')->where($g)->exists()];} +} +$company=$db::table('companies')->where(['id'=>1,'tenant_id'=>1])->value('name');$unit=$db::table('units')->where(['id'=>1,'company_id'=>1,'tenant_id'=>1])->value('name');if($company!=='Tempo Lda'||$unit!=='SEGI')throw new RuntimeException('Scope differs.'); +try{\App\Modules\Industry\IndustrialCosts::guard();throw new RuntimeException('Real industry costing not blocked.');}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){if($e->getStatusCode()!==403)throw $e;} +echo json_encode(['mode'=>'preview_only','company'=>$company,'unit'=>$unit,'proposed_grants'=>$grants,'applied'=>false,'industrial_real_blocked'=>true,'migration_100000_applied'=>$db::table('migrations')->where('migration','2026_10_27_100000_create_industrial_cost_simulation')->exists()],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR).PHP_EOL; diff --git a/scripts/industrial-cost-proof.php b/scripts/industrial-cost-proof.php new file mode 100644 index 0000000..0e8d3bb --- /dev/null +++ b/scripts/industrial-cost-proof.php @@ -0,0 +1,10 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();\App\Modules\Industry\IndustrialCosts::guard(); +$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$db=\Illuminate\Support\Facades\DB::class;$rows=$db::table('ic_results')->where('lot_id',$f['industrial']['lot'])->orderBy('version')->get();if($rows->count()<2)throw new RuntimeException('Two browser scenarios required.'); +$full=json_decode($rows[0]->snapshot,true);$partial=json_decode($rows[1]->snapshot,true); +foreach(['material_exact'=>'1000','labour_exact'=>'500','indirect_rate_exact'=>'60','allocated_exact'=>'300','residual_exact'=>'300','selected_total_exact'=>'1800','unit_cost_exact'=>'180'] as $k=>$v)if($full[$k]!==$v)throw new RuntimeException('Full scenario differs.'); +if($partial['known_cost_exact']!=='800'||$partial['unit_cost_exact']!==null||$partial['selected_total_exact']!==null||!$partial['incomplete']||$partial['reconciliation']['known_origins_exact']!=='1100')throw new RuntimeException('Partial scenario differs.'); +foreach([$rows[0],$rows[1]] as $r){$d=$db::table('ic_decisions')->where(['result_id'=>$r->id,'decision'=>'approved'])->first();if(!$d||$d->actor_id===$r->actor_id)throw new RuntimeException('Distinct validation required.');} +if($db::table('wo_entries')->exists()||$db::table('dc_calculations')->exists())throw new RuntimeException('Workshop/direct cost events unexpectedly created.'); +$proof=['policy'=>'INDUSTRIAL-COST-SIM-v1','database'=>'sgf_tempo_test','synthetic_only'=>true,'real_browser_login'=>true,'csrf_enabled'=>true,'distinct_roles'=>true,'full'=>['material'=>'1000','labour'=>'500','pool'=>'600','capacity_hm'=>'10','used_hm'=>'5','rate'=>'60','allocated'=>'300','residual'=>'300','cost'=>'1800','good_units'=>'10','unit_cost'=>'180','origins'=>'2100'],'partial'=>['known'=>'800','total'=>null,'unit_cost'=>null,'incomplete'=>true,'known_origins'=>'1100','residual'=>'300'],'workshop_events_created'=>0,'hypothesis_only'=>true,'development_migration_applied'=>false,'real_grants_applied'=>false,'user_acceptance'=>false]; +file_put_contents(base_path('docs/proofs/industrial-cost-browser-results.json'),json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Browser results checked: 1800/180 and 800 incomplete, residual 300, distinct validations and no duplicated workshop events.\n"; diff --git a/scripts/industrial-cost-worker.php b/scripts/industrial-cost-worker.php new file mode 100644 index 0000000..6536639 --- /dev/null +++ b/scripts/industrial-cost-worker.php @@ -0,0 +1,7 @@ +make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +\App\Modules\Industry\IndustrialCosts::guard();$in=json_decode(stream_get_contents(STDIN),true,32,JSON_THROW_ON_ERROR); +app(\App\Shared\Context\CompanyContext::class)->selectFor($in['actor'],$in['tenant'],$in['company'],$in['unit']); +if($in['pause']??false){$paused=false;\Illuminate\Support\Facades\DB::listen(function($q)use(&$paused){if(!$paused&&str_contains($q->sql,'"companies"')&&str_contains($q->sql,'for update')){$paused=true;echo "LOCK_HELD\n";flush();usleep(1200000);}});} +try{$c=app(\App\Modules\Industry\IndustrialCosts::class);$out=$c->{$in['method']}($in['lot'],$in['actor'],$in['input'],$in['key']);echo json_encode(['status'=>200,'result'=>$out],JSON_THROW_ON_ERROR).PHP_EOL;} +catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){echo json_encode(['status'=>$e->getStatusCode()],JSON_THROW_ON_ERROR).PHP_EOL;} diff --git a/tests/Feature/IndustrialCostsTest.php b/tests/Feature/IndustrialCostsTest.php new file mode 100644 index 0000000..d1c731c --- /dev/null +++ b/tests/Feature/IndustrialCostsTest.php @@ -0,0 +1,74 @@ +f=IndustrialCostFixture::create();$this->login();} + private function login(string $who='manager'): void {$this->postJson('/api/login',['email'=>$this->f[$who]->email,'password'=>$this->f['password']])->assertOk();$this->token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['unit']])->assertOk()->json('context.token');} + private function send(string $action,array $in,?string $key=null,?int $lot=null){return $this->withHeaders(['X-SGF-Context'=>$this->token,'Idempotency-Key'=>$key??(string)Str::uuid()])->postJson('/api/industrial-costs/lots/'.($lot??$this->f['lot']).'/'.$action,$in);} + private function calc(array $extra=[],?string $key=null){return $this->send('calculations',array_replace(['currency'=>'MZN','version'=>1,'previous_id'=>null,'refs'=>$this->f['refs']],$extra),$key);} + private function snapshot(int $id): array {return json_decode(DB::table('ic_results')->find($id)->snapshot,true,64,JSON_THROW_ON_ERROR);} + private function decide(string $type,int $id,array $extra=[],?string $key=null){$r=DB::table($type==='input'?'ic_inputs':'ic_results')->find($id);return $this->send('decisions',array_replace(['type'=>$type,'id'=>$id,'version'=>$r->version,'decision'=>'approved','reason'=>'Revisão sintética da versão industrial.'],$extra),$key);} + private function change(string $kind,array $data,array $extra=[]){$old=DB::table('ic_inputs')->find($this->f['refs'][$kind]['id']);return $this->send('inputs',array_replace(['kind'=>$kind,'source_ref'=>$old->source_ref,'version'=>$old->version+1,'previous_id'=>$old->id,'data'=>$data,'evidence'=>'Correcção sintética explícita, histórico preservado.'],$extra));} + private function listing(){return $this->getJson('/api/industrial-costs')->assertOk();} + public function test_full_and_partial_scenarios_with_exact_reconciliations_and_separate_approval(): void { + $before=DB::table('wo_entries')->count();$id=$this->calc()->assertOk()->json('id');$s=$this->snapshot($id); + foreach(['material_exact'=>'1000','labour_exact'=>'500','labour_hours_exact'=>'5','indirect_rate_exact'=>'60','allocated_exact'=>'300','residual_exact'=>'300','known_cost_exact'=>'1800','unit_cost_exact'=>'180','good_units'=>'10'] as $k=>$v)$this->assertSame($v,$s[$k]);$this->assertSame('2100',$s['reconciliation']['known_origins_exact']);$this->assertTrue($s['reconciliation']['balanced']);$this->assertFalse($s['incomplete']); + IdentityFixture::grant($this->f['manager']->id,$this->f['tenant'],$this->f['company'],'industrial_costs.validate',$this->f['unit']);$this->decide('result',$id)->assertForbidden();$this->login('reader');$this->decide('result',$id)->assertOk();$this->login(); + $refs=$this->f['refs'];$refs['material_rate']=null;$partial=$this->calc(['version'=>2,'previous_id'=>$id,'refs'=>$refs])->assertOk()->json('id');$p=$this->snapshot($partial);$this->assertSame('800',$p['known_cost_exact']);$this->assertSame('1100',$p['reconciliation']['known_origins_exact']);$this->assertSame('partial_known_only',$p['reconciliation']['coverage']);$this->assertNull($p['selected_total_exact']);$this->assertNull($p['unit_cost_exact']);$this->assertTrue($p['incomplete']);$this->assertCount(1,$p['pending']);$this->assertSame($before,DB::table('wo_entries')->count());$this->listing()->assertJsonPath('lots.0.results.0.current',false); + } + public function test_zero_rate_and_nonfinite_representation_remain_exact(): void { + $r=DB::table('ic_inputs')->find($this->f['refs']['material_rate']['id']);$d=json_decode($r->data,true);$d['amount']='0';$zero=$this->change('material_rate',$d)->assertOk()->json('id');$this->login('reader');$this->decide('input',$zero)->assertOk();$this->login();$refs=$this->f['refs'];$refs['material_rate']=['id'=>$zero,'version'=>2];$first=$this->calc(['refs'=>$refs])->assertOk()->json('id');$s=$this->snapshot($first);$this->assertSame('0',$s['material_exact']);$this->assertSame('800',$s['selected_total_exact']);$this->assertSame('80',$s['unit_cost_exact']);$this->assertFalse($s['incomplete']); + $production=DB::table('ic_inputs')->find($refs['production']['id']);$pd=json_decode($production->data,true);$pd['quantity']='3';$new=$this->change('production',$pd)->assertOk()->json('id');$this->login('reader');$this->decide('input',$new)->assertOk();$this->login();$refs['production']=['id'=>$new,'version'=>2];$second=$this->calc(['refs'=>$refs,'version'=>2,'previous_id'=>$first])->assertOk()->json('id');$this->assertSame('800/3',$this->snapshot($second)['unit_cost_exact']); + } + public function test_capacity_zero_overuse_unknown_sources_units_and_numeric_coercion_refused(): void { + $criterion=$this->f['criterionData'];foreach(['0','-1','1e2','1.000000001',''] as $capacity)$this->change('criterion',array_replace($criterion,['capacity'=>$capacity]))->assertStatus(422); + $this->change('criterion',array_replace($criterion,['capacity'=>10]))->assertStatus(422);$this->change('criterion',array_replace($criterion,['driver_unit'=>'H']))->assertStatus(422); + $small=$this->change('criterion',array_replace($criterion,['capacity'=>'4']))->assertOk()->json('id');$this->login('reader');$this->decide('input',$small)->assertOk();$this->login();$refs=$this->f['refs'];$refs['criterion']=['id'=>$small,'version'=>2];$this->calc(['refs'=>$refs])->assertStatus(409); + $pd=['quantity'=>'0','unit'=>'UN','event_date'=>'2026-10-27','completed'=>true];$this->change('production',$pd)->assertStatus(422);$this->change('production',array_replace($pd,['quantity'=>'10','completed'=>false]))->assertStatus(422); + $refs=$this->f['refs'];$refs['material']=['id'=>$refs['pool']['id'],'version'=>1];$this->calc(['refs'=>$refs])->assertStatus(409);$this->calc(['currency'=>'USD'])->assertStatus(422);$refs=$this->f['refs'];$refs['labour']=null;$this->calc(['refs'=>$refs])->assertStatus(422); + } + public function test_criterion_source_and_rate_changes_invalidate_results_without_editing_history(): void { + $id=$this->calc()->assertOk()->json('id');$snapshot=DB::table('ic_results')->find($id)->snapshot;$this->login('reader');$this->decide('result',$id)->assertOk();$this->login(); + $c=$this->change('criterion',array_replace($this->f['criterionData'],['capacity'=>'20']))->assertOk()->json('id');$this->listing()->assertJsonPath('lots.0.results.0.current',false);$this->calc(['version'=>2,'previous_id'=>$id])->assertStatus(409);$this->login('reader');$this->decide('input',$this->f['refs']['criterion']['id'])->assertStatus(409);$this->decide('input',$c)->assertOk();$this->login(); + $refs=$this->f['refs'];$refs['criterion']=['id'=>$c,'version'=>2];$new=$this->calc(['refs'=>$refs,'version'=>2,'previous_id'=>$id])->assertOk()->json('id');$this->assertSame('150',$this->snapshot($new)['allocated_exact']);$this->assertSame('450',$this->snapshot($new)['residual_exact']);$this->assertSame($snapshot,DB::table('ic_results')->find($id)->snapshot); + $rate=DB::table('ic_inputs')->find($refs['labour_rate']['id']);$d=json_decode($rate->data,true);$d['amount']='101';$this->change('labour_rate',$d)->assertOk();$this->listing()->assertJsonPath('lots.0.results.1.current',false); + } + public function test_idempotency_recovery_after_outbox_failure_and_response_loss(): void { + $key=(string)Str::uuid();DB::unprepared("CREATE FUNCTION ic_fail_test() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'IC_TEST_OUTBOX_FAILURE'; END; \$\$; CREATE TRIGGER ic_fail_test BEFORE INSERT ON ic_outbox FOR EACH ROW EXECUTE FUNCTION ic_fail_test()");$before=DB::table('ic_commands')->count();$this->assertGreaterThanOrEqual(500,$this->calc([],$key)->status());$this->assertSame(0,DB::table('ic_results')->count());$this->assertSame($before,DB::table('ic_commands')->count());DB::unprepared('DROP TRIGGER ic_fail_test ON ic_outbox; DROP FUNCTION ic_fail_test()'); + $id=$this->calc([],$key)->assertOk()->json('id');$this->calc([],$key)->assertOk()->assertJsonPath('id',$id)->assertJsonPath('replayed',true);$refs=$this->f['refs'];$refs['material_rate']=null;$this->calc(['refs'=>$refs],$key)->assertStatus(409);$this->assertSame(1,DB::table('ic_results')->count());$this->assertSame(DB::table('ic_commands')->count(),DB::table('ic_events')->count());$this->assertSame(DB::table('ic_events')->count(),DB::table('ic_outbox')->count()); + } + public function test_permissions_scope_revocation_and_direct_cost_grants_do_not_imply_industrial(): void { + $id=$this->calc()->assertOk()->json('id');$this->login('reader');$this->calc()->assertForbidden();$this->login('limited');IdentityFixture::grant($this->f['limited']->id,$this->f['tenant'],$this->f['company'],'direct_costs.view',$this->f['unit']);$this->getJson('/api/industrial-costs')->assertForbidden();$this->login(); + $this->send('calculations',['currency'=>'MZN','version'=>1,'refs'=>$this->f['refs']],null,$this->f['lot']+999)->assertForbidden();DB::table('permission_grants')->where(['user_id'=>$this->f['manager']->id,'permission'=>'industrial_costs.prepare'])->delete();$this->calc()->assertForbidden(); + $this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['hidden']])->assertForbidden(); + $this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['sibling'],'unit_id'=>$this->f['siblingUnit']])->assertOk();$this->getJson('/api/industrial-costs')->assertForbidden(); + } + public function test_canonical_origin_cannot_move_between_lots_or_direct_and_indirect_roles(): void { + $id=$this->f['refs']['material']['id'];$source=DB::table('ic_inputs')->find($id);$pool=DB::table('ic_inputs')->find($this->f['refs']['pool']['id']);$this->change('pool',json_decode($pool->data,true),['source_ref'=>$source->source_ref])->assertStatus(409); + $c=app(IndustrialCosts::class);app(CompanyContext::class)->selectFor($this->f['manager']->id,$this->f['tenant'],$this->f['company'],$this->f['unit']);$lot=$c->open($this->f['manager']->id,['reference'=>'LOT-OTHER','product_reference'=>'PROD-OTHER','product_name'=>'Produto sintético 2','centre'=>'Centro teste','period_start'=>'2026-10-01','period_end'=>'2026-10-31','currency'=>'MZN'],(string)Str::uuid())['id']; + $this->send('inputs',['kind'=>'material','source_ref'=>$source->source_ref,'version'=>1,'data'=>json_decode($source->data,true),'evidence'=>'Duplicação proibida.'],null,$lot)->assertStatus(409); + $this->assertSame(0,DB::table('wo_entries')->count()); + } + public function test_refusal_old_decisions_bulk_sql_and_environment_blocks(): void { + $new=$this->change('criterion',$this->f['criterionData'])->assertOk()->json('id');$this->login('reader');$this->decide('input',$new,['decision'=>'refused'])->assertOk();$this->login();$refs=$this->f['refs'];$refs['criterion']=['id'=>$new,'version'=>2];$this->calc(['refs'=>$refs])->assertStatus(409); + foreach(['UPDATE ic_inputs SET unit_id=999','DELETE FROM ic_inputs'] as $sql){try{DB::transaction(fn()=>DB::statement($sql));$this->fail('Immutable SQL accepted');}catch(\Illuminate\Database\QueryException){$this->assertTrue(true);}} + $this->app['env']='local';$this->getJson('/api/industrial-costs')->assertOk()->assertJsonPath('blocked',true);try{IndustrialCosts::guard();$this->fail('Real costing allowed');}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){$this->assertSame(403,$e->getStatusCode());}$this->app['env']='testing';$old=config('database.connections.pgsql.database');config(['database.connections.pgsql.database'=>'sgf_tempo_dev']);try{IndustrialCosts::guard();$this->fail('Wrong database accepted');}catch(\RuntimeException $e){$this->assertStringContainsString('SGF_TEST_DATABASE_REFUSED',$e->getMessage());}finally{config(['database.connections.pgsql.database'=>$old]);} + } + public function test_corrected_material_requires_new_rate_and_cannot_be_read_from_another_unit(): void { + $first=$this->calc()->assertOk()->json('id');$oldSnapshot=DB::table('ic_results')->find($first)->snapshot; + $material=DB::table('ic_inputs')->find($this->f['refs']['material']['id']);$data=json_decode($material->data,true);$data['quantity']='21';$new=$this->change('material',$data)->assertOk()->json('id');$this->listing()->assertJsonPath('lots.0.results.0.current',false); + $this->login('reader');$this->decide('input',$new)->assertOk();$this->decide('result',$first)->assertStatus(409);$this->login();$refs=$this->f['refs'];$refs['material']=['id'=>$new,'version'=>2];$this->calc(['version'=>2,'previous_id'=>$first,'refs'=>$refs])->assertStatus(409); + $rate=DB::table('ic_inputs')->find($refs['material_rate']['id']);$rd=json_decode($rate->data,true);$rd['source_id']=$new;$rd['source_version']=2;$newRate=$this->change('material_rate',$rd)->assertOk()->json('id');$this->login('reader');$this->decide('input',$newRate)->assertOk();$this->login();$refs['material_rate']=['id'=>$newRate,'version'=>2];$second=$this->calc(['version'=>2,'previous_id'=>$first,'refs'=>$refs])->assertOk()->json('id');$this->assertSame('1850',$this->snapshot($second)['selected_total_exact']);$this->assertSame($oldSnapshot,DB::table('ic_results')->find($first)->snapshot); + foreach(['unit.view','industrial_costs.view','industrial_costs.prepare'] as $right)IdentityFixture::grant($this->f['manager']->id,$this->f['tenant'],$this->f['company'],$right,$this->f['hidden']); + $this->token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['hidden']])->assertOk()->json('context.token');$this->listing()->assertJsonCount(0,'lots');$this->calc()->assertForbidden(); + } +} diff --git a/tests/Support/IndustrialCostFixture.php b/tests/Support/IndustrialCostFixture.php new file mode 100644 index 0000000..c50db1d --- /dev/null +++ b/tests/Support/IndustrialCostFixture.php @@ -0,0 +1,18 @@ +['view','prepare'],'reader'=>['view','validate']] as $who=>$rights)foreach($rights as $r)IdentityFixture::grant($f[$who]->id,$f['tenant'],$f['company'],'industrial_costs.'.$r,$f['unit']); + $c=app(IndustrialCosts::class);$key=fn()=>(string)Str::uuid();$select=fn($who)=>app(CompanyContext::class)->selectFor($f[$who]->id,$f['tenant'],$f['company'],$f['unit']);$select('manager'); + $lot=$c->open($f['manager']->id,['reference'=>'LOT-TEST-'.Str::uuid(),'product_reference'=>'PROD-TEST-'.Str::uuid(),'product_name'=>'Produto industrial sintético','centre'=>'Centro Fabrico A — ensaio','period_start'=>'2026-10-01','period_end'=>'2026-10-31','currency'=>'MZN'],$key())['id'];$refs=[]; + $add=function($kind,$data)use(&$refs,$c,$f,$lot,$select,$key){$select('manager');$id=$c->input($lot,$f['manager']->id,['kind'=>$kind,'source_ref'=>'IND-TEST-'.strtoupper($kind).'-'.Str::uuid(),'version'=>1,'previous_id'=>null,'data'=>$data,'evidence'=>'Origem/pressuposto industrial exclusivamente sintético.'],$key())['id'];$select('reader');$c->decide($lot,$f['reader']->id,['type'=>'input','id'=>$id,'version'=>1,'decision'=>'approved','reason'=>'Validação de dados sintéticos de custo, não verificação técnica real.'],$key());$refs[$kind]=['id'=>$id,'version'=>1];return $id;}; + foreach(['material'=>['20','KG'],'labour'=>['300','MIN'],'pool'=>['600','MZN'],'machine'=>['5','HM'],'production'=>['10','UN']] as $kind=>[$quantity,$unit])$add($kind,['quantity'=>$quantity,'unit'=>$unit,'event_date'=>'2026-10-27']+($kind==='production'?['completed'=>true]:[])); + foreach(['material'=>['50','KG'],'labour'=>['100','H']] as $kind=>[$amount,$unit])$add($kind.'_rate',['source_id'=>$refs[$kind]['id'],'source_version'=>1,'amount'=>$amount,'currency'=>'MZN','unit'=>$unit]); + $criterionData=['pool_id'=>$refs['pool']['id'],'pool_version'=>1,'machine_id'=>$refs['machine']['id'],'machine_version'=>1,'capacity'=>'10','driver_unit'=>'HM','hypothesis'=>'synthetic_only']; + if($criterion)$add('criterion',$criterionData);$select('manager');return $f+compact('lot','refs','criterionData'); + } +}