102 lines
7.3 KiB
PHP
102 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
/** Internal observations, never a public business directory or proof of demand. */
|
|
final class ProactiveSupply
|
|
{
|
|
public const LIMITATIONS = 'Informação administrativa histórica e incompleta. Actividade actual, capacidade, procura e compromissos por confirmar. Ausência de registos significa informação insuficiente, nunca ausência de oferta.';
|
|
|
|
public function authorize(User $actor, ?Opportunity $record = null): void
|
|
{
|
|
$access=app(PortalProfileAccess::class);
|
|
$internal=collect(OpportunityManagement::CATEGORIES)->contains(fn($type)=>$access->internalAssignment($actor,$type)!==null);
|
|
abort_unless($access->legacyWriteAllowed($actor)||$internal,403);
|
|
if ($record?->exists) $access->authorize($actor,'view',$record);
|
|
}
|
|
|
|
private function read(User $actor, callable $callback): mixed
|
|
{
|
|
return DB::transaction(function()use($actor,$callback){
|
|
DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)",[(string)$actor->id]);
|
|
return $callback();
|
|
});
|
|
}
|
|
|
|
public function search(User $actor,array $input,?Opportunity $record=null): array
|
|
{
|
|
$this->authorize($actor,$record);
|
|
$v=Validator::make($input,[
|
|
'territory_id'=>['required','uuid',Rule::exists('geo_locations','id')->whereNull('deleted_at')],
|
|
'activity'=>'nullable|string|max:150','sector'=>'nullable|string|max:250','page'=>'nullable|integer|min:1|max:100000',
|
|
])->validate();
|
|
if(!Schema::hasTable('proactive_supply_observations'))return ['installed'=>false,'rows'=>[],'total'=>null,'limitations'=>self::LIMITATIONS,'status'=>'Base interna ainda não carregada neste ambiente.'];
|
|
return $this->read($actor,function()use($v){
|
|
$ids=[$v['territory_id']];
|
|
// Canonical descendants, not a name-only cross-province match.
|
|
$front=$ids;
|
|
for($depth=0;$depth<5&&$front;$depth++){
|
|
$front=DB::table('geo_locations')->whereIn('parent_id',$front)->whereNull('deleted_at')->pluck('id')->all();
|
|
$ids=[...$ids,...$front];
|
|
}
|
|
$base=DB::table('proactive_supply_observations as o')->join('proactive_supply_sources as s','s.sha256','=','o.source_hash')->whereIn('o.territory_id',$ids);
|
|
$sectors=(clone $base)->where('o.sector','<>','')->distinct()->orderBy('o.sector')->pluck('o.sector')->all();
|
|
if(!empty($v['sector']))$base->where('o.sector',$v['sector']);
|
|
if(!empty($v['activity']))$base->where(fn($q)=>$q->where('o.cae','ilike','%'.$v['activity'].'%')->orWhereRaw("translate(lower(o.activity),'áàâãéêíóôõúç','aaaaeeiooouc') LIKE ?",['%'.strtolower(\Illuminate\Support\Str::ascii($v['activity'])).'%']));
|
|
$page=$v['page']??1;
|
|
// Exact repeated observations are retained in storage, counted separately.
|
|
$total=(clone $base)->whereNull('o.duplicate_of')->count();
|
|
$duplicates=(clone $base)->whereNotNull('o.duplicate_of')->count();
|
|
$rows=(clone $base)->whereNull('o.duplicate_of')->orderBy('o.name')->orderBy('o.id')->offset(($page-1)*25)->limit(25)->get(['o.id','o.entity_key','o.name','o.province','o.district','o.territory_id','o.territory_resolution','o.cae','o.activity','o.sector','o.source_date','o.sheet','o.row_number','o.source_hash','o.identity_conflict','s.name as source_name','s.period as source_period','s.imported_at']);
|
|
$coverage=DB::table('proactive_supply_observations')->selectRaw("count(*) FILTER (WHERE territory_id IS NULL) AS unmapped, count(*) FILTER (WHERE territory_resolution='province_only_district_unknown') AS province_only")->first();
|
|
return ['installed'=>true,'rows'=>$rows,'total'=>$total,'duplicate_sightings'=>$duplicates,'page'=>$page,'per_page'=>25,'sectors'=>$sectors,'territory_coverage'=>$coverage,'limitations'=>self::LIMITATIONS,'status'=>$total?'Registos documentais encontrados; não equivalem a empresas únicas ou oferta actual confirmada.':'Informação insuficiente nesta base e nestes filtros.'];
|
|
});
|
|
}
|
|
|
|
/** Resolve server-side source lineage; a caller cannot fabricate an internal observation. */
|
|
public function comparisons(User $actor,array $input,?Opportunity $record): array
|
|
{
|
|
$this->authorize($actor,$record);
|
|
$v=Validator::make(['rows'=>$input],[
|
|
'rows'=>'array|max:40','rows.*'=>'array:observation_id,relationship,rationale,limitations',
|
|
'rows.*.observation_id'=>'required|string|size:64|distinct',
|
|
'rows.*.relationship'=>'required|in:competition,complementarity,partner,uncertain',
|
|
'rows.*.rationale'=>'required|string|max:4000','rows.*.limitations'=>'required|string|max:4000',
|
|
])->validate()['rows'];
|
|
if(!$v)return [];
|
|
abort_unless(Schema::hasTable('proactive_supply_observations'),422,'Base interna indisponível.');
|
|
return $this->read($actor,function()use($v){
|
|
$fingerprints=[];
|
|
foreach($v as $row){
|
|
$o=DB::table('proactive_supply_observations')->where('id',$row['observation_id'])->first();
|
|
if(!$o)throw \Illuminate\Validation\ValidationException::withMessages(['proactive.supply_comparisons'=>'Registo da oferta indisponível; preserve a referência anterior e confira o ambiente.']);
|
|
if($o->duplicate_of!==null||isset($fingerprints[$o->fingerprint]))throw \Illuminate\Validation\ValidationException::withMessages(['proactive.supply_comparisons'=>'Esta observação administrativa é repetida. Compare a referência original uma única vez; a repetição e a sua fonte permanecem conservadas na base interna.']);
|
|
$fingerprints[$o->fingerprint]=true;
|
|
}
|
|
return $v;
|
|
});
|
|
}
|
|
|
|
public function evidence(User $actor,string $id,?Opportunity $record): array
|
|
{
|
|
$this->authorize($actor,$record);
|
|
return $this->read($actor,function()use($id){
|
|
$o=DB::table('proactive_supply_observations as o')->join('proactive_supply_sources as s','s.sha256','=','o.source_hash')->where('o.id',$id)->first(['o.*','s.name as source_name','s.period as source_period']);
|
|
if(!$o)throw \Illuminate\Validation\ValidationException::withMessages(['proactive.evidence'=>'Observação interna indisponível neste ambiente.']);
|
|
return ['observation_id'=>$o->id,'category'=>'supply','purpose'=>'local','access'=>'internal','document_id'=>null,
|
|
'statement'=>'Registo administrativo: '.$o->name.'; CAE '.$o->cae.'; '.($o->activity?:'actividade por classificar').'; '.$o->district.', '.$o->province.'. Não confirma actividade actual.',
|
|
'source'=>$o->source_name.'; SHA256 '.$o->source_hash,'locator'=>$o->sheet.'!linha '.$o->row_number,
|
|
'period'=>$o->source_date.'; '.$o->source_period,'scope'=>$o->district.', '.$o->province,
|
|
'coverage'=>self::LIMITATIONS.($o->identity_conflict?' Identificador partilhado por denominações divergentes: identidade por conferir.':''),
|
|
];
|
|
});
|
|
}
|
|
}
|