commit de compras

This commit is contained in:
2026-11-04 09:37:20 +02:00
parent fcf31e8d48
commit 0b05a1192c
741 changed files with 114127 additions and 116 deletions
+16 -2
View File
@@ -9,10 +9,10 @@ final class ModuleNavigation
public const LABELS = ['commercial'=>'Comercial', 'purchasing'=>'Compras', 'stock'=>'Stock', 'workshop'=>'Oficinas', 'finance'=>'Financeiro', 'accounting'=>'Contabilidade'];
private const RIGHTS = [
'commercial'=>['clients.', 'services.', 'service_drafts.', 'billing_drafts.', 'issuer_documents.', 'client_documents.', 'draft_documents.', 'service_invoices.'],
'purchasing'=>['purchasing_budget.', 'supplier_payables.'],
'purchasing'=>['purchasing_budget.', 'purchasing_requisitions.', 'purchasing_receipts.', 'purchasing_orders.', 'purchasing_sourcing.', 'purchasing_suppliers.', 'purchasing_requests.', 'purchasing_invoices.', 'purchasing_obligations.', 'supplier_payables.'],
'stock'=>['stock.'], 'workshop'=>['workshop.', 'workshop_billing.', 'workshop_corrections.'],
'finance'=>['direct_costs.', 'industrial_costs.', 'industrial_budgets.', 'industrial_budget_comparisons.', 'service_receivables.'],
'accounting'=>['catalogue_accounting.', 'service_fiscal.'],
'accounting'=>['catalogue_accounting.', 'service_fiscal.', 'general_ledger.'],
];
public function available(CompanyContext $context): array
@@ -50,6 +50,20 @@ final class ModuleNavigation
// Company, unit and access configuration are explicitly general.
public static function owner(string $path): ?string
{
if (preg_match('~^(geography|geography/segments|api/geography/entities|api/geography/segments)/client(/|$)~', $path)) return 'commercial';
if (preg_match('~^(geography|geography/segments|api/geography/entities|api/geography/segments)/supplier(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/dossiers|api/purchasing-dossiers)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^purchasing/workspace(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/obligations|api/purchasing-obligations)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/invoices|api/purchasing-invoices)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/requests|api/purchasing-requests)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/suppliers|api/purchasing-suppliers)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/sourcing|api/purchasing-sourcing)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/orders|api/purchasing-orders)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(purchasing/receipts|api/purchasing-receipts)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^api/stock-valuation-read(/|$)~', $path)) return 'accounting';
if (preg_match('~^(purchasing/internal-requisitions|api/internal-requisitions)(/|$)~', $path)) return 'purchasing';
if (preg_match('~^(general-ledger|api/general-ledger)(/|$)~', $path)) return 'accounting';
if (preg_match('~^api/stock-read(/|$)~', $path)) return 'stock';
if (preg_match('~^(supplier-payables|supplier-payments|api/(supplier-payables|supplier-payments|purchasing-budget))(/|$)~', $path)) return 'purchasing';
if (preg_match('~^api/(workshop|workshop-stock|workshop-billing|workshop-corrections)(/|$)~', $path)) return 'workshop';
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Shared\Geography;
use InvalidArgumentException;
use DomainException;
/** Versioned preparation of a supplied territorial reference, without changing business records. */
final class Cartography
{
public const FIELDS=['province','district','administrative_post','locality','urban_neighbourhood','village_quarter'];
public const HEADERS=['A'=>'ID','B'=>'Provincia','C'=>'Distrito','D'=>'Posto_Administrativo','E'=>'Localidade','F'=>'Bairro_Urbano','G'=>'Aldeia_Quarteirao','H'=>'Designacao'];
public static function prepare(array $source):array
{
if(!preg_match('/^[a-f0-9]{64}$/D',$source['sha256']??'')||($source['rows'][0]['cells']??null)!==self::HEADERS||($source['formula_cells']??1)!==0)throw new InvalidArgumentException('CARTOGRAPHY_SOURCE_REFUSED');
$nodes=[];$paths=[];$ids=[];$levels=array_fill(1,6,0);
foreach(array_slice($source['rows'],1)as$row){
$c=$row['cells'];$id=$c['A']??'';if(!is_string($id)||!preg_match('/^[0-9]+$/D',$id)||isset($ids[$id]))throw new InvalidArgumentException('CARTOGRAPHY_SOURCE_ID_REFUSED');$ids[$id]=true;
$parts=[];foreach(['B','C','D','E','F','G']as$col){$code=$c[$col]??'';if(!is_string($code)||($code!==''&&!preg_match('/^[A-Za-z0-9]+$/D',$code)))throw new InvalidArgumentException('CARTOGRAPHY_CODE_REFUSED');$parts[]=$code;}
$level=0;foreach($parts as$i=>$code)if($code!=='')$level=$i+1;
if(!$level||in_array('',array_slice($parts,0,$level),true))throw new InvalidArgumentException('CARTOGRAPHY_PATH_REFUSED');
$path=implode('.',$parts);$key=$source['sha256'].':'.$id;$index=count($nodes);$paths[$path][]=$index;$levels[$level]++;
$nodes[]=['key'=>$key,'source_id'=>$id,'source_row'=>$row['row'],'level'=>$level,'designation'=>$c['H']??null,'codes'=>array_combine(self::FIELDS,$parts),'path'=>$path,'parent_candidates'=>[],'issues'=>[]];
}
$summary=['duplicate_path_groups'=>0,'duplicate_extra_rows'=>0,'conflicting_designation_groups'=>0,'missing_designations'=>0,'missing_parent_records'=>0,'ambiguous_parent_records'=>0,'incomplete_ancestry_records'=>0,'selectable_records'=>0];
foreach($paths as$group)if(count($group)>1){$summary['duplicate_path_groups']++;$summary['duplicate_extra_rows']+=count($group)-1;if(count(array_unique(array_map(fn($i)=>$nodes[$i]['designation'],$group)))>1)$summary['conflicting_designation_groups']++;}
foreach($nodes as&$node){
if($node['designation']===null||trim($node['designation'])===''){$node['issues'][]='missing_designation';$summary['missing_designations']++;}
if(count($paths[$node['path']])>1)$node['issues'][]='duplicate_path';
if($node['level']>1){$parts=array_values($node['codes']);$parts[$node['level']-1]='';$parents=$paths[implode('.',$parts)]??[];$node['parent_candidates']=array_map(fn($i)=>$nodes[$i]['key'],$parents);if(!$parents){$node['issues'][]='parent_record_missing';$summary['missing_parent_records']++;}elseif(count($parents)>1){$node['issues'][]='parent_record_ambiguous';$summary['ambiguous_parent_records']++;}}
$ancestry=true;for($depth=1;$depth<$node['level'];$depth++){$prefix=array_values($node['codes']);for($i=$depth;$i<6;$i++)$prefix[$i]='';$ancestors=$paths[implode('.',$prefix)]??[];if(count($ancestors)!==1||trim($nodes[$ancestors[0]]['designation']??'')===''){$ancestry=false;break;}}
if(!$ancestry){$node['issues'][]='ancestry_requires_review';$summary['incomplete_ancestry_records']++;}
$node['selectable']=$node['issues']===[];if($node['selectable'])$summary['selectable_records']++;
}unset($node);
return ['format'=>'sgf.cartography.staging.v1','source'=>['name'=>$source['source_name'],'sha256'=>$source['sha256'],'sheet'=>'Sheet1','range'=>$source['dimension'],'authority_declared_by_user'=>'INE','authority_independently_verified'=>false],'status'=>'prepared_for_review','levels'=>$levels,'total'=>count($nodes),'diagnostics'=>$summary,'nodes'=>$nodes];
}
/** A human selection carries version and source identity; a name alone cannot select a territory. */
public static function selection(array $catalogue,string $version,string $key):array
{
if(!hash_equals($catalogue['source']['sha256'],$version))throw new DomainException('CARTOGRAPHY_VERSION_CHANGED');
foreach($catalogue['nodes']as$node)if($node['key']===$key){if(!$node['selectable'])throw new DomainException('CARTOGRAPHY_REVIEW_REQUIRED');return ['version'=>$version,'territory_key'=>$key,'source_id'=>$node['source_id'],'codes'=>$node['codes'],'designation'=>$node['designation']];}
throw new DomainException('CARTOGRAPHY_TERRITORY_UNKNOWN');
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Shared\Geography;
use Illuminate\Support\Facades\DB;
final class Catalogue
{
public static function guard():void
{
abort_unless(config('geography.enabled')===true,404);
abort_unless(app()->environment('budget-testing'),403);
if(request()->is('api/geography/*'))abort_unless(request()->hasSession()&&hash_equals((string)request()->session()->get('company_context.token',''),(string)request()->header('X-SGF-Context')),409,'O contexto mudou. Reabra o formulário.');
\App\Modules\Purchasing\BudgetAdapter::guard();
$c=DB::connection();$p=$c->getConfig();$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN');
abort_unless(preg_match('/^[a-f0-9]{12}$/D',$run)&&preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$c->getDatabaseName(),$m)&&$p['username']==='ir_composition_'.$m[1]&&$p['host']==='127.0.0.1'&&(int)$p['port']===55439&&empty($p['url'])&&empty($p['read'])&&empty($p['write'])&&($p['search_path']??'public')==='public',403);
$i=$c->selectOne("SELECT current_user role,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
abort_unless(!$i->elevated&&$i->role===$p['username']&&$i->cluster==='sgf_modular_regression_'.$run,403);
}
public static function active(bool $lock=false):object
{
$q=DB::table('geo_catalogue_state')->where('id',1);$row=($lock?$q->lockForUpdate():$q)->first();
abort_unless($row&&hash_equals((string)config('geography.source_sha256'),$row->source_version),409,'A versão do catálogo mudou. Reabra a conferência.');return $row;
}
public static function describe(object $row):array
{
return ['key'=>$row->territory_key,'source_id'=>$row->source_id,'level'=>(int)$row->level,'designation'=>$row->designation,'codes'=>array_combine(Cartography::FIELDS,array_map(fn($f)=>$row->$f,Cartography::FIELDS)),'issues'=>json_decode($row->issues,true),'selectable'=>(bool)$row->selectable];
}
public static function selection(string $version,string $key):array
{
$active=self::active(true);abort_unless(hash_equals($active->source_version,$version),409,'O catálogo mudou.');
$row=DB::table('geo_territories')->where('source_version',$version)->where('territory_key',$key)->first();abort_unless($row,422,'Território desconhecido nesta versão.');abort_unless($row->selectable,422,'Este território tem pendências de conferência na fonte.');return ['country_code'=>'MZ','version'=>$version]+self::describe($row);
}
public static function page(array $d):array
{
$version=self::active()->source_version;$q=DB::table('geo_territories')->where('source_version',$version);
if(isset($d['parent'])){$parent=DB::table('geo_territories')->where('source_version',$version)->where('territory_key',$d['parent'])->first();abort_unless($parent&&$parent->selectable,422,'Ascendente por conferir.');$q->where('parent_key',$parent->territory_key);}
elseif(empty($d['search']))$q->where('level',1);
if(!empty($d['search']))$q->where('designation','ilike','%'.$d['search'].'%');
return ['version'=>$version,'records'=>$q->orderBy('designation')->orderBy('source_id')->paginate(25,['*'],'page',(int)($d['page']??1))->through(fn($r)=>self::describe($r))];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Shared\Geography;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{DB,Crypt};
final class GeographyController
{
private function input(Request $r,array $rules):array{$v=$r->validate($rules);abort_if(array_diff(array_keys($r->all()),array_keys($v)),422);return $v;}
public function catalogue(Request $r){Catalogue::guard();$d=$this->input($r,['parent'=>'nullable|string|max:100','search'=>'nullable|string|max:100','page'=>'sometimes|integer|min:1|max:10000']);return response()->json(Catalogue::page($d))->header('Cache-Control','no-store');}
public function segments(string $kind,Request $r){Catalogue::guard();$rules=['after'=>'sometimes|integer|min:0'];foreach(Cartography::FIELDS as$f)$rules[$f]=['sometimes','string','regex:/^[A-Za-z0-9]{1,20}$/D'];return response()->json(Segments::page($kind,$this->input($r,$rules)))->header('Cache-Control','no-store');}
public function segmentScreen(string $kind,Request $r){$this->segments($kind,$r);return response()->view('geography-workspace',['kind'=>$kind,'entityId'=>0,'context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()]);}
public function screen(string $kind,int $id,Request $r,Locations $locations){$locations->run($kind,$id,false,fn()=>null);return response()->view('geography-workspace',['kind'=>$kind,'entityId'=>$id,'context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()]);}
public function show(string $kind,int $id,Locations $locations){return $locations->run($kind,$id,false,function($db,$scope,$entity,$actor,$binding){return Locations::useHost($db,function()use($db,$scope,$entity){$old=Locations::current($db,$scope);return response()->json(['entity'=>$entity,'location'=>$old,'revision'=>Locations::hash([$scope,$entity,$old]),'catalogue_version'=>Catalogue::active()->source_version,'purpose'=>'segmentation']);});});}
public function history(string $kind,int $id,Request $r,Locations $locations){Catalogue::guard();$d=$this->input($r,['page'=>'sometimes|integer|min:1|max:10000']);return $locations->run($kind,$id,false,fn($db,$scope)=>response()->json($db->table('geo_location_history')->where($scope)->orderByDesc('id')->paginate(15,['id','actor_id','reason','before','after','created_at'],'page',(int)($d['page']??1))->through(function($row){$row->before=json_decode($row->before,true);$row->after=json_decode($row->after,true);return $row;})));}
public function preview(string $kind,int $id,Request $r,Locations $locations){Catalogue::guard();$d=$this->input($r,['operation_key'=>'required|uuid','revision'=>'required|string|size:64','catalogue_version'=>'required|string|size:64','territory_key'=>'present|nullable|string|max:100','reason'=>'required|string|min:10|max:2000']);$d['operation_key']=strtolower($d['operation_key']);return $locations->run($kind,$id,true,function($db,$scope,$entity,$actor,$binding)use($d){return Locations::useHost($db,function()use($db,$scope,$entity,$actor,$binding,$d){$old=Locations::current($db,$scope);abort_unless(hash_equals(Locations::hash([$scope,$entity,$old]),$d['revision']),409,'O cadastro ou a localização mudou. Confira novamente.');abort_unless(hash_equals(Catalogue::active(true)->source_version,$d['catalogue_version']),409,'O catálogo mudou.');$selection=$d['territory_key']===null?null:Catalogue::selection($d['catalogue_version'],$d['territory_key']);$cut=$d+['scope'=>$scope,'entity'=>$entity,'actor'=>$actor,'binding'=>$binding,'observed'=>time()];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>['entity'=>$entity['name'],'purpose'=>'Localização para segmentação','before'=>$old['selection']??null,'after'=>$selection,'reason'=>$d['reason'],'canonical_address_changed'=>false,'dependent_records_changed'=>false]]);});});}
public function confirm(string $kind,int $id,Request $r,Locations $locations){Catalogue::guard();$d=$this->input($r,['preview'=>'required|string|max:131072']);try{$cut=json_decode(Crypt::decryptString($d['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}return $locations->run($kind,$id,true,function($db,$scope,$entity,$actor,$binding)use($cut){return Locations::useHost($db,function()use($db,$scope,$entity,$actor,$binding,$cut){abort_unless(($cut['scope']??null)===$scope&&($cut['actor']??null)===$actor&&($cut['binding']??null)===$binding&&is_int($cut['observed']??null)&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'A prévia ou o contexto mudou.');$hash=Locations::hash([$scope,$cut['revision'],$cut['catalogue_version'],$cut['territory_key'],$cut['reason']]);$old=$db->table('geo_commands')->where(['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'actor_id'=>$actor,'operation_key'=>$cut['operation_key']])->first();if($old){abort_unless(hash_equals($old->request_hash,$hash),409,'Chave já usada com outros dados.');return response()->json(json_decode($old->result,true)+['replayed'=>true]);}$before=Locations::current($db,$scope);abort_unless(hash_equals(Locations::hash([$scope,$entity,$before]),$cut['revision']),409,'O cadastro ou a localização mudou desde a conferência.');$state=Catalogue::active(true);abort_unless(hash_equals($state->source_version,$cut['catalogue_version']),409,'O catálogo mudou.');$selection=$cut['territory_key']===null?null:Catalogue::selection($cut['catalogue_version'],$cut['territory_key']);$revision=($before['revision']??0)+1;$values=['revision'=>$revision,'selection'=>$selection===null?null:json_encode($selection,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR),'actor_id'=>$actor,'reason'=>$cut['reason'],'updated_at'=>now()];$db->table('geo_locations')->updateOrInsert($scope,$values);$after=Locations::current($db,$scope);$db->table('geo_location_history')->insert($scope+['actor_id'=>$actor,'reason'=>$cut['reason'],'before'=>json_encode($before,JSON_THROW_ON_ERROR),'after'=>json_encode($after,JSON_THROW_ON_ERROR),'created_at'=>now()]);$result=['revision'=>$revision,'selection'=>$selection];$db->table('geo_commands')->insert(['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'actor_id'=>$actor,'operation_key'=>$cut['operation_key'],'request_hash'=>$hash,'result'=>json_encode($result,JSON_THROW_ON_ERROR),'created_at'=>now()]);return response()->json($result+['replayed'=>false],201);});});}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Shared\Geography;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Modules\Identity\Services\Permissions;
use App\Modules\Purchasing\{ReceiptRuntime,SupplierAccess};
use Illuminate\Support\Facades\DB;
/** Geographic segmentation metadata; canonical addresses and supplier financial review are unchanged. */
final class Locations
{
public function run(string $kind,int $id,bool $edit,\Closure $work):mixed
{
Catalogue::guard();abort_unless(in_array($kind,['company','unit','client','supplier'],true)&&$id>0,404);
$ctx=app(CompanyContext::class);$s=$ctx->identifiers();$unit=$ctx->unitId();$actor=$ctx->userId();$host=DB::connection();$generation=hash('sha256',json_encode([$s,$unit,request()->session()->get('company_context.module_id'),request()->session()->get('company_context.token')],JSON_THROW_ON_ERROR));
return $host->transaction(function()use($kind,$id,$edit,$work,$ctx,$s,$unit,$actor,$host,$generation){
$host->table('companies')->where('id',$s['company_id'])->where('tenant_id',$s['tenant_id'])->lockForUpdate()->firstOrFail();$host->table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();
abort_unless($host->table('company_memberships')->where($s)->where('user_id',$actor)->lockForUpdate()->first(),403);
$require=function(string $right,?int $u=null)use($host,$s,$actor){abort_unless($host->table('permission_grants')->where($s)->where(['user_id'=>$actor,'permission'=>$right,'unit_id'=>$u])->lockForUpdate()->first(),403);};
$require('company.view');if($unit!==null)$require('unit.view',$unit);
$base=$s+['unit_key'=>0,'module_key'=>'general','kind'=>$kind,'entity_id'=>$id,'provider'=>'sgf'];
if($kind==='supplier'){
abort_unless($unit!==null,403);$base['unit_key']=$unit;$base['module_key']='purchasing';
return app(ReceiptRuntime::class)->run($edit?'purchasing_suppliers.prepare':'purchasing_suppliers.view',function($native,$binding,$fingerprint,$origin)use($id,$edit,$work,$base,$host,$actor,$generation){
abort_unless($native->hasFunctions($edit?['procurement']:['admin','procurement','finance','department_head']),403);
$supplier=SupplierAccess::query($binding,$origin['sgf_context_key'])->lockForUpdate()->findOrFail($id);
if($edit)abort_unless(app(\Tempo\Purchasing\Embedded\Services\SupplierParticipation::class)->canPrepare($native),403);
$scope=$base;$scope['provider']=hash('sha256',DB::connection()->getDatabaseName());$entity=['id'=>$id,'name'=>$supplier->name,'version'=>(int)$supplier->version,'editable'=>$host->table('permission_grants')->where(['tenant_id'=>$base['tenant_id'],'company_id'=>$base['company_id'],'unit_id'=>$base['unit_key'],'user_id'=>$actor,'permission'=>'purchasing_suppliers.prepare'])->exists()&&app(\Tempo\Purchasing\Embedded\Services\SupplierParticipation::class)->canPrepare($native)];
return $work($host,$scope,$entity,$actor,hash('sha256',$fingerprint.$generation));
});
}
if($kind==='company'){abort_unless($id===$s['company_id'],403);$require($edit?'company.edit':'company.view');$entity=$host->table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$id)->lockForUpdate()->first();}
elseif($kind==='unit'){abort_unless($id===$unit,403);$require($edit?'unit.edit':'unit.view',$id);$base['unit_key']=$id;$entity=$host->table('units')->where($s)->where('id',$id)->lockForUpdate()->first();}
else{$require('clients.view');if($edit)$require('clients.edit');abort_unless(ModuleRecords::active()==='commercial',403);$base['module_key']='commercial';$entity=ModuleRecords::query('a_clients')->where($s)->whereNull('unit_id')->where('id',$id)->lockForUpdate()->first();}
abort_unless($entity,403,'Registo fora do âmbito autorizado.');return $work($host,$base,['id'=>$id,'name'=>$entity->name,'version'=>(int)($entity->version??0),'updated_at'=>$entity->updated_at??null,'editable'=>$host->table('permission_grants')->where($s)->where(['unit_id'=>$kind==='unit'?$id:null,'user_id'=>$actor,'permission'=>match($kind){'company'=>'company.edit','unit'=>'unit.edit','client'=>'clients.edit'}])->exists()],$actor,$generation);
});
}
public static function current($db,array $scope):?array
{
$r=$db->table('geo_locations')->where($scope)->first();return $r?['revision'=>(int)$r->revision,'selection'=>$r->selection===null?null:json_decode($r->selection,true),'actor_id'=>(int)$r->actor_id,'reason'=>$r->reason,'updated_at'=>$r->updated_at]:null;
}
public static function hash(mixed $v):string{return hash('sha256',json_encode($v,JSON_THROW_ON_ERROR));}
public static function useHost($host,\Closure $work):mixed
{
$old=DB::getDefaultConnection();DB::setDefaultConnection($host->getName());try{return $work();}finally{DB::setDefaultConnection($old);}
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Shared\Geography;
use Illuminate\Support\Facades\DB;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Modules\Identity\Services\Permissions;
use App\Modules\Purchasing\{ReceiptRuntime,SupplierAccess};
final class Segments
{
public static function page(string $kind,array $filters):array
{
Catalogue::guard();abort_unless(in_array($kind,['company','unit','client','supplier'],true),404);$ctx=app(CompanyContext::class);$s=$ctx->identifiers();$unit=$ctx->unitId();$host=DB::connection();$version=Catalogue::active()->source_version;$p=app(Permissions::class);$actor=$ctx->userId();
$read=function(string $provider,int $unitKey,string $module,\Closure $names,?int $only=null)use($host,$s,$kind,$filters,$version){
$query=$host->table('geo_locations')->where($s)->where(['unit_key'=>$unitKey,'module_key'=>$module,'kind'=>$kind,'provider'=>$provider])->whereNotNull('selection')->where('selection->version',$version);if($only!==null)$query->where('entity_id',$only);foreach(Cartography::FIELDS as$f)if(isset($filters[$f]))$query->where('selection->codes->'.$f,$filters[$f]);$after=(int)($filters['after']??0);$out=[];
do{$rows=(clone $query)->where('entity_id','>',$after)->orderBy('entity_id')->limit(100)->get();if($rows->isEmpty())break;$allowed=$names($rows->pluck('entity_id')->all());foreach($rows as$row){$after=(int)$row->entity_id;if(isset($allowed[$after]))$out[]=['entity_id'=>$after,'name'=>$allowed[$after],'location'=>json_decode($row->selection,true),'revision'=>(int)$row->revision];if(count($out)===26)break 2;}}while($rows->count()===100);
$more=count($out)>25;if($more)array_pop($out);return ['version'=>$version,'data'=>$out,'next_after'=>$more?end($out)['entity_id']:null,'page_size'=>25,'population'=>'explicit_segment_locations'];
};
if($kind==='supplier')return app(ReceiptRuntime::class)->run('purchasing_suppliers.view',function($native,$binding,$fingerprint,$origin)use($read,$unit){abort_unless($native->hasFunctions(['admin','procurement','finance','department_head']),403);return $read(hash('sha256',DB::connection()->getDatabaseName()),$unit,'purchasing',fn($ids)=>SupplierAccess::query($binding,$origin['sgf_context_key'])->whereIn('id',$ids)->pluck('name','id')->all());});
if($kind==='client'){$p->require($actor,$s['tenant_id'],$s['company_id'],'clients.view');abort_unless(ModuleRecords::active()==='commercial',403);return $read('sgf',0,'commercial',fn($ids)=>ModuleRecords::query('a_clients')->where($s)->whereNull('unit_id')->whereIn('id',$ids)->pluck('name','id')->all());}
if($kind==='unit'){abort_unless($unit!==null,403);$p->require($actor,$s['tenant_id'],$s['company_id'],'unit.view',$unit);return $read('sgf',$unit,'general',fn($ids)=>$host->table('units')->where($s)->whereIn('id',$ids)->pluck('name','id')->all(),$unit);}
return $read('sgf',0,'general',fn($ids)=>$host->table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$s['company_id'])->whereIn('id',$ids)->pluck('name','id')->all(),$s['company_id']);
}
}
@@ -11,6 +11,14 @@ final class TestingDatabaseGuard
{
$name = $connection ?: config('database.default');
$config = config("database.connections.{$name}", []);
if(app()->environment(['testing','browser-testing'])&&(config('sales_ledger.synthetic_enabled')===true||config('cost_ledger.synthetic_enabled')===true||config('industrial_ledger.synthetic_enabled')===true)
&&($config['driver']??null)==='pgsql'&&($config['host']??null)==='127.0.0.1'&&(string)($config['port']??'')==='55439'
&&preg_match('/^payable_sgf_([a-f0-9]{12})$/D',$config['database']??'',$m)&&($config['username']??null)==='payable_history_sgf_'.$m[1]
&&empty($config['url'])&&empty($config['read'])&&empty($config['write'])&&($config['search_path']??'public')==='public'){
\Tempo\Finance\GeneralLedger\LedgerSchema::assertSynthetic(DB::connection($name));
$actual=DB::connection($name)->selectOne('SELECT current_database() db,current_user role');
if($actual->db!==$config['database']||$actual->role!==$config['username'])throw new RuntimeException('SGF_TEST_DATABASE_REFUSED: configuração QA e destino efectivo divergem.');return;
}
$expected = [
'driver' => 'pgsql', 'host' => '127.0.0.1', 'port' => '55432',
'database' => 'sgf_tempo_test', 'username' => 'sgf_test_runner',