Files
SGF/app/Modules/Accounting/ComposedMaterialIsolation.php
2026-11-11 09:00:17 +02:00

35 lines
3.6 KiB
PHP

<?php
namespace App\Modules\Accounting;
use App\Shared\Installation\CompositionRuntime;
use Tempo\Finance\Payables\Payables;
/** Explicit, fixed synthetic material perimeter. Unknown or shared origins fail closed. */
final class ComposedMaterialIsolation
{
public function check($host,$native,array $scope):void
{
$physical=CompositionRuntime::physical($host);$path='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-'.$physical['composition'].'/single-material-perimeter.json';
$pin=config('sgf_single_installation.material_perimeter_sha256');
if(!is_string($pin)||!preg_match('/^[a-f0-9]{64}$/D',$pin)||!is_file($path)||is_link($path)||!hash_equals($pin,hash_file('sha256',$path)))throw new \DomainException('Perímetro de materiais não conferido; não combinar custos e valorização.');
$p=json_decode(file_get_contents($path),true,32,JSON_THROW_ON_ERROR);
if(($p['contract']??null)!=='SGF-QA-DISJOINT-MATERIALS-v1'||($p['scope']??null)!==$scope||($p['host_database']??null)!==$host->getDatabaseName()||($p['composition']??null)!==$physical['composition'])throw new \DomainException('Perímetro pertence a outro contexto.');
$stocks=$native->table('gl_stock_books')->where($scope)->orderBy('id')->get(['stock_item_id','warehouse_id']);
$actual=$stocks->map(fn($r)=>(array)$r)->all();if(Payables::canonical($actual)!==Payables::canonical($p['valued_stock']))throw new \DomainException('Artigo/armazém de valorização mudou; nova conferência obrigatória.');
$codes=$native->table('stock_items')->where('tenant_id',$scope['tenant_id'])->whereIn('id',$stocks->pluck('stock_item_id'))->pluck('code')->all();
foreach($native->table('gl_cost_sources')->where($scope)->orderBy('id')->cursor() as $map){
$claim=$p['direct_costs'][(string)$map->order_id]??null;
if(!$claim||$claim['material_origin']!==$map->material_origin)throw new \DomainException('Custo sem declaração individual de exclusão do Stock valorizado.');
if(!$host->table('wo_orders')->where($scope)->where('id',$map->order_id)->exists())throw new \DomainException('Ordem de custo fora da empresa/unidade.');
$entry=$host->table('wo_entries')->where('order_id',$map->order_id)->where('origin',$map->material_origin)->orderByDesc('version')->first();$data=$entry?json_decode($entry->data,true):null;
if(!$entry||$entry->kind!=='material'||($data['resource']??null)!==$claim['resource']||in_array($claim['resource'],$codes,true)||!str_starts_with($claim['resource'],'QA-MATERIAL-DIRECTO-')||isset($data['stock_item_id'])||isset($data['delivery_movement_id']))throw new \DomainException('Material directo partilhado, alterado ou sem fronteira física explícita.');
}
foreach($native->table('gl_industrial_sources')->where($scope)->orderBy('id')->cursor() as $map){
$claim=$p['industrial_costs'][(string)$map->lot_id]??null;$origins=json_decode($map->origins,true);
if(!$claim||$claim['material_origin']!==($origins['material']??null)||in_array($claim['material_origin'],$codes,true))throw new \DomainException('Material industrial sem exclusão individual do Stock valorizado.');
$entry=$host->table('ic_inputs')->where($scope)->where('lot_id',$map->lot_id)->where('kind','material')->where('source_ref',$claim['material_origin'])->orderByDesc('version')->first();
if(!$entry||!str_starts_with($entry->source_ref,'IND-TEST-MATERIAL-'))throw new \DomainException('Origem industrial fora do perímetro sintético seleccionado.');
}
}
}