From 9f480045fad25b80a3e4081a187f7164cbd2f6bf Mon Sep 17 00:00:00 2001 From: nala Date: Mon, 26 Oct 2026 16:48:44 +0200 Subject: [PATCH] Accept conditional service calculation with versioned fiscal profile --- AGENTS.md | 2 + README.md | 2 + app/Modules/Services/DocumentPreview.php | 1 + .../Services/Http/ServiceFiscalController.php | 16 ++ .../Services/LocalFiscalActivation.php | 52 +++++ app/Modules/Services/ServiceCalculation.php | 14 +- app/Modules/Services/ServiceFiscal.php | 181 +++++++++++++++++ ...050000_create_service_fiscal_lifecycle.php | 68 +++++++ docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md | 39 ++++ docs/proofs/service-fiscal-browser.png | Bin 0 -> 63765 bytes docs/proofs/service-fiscal-concurrency.json | 7 + ...ervice-fiscal-dev-preservation-grants.json | 9 + .../service-fiscal-dev-preservation.json | 9 + resources/css/app.css | 1 + resources/js/DocumentPreview.jsx | 1 + resources/js/ServiceFiscalWorkspace.jsx | 52 +++++ resources/js/app.jsx | 4 +- routes/web.php | 5 + scripts/service-fiscal-access.php | 35 ++++ scripts/service-fiscal-activate.php | 9 + scripts/service-fiscal-concurrency.php | 29 +++ scripts/service-fiscal-fixture.php | 35 ++++ scripts/service-fiscal-preservation.php | 34 ++++ scripts/service-fiscal-worker.php | 11 + tests/Feature/ServiceFiscalTest.php | 188 ++++++++++++++++++ 25 files changed, 801 insertions(+), 3 deletions(-) create mode 100644 app/Modules/Services/Http/ServiceFiscalController.php create mode 100644 app/Modules/Services/LocalFiscalActivation.php create mode 100644 app/Modules/Services/ServiceFiscal.php create mode 100644 database/migrations/2026_10_26_050000_create_service_fiscal_lifecycle.php create mode 100644 docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md create mode 100644 docs/proofs/service-fiscal-browser.png create mode 100644 docs/proofs/service-fiscal-concurrency.json create mode 100644 docs/proofs/service-fiscal-dev-preservation-grants.json create mode 100644 docs/proofs/service-fiscal-dev-preservation.json create mode 100644 resources/js/ServiceFiscalWorkspace.jsx create mode 100644 scripts/service-fiscal-access.php create mode 100644 scripts/service-fiscal-activate.php create mode 100644 scripts/service-fiscal-concurrency.php create mode 100644 scripts/service-fiscal-fixture.php create mode 100644 scripts/service-fiscal-preservation.php create mode 100644 scripts/service-fiscal-worker.php create mode 100644 tests/Feature/ServiceFiscalTest.php diff --git a/AGENTS.md b/AGENTS.md index 492cda3..21575c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # SGF Tempo — instruções locais +- Em 26/10/2026, o utilizador aceitou o cálculo condicionado e autorizou o registo e checkpoint Git exclusivamente local, sem credenciais/dados locais. Ver `docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md`. Perfil, concessões e activação local foram objecto de confirmações específicas; os detalhes nominativos permanecem na ficha local não versionada. Exigibilidade e período de apuramento permanecem pendentes; aprovação temporal não os determina. Preservar versões/âmbito exactos, separação de proponente/validador, B1 e aprovações contabilísticas. Calcular não altera decisão temporal validada. Sem avançar para emissão documental, recebimentos, lançamentos ou publicação. + - Em 26/10/2026, o utilizador aceitou expressamente o cálculo de ensaio demonstrado (base 1000,03; IVA exacto 160,0048; IVA proposto 160,00; diferença preservada 0,0048; total proposto 1160,03) e autorizou registo e checkpoint Git apenas local. Ver `docs/SERVICE_CALCULATION_SEPARATE_REVIEW.md`. Esta confirmação substitui apenas a pendência de aceitação do ensaio: a aplicabilidade fiscal real permanece separada e pendente, sem autorização para activar perfil fiscal real, emitir, contabilizar ou publicar. - Em 26/10/2026, após informar que entrou e verificou, o utilizador confirmou a aceitação funcional do primeiro incremento de classificação contabilística e autorizou o encerramento e checkpoint Git exclusivamente local, sem credenciais ou dados locais. Preservar a entrega e os limites de `docs/CATALOGUE_ACCOUNTING_DELIVERY.md`. Esta aceitação não abrange a validação do cálculo fiscal, que continua separada e pendente; não autoriza lançamentos, publicação ou outro incremento. diff --git a/README.md b/README.md index 07b9240..d8cecc0 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ Não existe criação de tenants ou empresas pela interface, auto-registo, opç ## Testes e ferramentas +O incremento de cálculo condicionado foi aceite; ver [entrega, verificações e limites](docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md). Na empresa/unidade autorizadas, consultar **Perfil fiscal do serviço** e **Rascunhos de facturação → Pré-visualizar rascunho**. A activação é uma operação local especificamente autorizada, sem via no navegador. O cálculo exige decisão temporal aprovada e compatível; quando há pendência, não determina exigibilidade ou período de apuramento. Sem emissão, recebimentos ou lançamentos. + ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\sgf.ps1 test powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\sgf.ps1 npm run build diff --git a/app/Modules/Services/DocumentPreview.php b/app/Modules/Services/DocumentPreview.php index 791e826..edc65ac 100644 --- a/app/Modules/Services/DocumentPreview.php +++ b/app/Modules/Services/DocumentPreview.php @@ -105,6 +105,7 @@ final class DocumentPreview 'issuer' => ['name' => $issuer['name'], 'data' => $issuer['data']], 'client' => ['name' => $client['name'], 'data' => $client['data']], 'data' => $billing['data'], 'prepared' => $prepared['data'], 'base_amount' => $billing['base_amount'], 'pending' => $pending, 'calculation' => app(ServiceCalculation::class)->read($draft, $actor), + 'fiscal_history' => $this->permissions->has($actor,$scope['tenant_id'],$scope['company_id'],'service_fiscal.view') ? app(ServiceFiscal::class)->history($draft,$actor) : [], 'payments' => $payments, 'logo' => $logo, 'contacts' => $model['data']['show_contacts'] ? $model['data']['contact_text'] : null, 'observations' => $model['data']['show_observations'] ? $model['data']['observations'] : null, 'versions' => ['issuer' => $issuer['version'], 'client' => $client['version'], 'company_name' => $issuer['name'], 'client_name' => $client['name'], 'instructions' => $paymentVersions, 'model' => $model['version']]]; diff --git a/app/Modules/Services/Http/ServiceFiscalController.php b/app/Modules/Services/Http/ServiceFiscalController.php new file mode 100644 index 0000000..aa95949 --- /dev/null +++ b/app/Modules/Services/Http/ServiceFiscalController.php @@ -0,0 +1,16 @@ +json($data)->header('Cache-Control','no-store, private'); } + public function index(Request $r,ServiceFiscal $f) { return $this->response($f->listing($r->user()->id)); } + public function propose(Request $r,ServiceFiscal $f) { return $this->response($f->propose($r->user()->id,$r->all())); } + public function decide(Request $r,int $id,ServiceFiscal $f) { return $this->response($f->decide($id,$r->user()->id,$r->all())); } + public function calculate(Request $r,int $id,ServiceFiscal $f) { return $this->response($f->calculate($id,$r->user()->id,$r->all())); } + public function history(Request $r,int $id,ServiceFiscal $f) { return $this->response(['history'=>$f->history($id,$r->user()->id)]); } +} diff --git a/app/Modules/Services/LocalFiscalActivation.php b/app/Modules/Services/LocalFiscalActivation.php new file mode 100644 index 0000000..7fd7313 --- /dev/null +++ b/app/Modules/Services/LocalFiscalActivation.php @@ -0,0 +1,52 @@ +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]); + }); + } +} diff --git a/app/Modules/Services/ServiceCalculation.php b/app/Modules/Services/ServiceCalculation.php index 2691e4c..6981202 100644 --- a/app/Modules/Services/ServiceCalculation.php +++ b/app/Modules/Services/ServiceCalculation.php @@ -18,6 +18,15 @@ final class ServiceCalculation public static function isolated(): bool { return app()->environment(['testing','browser-testing']); } + public static function evaluateReal(array $data, array $prepared, array $profile): array + { + abort_unless(($profile['kind'] ?? null)==='real',422,'Perfil real identificado obrigatório.'); + // Shared decimal arithmetic; authority/applicability is checked by ServiceFiscal before calling. + $result=self::evaluate($data,$prepared,array_replace($profile,['kind'=>'synthetic'])); + if ($result['state']!=='pending') $result['state']='real_proposed_conditioned'; + return $result; + } + public static function evaluate(array $data, array $prepared, ?array $profile): array { $pending = []; @@ -97,14 +106,15 @@ final class ServiceCalculation public function read(int $draft, int $actor): array { $scope = $this->workflow->scope('billing_drafts',$actor,'view'); - if (!self::isolated()) { return ['environment'=>'normal','history'=>[], 'pending'=>['Aplicabilidade à empresa, serviço/localização, base e acontecimento fiscal por demonstrar; nenhum perfil real activado.']]; } + if (!self::isolated()) { return ['environment'=>'normal','history'=>[], 'pending'=>['O cálculo proposto requer perfil aprovado, activo e compatível e decisão temporal aprovada. Consulte o resultado e as pendências abaixo, quando disponíveis.']]; } TestingDatabaseGuard::assertSafe(); [$b,$p] = $this->sources($draft,$actor); - $profiles = DB::table('service_calculation_profiles')->where($scope)->orderByDesc('version')->get(); + $profiles = DB::table('service_calculation_profiles')->where($scope)->whereRaw("definition->>'kind' = ?",['synthetic'])->orderByDesc('version')->get(); $latest = []; foreach ($profiles as $profile) { $latest[$profile->identifier] ??= $profile; } $history = []; foreach (DB::table('service_calculation_results')->where($scope)->where('billing_draft_id',$draft)->orderByDesc('id')->get() as $row) { $snapshot = json_decode($row->snapshot,true,32,JSON_THROW_ON_ERROR); + if (isset($snapshot['input']['profile_proposal_id'])) continue; $profile = $latest[$snapshot['input']['profile_identifier']] ?? null; $current = $profile && hash_equals($row->fingerprint,$this->fingerprint($this->input($b,$p,$profile))); $history[] = ['id'=>$row->id,'current'=>(bool)$current,'snapshot'=>$snapshot]; diff --git a/app/Modules/Services/ServiceFiscal.php b/app/Modules/Services/ServiceFiscal.php new file mode 100644 index 0000000..0cc2a77 --- /dev/null +++ b/app/Modules/Services/ServiceFiscal.php @@ -0,0 +1,181 @@ +context->identifiers(); $unit=$this->context->unitId(); + abort_unless($actor===$this->context->userId() && $unit!==null,403,'Seleccione uma unidade autorizada.'); + $this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'unit.view',$unit); + foreach (array_unique(['view',$action]) as $a) $this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_fiscal.'.$a,$a==='calculate' ? $unit : null); + return $s+['unit_id'=>$unit]; + } + private function command(int $actor,string $action,callable $fn): array + { + $s=$this->scope($actor,$action); + return DB::transaction(function() use($actor,$action,$fn,$s) { + DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail(); + $this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); $this->scope($actor,$action); + return $fn($s); + }); + } + private function row(int $id,array $s): object + { + $r=DB::table('sf_proposals')->where($s)->where('id',$id)->first(); abort_unless($r,403,'Proposta fora do âmbito.'); return $r; + } + private function data(object $r): array { return json_decode($r->data,true,32,JSON_THROW_ON_ERROR); } + private function decision(int $id): ?object { return DB::table('sf_decisions')->where('proposal_id',$id)->first(); } + private function approved(object $r): bool { return $this->decision($r->id)?->decision==='approved'; } + private function activated(object $r): bool { return (bool)(DB::table('sf_activation_events')->where('proposal_id',$r->id)->orderByDesc('id')->first()?->active ?? false); } + private function latest(object $r,array $s,?string $date=null): bool + { + // Pending/refused proposals do not supersede a currently approved version. + $q=DB::table('sf_proposals')->where($s)->where('kind',$r->kind)->where('identifier',$r->identifier)->where('version','>',$r->version) + ->whereExists(fn($q)=>$q->selectRaw('1')->from('sf_decisions')->whereColumn('sf_decisions.proposal_id','sf_proposals.id')->where('decision','approved')); + if($r->kind==='profile' && $date!==null) $q->whereRaw("data->>'valid_from' <= ?",[$date]); + return !$q->exists(); + } + private function exact(array $data,array $rules): array + { + abort_if(array_diff(array_keys($data),array_keys($rules))!==[],422,'Campo protegido ou fora do âmbito fiscal.'); + return Validator::make($data,$rules)->validate(); + } + public function listing(int $actor): array + { + $s=$this->scope($actor); + if (!\Illuminate\Support\Facades\Schema::hasTable('sf_proposals')) return ['migration_pending'=>true,'proposals'=>[]]; + $rows=DB::table('sf_proposals')->where($s)->orderByDesc('id')->get()->map(function($r) { return (array)$r+['definition'=>$this->data($r),'decision'=>$this->decision($r->id),'active'=>$r->kind==='profile' && $this->activated($r)]; })->all(); + return ['proposals'=>$rows,'migration_pending'=>false,'notice'=>'RASCUNHO — SEM VALIDADE FISCAL; aprovação não activa o perfil.']; + } + public function propose(int $actor,array $input): array + { + return $this->command($actor,'propose',function($s) use($actor,$input) { + $in=$this->exact($input,['kind'=>'required|in:profile,temporal','identifier'=>'required|string|max:100|regex:/^[A-Za-z0-9_-]+$/D','previous_id'=>'nullable|integer|min:1','data'=>'required|array']); + $previous=isset($in['previous_id']) ? $this->row((int)$in['previous_id'],$s) : null; + $last=DB::table('sf_proposals')->where($s)->where('kind',$in['kind'])->where('identifier',$in['identifier'])->orderByDesc('version')->first(); + abort_unless(($last?->id ?? null)===($previous?->id ?? null),409,'Indique a versão anterior exacta.'); + $version=(int)($last?->version ?? 0)+1; + if ($in['kind']==='profile') { + $d=$this->exact($in['data'],['service_id'=>'required|integer|min:1','service_version'=>'required|integer|min:1','currency'=>'required|in:MZN','price_mode'=>'required|in:added','rate_percent'=>'required|in:16','valid_from'=>'required|date_format:Y-m-d','valid_until'=>'nullable|date_format:Y-m-d|after_or_equal:valid_from','treatment'=>'required|in:domestic_normal','conditions'=>'required|string|max:4000','evidence'=>'required|string|max:4000']); + $service=DB::table('a_services')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where('id',$d['service_id'])->first(); + abort_unless($service && (int)$service->version===(int)$d['service_version'],409,'Serviço/versão incompatível.'); + if ($previous) abort_unless((int)$this->data($previous)['service_id']===(int)$d['service_id'],422,'Nova versão não pode trocar a identidade do serviço.'); + abort_if(DB::table('sf_proposals')->where($s)->where('kind','profile')->whereRaw("data->>'service_id' = ?",[(string)$d['service_id']])->where('identifier','<>',$in['identifier'])->exists(),409,'Use a identidade existente deste serviço e uma nova versão.'); + $profile=DB::table('service_calculation_profiles')->insertGetId($s+['identifier'=>$in['identifier'],'version'=>$version,'definition'=>json_encode($d+['kind'=>'real','policy'=>ServiceCalculation::POLICY,'sources'=>ServiceCalculation::SOURCES+['fiscal_2026'=>'Lei10/2025:6,32;vigencia2026-01-01']],JSON_THROW_ON_ERROR),'created_at'=>now()]); + $draft=null; + } else { + $d=$this->exact($in['data'],['profile_proposal_id'=>'required|integer|min:1','draft_id'=>'required|integer|min:1','draft_version'=>'required|integer|min:1','preparation_version'=>'required|integer|min:0','reference_date'=>'required|date_format:Y-m-d','reference_basis'=>'required|string|max:2000','period_start'=>'required|date_format:Y-m-d','period_end'=>'nullable|date_format:Y-m-d|after_or_equal:period_start','next_billing'=>'nullable|date_format:Y-m-d','event'=>'required|in:pending,invoice_in_time,invoice_deadline','event_date'=>'nullable|required_unless:event,pending|date_format:Y-m-d','evidence'=>'required|string|max:4000']); + abort_if($d['event']==='pending' && !empty($d['event_date']),422,'Pendência temporal não tem data de exigibilidade.'); + abort_if($d['event']!=='pending' && $d['reference_date']!==$d['event_date'],422,'A referência de vigência deve corresponder à exigibilidade fundamentada quando determinada.'); + $p=$this->row((int)$d['profile_proposal_id'],$s); abort_unless($p->kind==='profile' && $this->approved($p),422,'Perfil exacto aprovado necessário.'); + [$b,$prepared]=$this->draft((int)$d['draft_id'],$actor); + $this->versions($b,$prepared,$d); + abort_unless(($prepared['data']['period_start'] ?? null)===$d['period_start'] && ($prepared['data']['period_end'] ?? null)===($d['period_end'] ?? null),409,'Período deve corresponder à preparação revista.'); + if ($previous) abort_unless((int)$previous->draft_id===(int)$d['draft_id'],422,'Decisão temporal pertence ao mesmo rascunho.'); + abort_if(DB::table('sf_proposals')->where($s)->where('kind','temporal')->where('draft_id',$d['draft_id'])->where('identifier','<>',$in['identifier'])->exists(),409,'Use a identidade temporal existente e uma nova versão.'); + $profile=$p->profile_id; $draft=$d['draft_id']; + } + $id=DB::table('sf_proposals')->insertGetId($s+['kind'=>$in['kind'],'identifier'=>$in['identifier'],'version'=>$version,'previous_id'=>$previous?->id,'profile_id'=>$profile,'draft_id'=>$draft,'data'=>json_encode($d,JSON_THROW_ON_ERROR),'actor_id'=>$actor,'created_at'=>now()]); + Audit::record('service_fiscal.proposed',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'version'=>$version,'kind'=>$in['kind']]); + return ['id'=>$id,'version'=>$version,'active'=>false]; + }); + } + public function decide(int $id,int $actor,array $input): array + { + return $this->command($actor,'approve',function($s) use($id,$actor,$input) { + $d=$this->exact($input,['version'=>'required|integer|min:1','decision'=>'required|in:approved,refused','reason'=>'required|string|max:4000']); + $r=$this->row($id,$s); abort_unless((int)$r->version===(int)$d['version'] && (int)$r->actor_id!==$actor,403,'Versão exacta e validador distinto obrigatórios.'); + abort_if($this->decision($id),409,'Decisão imutável; correcção exige nova versão.'); + if ($r->kind==='temporal' && $d['decision']==='approved') { + $data=$this->data($r); [$b,$p]=$this->draft((int)$r->draft_id,$actor); $this->versions($b,$p,$data); + $profile=$this->row((int)$data['profile_proposal_id'],$s); $this->compatible($profile,$s,$data['reference_date'],$b,$p,false); + } + DB::table('sf_decisions')->insert(['proposal_id'=>$id,'actor_id'=>$actor,'decision'=>$d['decision'],'reason'=>$d['reason'],'created_at'=>now()]); + Audit::record('service_fiscal.decided',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'version'=>$r->version,'decision'=>$d['decision']]); + return ['id'=>$id,'decision'=>$d['decision'],'active'=>false]; + }); + } + /** No HTTP activation endpoint. Only isolated proofs may exercise active/revoked states in this increment. */ + public function proofActivation(int $id,int $actor,int $version,bool $active): void + { + abort_unless(ServiceCalculation::isolated(),403,'Activação real não autorizada neste incremento.'); TestingDatabaseGuard::assertSafe(); + $this->command($actor,'approve',function($s) use($id,$actor,$version,$active) { + $p=$this->row($id,$s); abort_unless($p->kind==='profile' && (int)$p->version===$version && $this->approved($p) && $this->latest($p,$s),409); + DB::table('sf_activation_events')->insert(['proposal_id'=>$id,'actor_id'=>$actor,'active'=>$active,'reason'=>'Prova sintética isolada; não é activação real.','created_at'=>now()]); + Audit::record('service_fiscal.proof_activation',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'version'=>$version,'active'=>$active]); + return []; + }); + } + private function draft(int $id,int $actor): array + { + $b=$this->workflow->show('billing_drafts',$id,$actor); abort_if($b['internal_confirmation'],409,'B1 preservada.'); + return [$b,$this->preparation->show('billing_drafts',$id,$actor)]; + } + private function versions(array $b,array $p,array $d): void + { + abort_unless($b['version']===(int)$d['draft_version'] && $p['version']===(int)$d['preparation_version'] && ($p['data']['draft_version'] ?? null)===$b['version'],409,'Rascunho/preparação desactualizados.'); + $current=DB::table('a_service_drafts')->where('id',$b['source_id'])->value('version'); + abort_unless((int)$current===(int)$b['data']['source_version'],409,'Origem desactualizada.'); + } + private function compatible(object $p,array $s,string $date,array $b,array $prepared,bool $active=true): array + { + $d=$this->data($p); $line=$b['data']['lines'][0] ?? []; + abort_unless($p->kind==='profile' && $this->approved($p) && (!$active || $this->activated($p)) && $this->latest($p,$s,$date),409,'Perfil sem aprovação/activação exacta ou substituído.'); + abort_unless($date!=='' && $d['valid_from']<=$date && (empty($d['valid_until']) || $date<=$d['valid_until']),422,'Vigência não demonstrada.'); + $service=DB::table('a_services')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where('id',$d['service_id'])->first(); + abort_unless($service && (int)$service->version===(int)$d['service_version'] && count($b['data']['lines'])===1 && (int)($line['service_id'] ?? 0)===(int)$d['service_id'] && $b['data']['currency']===$d['currency'] && ($prepared['data']['price_mode'] ?? null)===$d['price_mode'],422,'Serviço, moeda ou modo incompatível.'); + return $d; + } + public function calculate(int $draft,int $actor,array $input): array + { + return $this->command($actor,'calculate',function($s) use($draft,$actor,$input) { + $d=$this->exact($input,['profile_proposal_id'=>'required|integer|min:1','profile_version'=>'required|integer|min:1','temporal_proposal_id'=>'required|integer|min:1','temporal_version'=>'required|integer|min:1','draft_version'=>'required|integer|min:1','preparation_version'=>'required|integer|min:0']); + [$b,$prepared]=$this->draft($draft,$actor); $this->versions($b,$prepared,$d); + $p=$this->row((int)$d['profile_proposal_id'],$s); $t=$this->row((int)$d['temporal_proposal_id'],$s); + abort_unless((int)$p->version===(int)$d['profile_version'] && $t->kind==='temporal' && (int)$t->version===(int)$d['temporal_version'] && (int)$t->draft_id===$draft && (int)$t->profile_id===(int)$p->profile_id && $this->approved($t) && $this->latest($t,$s),409,'Decisão temporal exacta e aprovada necessária.'); + $td=$this->data($t); $this->versions($b,$prepared,$td); + $definition=$this->compatible($p,$s,$td['reference_date'],$b,$prepared); + $profile=json_decode(DB::table('service_calculation_profiles')->where('id',$p->profile_id)->value('definition'),true,32,JSON_THROW_ON_ERROR); + $result=ServiceCalculation::evaluateReal($b['data'],$prepared['data'],$profile); + abort_if($result['state']==='pending',422,implode(' ',$result['pending'])); + $pending=$td['event']==='pending'; + $result['temporal_state']=$pending ? 'temporal_pending' : 'temporal_decision_validated'; + $result['exigibility_date']=$pending ? null : $td['event_date']; + $result['assessment_period']=$pending ? null : substr($td['event_date'],0,7); + if ($pending) $result['pending'][]='Exigibilidade e período de apuramento não determinados; resultado condicionado.'; + $input=['draft_id'=>$draft,'draft_version'=>$b['version'],'preparation_version'=>$prepared['version'],'profile_proposal_id'=>$p->id,'profile_id'=>$p->profile_id,'profile_version'=>(int)$p->version,'profile_identifier'=>$p->identifier,'profile'=>$profile,'temporal_proposal_id'=>$t->id,'temporal_version'=>(int)$t->version,'temporal'=>$td,'data'=>$b['data'],'prepared'=>$prepared['data'],'policy'=>ServiceCalculation::POLICY]; + $hash=hash('sha256',json_encode($input,JSON_THROW_ON_ERROR)); + $old=DB::table('service_calculation_results')->where('billing_draft_id',$draft)->where('fingerprint',$hash)->first(); + if ($old) return ['id'=>$old->id,'replayed'=>true]; + $id=DB::table('service_calculation_results')->insertGetId($s+['billing_draft_id'=>$draft,'profile_id'=>$p->profile_id,'actor_id'=>$actor,'fingerprint'=>$hash,'snapshot'=>json_encode(['input'=>$input,'result'=>$result],JSON_THROW_ON_ERROR),'created_at'=>now()]); + Audit::record('service_fiscal.calculated',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['result_id'=>$id,'profile_proposal_id'=>$p->id,'temporal_proposal_id'=>$t->id]); + return ['id'=>$id,'replayed'=>false]; + }); + } + public function history(int $draft,int $actor): array + { + $s=$this->scope($actor); [$b,$prepared]=$this->draft($draft,$actor); $history=[]; + if (!\Illuminate\Support\Facades\Schema::hasTable('sf_proposals')) return []; + foreach(DB::table('service_calculation_results')->where($s)->where('billing_draft_id',$draft)->orderByDesc('id')->get() as $r) { + $snapshot=json_decode($r->snapshot,true,32,JSON_THROW_ON_ERROR); $in=$snapshot['input']; + if (!isset($in['profile_proposal_id'])) continue; + $current=true; + try { $this->versions($b,$prepared,$in); $p=$this->row($in['profile_proposal_id'],$s); $t=$this->row($in['temporal_proposal_id'],$s); abort_unless($this->approved($t) && $this->latest($t,$s),409); $this->compatible($p,$s,$in['temporal']['reference_date'],$b,$prepared); } + catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $current=false; } + $history[]=['id'=>$r->id,'current'=>$current,'snapshot'=>$snapshot]; + } + return $history; + } +} diff --git a/database/migrations/2026_10_26_050000_create_service_fiscal_lifecycle.php b/database/migrations/2026_10_26_050000_create_service_fiscal_lifecycle.php new file mode 100644 index 0000000..f017f4e --- /dev/null +++ b/database/migrations/2026_10_26_050000_create_service_fiscal_lifecycle.php @@ -0,0 +1,68 @@ +definition; + $expression=substr($existing,6); // retain every existing permission/scope branch + DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check'); + DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (($expression) OR (permission IN ('service_fiscal.view','service_fiscal.propose','service_fiscal.approve') AND unit_id IS NULL) OR (permission='service_fiscal.calculate' AND unit_id IS NOT NULL))"); + Schema::create('sf_proposals', function (Blueprint $t) { + $t->id(); $t->unsignedBigInteger('tenant_id'); $t->unsignedBigInteger('company_id'); $t->unsignedBigInteger('unit_id'); + $t->string('kind'); $t->string('identifier'); $t->unsignedInteger('version'); + $t->foreignId('profile_id')->constrained('service_calculation_profiles')->restrictOnDelete(); + $t->foreignId('draft_id')->nullable()->constrained('a_billing_drafts')->restrictOnDelete(); + $t->foreignId('previous_id')->nullable()->constrained('sf_proposals')->restrictOnDelete(); + $t->foreignId('actor_id')->constrained('users'); $t->jsonb('data'); $t->timestampTz('created_at'); + $t->unique(['unit_id','kind','identifier','version']); + $t->foreign(['tenant_id','company_id','unit_id'])->references(['tenant_id','company_id','id'])->on('units')->restrictOnDelete(); + }); + Schema::create('sf_decisions', function (Blueprint $t) { + $t->id(); $t->foreignId('proposal_id')->unique()->constrained('sf_proposals')->restrictOnDelete(); + $t->foreignId('actor_id')->constrained('users'); $t->string('decision'); $t->text('reason'); $t->timestampTz('created_at'); + }); + Schema::create('sf_activation_events', function (Blueprint $t) { + $t->id(); $t->foreignId('proposal_id')->constrained('sf_proposals')->restrictOnDelete(); + $t->foreignId('actor_id')->constrained('users'); $t->boolean('active'); $t->text('reason'); $t->timestampTz('created_at'); + }); + DB::unprepared(<<<'SQL' + CREATE OR REPLACE FUNCTION sgf_fiscal_proposal_guard() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF NEW.kind NOT IN ('profile','temporal') OR NOT EXISTS( + SELECT 1 FROM service_calculation_profiles p WHERE p.id=NEW.profile_id AND p.tenant_id=NEW.tenant_id AND p.company_id=NEW.company_id AND p.unit_id=NEW.unit_id + ) OR (NEW.kind='profile' AND NEW.draft_id IS NOT NULL) OR (NEW.kind='temporal' AND NEW.draft_id IS NULL) + OR (NEW.draft_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM a_billing_drafts b WHERE b.id=NEW.draft_id AND b.tenant_id=NEW.tenant_id AND b.company_id=NEW.company_id AND b.unit_id=NEW.unit_id)) + OR (NEW.previous_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM sf_proposals p WHERE p.id=NEW.previous_id AND p.tenant_id=NEW.tenant_id AND p.company_id=NEW.company_id AND p.unit_id=NEW.unit_id AND p.kind=NEW.kind AND p.identifier=NEW.identifier AND p.version=NEW.version-1)) + THEN RAISE EXCEPTION 'SGF_FISCAL_SCOPE'; END IF; + IF NEW.previous_id IS NULL AND NEW.version<>1 THEN RAISE EXCEPTION 'SGF_FISCAL_INITIAL_VERSION'; END IF; + IF NEW.kind='profile' AND NOT EXISTS(SELECT 1 FROM a_services s WHERE s.id=(NEW.data->>'service_id')::bigint AND s.tenant_id=NEW.tenant_id AND s.company_id=NEW.company_id) + THEN RAISE EXCEPTION 'SGF_FISCAL_SERVICE_SCOPE'; END IF; + RETURN NEW; + END; $$; + CREATE TRIGGER fiscal_proposal_scope BEFORE INSERT ON sf_proposals FOR EACH ROW EXECUTE FUNCTION sgf_fiscal_proposal_guard(); + CREATE OR REPLACE FUNCTION sgf_fiscal_decision_guard() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF NEW.decision NOT IN ('approved','refused') OR EXISTS(SELECT 1 FROM sf_proposals WHERE id=NEW.proposal_id AND actor_id=NEW.actor_id) + THEN RAISE EXCEPTION 'SGF_FISCAL_DECISION'; END IF; + RETURN NEW; + END; $$; + CREATE TRIGGER fiscal_decision_guard BEFORE INSERT ON sf_decisions FOR EACH ROW EXECUTE FUNCTION sgf_fiscal_decision_guard(); + CREATE OR REPLACE FUNCTION sgf_fiscal_activation_guard() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF NOT EXISTS(SELECT 1 FROM sf_proposals p JOIN sf_decisions d ON d.proposal_id=p.id AND d.decision='approved' WHERE p.id=NEW.proposal_id AND p.kind='profile') + THEN RAISE EXCEPTION 'SGF_FISCAL_ACTIVATION'; END IF; + RETURN NEW; + END; $$; + CREATE TRIGGER fiscal_activation_guard BEFORE INSERT ON sf_activation_events FOR EACH ROW EXECUTE FUNCTION sgf_fiscal_activation_guard(); + SQL); + foreach (['sf_proposals','sf_decisions','sf_activation_events'] as $table) { + DB::unprepared("CREATE TRIGGER {$table}_immutable BEFORE UPDATE OR DELETE ON {$table} FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()"); + } + } + public function down(): void { throw new RuntimeException('Fiscal history requires a reviewed preservation procedure.'); } +}; diff --git a/docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md b/docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md new file mode 100644 index 0000000..a874a15 --- /dev/null +++ b/docs/CONDITIONAL_CALCULATION_ACCEPTANCE.md @@ -0,0 +1,39 @@ +# Cálculo condicionado — entrega aceite + +26/10/2026, Africa/Johannesburg. O utilizador declarou: **«Aceito esta entrega do cálculo condicionado. Regista a aceitação e cria o checkpoint Git local, excluindo credenciais e dados locais. Exigibilidade e período de apuramento permanecem pendentes. Não avances para emissão documental.»** + +## Âmbito aceite e limites + +Perfil fiscal versionado, proposta e validação por identidades distintas, activação por operação local especificamente autorizada e cálculo exacto sobre uma linha de serviço com preço sem imposto. A versão, empresa, unidade, serviço, vigência, fontes e condições são verificadas. O cálculo não altera decisões fiscais validadas. A classificação contabilística não determina IVA. + +A decisão temporal aprovada pode manter o acontecimento pendente. Nesse caso o resultado permanece condicionado, com exigibilidade e período de apuramento nulos, sem presumir datas a partir do início do serviço ou da próxima facturação. O resultado é apresentado como **RASCUNHO — SEM VALIDADE FISCAL**. Não existe emissão, recebimento, apuramento líquido ou lançamento contabilístico neste incremento. + +O caso local foi conferido no navegador: base 7 850,00, IVA exacto/proposto 1 256,00, diferença 0,00 e total proposto 9 106,00 MZN, com perfil/versões identificados. Os mesmos montantes constam das provas sintéticas; este registo não exporta cliente, documentos, decisões, identificadores ou movimentos da base local. A repetição do cálculo conservou um único resultado. + +## Implementação e verificações + +- `ServiceFiscal` e controlador: propostas/decisões imutáveis, autorização por âmbito e comando de cálculo condicionado; integração na pré-visualização existente. +- Migração aditiva `2026_10_26_050000_create_service_fiscal_lifecycle.php`: três tabelas, relações e triggers, ampliação do CHECK de permissões preservando os direitos anteriores. Migração e cinco concessões iniciais foram confirmadas especificamente e aplicadas; comparação verificou preservação de 28 tabelas preexistentes. As provas JSON conservam apenas resultados agregados dessas fases, não o estado actual de activação. +- `LocalFiscalActivation` / `scripts/service-fiscal-activate.php`: operação de console, base local própria, manifesto exacto e hash da definição revista. Sem rota HTTP ou concessão permanente de activação. Repetição não duplica eventos. A activação local posteriormente autorizada foi executada e auditada; não é executada automaticamente pelo checkout ou migração. +- Interface com nomes em português, resumo de taxa/moeda/preço/vigência, estados distintos, condições e fontes acessíveis, fundamento da decisão e indicação permanente dos limites do rascunho. +- Regressões dirigidas do circuito inicial: **52 testes / 1 171 asserções** em `sgf_tempo_test`. Após acrescentar a activação local: **17 testes / 423 asserções**, filtro `ServiceFiscalTest|ServiceCalculationTest|DatabaseSafetyTest`. Compilação Vite passou. Sem novas dependências ou versões. +- Provas de concorrência: dois processos PHP sobrepostos, uma inserção e um replay, preservando o histórico; [registo sintético](proofs/service-fiscal-concurrency.json). [Imagem sintética](proofs/service-fiscal-browser.png). Os testes destrutivos e demonstrações sintéticas usam exclusivamente a instância isolada e a guarda de `sgf_tempo_test`. + +Não foram repetidos testes destrutivos para registar esta aceitação. As alterações posteriores às provas foram apresentação e correcção de uma mensagem obsoleta, sem mudança das fórmulas/autorizações. + +## Utilização e reprodução técnica + +Seleccionar empresa e unidade autorizadas → **Perfil fiscal do serviço** para consultar/propor/validar versões. Consultar **Rascunhos de facturação → registo autorizado → Pré-visualizar rascunho** para o resultado e pendências. Apenas o detentor da permissão unitária de cálculo pode executar o comando; a validação temporal requer direito próprio e identidade distinta de quem propôs. + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sgf.ps1 test --stop-on-failure '--filter=ServiceFiscalTest|ServiceCalculationTest|DatabaseSafetyTest' +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/sgf.ps1 npm run build +``` + +Os scripts `service-fiscal-fixture.php`, `service-fiscal-worker.php` e `service-fiscal-concurrency.php` são para provas sintéticas guardadas. Parar a instância de prova antes de testes que recriam a base de testes. Nunca usar reset/fresh em desenvolvimento. Os scripts de concessão/activação não são instruções para repetir as operações reais sem autorização específica. + +## Preservação do checkpoint + +Checkpoint exclusivamente local, sem push. Contém código, migração, testes, documentação de aceitação e provas sintéticas/agregadas. Ficam fora: `.env*` privados, credenciais, manifestos, fingerprints locais, bases, logs, imagens da operação real e a ficha nominativa `REAL_SERVICE_FISCAL_ASSESSMENT.md`, que permanece no posto local. A ficha contém o histórico detalhado do caso e este registo não a substitui. + +**Entrega encerrada no âmbito aceite.** Exigibilidade e período de apuramento permanecem pendentes. A emissão documental exige incremento separado; esta aceitação não o autoriza e não autoriza publicação. diff --git a/docs/proofs/service-fiscal-browser.png b/docs/proofs/service-fiscal-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..b53a008ffecda5bf4349f2157c62301b60fa68ff GIT binary patch literal 63765 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 literal 0 HcmV?d00001 diff --git a/docs/proofs/service-fiscal-concurrency.json b/docs/proofs/service-fiscal-concurrency.json new file mode 100644 index 0000000..a788ef5 --- /dev/null +++ b/docs/proofs/service-fiscal-concurrency.json @@ -0,0 +1,7 @@ +{ + "database": "sgf_tempo_test", + "overlapping_php_processes": 2, + "lock_overlap_observed": true, + "one_insert_one_replay": true, + "prior_history_preserved": true +} \ No newline at end of file diff --git a/docs/proofs/service-fiscal-dev-preservation-grants.json b/docs/proofs/service-fiscal-dev-preservation-grants.json new file mode 100644 index 0000000..c1a2e41 --- /dev/null +++ b/docs/proofs/service-fiscal-dev-preservation-grants.json @@ -0,0 +1,9 @@ +{ + "database": "sgf_tempo_dev", + "preexisting_tables_checked": 28, + "preexisting_records_unchanged": true, + "new_fiscal_tables_empty": true, + "real_profile_activated": false, + "confirmed_grants_verified": 5, + "prior_grants_and_audit_unchanged": true +} \ No newline at end of file diff --git a/docs/proofs/service-fiscal-dev-preservation.json b/docs/proofs/service-fiscal-dev-preservation.json new file mode 100644 index 0000000..379d3b0 --- /dev/null +++ b/docs/proofs/service-fiscal-dev-preservation.json @@ -0,0 +1,9 @@ +{ + "database": "sgf_tempo_dev", + "preexisting_tables_checked": 28, + "preexisting_records_unchanged": true, + "new_fiscal_tables_empty": true, + "real_profile_activated": false, + "confirmed_grants_verified": 0, + "prior_grants_and_audit_unchanged": true +} \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index 4e39b10..f536226 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -9,3 +9,4 @@ .accounting-proposal dl { display: grid; gap: .5rem; } .accounting-proposal dl > div { display: grid; grid-template-columns: minmax(8rem, 1fr) 2fr; gap: 1rem; } .accounting-proposal dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; } +.fiscal-page-heading h1{margin:8px 0 12px}.fiscal-page-heading>p:last-child{color:#5d716d;margin:0 0 24px}.fiscal-notice{display:flex;flex-direction:column;gap:7px;background:#fff7e7;border:1px solid #ead7ad;border-left:4px solid #c5983e;border-radius:8px;padding:16px 20px;margin-bottom:24px;color:#745723;font-size:.85rem;line-height:1.6}.fiscal-notice strong{font-size:.75rem;letter-spacing:.04em}.fiscal-list-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:26px 0 16px}.fiscal-list-heading h2{margin:0}.fiscal-list-heading>span{font-size:.8rem;color:#64796f}.fiscal-proposal{background:#fff;border:1px solid #dce6e1;border-radius:12px;padding:28px;margin-bottom:24px;box-shadow:0 3px 12px #123f3505}.fiscal-card-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:20px}.fiscal-card-heading h3{font-size:1.3rem;margin:8px 0;letter-spacing:-.02em}.fiscal-card-heading .eyebrow{margin-top:0}.fiscal-reference{color:#65786f;font-size:.78rem;margin:0;overflow-wrap:anywhere}.fiscal-statuses{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.fiscal-status{border-radius:20px;padding:6px 10px;white-space:nowrap;font-size:.72rem;font-weight:650;background:#edf2ef;color:#53665d}.fiscal-status.pending{background:#fff2d3;color:#80601c}.fiscal-status.approved{background:#e5f3e9;color:#286444}.fiscal-status.refused{background:#fbece8;color:#963f2d}.fiscal-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:24px 0;background:#f3f7f4;border:1px solid #e0e9e3;border-radius:8px;padding:18px;gap:20px}.fiscal-summary dt,.fiscal-facts dt{color:#667b70;font-size:.75rem;margin-bottom:8px}.fiscal-summary dd{margin:0;font-weight:650;font-size:.95rem;line-height:1.5}.fiscal-facts{display:grid;grid-template-columns:1fr 1fr;gap:18px 30px;margin:0 0 24px}.fiscal-facts dd{margin:0;line-height:1.5;font-size:.9rem}.fiscal-conditions{border-top:1px solid #e5ece7;padding-top:18px}.fiscal-conditions h4,.fiscal-review h4{font-size:.95rem;margin:0 0 10px}.fiscal-conditions p,.fiscal-evidence p,.fiscal-recorded p{font-size:.88rem;line-height:1.85;white-space:pre-wrap;overflow-wrap:anywhere;margin:0 0 18px;max-width:75ch}.fiscal-evidence{border-top:1px solid #e5ece7;padding:16px 0}.fiscal-evidence summary,.fiscal-new>summary{cursor:pointer;color:#28634f;font-weight:650;font-size:.9rem}.fiscal-evidence p{margin:15px 0 0;color:#5d716d}.fiscal-review{background:#f5f8f6;border-top:1px solid #dce6e1;margin:8px -28px -28px;padding:24px 28px;border-radius:0 0 12px 12px}.fiscal-review>p{font-size:.85rem;color:#5d716d;margin:0 0 18px}.fiscal-review label{max-width:700px}.fiscal-review textarea{resize:vertical;min-height:110px}.fiscal-refuse{color:#923f2c;border-color:#dbb7ac}.fiscal-recorded{padding-top:16px;border-top:1px solid #e5ece7}.fiscal-recorded>strong{font-size:.85rem}.fiscal-new form{margin-top:24px}.fiscal-new textarea{resize:vertical}.fiscal-proposal dd{overflow-wrap:anywhere}@media(max-width:1000px){.fiscal-summary{grid-template-columns:1fr 1fr}.fiscal-card-heading{flex-direction:column}.fiscal-statuses{justify-content:flex-start}}@media(max-width:560px){.fiscal-proposal{padding:20px}.fiscal-review{margin:8px -20px -20px;padding:20px}.fiscal-list-heading{display:block}.fiscal-list-heading>span{display:block;margin-top:8px}.fiscal-facts{grid-template-columns:1fr}.fiscal-summary{gap:16px;padding:14px}.fiscal-card-heading h3{font-size:1.12rem}.fiscal-review .actions button{flex:1}} diff --git a/resources/js/DocumentPreview.jsx b/resources/js/DocumentPreview.jsx index e1bc2c6..0b628cd 100644 --- a/resources/js/DocumentPreview.jsx +++ b/resources/js/DocumentPreview.jsx @@ -52,6 +52,7 @@ export default function DocumentPreview({ id, historical, context, api, onClose

Base aritmética exacta: {entry.snapshot.result.base_exact}

IVA exacto: {entry.snapshot.result.vat_exact}

IVA truncado proposto: {entry.snapshot.result.vat_document_proposed}

Diferença descartada: {entry.snapshot.result.discarded_difference}

Total documental proposto: {entry.snapshot.result.total_document_proposed ?? 'Pendente: representação da base/total'} {entry.snapshot.result.currency}

{entry.snapshot.result.pending.map((text,i)=>

{text}

)}

Sem validação fiscal. {notice}

)} } } + {!historical && view.fiscal_history?.length>0 &&

Cálculo proposto do serviço — condicionado

{view.fiscal_history.map(entry=>

Resultado #{entry.id} · {entry.current?'Actual para as versões validadas':'Desactualizado — histórico'}

Perfil {entry.snapshot.input.profile_identifier} v{entry.snapshot.input.profile_version} · decisão temporal v{entry.snapshot.input.temporal_version}

Base exacta: {entry.snapshot.result.base_exact} {entry.snapshot.result.currency}

IVA exacto: {entry.snapshot.result.vat_exact}

IVA truncado proposto: {entry.snapshot.result.vat_document_proposed}

Diferença preservada: {entry.snapshot.result.discarded_difference}

Total proposto: {entry.snapshot.result.total_document_proposed ?? 'Pendente'}

Exigibilidade: {entry.snapshot.result.exigibility_date ?? 'Não determinada'}

Período de apuramento: {entry.snapshot.result.assessment_period ?? 'Não determinado'}

{entry.snapshot.result.pending.map((p,i)=>

{p}

)}

{notice} · Não representa emissão ou apuramento líquido do imposto.

)}
} {!!view.payments.length &&

Instruções do emitente para facilitar o pagamento

Informação introduzida pelo utilizador, sem verificação bancária. Não regista recebimento nem inicia operação bancária.

{view.payments.map(payment =>

{payment.data.label} · versão {payment.version}

{Object.entries({ bank:'Banco', holder:'Titular', account_number:'Conta', nib:'NIB', iban:'IBAN', swift:'SWIFT', bank_code:'Código bancário', currency:'Moeda' }).filter(([key]) => payment.data[key]).map(([key,label]) =>

{label}: {payment.data[key]}

)}{payment.data.instructions &&

{payment.data.instructions}

}
)}
} {view.observations &&

Observações

{view.observations}

}

Dados em falta e validações pendentes

Preenchido não significa validado. Imposto e total definitivo continuam por apurar.

    {Object.entries(view.pending).map(([code,text]) =>
  • {text}
  • )}

{notice}

diff --git a/resources/js/ServiceFiscalWorkspace.jsx b/resources/js/ServiceFiscalWorkspace.jsx new file mode 100644 index 0000000..30eed97 --- /dev/null +++ b/resources/js/ServiceFiscalWorkspace.jsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; + +const fieldLabels = {currency:'Moeda',treatment:'Tratamento fiscal',price_mode:'Modo de preço',service_id:'Serviço',service_version:'Versão do serviço',valid_from:'Vigência desde',valid_until:'Vigência até',rate_percent:'Taxa de IVA',profile_proposal_id:'Proposta do perfil',draft_id:'Rascunho',draft_version:'Versão do rascunho',preparation_version:'Versão da preparação',reference_date:'Data de referência',reference_basis:'Fundamento da referência',period_start:'Início do período',period_end:'Fim do período',next_billing:'Próxima facturação prevista',event:'Acontecimento',event_date:'Data do acontecimento'}; +const valueLabels = {domestic_normal:'Operação interna · regime normal',added:'Preço sem IVA · imposto acrescido',included:'Imposto incluído',pending:'Por determinar',invoice_in_time:'Emissão dentro do prazo fundamentado',invoice_deadline:'Termo do prazo de emissão fundamentado'}; +function displayValue(key,value) { + if(value===null || value===undefined || value==='') return 'Não indicado'; + if(['valid_from','valid_until','reference_date','period_start','period_end','next_billing','event_date'].includes(key) && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value.split('-').reverse().join('/'); + if(key==='rate_percent') return `${value}%`; + if(['service_id','draft_id','profile_proposal_id'].includes(key)) return `#${value}`; + return valueLabels[value] || String(value); +} +function FiscalProposal({p,can,busy,reasons,setReasons,perform,api,load,listing,context}) { + const d=p.definition, status=p.decision?.decision; + const stateLabel=status==='approved'?'Validada':status==='refused'?'Recusada':'Aguarda validação'; + const summary=p.kind==='profile'?['rate_percent','currency','price_mode','valid_from']:['period_start','period_end','event','event_date']; + return
+

{p.kind==='profile'?'PERFIL FISCAL':'DECISÃO TEMPORAL'} · PROPOSTA #{p.id}

{p.kind==='profile'?`Enquadramento de IVA · serviço #${d.service_id}`:`Período e exigibilidade · rascunho #${d.draft_id}`}

{p.identifier} · Versão {p.version}

{stateLabel}{p.kind==='profile' && {p.active?'Activo para cálculo proposto':'Sem activação'}}
+
{summary.map(key=>
{fieldLabels[key]}
{displayValue(key,d[key])}
)}
+
{Object.entries(d).filter(([key])=>!summary.includes(key) && !['evidence','conditions','reference_basis'].includes(key)).map(([key,value])=>
{fieldLabels[key] || key}
{displayValue(key,value)}
)}
+ {d.conditions &&

Condições de aplicação

{d.conditions}

} + {d.reference_basis &&

Fundamento da data de referência

{d.reference_basis}

} + {d.evidence &&
Fontes e elementos de suporte

{d.evidence}

} + {p.decision &&
Fundamento da decisão

{p.decision.reason}

} + {!p.decision && can('service_fiscal.approve') &&

Validar esta versão

A decisão refere-se à versão {p.version}, na unidade seleccionada. Validar não activa o perfil nem emite uma factura.