53 lines
5.9 KiB
PHP
53 lines
5.9 KiB
PHP
<?php
|
|
namespace App\Modules\Services;
|
|
|
|
use App\Modules\Identity\Services\Audit;
|
|
use App\Modules\Identity\Services\LocalDatabaseGuard;
|
|
use App\Shared\Context\CompanyContext;
|
|
use App\Shared\Testing\TestingDatabaseGuard;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
/** Local operator command only. No HTTP route or standing activation grant. */
|
|
final class LocalFiscalActivation
|
|
{
|
|
public function execute(array $input,bool $apply=false): array
|
|
{
|
|
abort_unless(app()->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(\App\Shared\Context\ModuleRecords::query('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]);
|
|
});
|
|
}
|
|
}
|