107 lines
9.4 KiB
PHP
107 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
/** Owner-directed staff assignments use existing identities, organizations and grants. */
|
|
class PortalInternalDesignation
|
|
{
|
|
public function organizations(): array
|
|
{
|
|
return DB::table('organizations')->whereIn(DB::raw('id::text'), DB::table('audit_events')->where('source_system','portal-profiles-v1')->where('action','operator_organization_registered')->selectRaw("after_data->>'organization_id'"))->whereNull('deleted_at')
|
|
->get(['id','name','external_id','version_no'])->map(fn($r)=>$this->organization($r->id))->filter()->values()->all();
|
|
}
|
|
|
|
public function organization(?string $id): ?array
|
|
{
|
|
if ($id===null) return null;
|
|
$r=DB::table('organizations')->where('id',$id)->whereNull('deleted_at')->first();
|
|
if (!$r) return null;
|
|
$event=DB::table('audit_events')->where('source_system','portal-profiles-v1')->where('action','operator_organization_registered')
|
|
->where('after_data->organization_id',$id)->orderByDesc('occurred_at')->first();
|
|
if (!$event) return null;
|
|
$data=json_decode($event->after_data,true,512,JSON_THROW_ON_ERROR);
|
|
if (($data['organization']['name'] ?? null)!==$r->name || ($data['organization']['nuit'] ?? null)!==$r->external_id) return null;
|
|
return ['id'=>$r->id,'version'=>$r->version_no,...$data['organization'],'source_reference'=>$data['source_reference']];
|
|
}
|
|
|
|
public function registerOrganization(User $actor,array $input): array
|
|
{
|
|
app(PortalProfileAdministration::class)->administrative($actor);
|
|
$v=Validator::make($input,['operation_key'=>'required|uuid','name'=>'required|string|max:200','nuit'=>'required|string|regex:/^[0-9]{9}$/',
|
|
'contact'=>'required|string|regex:/^\+258[0-9]{9}$/','director'=>'required|string|max:200',
|
|
'function'=>'required|in:portal_owner_operator','source_reference'=>'required|string|min:20|max:1000'])->validate();
|
|
return DB::transaction(function()use($actor,$v){
|
|
DB::select('SELECT pg_advisory_xact_lock(101026,40001)');
|
|
$payload=array_intersect_key($v,array_flip(['name','nuit','contact','director','function']));
|
|
$existingEvent=DB::table('audit_events')->where('source_system','portal-profiles-v1')->whereIn('action',['operator_organization_registered','operator_organization_registration_recovered'])->where('after_data->operation_key',$v['operation_key'])->first();
|
|
if($existingEvent){
|
|
$e=json_decode($existingEvent->after_data,true);
|
|
abort_unless($e['organization']==$payload && $e['source_reference']===$v['source_reference'],409,'Esta chave corresponde a outro cadastro.');
|
|
return ['id'=>$e['organization_id'],'organization'=>$this->organization($e['organization_id']),'message'=>'Cadastro recuperado; não repetido.'];
|
|
}
|
|
$names=array_unique([$v['name'],preg_replace('/\s*,\s*/',' ',$v['name'])]);
|
|
$matches=DB::table('organizations')->where(fn($q)=>$q->where('external_id',$v['nuit'])->orWhereIn('name',$names)->orWhere('name',str_replace(' Lda',', Lda',$v['name'])))->lockForUpdate()->get();
|
|
abort_if($matches->count()>1,409,'Correspondência ambígua no cadastro mestre; não criar duplicado.');
|
|
$r=$matches->first();
|
|
if($r){
|
|
abort_if($r->deleted_at!==null || ($r->external_id!==null && $r->external_id!==$v['nuit']),409,'Identidade ou estado da organização exige reconciliação.');
|
|
$known=$this->organization($r->id);
|
|
if($known){
|
|
abort_unless(array_intersect_key($known,$payload)==$payload,409,'Dados do cadastro já existente diferem; preservar a versão anterior.');
|
|
app(PortalProfileAccess::class)->audit($actor,'operator_organization_registration_recovered',['operation_key'=>$v['operation_key'],'organization_id'=>$r->id,'organization'=>$payload,'source_reference'=>$v['source_reference']]);
|
|
return ['id'=>$r->id,'organization'=>$known,'message'=>'Organização existente recuperada.'];
|
|
}
|
|
$id=$r->id;
|
|
$before=['name'=>$r->name,'external_id'=>$r->external_id,'source_system'=>$r->source_system,'version_no'=>$r->version_no];
|
|
abort_unless(in_array($r->name,$names,true) || $r->name===str_replace(' Lda',', Lda',$v['name']),409,'Denominação divergente exige reconciliação expressa.');
|
|
DB::table('organizations')->where('id',$id)->update(['name'=>$v['name'],'external_id'=>$v['nuit'],'updated_at'=>now(),'version_no'=>$r->version_no+1]);
|
|
// Reconcile only the explicit comma alias; preserve previous values in audit.
|
|
}else{
|
|
$id=(string)Str::uuid();
|
|
DB::table('organizations')->insert(['id'=>$id,'name'=>$v['name'],'external_id'=>$v['nuit'],'source_system'=>'portal-operator-master','organization_type'=>'private']);
|
|
}
|
|
app(PortalProfileAccess::class)->audit($actor,'operator_organization_registered',['operation_key'=>$v['operation_key'],'organization_id'=>$id,
|
|
'organization'=>$payload,'previous_master_values'=>$before??null,'source_reference'=>$v['source_reference'],'metadata_storage'=>'existing_audit_snapshot','no_corporate_membership_created'=>true]);
|
|
return ['id'=>$id,'organization'=>$this->organization($id),'message'=>'Organização operadora registada no cadastro mestre. Nenhuma representação ou papel foi atribuído.'];
|
|
});
|
|
}
|
|
|
|
public function grant(User $actor,array $input): string
|
|
{
|
|
app(PortalProfileAdministration::class)->administrative($actor);
|
|
$v=Validator::make($input,['operation_key'=>'required|uuid','user_id'=>'required|integer|exists:users,id','role'=>['required',Rule::in(['promoter','validator','publisher'])],
|
|
'operator_organization_id'=>'required|uuid|exists:organizations,id','organization_id'=>'nullable|uuid|exists:organizations,id',
|
|
'opportunity_types'=>'required|array|min:1','opportunity_types.*'=>['required',Rule::in(OpportunityCanonicalContract::BASE_CATEGORIES)],
|
|
'confirmed_internal_designation'=>'required|accepted','reason'=>'required|string|min:20|max:1000',
|
|
'responsibilities'=>'sometimes|array|max:0','expires_at'=>'nullable|date|after:now'])->validate();
|
|
abort_if((int)$v['user_id']===$actor->id,403,'Não pode aprovar a sua atribuição.');
|
|
$subject=User::findOrFail($v['user_id']);
|
|
abort_unless($subject->hasVerifiedEmail() && $subject->two_factor_secret!==null && $subject->two_factor_confirmed_at!==null,422,'Confirmar contacto e o próprio MFA antes da atribuição interna.');
|
|
$organization=$this->organization($v['operator_organization_id']);
|
|
abort_unless($organization!==null,422,'Selecione a organização operadora cadastrada; não exige comprovativos de Promotor externo.');
|
|
if($v['role']==='promoter')abort_unless(($v['organization_id']??null)===$v['operator_organization_id'],422,'O vínculo interno é à organização operadora, não uma representação de gestão empresarial.');
|
|
return DB::transaction(function()use($actor,$v){
|
|
DB::select('SELECT pg_advisory_xact_lock(101026,40002)');
|
|
$subject=User::whereKey($v['user_id'])->lockForUpdate()->firstOrFail();
|
|
abort_unless($subject->hasVerifiedEmail() && $subject->two_factor_secret!==null && $subject->two_factor_confirmed_at!==null && $this->organization($v['operator_organization_id'])!==null,422,'Conta ou organização já não reúne as condições da atribuição.');
|
|
$e=DB::table('audit_events')->where('source_system','portal-profiles-v1')->where('action','role_granted')->where('after_data->operation_key',$v['operation_key'])->first();
|
|
$payload=array_intersect_key($v,array_flip(['user_id','role','operator_organization_id','organization_id','opportunity_types','reason','expires_at']));
|
|
if($e){$d=json_decode($e->after_data,true);abort_unless($d['designation']==$payload,409,'Chave usada numa atribuição diferente.');abort_unless(DB::table('portal_role_assignments')->where('id',$d['assignment_id'])->whereNull('revoked_at')->exists(),409,'A atribuição foi revogada.');return $d['assignment_id'];}
|
|
$duplicate=DB::table('portal_role_assignments')->where('user_id',$v['user_id'])->where('role',$v['role'])->where('source','internal-portal-designation')->whereNull('revoked_at')->where('starts_at','<=',now())->where(fn($q)=>$q->whereNull('expires_at')->orWhere('expires_at','>',now()))->exists();
|
|
abort_if($duplicate,409,'Já existe atribuição interna ativa: recupere-a antes de repetir ou alterar.');
|
|
$id=(string)Str::uuid();
|
|
app(PortalProfileAdministration::class)->insertGrant($id,$actor,[...$v,'source'=>'internal-portal-designation']);
|
|
app(PortalProfileAccess::class)->audit($actor,'role_granted',['assignment_id'=>$id,'subject_user_id'=>$v['user_id'],'role'=>$v['role'],
|
|
'operation_key'=>$v['operation_key'],'designation'=>$payload,'operator_organization_id'=>$v['operator_organization_id'],
|
|
'reason'=>$v['reason'],'authority'=>'portal-owner-internal-designation','external_promoter_documents_required'=>false,'no_corporate_membership_created'=>true]);
|
|
return $id;
|
|
});
|
|
}
|
|
}
|