runningInConsole(),403,'Operação exclusivamente no terminal local.'); if (ServiceCalculation::isolated()) TestingDatabaseGuard::assertSafe(); else abort_unless(LocalDatabaseGuard::allows(),403,'Base própria de desenvolvimento local obrigatória.'); $rules=['proposal_id'=>'required|integer|min:1','version'=>'required|integer|min:1','identifier'=>'required|string','tenant_id'=>'required|integer|min:1','company_id'=>'required|integer|min:1','unit_id'=>'required|integer|min:1','service_id'=>'required|integer|min:1','service_version'=>'required|integer|min:1','valid_from'=>'required|date_format:Y-m-d','valid_until'=>'nullable|date_format:Y-m-d|after_or_equal:valid_from','operator_id'=>'required|integer|min:1','definition_hash'=>($apply?'required':'nullable').'|string|regex:/^[a-f0-9]{64}$/D']; abort_if(array_diff(array_keys($input),array_keys($rules))!==[],422); $in=Validator::make($input,$rules)->validate(); return DB::transaction(function() use($in,$apply) { $scope=array_intersect_key($in,array_flip(['tenant_id','company_id','unit_id'])); DB::table('companies')->where('id',$scope['company_id'])->where('tenant_id',$scope['tenant_id'])->lockForUpdate()->firstOrFail(); app(CompanyContext::class)->selectFor((int)$in['operator_id'],(int)$scope['tenant_id'],(int)$scope['company_id'],(int)$scope['unit_id']); app(\App\Modules\Identity\Services\Permissions::class)->require((int)$in['operator_id'],(int)$scope['tenant_id'],(int)$scope['company_id'],'service_fiscal.view'); $p=DB::table('sf_proposals')->where($scope)->where('id',$in['proposal_id'])->where('kind','profile')->where('identifier',$in['identifier'])->where('version',$in['version'])->first(); abort_unless($p,409,'Perfil, versão ou âmbito diferente da operação autorizada.'); $d=json_decode($p->data,true,32,JSON_THROW_ON_ERROR); foreach(['service_id','service_version'] as $key) abort_unless((int)$d[$key]===(int)$in[$key],409,'Serviço/versão diferente.'); abort_unless($d['valid_from']===$in['valid_from'] && ($d['valid_until'] ?? null)===($in['valid_until'] ?? null),409,'Vigência diferente.'); abort_unless($d['currency']==='MZN' && $d['price_mode']==='added' && $d['rate_percent']==='16' && $d['treatment']==='domestic_normal',409,'Tratamento diferente do recorte aprovado.'); abort_unless(DB::table('a_services')->where('id',$in['service_id'])->where('tenant_id',$scope['tenant_id'])->where('company_id',$scope['company_id'])->where('version',$in['service_version'])->exists(),409,'Serviço desactualizado.'); abort_unless(DB::table('sf_decisions')->where('proposal_id',$p->id)->where('decision','approved')->exists(),409,'Aprovação exacta em falta.'); abort_if(DB::table('sf_proposals')->where($scope)->where('kind','profile')->where('identifier',$p->identifier)->where('version','>',$p->version)->whereExists(fn($q)=>$q->selectRaw('1')->from('sf_decisions')->whereColumn('sf_decisions.proposal_id','sf_proposals.id')->where('decision','approved'))->exists(),409,'Outra versão já foi aprovada; reveja a operação.'); $profile=DB::table('service_calculation_profiles')->where($scope)->where('id',$p->profile_id)->where('identifier',$p->identifier)->where('version',$p->version)->first(); abort_unless($profile,409,'Perfil de cálculo incompatível.'); $definition=json_decode($profile->definition,true,32,JSON_THROW_ON_ERROR); abort_unless(($definition['kind'] ?? null)==='real' && ($definition['policy'] ?? null)===ServiceCalculation::POLICY,409,'Política/perfil incompatível.'); foreach($d as $key=>$value) abort_unless(($definition[$key] ?? null)===$value,409,'Definição diverge da proposta aprovada.'); $hash=hash('sha256',json_encode([$p->id,(int)$p->version,$scope,$d,$definition],JSON_THROW_ON_ERROR)); if($apply) abort_unless(hash_equals($hash,$in['definition_hash']),409,'A definição não corresponde à operação revista.'); $last=DB::table('sf_activation_events')->where('proposal_id',$p->id)->orderByDesc('id')->first(); $active=(bool)($last?->active ?? false); $result=['proposal_id'=>$p->id,'identifier'=>$p->identifier,'version'=>(int)$p->version,'scope'=>$scope,'service_id'=>(int)$d['service_id'],'service_version'=>(int)$d['service_version'],'valid_from'=>$d['valid_from'],'valid_until'=>$d['valid_until'] ?? null,'definition_hash'=>$hash,'active'=>$active,'applied'=>false]; if(!$apply || $active) return $result+['replayed'=>$apply && $active]; $event=DB::table('sf_activation_events')->insertGetId(['proposal_id'=>$p->id,'actor_id'=>$in['operator_id'],'active'=>true,'reason'=>'Operação local especificamente autorizada pelo utilizador para este perfil/versão/âmbito; apenas cálculo proposto, sem emissão, recebimentos ou lançamentos. Definição SHA256: '.$hash,'created_at'=>now()]); Audit::record('service_fiscal.local_activated',(int)$in['operator_id'],(int)$scope['tenant_id'],(int)$scope['company_id'],(int)$scope['unit_id'],details:['record_id'=>$p->id,'version'=>(int)$p->version,'event_id'=>$event,'reason'=>'Activação local especificamente autorizada; sem poderes de emissão ou lançamentos.']); return array_replace($result,['active'=>true,'applied'=>true,'event_id'=>$event,'replayed'=>false]); }); } }