98 lines
10 KiB
PHP
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;
|
|
}
|
|
}
|