Files
Investidor-V2/app/Services/InvestmentZoneIntegralDetails.php
T
nala cf003c8623
tests / ci (push) Canceled after 0s
versao nrgocios 4
2026-10-18 15:57:06 +02:00

98 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\InvestmentZone;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
/** Subordinate documentary records in the existing encrypted park envelope. */
class InvestmentZoneIntegralDetails
{
public const GROUPS = ['titles', 'phases', 'plots', 'occupants', 'observations', 'chains'];
public const EXTRA = [
'titles' => ['kind', 'identifier', 'valid_from', 'valid_until'],
'phases' => ['code', 'status', 'starts_at', 'ends_at', 'expansion'],
'plots' => ['code', 'phase_id', 'area', 'area_unit', 'status', 'geometry_id'],
'occupants' => ['organization_id', 'documentary_name', 'phase_id', 'plot_id', 'status', 'valid_from', 'valid_until'],
'observations' => ['indicator', 'value', 'unit', 'period_start', 'period_end', 'population', 'methodology', 'phase_id', 'plot_id'],
'chains' => ['kind', 'organization_id', 'documentary_name', 'zone_link_id', 'phase_id', 'plot_id'],
];
public const COMMON = ['id', 'label', 'label_en', 'description', 'description_en', 'knowledge', 'source_reference', 'source_date', 'scope', 'disclosure', 'private_note', 'support_document_ids'];
public function merge(InvestmentZone $zone, array $before, array $input): array
{
abort_if(array_diff(array_keys($input), ['schema_version', 'remove_ids', ...self::GROUPS]), 422, 'Campo da ficha integral desconhecido.');
$rules = ['schema_version' => 'sometimes|in:1', 'remove_ids' => 'sometimes|array:'.implode(',', self::GROUPS)];
foreach (self::GROUPS as $group) {
$rules[$group] = 'sometimes|array|max:200';
$rules[$group.'.*'] = 'array:'.implode(',', [...self::COMMON, ...self::EXTRA[$group]]);
foreach (['id'=>'nullable|uuid','label'=>'required|string|max:300','label_en'=>'nullable|string|max:300','description'=>'nullable|string|max:3000','description_en'=>'nullable|string|max:3000','knowledge'=>'required|in:unknown,documented,disputed','source_reference'=>'nullable|string|max:2000','source_date'=>'nullable|date_format:Y-m-d','scope'=>'nullable|string|max:500','disclosure'=>'required|boolean','private_note'=>'nullable|string|max:4000','support_document_ids'=>'sometimes|array|max:20','support_document_ids.*'=>'uuid'] as $key=>$rule) $rules[$group.'.*.'.$key]=$rule;
foreach (self::EXTRA[$group] as $key) $rules[$group.'.*.'.$key]=match ($key) {
'organization_id','phase_id','plot_id','zone_link_id','geometry_id'=>'nullable|uuid',
'area'=>'nullable|numeric|min:0|max:999999999999999',
'value'=>'nullable|numeric|between:-999999999999999,999999999999999',
'starts_at','ends_at','valid_from','valid_until','period_start','period_end'=>'nullable|date_format:Y-m-d',
'expansion'=>'nullable|boolean',
default=>'nullable|string|max:2000',
};
$rules['remove_ids.'.$group]='sometimes|array|max:200';$rules['remove_ids.'.$group.'.*']='uuid';
}
$rules['plots.*.area_unit']='nullable|in:ha,m2';
$rules['plots.*.status']='nullable|in:unknown,planned,available_declared,reserved,occupied,construction,inactive';
$rules['phases.*.status']='nullable|in:unknown,planned,construction,operational,suspended,closed';
$rules['occupants.*.status']='nullable|in:unknown,approved,construction,installed,inactive';
$rules['titles.*.kind']='nullable|in:creation,land_use,licence,special_regime,other';
$rules['chains.*.kind']='nullable|in:anchor,supplier,unmet_need,other';
$rules['observations.*.indicator']='required|in:employment,investment,production,exports,local_procurement,resource_use,waste,emissions,other';
$data=Validator::make($input,$rules)->validate();$result=$before;
foreach(self::GROUPS as$group){
$rows=collect($before[$group]??[])->keyBy('id')->all();$remove=$data['remove_ids'][$group]??[];$seen=[];
foreach($data[$group]??[]as$item){
$id=$item['id']??(string)Str::uuid();abort_if(in_array($id,$seen,true)||in_array($id,$remove,true),422,'Identificador repetido ou removido na mesma operação.');$seen[]=$id;
$item=[...($rows[$id]??[]),...$item,'id'=>$id];
if($item['knowledge']!=='unknown')abort_unless(trim($item['source_reference']??'')!=='',422,'Indique a fonte documental ou preserve como desconhecido.');
if($item['knowledge']==='unknown')abort_if(isset($item['value'])||isset($item['area']),422,'Quantidade desconhecida deve permanecer vazia, não zero.');
if($item['knowledge']==='unknown'&&!empty($item['status']))abort_unless($item['status']==='unknown',422,'Situação operacional declarada exige fonte ou indicação de divergência.');
foreach([['starts_at','ends_at'],['valid_from','valid_until'],['period_start','period_end']]as[$a,$b])if(isset($item[$a],$item[$b]))abort_if($item[$b]<$item[$a],422,'Período final anterior ao início.');
if($group==='plots'&&isset($item['area']))abort_unless(!empty($item['area_unit'])&&!empty($item['scope']),422,'Área declarada exige unidade e âmbito.');
if($group==='plots'&&($item['status']??'')==='available_declared')abort_unless($item['knowledge']==='documented'&&!empty($item['source_date']),422,'Disponibilidade declarada exige fonte e data; não é disponibilidade em tempo real.');
if($group==='observations'&&isset($item['value']))abort_unless(!empty($item['unit'])&&!empty($item['period_start'])&&!empty($item['period_end'])&&!empty($item['scope'])&&!empty($item['population']),422,'Indicador numérico exige unidade, período, âmbito e base/população.');
if(!empty($item['organization_id']))abort_unless(DB::table('organizations')->where('id',$item['organization_id'])->whereNull('deleted_at')->exists(),422,'Reutilize uma entidade existente.');
if(!empty($item['geometry_id']))abort_unless(DB::table('investment_zone_geometries')->where('zone_id',$zone->id)->where('geometry_id',$item['geometry_id'])->exists(),422,'Geometria deve estar associada a este parque; limite administrativo não é perímetro.');
if(!empty($item['zone_link_id']))abort_unless(DB::table('investment_zone_links')->where('zone_id',$zone->id)->where('id',$item['zone_link_id'])->exists(),422,'Relação económica deve pertencer a este parque.');
foreach($item['support_document_ids']??[]as$doc)abort_unless(DB::table('investment_zone_attachments as a')->join('documents as d','d.id','=','a.document_id')->where('a.zone_id',$zone->id)->where('d.id',$doc)->whereNull('d.deleted_at')->where('d.status','active')->where('d.malware_scan_status','clean')->whereColumn('d.sha256','d.malware_scanned_sha256')->exists(),422,'Suporte deve estar consultável e associado a este parque.');
$rows[$id]=$item;
}
foreach($remove as$id){abort_unless(isset($rows[$id]),422,'O detalhe a remover não pertence ao parque.');unset($rows[$id]);}
abort_if(count($rows)>200,422,'Limite de detalhes excedido.');$result[$group]=array_values($rows);
}
// References resolve only inside this park, including new items in the same transaction.
foreach(self::GROUPS as$group)foreach($result[$group]as$item){
foreach(['phase_id'=>'phases','plot_id'=>'plots']as$key=>$target)if(!empty($item[$key]))abort_unless(in_array($item[$key],array_column($result[$target],'id'),true),422,'Referência interna inexistente ou de outro parque.');
if(!empty($item['phase_id'])&&!empty($item['plot_id'])){$plot=collect($result['plots'])->firstWhere('id',$item['plot_id']);abort_if(!empty($plot['phase_id'])&&$plot['phase_id']!==$item['phase_id'],422,'Lote e fase não correspondem.');}
}
foreach(['phases','plots']as$group){$codes=[];foreach($result[$group]as$item)if(trim($item['code']??'')!==''){$code=mb_strtolower(trim($item['code']));abort_if(in_array($code,$codes,true),422,'Código interno repetido no parque.');$codes[]=$code;}}
$keys=[];foreach($result['occupants']as$item){$key=implode('|',[$item['organization_id']??$item['documentary_name']??$item['id'],$item['plot_id']??'',$item['valid_from']??'',$item['valid_until']??'']);abort_if(in_array($key,$keys,true),422,'Ocupante/período já registado; reutilize o detalhe.');$keys[]=$key;}
$keys=[];foreach($result['observations']as$item){if(!isset($item['period_start'],$item['period_end']))continue;$key=implode('|',[$item['indicator'],$item['period_start'],$item['period_end'],$item['scope']??'',$item['population']??'',$item['unit']??'',$item['methodology']??'',$item['source_reference']??'',$item['source_date']??'',$item['phase_id']??'',$item['plot_id']??'']);abort_if(in_array($key,$keys,true),422,'A mesma observação/fonte já está registada; altere o detalhe existente para criar a sua nova revisão.');$keys[]=$key;}
return [...$result,'schema_version'=>1];
}
public function publicData(array $fields): array
{
$public=['schema_version'=>1];
foreach(self::GROUPS as$group){$public[$group]=[];foreach($fields[$group]??[]as$item){
if(!($item['disclosure']??false))continue;
$keys=['id','label','label_en','description','description_en','knowledge','source_reference','source_date','scope',...self::EXTRA[$group]];
$visible=array_intersect_key($item,array_flip($keys));unset($visible['organization_id'],$visible['documentary_name'],$visible['geometry_id'],$visible['zone_link_id']);
if(in_array($group,['occupants','chains'],true)&&!empty($item['organization_id'])){$org=DB::table('organizations')->where('id',$item['organization_id'])->whereNull('deleted_at')->where('data_classification','public')->first();abort_unless($org&&$item['knowledge']==='documented',422,'Divulgar entidade exige cadastro público e fonte; a menção preliminar permanece privada.');$visible['organization_name']=$org->name;}
if($group==='occupants')abort_unless(!empty($visible['organization_name']),422,'Ocupante por identificar permanece privado.');
$public[$group][]=$visible;
}}
// A disclosed item must not leak the identifier of an undisclosed phase or plot.
foreach(self::GROUPS as$group)foreach($public[$group]as&$item)foreach(['phase_id'=>'phases','plot_id'=>'plots']as$key=>$target)if(!empty($item[$key])&&!in_array($item[$key],array_column($public[$target],'id'),true))unset($item[$key]);unset($item);
return $public;
}
}