feat(iva): determine public authority compensation tax bases
This commit is contained in:
@@ -6,6 +6,7 @@ import { type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
|
||||
import { SimulationModeloA } from './simulation-modelo-a';
|
||||
import { SimulationAcquisitions } from './simulation-acquisitions';
|
||||
import { SimulationImportExemptions } from './simulation-import-exemptions';
|
||||
import { SimulationPublicAuthorityBase } from './simulation-public-authority-base';
|
||||
import { SimulationOfferPreparations } from './simulation-offer-preparations';
|
||||
import { SimulationOperations } from './simulation-operations';
|
||||
|
||||
@@ -90,6 +91,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationOperations draft={draft} onChange={onChange} />
|
||||
<SimulationAcquisitions draft={draft} onChange={onChange} />
|
||||
<SimulationImportExemptions draft={draft} onChange={onChange} />
|
||||
<SimulationPublicAuthorityBase draft={draft} onChange={onChange} />
|
||||
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
||||
<SimulationModeloA draft={draft} />
|
||||
</section>;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {useState} from 'react';
|
||||
import {newPublicBase,publicBaseBook,publicBaseConditions,publicBaseCurrent,publicBaseReportRows,publicBaseSource,publicBaseTreatments,recordPublicBases,savePublicBaseBook,type PublicBaseBook,type PublicBaseRow} from '@/lib/iva-public-authority-base';
|
||||
|
||||
export function SimulationPublicAuthorityBase({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:PublicBaseBook;
|
||||
try{book=publicBaseBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:PublicBaseBook)=>{try{onChange('modelo_a_snapshot',savePublicBaseBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
const change=(id:string,patch:Partial<PublicBaseRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
|
||||
return<details className="space-y-3 rounded border p-3">
|
||||
<summary>Base tributável — actos de autoridades públicas</summary>
|
||||
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Prepare apenas as operações relevantes. A base do artigo15(2)(d) é a indemnização ou outra compensação ligada à transmissão de bens ou prestação de serviços resultante do acto público. Um pagamento ou a natureza pública da entidade não determinam incidência nem taxa de IVA.</p>
|
||||
<p>A indemnização judicial por incumprimento de contrato tem exclusão própria no artigo15(6)(a); não é incluída por analogia. Indique as componentes sem IVA e o fundamento da composição. Na compensação em espécie ou mista, identifique o valor normal conforme15(3)/(4). Valores em falta permanecem indeterminados; não se convertem em zero.</p>
|
||||
<p>{publicBaseSource}</p>
|
||||
{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newPublicBase(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar operação de acto público</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Operação por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Tratamento da compensação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value})}><option value="">Por indicar</option>{publicBaseTreatments.map(item=><option key={item.id} value={item.id}>{item.label}</option>)}</select></label>
|
||||
<label>Natureza da operação<select className="block rounded border p-2" value={row.operationNature} onChange={e=>change(row.id,{operationNature:e.target.value as PublicBaseRow['operationNature']})}><option value="">Por indicar</option><option value="goods">Transmissão de bens</option><option value="services">Prestação de serviços</option></select></label>
|
||||
<label>Natureza dos dados<select className="block rounded border p-2" value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as PublicBaseRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Operação identificada'],['authority','Autoridade pública'],['actRef','Acto da autoridade / referência da previsão'],['documentRef','Documento da compensação / referência da previsão'],['documentDate','Data do documento / prevista'],['period','Mês da operação'],['currency','Moeda de origem'],['compositionRef','Referência da composição sem IVA — inclusões e exclusões aplicáveis'],['evidence','Fundamento factual / pressupostos']] as const).map(([field,label])=><label key={field}>{label}<input className="block rounded border p-2" type={field==='documentDate'?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
<label>Forma da compensação<select className="block rounded border p-2" value={row.compensationForm} onChange={e=>change(row.id,{compensationForm:e.target.value as PublicBaseRow['compensationForm']})}><option value="">Por indicar</option><option value="money">Em dinheiro</option><option value="non_money">Em espécie</option><option value="mixed">Mista — dinheiro e espécie</option></select></label>
|
||||
{['money','mixed'].includes(row.compensationForm)&&<label>Componente monetária sem IVA (moeda indicada)<input className="block rounded border p-2" value={row.moneyAmount} onChange={e=>change(row.id,{moneyAmount:e.target.value})}/></label>}
|
||||
{['non_money','mixed'].includes(row.compensationForm)&&<><label>Valor normal não monetário sem IVA (moeda indicada)<input className="block rounded border p-2" value={row.nonMoneyValue} onChange={e=>change(row.id,{nonMoneyValue:e.target.value})}/></label><label>Referência da valorização / pressuposto do valor normal<input className="block rounded border p-2" value={row.valuationRef} onChange={e=>change(row.id,{valuationRef:e.target.value})}/></label></>}
|
||||
</div>
|
||||
<p>Ao mudar de tratamento ou forma, os valores e referências anteriores conservam-se; só entram no cálculo as componentes aplicáveis à forma escolhida.</p>
|
||||
<fieldset><legend>Condições próprias desta base</legend>{publicBaseConditions.map(([key,label])=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={row.conditions[key]??''} onChange={e=>change(row.id,{conditions:{...row.conditions,[key]:e.target.value}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>
|
||||
</details>)}
|
||||
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordPublicBases(draft,book));}catch(cause){setError(String(cause));}}}>Determinar bases e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{publicBaseCurrent(draft,book)?'Bases correspondentes às entradas guardadas.':'Entradas alteradas; bases históricas conservadas. Reveja e determine novamente.'} {book.history?.length??0} versões anteriores. IVA e campo11 não são preenchidos por estas bases.</p><dl>{publicBaseReportRows(draft).map(([,label,value,origin],index)=><div key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
|
||||
</details>;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||||
|
||||
export const publicBaseVersion='public-authority-base/1';
|
||||
export const publicBaseSource='CIVA republicado Lei13/2016,15(2)(d),PDF13/p1426(271): base das transmissões de bens e prestações de serviços resultantes de actos de autoridades públicas é a indemnização ou qualquer outra compensação. Art15(3)/(4),PDF14/p1426(272): componente não monetária pelo valor normal;15(5)/(6),composição/exclusões,incluindo exclusão do próprio IVA e indemnização judicial por incumprimento de contratos. Lei22/2022PDF3 mantém15(2)(d)/(3–6)por remissão;Lei10/2025não altera15. Recorte2026/MZN,determinação da base;sem inferir incidência,taxa,isenção,IVA,dedução/campo11 ou totaisModeloA.';
|
||||
export const publicBaseTreatments=[
|
||||
{id:'public_authority_indemnity',label:'Indemnização por acto de autoridade pública'},
|
||||
{id:'public_authority_compensation',label:'Outra compensação por acto de autoridade pública'},
|
||||
] as const;
|
||||
export const publicBaseConditions=[
|
||||
['taxableDomesticSupply','Está identificada uma transmissão de bens ou prestação de serviços sujeita a IVA no território nacional; não basta existir um pagamento ou uma entidade pública'],
|
||||
['authorityAct','A transmissão ou prestação resulta do acto da autoridade pública identificado'],
|
||||
['compensationLink','O montante e as componentes indicadas correspondem à indemnização ou compensação desta operação'],
|
||||
['notJudicialContractBreach','Não se trata de quantia recebida a título de indemnização declarada judicialmente por incumprimento total ou parcial de contrato, excluída pelo artigo15(6)(a)'],
|
||||
['netComposition','Os valores indicados excluem o próprio IVA e reflectem a composição aplicável dos números5/6 do artigo15, sem omissões nem duplicações; a referência explica a composição'],
|
||||
] as const;
|
||||
type Condition=typeof publicBaseConditions[number][0];
|
||||
type Choice=''|'yes'|'no';
|
||||
export type PublicBaseRow={id:string;treatment:string;description:string;operationNature:''|'goods'|'services';period:string;origin:'projected'|'effective';currency:string;authority:string;actRef:string;documentRef:string;documentDate:string;compensationForm:''|'money'|'non_money'|'mixed';moneyAmount:string;nonMoneyValue:string;valuationRef:string;compositionRef:string;evidence:string;conditions:Partial<Record<Condition,Choice>>};
|
||||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
|
||||
export type PublicBaseCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:PublicBaseRow[];rows:LineResult[]};
|
||||
export type PublicBaseBook={rows:PublicBaseRow[];result?:PublicBaseCalculation;history?:PublicBaseCalculation[]};
|
||||
type Store=ReturnType<typeof readAcquisitionStore>&{publicAuthorityBases?:Record<string,PublicBaseBook>};
|
||||
export function newPublicBase(id:string,currency:string):PublicBaseRow{return{id,treatment:'',description:'',operationNature:'',period:'',origin:'projected',currency,authority:'',actRef:'',documentRef:'',documentDate:'',compensationForm:'',moneyAmount:'',nonMoneyValue:'',valuationRef:'',compositionRef:'',evidence:'',conditions:{}};}
|
||||
function readStore(draft:Record<string,unknown>):Store{
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.publicAuthorityBases!==undefined&&(!store.publicAuthorityBases||typeof store.publicAuthorityBases!=='object'||Array.isArray(store.publicAuthorityBases)))throw new Error('Bases de actos públicos não recuperáveis; originais preservados.');
|
||||
const fields=Object.keys(newPublicBase('','')).filter(key=>key!=='conditions');
|
||||
for(const book of Object.values(store.publicAuthorityBases??{})){
|
||||
if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof PublicBaseRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de base tributável não recuperáveis; originais preservados,sem substituição por lista vazia.');
|
||||
}
|
||||
return store;
|
||||
}
|
||||
export function publicBaseBook(draft:Record<string,unknown>):PublicBaseBook{return readStore(draft).publicAuthorityBases?.[acquisitionScope(draft)]??{rows:[]};}
|
||||
export function savePublicBaseBook(draft:Record<string,unknown>,book:PublicBaseBook):string{
|
||||
const current=readStore(draft),encoded=JSON.stringify({...current,publicAuthorityBases:{...current.publicAuthorityBases,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;
|
||||
}
|
||||
function decimal(raw:unknown):bigint|null{if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[whole,fraction='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(fraction.padEnd(6,'0'));}
|
||||
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
|
||||
function validDate(value:string):boolean{const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
|
||||
export function evaluatePublicBase(row:PublicBaseRow,currency:unknown):LineResult{
|
||||
const result:LineResult={id:row.id,status:'indeterminate',baseAmount:null,vat:null,rate:null,reason:''};
|
||||
const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||||
if(!publicBaseTreatments.some(item=>item.id===row.treatment))return stop('Escolha indemnização ou outra compensação por acto de autoridade pública; um pagamento genérico não determina esta base.');
|
||||
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbito MZN ou divergente do projecto; originais conservados,sem conversão.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin))return stop('Indique o período em2026 e dados efectivos declarados ou pressupostos de projecção; não são inferidos do orçamento.');
|
||||
const failed=publicBaseConditions.filter(([key])=>row.conditions[key]==='no');
|
||||
if(failed.length)return stop('Não se aplica esta determinação de base: '+failed.map(([,label])=>label).join('; ')+'. Não é isenção nem IVA zero; conserve o enquadramento próprio da operação.','not_applicable');
|
||||
const missing=publicBaseConditions.filter(([key])=>row.conditions[key]!=='yes');
|
||||
if(missing.length)return stop('Condições por indicar: '+missing.map(([,label])=>label).join('; ')+'. Desconhecido não significa não aplicável ou zero.');
|
||||
if(!['goods','services'].includes(row.operationNature)||!row.description.trim()||!row.authority.trim()||!row.actRef.trim()||!row.documentRef.trim()||!row.compositionRef.trim()||!row.evidence.trim()||!validDate(row.documentDate))return stop('Identifique a operação,natureza,autoridade,acto,documento/data e fundamento da composição,ou pressupostos expressos; reutilize referências existentes,sem novo certificado.');
|
||||
if(!['money','non_money','mixed'].includes(row.compensationForm))return stop('Indique se a compensação é em dinheiro,em espécie ou mista; ausência de resposta não elimina uma componente.');
|
||||
const cash=row.compensationForm==='non_money'?0n:decimal(row.moneyAmount),kind=row.compensationForm==='money'?0n:decimal(row.nonMoneyValue);
|
||||
if(cash===null||kind===null)return stop('Indique os valores sem IVA das componentes aplicáveis; montante em falta ou inválido não é zero.');
|
||||
if(row.compensationForm!=='money'&&!row.valuationRef.trim())return stop('Identifique o fundamento do valor normal da componente não monetária,conforme15(3)/(4),ou o pressuposto expresso; não é avaliado pelo portal.');
|
||||
const base=money(cash+kind);
|
||||
return{...result,status:'determined',baseAmount:base,reason:`Base15(2)(d): componente monetária ${money(cash)} + valor normal não monetário ${money(kind)} = ${base}MZN,${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. Componentes sem IVA,não duplicadas,composição15(5)/(6) identificada. Base não é imposto; incidência/taxa/IVA/dedução/campo11 não são determinados por este cálculo.`};
|
||||
}
|
||||
function key(draft:Record<string,unknown>,book:PublicBaseBook):string{return JSON.stringify({version:publicBaseVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||||
function natureLabel(value:string):string{return value==='goods'?'Transmissão de bens':value==='services'?'Prestação de serviços':'Por indicar';}
|
||||
function formLabel(value:string):string{return value==='money'?'Em dinheiro':value==='non_money'?'Em espécie':value==='mixed'?'Mista — dinheiro e espécie':'Por indicar';}
|
||||
export function publicBaseCurrent(draft:Record<string,unknown>,book:PublicBaseBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||||
export function recordPublicBases(draft:Record<string,unknown>,book:PublicBaseBook):PublicBaseBook{
|
||||
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos; originais conservados.');
|
||||
const result:PublicBaseCalculation={version:publicBaseVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:publicBaseSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluatePublicBase(row,draft.currency))};
|
||||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};savePublicBaseBook(draft,next);return next;
|
||||
}
|
||||
export function publicBaseReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||||
try{const book=publicBaseBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base P090 — ${row.period} — ${row.id}`,`${row.description}; base tributável: ${result.rows[index].baseAmount??'Indeterminada'} ${row.currency}; IVA: Indeterminado; ${result.rows[index].reason}`,`${result.opportunity}; ${result.location}; ${publicBaseCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'}; versão${result.revision}; ${result.calculatedAt}; ${result.source}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${publicBaseTreatments.find(item=>item.id===row.treatment)?.label??row.treatment}; natureza ${natureLabel(row.operationNature)}; autoridade ${row.authority}; acto ${row.actRef}; documento ${row.documentRef},${row.documentDate}; forma ${formLabel(row.compensationForm)}; ${row.compensationForm!=='non_money'?`dinheiro ${row.moneyAmount} ${row.currency}; `:''}${row.compensationForm!=='money'?`valor normal não monetário ${row.nonMoneyValue} ${row.currency},fundamento ${row.valuationRef}; `:''}composição ${row.compositionRef}; condições ${publicBaseConditions.map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')}; fundamento ${row.evidence}; não preenche totaisModeloA ou campo11.`]);}
|
||||
catch{return[['IVA mensal','Base de actos públicos','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSet
|
||||
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||||
import { acquisitionReportRows } from './iva-acquisitions.ts';
|
||||
import { importExemptionReportRows } from './iva-import-exemptions.ts';
|
||||
import { publicBaseReportRows } from './iva-public-authority-base.ts';
|
||||
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
|
||||
import { salesReportRows } from './iva-operations.ts';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
@@ -620,6 +621,7 @@ export function buildProjectDossierRows(
|
||||
}
|
||||
rows.push(...acquisitionReportRows(draft));
|
||||
rows.push(...importExemptionReportRows(draft));
|
||||
rows.push(...publicBaseReportRows(draft));
|
||||
rows.push(...offerPreparationReportRows(draft));
|
||||
const operationRows = salesReportRows(draft);
|
||||
rows.push(...operationRows);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IvaPublicAuthorityBaseDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_public_act_base_recovers_privately_without_changing_delivered_fiscal_books(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
|
||||
$draft = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g7a-bases.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
|
||||
$this->assertSame('opportunity:339', $recovered['context_opportunity']);
|
||||
$this->assertSame('Manica', $recovered['selection_labels']['province_id']);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['publicAuthorityBases']['opportunity:339'];
|
||||
$this->assertCount(2, $book['rows']);
|
||||
$this->assertSame(['public_authority_indemnity','public_authority_compensation'], array_column($book['rows'], 'treatment'));
|
||||
$this->assertSame(['1500.25','1250.30'], array_column($book['result']['rows'], 'baseAmount'));
|
||||
$this->assertSame(['determined','determined'], array_column($book['result']['rows'], 'status'));
|
||||
$this->assertSame([null,null], array_column($book['result']['rows'], 'vat'));
|
||||
$this->assertSame([null,null], array_column($book['result']['rows'], 'rate'));
|
||||
$this->assertSame('2026-12', $book['rows'][0]['period']);
|
||||
$this->assertSame('SYNTHETIC QA acto A,operação própria', $book['rows'][0]['actRef']);
|
||||
$this->assertSame('mixed', $book['rows'][1]['compensationForm']);
|
||||
$this->assertSame('1000.10', $book['rows'][1]['moneyAmount']);
|
||||
$this->assertSame('250.20', $book['rows'][1]['nonMoneyValue']);
|
||||
$this->assertStringContainsString('valor normal', $book['rows'][1]['valuationRef']);
|
||||
$this->assertStringContainsString('composição15(5)/(6)', $book['rows'][1]['compositionRef']);
|
||||
$this->assertStringContainsString('15(2)(d)', $book['result']['source']);
|
||||
$this->assertCount(2, $book['history']);
|
||||
$this->assertNull($book['history'][0]['rows'][0]['baseAmount']);
|
||||
$this->assertNull($book['history'][1]['rows'][0]['vat']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g7a-preserved-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$this->assertSame($prior, array_diff_key($store, ['publicAuthorityBases' => true]));
|
||||
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
|
||||
$this->assertStringContainsString('Plano de importações', $store['importExemptions']['opportunity:338']['rows'][1]['developmentPlanRef']);
|
||||
$this->assertSame('0.00', $store['importExemptions']['opportunity:336']['result']['rows'][0]['serviceVat']);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
|
||||
file_put_contents(base_path('outputs/iva-l1-g7a-public-base-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {newPublicBase,publicBaseTreatments,publicBaseConditions,evaluatePublicBase,recordPublicBases,savePublicBaseBook,publicBaseBook,publicBaseCurrent,publicBaseReportRows} from '../../resources/js/lib/iva-public-authority-base.ts';
|
||||
import {importExemptionCurrent} from '../../resources/js/lib/iva-import-exemptions.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g7a-preserved-inputs.json',import.meta.url),'utf8'));
|
||||
const row=(treatment=publicBaseTreatments[0].id)=>({...newPublicBase('g7a-'+treatment,'MZN'),treatment,description:'SYNTHETIC QA compensação por acto público',operationNature:treatment==='public_authority_indemnity'?'goods':'services',period:'2026-12',authority:'SYNTHETIC QA autoridade pública',actRef:'SYNTHETIC QA acto A,operação própria',documentRef:'SYNTHETIC QA G7A '+treatment,documentDate:'2026-11-14',compensationForm:treatment==='public_authority_indemnity'?'money':'mixed',moneyAmount:treatment==='public_authority_indemnity'?'1500.25':'1000.10',nonMoneyValue:'250.20',valuationRef:'SYNTHETIC QA valor normal nos termos15(4),pressuposto explícito',compositionRef:'SYNTHETIC QA documento A,composição15(5)/(6),semIVA e sem duplicações',evidence:'SYNTHETIC QA pressupostos;operação sujeita e ligada ao acto,não incumprimento contratual judicial',conditions:Object.fromEntries(publicBaseConditions.map(([key])=>[key,'yes']))});
|
||||
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:339',project_title:'SYNTHETIC QA — G7A / Base de actos públicos'});
|
||||
|
||||
test('P090 bases distinguish indemnity/other compensation from incidence,tax and judicial contract-breach exclusion',()=>{
|
||||
for(const treatment of publicBaseTreatments){const input=row(treatment.id),ok=evaluatePublicBase(input,'MZN');assert.equal(ok.status,'determined');assert.equal(ok.baseAmount,treatment.id==='public_authority_indemnity'?'1500.25':'1250.30');assert.equal(ok.vat,null);assert.equal(ok.rate,null);
|
||||
for(const [key] of publicBaseConditions)for(const value of ['','unknown','no']){const r=evaluatePublicBase({...input,conditions:{...input.conditions,[key]:value}},'MZN');assert.equal(r.status,value==='no'?'not_applicable':'indeterminate');assert.equal(r.baseAmount,null);assert.equal(r.vat,null);}
|
||||
for(const patch of [{authority:''},{actRef:''},{compositionRef:''},{operationNature:''},{documentDate:'2026-02-30'},{documentRef:''},{period:'2025-12'},{currency:'USD'}])assert.equal(evaluatePublicBase({...input,...patch},'MZN').baseAmount,null);
|
||||
}
|
||||
assert.match(evaluatePublicBase({...row(),conditions:{...row().conditions,notJudicialContractBreach:'no'}},'MZN').reason,/15\(6\)\(a\)/);
|
||||
});
|
||||
|
||||
test('money,in-kind,mixed compute exact net components;unknown differs from explicit zero and hidden values are retained',()=>{
|
||||
const input=row();
|
||||
for(const [patch,expected] of [[{compensationForm:'money',moneyAmount:'0'},'0.00'],[{compensationForm:'non_money',moneyAmount:'',nonMoneyValue:'250.20'},'250.20'],[{compensationForm:'mixed',moneyAmount:'0.000001',nonMoneyValue:'0.000002'},'0.000003'],[{compensationForm:'mixed',moneyAmount:'999999999999999.999999',nonMoneyValue:'999999999999999.999999'},'1999999999999999.999998']]){const r=evaluatePublicBase({...input,...patch},'MZN');assert.equal(r.baseAmount,expected);assert.equal(r.vat,null);}
|
||||
for(const patch of [{compensationForm:''},{moneyAmount:''},{moneyAmount:'-1'},{moneyAmount:'NaN'},{moneyAmount:'1.0000001'},{compensationForm:'mixed',nonMoneyValue:''},{compensationForm:'non_money',valuationRef:''}])assert.equal(evaluatePublicBase({...input,...patch},'MZN').baseAmount,null);
|
||||
const switched={...input,compensationForm:'non_money'};assert.equal(switched.moneyAmount,'1500.25');assert.equal(evaluatePublicBase(switched,'MZN').baseAmount,'250.20');assert.equal(evaluatePublicBase({...switched,compensationForm:'mixed'},'MZN').baseAmount,'1750.45');
|
||||
});
|
||||
|
||||
test('save/recovery/report isolate P090 from78delivered branches and partial offers,while keeping exact versions and contextual invalidation',()=>{
|
||||
const d=draft(),prior=JSON.parse(d.modelo_a_snapshot);assert.deepEqual(publicBaseBook(d),{rows:[]});assert.deepEqual(publicBaseReportRows(d),[]);
|
||||
let book=recordPublicBases(d,{rows:publicBaseTreatments.map(rule=>({...row(rule.id),conditions:{}}))});book=recordPublicBases(d,{...book,rows:publicBaseTreatments.map(rule=>({...row(rule.id),conditions:{...row(rule.id).conditions,notJudicialContractBreach:'no'}}))});book=recordPublicBases(d,{...book,rows:publicBaseTreatments.map(rule=>row(rule.id))});assert.equal(book.history.length,2);assert.ok(book.history.every(h=>h.rows.every(r=>r.baseAmount===null&&r.vat===null)));assert.ok(publicBaseCurrent(d,book));
|
||||
for(const field of ['actRef','documentRef','compositionRef','compensationForm','valuationRef','moneyAmount'])assert.ok(!publicBaseCurrent(d,{...book,rows:book.rows.map(r=>({...r,[field]:'changed'}))}));for(const patch of [{context_opportunity:'opportunity:340'},{currency:'USD'},{selection_labels:{province_id:'Maputo'}}])assert.ok(!publicBaseCurrent({...d,...patch},book));
|
||||
d.modelo_a_snapshot=savePublicBaseBook(d,book);assert.deepEqual(publicBaseBook(JSON.parse(JSON.stringify(d))),book);const next=JSON.parse(d.modelo_a_snapshot);const{publicAuthorityBases,...untouched}=next;assert.deepEqual(untouched,prior);for(const[scope,previous]of Object.entries(prior.importExemptions))assert.ok(importExemptionCurrent({...d,context_opportunity:scope},previous));
|
||||
assert.equal(next.importExemptions['opportunity:338'].result.rows[1].importVat,'400.00');assert.equal(next.importExemptions['opportunity:336'].result.rows[0].serviceVat,'0.00');assert.equal(next.books['opportunity:329'].result.periods[0].fields['11'],null);
|
||||
const report=JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA G7A'}));for(const token of ['Base P090','1500.25','1250.30','IVA: Indeterminado','opportunity:339','Manica','2026-12','acto A','15(2)(d)','composição15(5)/(6)','campo11'])assert.ok(report.includes(token),token);assert.deepEqual(publicBaseReportRows({...d,context_opportunity:'opportunity:340'}),[]);
|
||||
if(process.env.IVA_G7A_CALCULATED)fs.writeFileSync(process.env.IVA_G7A_CALCULATED,JSON.stringify(d));
|
||||
});
|
||||
|
||||
test('malformed public-base rows/store cannot replace originals;duplicate identifiers cannot create calculations',()=>{
|
||||
const d=draft(),original=d.modelo_a_snapshot;
|
||||
for(const patch of [{moneyAmount:4},{conditions:[]},{valuationRef:null}])assert.throws(()=>savePublicBaseBook(d,{rows:[{...row(),...patch}]}),/originais preservados/);
|
||||
assert.equal(d.modelo_a_snapshot,original);assert.throws(()=>recordPublicBases(d,{rows:[row(),row()]}),/distintos/);assert.throws(()=>recordPublicBases(d,{rows:[]}),/distintos/);
|
||||
const bad={...d,modelo_a_snapshot:JSON.stringify({...JSON.parse(original),publicAuthorityBases:[]})};assert.throws(()=>publicBaseBook(bad),/originais preservados/);assert.equal(d.modelo_a_snapshot,original);
|
||||
});
|
||||
Reference in New Issue
Block a user