feat(iva): prepare P095 regulated fuel base excluding TC in QA
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import {useState} from 'react';
|
||||
import {newFuelBase,switchFuelTaxTreatment,fuelBaseBook,fuelBaseConditions,fuelBaseCurrent,fuelBaseReportRows,fuelBaseSource,fuelBaseTreatments,recordFuelBases,saveFuelBaseBook,type FuelBaseBook,type FuelBaseRow} from '@/lib/iva-fuel-base';
|
||||
|
||||
export function SimulationFuelBase({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:FuelBaseBook;
|
||||
try{book=fuelBaseBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:FuelBaseBook)=>{try{onChange('modelo_a_snapshot',saveFuelBaseBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
const change=(id:string,patch:Partial<FuelBaseRow>)=>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 — combustíveis com preço fixado</summary>
|
||||
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Prepare transmissões por revendedores de combustíveis cujo preço é fixado pela Autoridade Pública. Identifique a referência e o âmbito temporal do preço aplicável;na projecção,pode declarar pressupostos expressos. Não se presume um preço actual para outro período.</p>
|
||||
<p>A base exclui a Taxa sobre Combustíveis (TC). Se o montante já a exclui,não se subtrai novamente. Indique valores sem o próprio IVA e com a composição aplicável identificada. Base e IVA são resultados distintos.</p>
|
||||
<p>{fuelBaseSource}</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newFuelBase(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar transmissão de combustível</button>
|
||||
{book.rows.map(row=>{const rule=fuelBaseTreatments.find(item=>item.id===row.treatment);return<details key={row.id} className="rounded border p-3"><summary>{row.description||'Transmissão por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Composição quanto à TC<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,switchFuelTaxTreatment(row,e.target.value))}><option value="">Por indicar</option>{fuelBaseTreatments.map(item=><option key={item.id} value={item.id}>{item.label}</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 FuelBaseRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Combustível e transmissão identificados'],['operationDate','Data da transmissão / prevista'],['period','Mês da transmissão'],['currency','Moeda de origem'],['priceRef','Referência do preço fixado / pressuposto'],['priceScope','Âmbito do preço: combustível, operação, local e condições'],['priceFrom','Preço aplicável desde / pressuposto'],['priceTo','Preço aplicável até / pressuposto (vazio se sem termo fixado)'],['documentRef','Documento da transmissão / referência da previsão'],['documentDate','Data do documento / prevista'],['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={['operationDate','priceFrom','priceTo','documentDate'].includes(field)?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
{rule&&<label>{row.treatment==='fuel_tax_included'?'Contraprestação sem IVA,com TC incluída (moeda indicada)':'Contraprestação sem IVA,já sem TC (moeda indicada)'}<input className="block rounded border p-2" value={row.consideration} onChange={e=>change(row.id,{consideration:e.target.value})}/></label>}
|
||||
{row.treatment==='fuel_tax_included'&&<><label>TC incluída nesta contraprestação (moeda indicada)<input className="block rounded border p-2" value={row.fuelTax} onChange={e=>change(row.id,{fuelTax:e.target.value})}/></label><label>Referência da TC incluída / pressuposto<input className="block rounded border p-2" value={row.fuelTaxRef} onChange={e=>change(row.id,{fuelTaxRef:e.target.value})}/></label></>}
|
||||
</div>
|
||||
<p>Mudar o tratamento conserva os montantes e documentos,mas exige rever se a TC ainda está incluída. Os valores ocultos e as versões anteriores mantêm-se.</p>
|
||||
{rule?<fieldset><legend>Condições próprias desta transmissão</legend>{fuelBaseConditions.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>:<p>Escolha a composição para apresentar os montantes aplicáveis;os valores anteriores conservam-se.</p>}
|
||||
</details>;})}
|
||||
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordFuelBases(draft,book));}catch(cause){setError(String(cause));}}}>Determinar bases de combustíveis e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{fuelBaseCurrent(draft,book)?'Bases de combustíveis correspondentes às entradas guardadas.':'Entradas alteradas;bases de combustíveis 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>{fuelBaseReportRows(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>;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { SimulationImportExemptions } from './simulation-import-exemptions';
|
||||
import { SimulationPublicAuthorityBase } from './simulation-public-authority-base';
|
||||
import { SimulationCommissionBase } from './simulation-commission-base';
|
||||
import { SimulationActSaleBase } from './simulation-act-sale-base';
|
||||
import { SimulationFuelBase } from './simulation-fuel-base';
|
||||
import { SimulationOfferPreparations } from './simulation-offer-preparations';
|
||||
import { SimulationOperations } from './simulation-operations';
|
||||
|
||||
@@ -96,6 +97,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationPublicAuthorityBase draft={draft} onChange={onChange} />
|
||||
<SimulationCommissionBase draft={draft} onChange={onChange} />
|
||||
<SimulationActSaleBase draft={draft} onChange={onChange} />
|
||||
<SimulationFuelBase draft={draft} onChange={onChange} />
|
||||
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
||||
<SimulationModeloA draft={draft} />
|
||||
</section>;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||||
|
||||
export const fuelBaseVersion='regulated-fuel-base/1';
|
||||
export const fuelBaseSource='CIVA republicado Lei13/2016,15(2)(i),PDF13/p1426(271): nas transmissões de combustíveis cujo preço é fixado por Autoridade Pública,efectuadas por revendedores,o valor da contraprestação determinado nos termos dos números1 e5 não inclui a Taxa sobre os Combustíveis. Art15(5)/(6),PDF14/p1426(272),composição/exclusões,sem próprioIVA. Lei22/2022PDF3mantém15(2)(i)/(1)/(5)/(6)por remissão;Lei10/2025não altera15. Recorte2026/MZN;base apenas,sem presumir preço regulado,valorTC,incidência,taxaIVA,IVA,dedução/campo11 ou totaisModeloA.';
|
||||
export const fuelBaseTreatments=[{id:'fuel_tax_included',label:'Montante sem IVA ainda inclui a Taxa sobre Combustíveis'},{id:'fuel_tax_excluded',label:'Montante sem IVA já exclui a Taxa sobre Combustíveis'}] as const;
|
||||
export const fuelBaseConditions=[
|
||||
['taxableFuelResale','Está identificada uma transmissão de combustíveis sujeita a IVA,efectuada por revendedor; não é qualquer aquisição ou prestação de serviços'],
|
||||
['publicFixedPrice','O preço deste combustível é fixado pela Autoridade Pública identificada na referência aplicável'],
|
||||
['priceScopeMatches','A referência do preço e o respectivo âmbito abrangem este combustível,operação,local e data; não se presume um preço actual para outro período'],
|
||||
['netComposition','O montante indicado exclui o próprio IVA e reflecte a composição aplicável do artigo15(1)/(5)/(6),sem omissões ou duplicações'],
|
||||
['fuelTaxTreatment','O tratamento escolhido corresponde à composição do montante:TC ainda incluída ou já excluída; a mesma TC não será subtraída duas vezes'],
|
||||
] as const;
|
||||
type Choice=''|'yes'|'no';type Condition=typeof fuelBaseConditions[number][0];
|
||||
export type FuelBaseRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationDate:string;priceRef:string;priceScope:string;priceFrom:string;priceTo:string;documentRef:string;documentDate:string;consideration:string;fuelTax:string;fuelTaxRef: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 FuelBaseCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:FuelBaseRow[];rows:LineResult[]};
|
||||
export type FuelBaseBook={rows:FuelBaseRow[];result?:FuelBaseCalculation;history?:FuelBaseCalculation[]};
|
||||
type Store=ReturnType<typeof readAcquisitionStore>&{regulatedFuelBases?:Record<string,FuelBaseBook>};
|
||||
export function newFuelBase(id:string,currency:string):FuelBaseRow{return{id,treatment:'',description:'',period:'',origin:'projected',currency,operationDate:'',priceRef:'',priceScope:'',priceFrom:'',priceTo:'',documentRef:'',documentDate:'',consideration:'',fuelTax:'',fuelTaxRef:'',compositionRef:'',evidence:'',conditions:{}};}
|
||||
export function switchFuelTaxTreatment(row:FuelBaseRow,treatment:string):FuelBaseRow{return{...row,treatment,conditions:{...row.conditions,fuelTaxTreatment:''}};}
|
||||
function readStore(draft:Record<string,unknown>):Store{
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.regulatedFuelBases!==undefined&&(!store.regulatedFuelBases||typeof store.regulatedFuelBases!=='object'||Array.isArray(store.regulatedFuelBases)))throw new Error('Bases de combustíveis não recuperáveis;originais preservados.');
|
||||
const fields=Object.keys(newFuelBase('','')).filter(key=>key!=='conditions');
|
||||
for(const book of Object.values(store.regulatedFuelBases??{}))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 FuelBaseRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de combustíveis não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
||||
return store;
|
||||
}
|
||||
export function fuelBaseBook(draft:Record<string,unknown>):FuelBaseBook{return readStore(draft).regulatedFuelBases?.[acquisitionScope(draft)]??{rows:[]};}
|
||||
export function saveFuelBaseBook(draft:Record<string,unknown>,book:FuelBaseBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,regulatedFuelBases:{...current.regulatedFuelBases,[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 evaluateFuelBase(row:FuelBaseRow,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(!fuelBaseTreatments.some(item=>item.id===row.treatment))return stop('Indique se o montante sem IVA ainda inclui a TC ou já a exclui;não se presume a composição.');
|
||||
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin))return stop('Indique mês em2026 e dados efectivos declarados ou pressupostos de projecção;sem inferir do orçamento.');
|
||||
const failed=fuelBaseConditions.filter(([key])=>row.conditions[key]==='no');if(failed.length)return stop('Não se aplica esta base especial de combustíveis: '+failed.map(([,label])=>label).join('; ')+'. Não é isenção nem IVA zero.','not_applicable');
|
||||
const missing=fuelBaseConditions.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(!row.description.trim()||!row.priceRef.trim()||!row.priceScope.trim()||!row.documentRef.trim()||!row.compositionRef.trim()||!row.evidence.trim()||!validDate(row.documentDate))return stop('Identifique combustível/transmissão,referência e âmbito do preço,documento/data,composição e fundamento ou pressupostos expressos;reutilize referências existentes,sem novo certificado/aprovador.');
|
||||
if(!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period||!validDate(row.priceFrom)||row.operationDate<row.priceFrom||(row.priceTo!==''&&(!validDate(row.priceTo)||row.priceFrom>row.priceTo||row.operationDate>row.priceTo)))return stop('Identifique a data da transmissão no mês escolhido e o intervalo aplicável do preço que a abrange. Sem aplicar preço de outro período;na projecção indique pressupostos temporais expressos. O fim pode ficar vazio quando não existe termo fixado,com a correspondência temporal confirmada.');
|
||||
const amount=decimal(row.consideration);if(amount===null)return stop('Indique o montante da contraprestação sem IVA;valor desconhecido ou inválido não é zero.');
|
||||
let base=amount,formula='montante já sem TC,sem nova subtracção';
|
||||
if(row.treatment==='fuel_tax_included'){
|
||||
const tc=decimal(row.fuelTax);if(tc===null||!row.fuelTaxRef.trim())return stop('Identifique o montante exacto e a referência da TC incluída nesta contraprestação,ou pressuposto expresso;sem taxa TC presumida ou valor desconhecido igual a zero.');
|
||||
if(tc>amount)return stop('A TC indicada excede a contraprestação sem IVA;reveja a composição e os montantes,sem base negativa ou zero substituto.');
|
||||
base=amount-tc;formula=`${money(amount)} − ${money(tc)} de TC`;
|
||||
}
|
||||
return{...result,status:'determined',baseAmount:money(base),reason:`Base15(2)(i): ${formula} = ${money(base)}MZN,${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. Preço/âmbito/período e composição identificados;sem determinar automaticamente preço ou taxaTC. Base não é imposto;IVA/taxa,dedução/campo11 e totaisModeloA não são determinados por este cálculo.`};
|
||||
}
|
||||
function key(draft:Record<string,unknown>,book:FuelBaseBook):string{return JSON.stringify({version:fuelBaseVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||||
export function fuelBaseCurrent(draft:Record<string,unknown>,book:FuelBaseBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||||
export function recordFuelBases(draft:Record<string,unknown>,book:FuelBaseBook):FuelBaseBook{
|
||||
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:FuelBaseCalculation={version:fuelBaseVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:fuelBaseSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateFuelBase(row,draft.currency))};
|
||||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveFuelBaseBook(draft,next);return next;
|
||||
}
|
||||
export function fuelBaseReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||||
try{const book=fuelBaseBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base P095 — ${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};${fuelBaseCurrent(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'};transmissão ${row.operationDate};preço ${row.priceRef};âmbito ${row.priceScope};intervalo ${row.priceFrom} a ${row.priceTo||'sem termo fixado,correspondência temporal confirmada'};${fuelBaseTreatments.find(item=>item.id===row.treatment)?.label??'Composição por indicar'};contraprestação ${row.consideration} ${row.currency};${row.treatment==='fuel_tax_included'?`TC incluída ${row.fuelTax} ${row.currency},referência ${row.fuelTaxRef}`:'TC já excluída,sem segunda subtracção'};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${fuelBaseConditions.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 combustíveis','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { importExemptionReportRows } from './iva-import-exemptions.ts';
|
||||
import { publicBaseReportRows } from './iva-public-authority-base.ts';
|
||||
import { commissionBaseReportRows } from './iva-commission-base.ts';
|
||||
import { actSaleReportRows } from './iva-act-sale-base.ts';
|
||||
import { fuelBaseReportRows } from './iva-fuel-base.ts';
|
||||
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
|
||||
import { salesReportRows } from './iva-operations.ts';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
@@ -626,6 +627,7 @@ export function buildProjectDossierRows(
|
||||
rows.push(...publicBaseReportRows(draft));
|
||||
rows.push(...commissionBaseReportRows(draft));
|
||||
rows.push(...actSaleReportRows(draft));
|
||||
rows.push(...fuelBaseReportRows(draft));
|
||||
rows.push(...offerPreparationReportRows(draft));
|
||||
const operationRows = salesReportRows(draft);
|
||||
rows.push(...operationRows);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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 IvaFuelBaseDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_fuel_bases_recover_privately_preserving_all_delivered_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-g7d-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:342', $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['regulatedFuelBases']['opportunity:342'];
|
||||
$this->assertCount(2, $book['rows']);
|
||||
$this->assertSame(['fuel_tax_included','fuel_tax_excluded'], array_column($book['rows'], 'treatment'));
|
||||
$this->assertSame(['1400.15','1500.25'], 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('1500.25', $book['rows'][0]['consideration']);
|
||||
$this->assertSame('100.10', $book['rows'][0]['fuelTax']);
|
||||
$this->assertStringContainsString('TC desta operação', $book['rows'][0]['fuelTaxRef']);
|
||||
$this->assertStringContainsString('preço A', $book['rows'][0]['priceRef']);
|
||||
$this->assertStringContainsString('Manica', $book['rows'][0]['priceScope']);
|
||||
$this->assertSame('2026-12-12', $book['rows'][0]['operationDate']);
|
||||
$this->assertSame('2026-12-01', $book['rows'][0]['priceFrom']);
|
||||
$this->assertSame('2026-12-31', $book['rows'][0]['priceTo']);
|
||||
$this->assertStringContainsString('15(2)(i)', $book['result']['source']);
|
||||
$this->assertSame($book['rows'], $book['result']['inputs']);
|
||||
$this->assertCount(2, $book['history']);
|
||||
$this->assertNull($book['history'][0]['rows'][0]['baseAmount']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g7d-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, ['regulatedFuelBases' => true]));
|
||||
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
|
||||
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
|
||||
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
|
||||
file_put_contents(base_path('outputs/iva-l1-g7d-fuel-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,39 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {newFuelBase,switchFuelTaxTreatment,fuelBaseTreatments,fuelBaseConditions,evaluateFuelBase,recordFuelBases,saveFuelBaseBook,fuelBaseBook,fuelBaseCurrent,fuelBaseReportRows} from '../../resources/js/lib/iva-fuel-base.ts';
|
||||
import {actSaleCurrent} from '../../resources/js/lib/iva-act-sale-base.ts';
|
||||
import {commissionBaseCurrent} from '../../resources/js/lib/iva-commission-base.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g7d-preserved-inputs.json',import.meta.url),'utf8'));
|
||||
const row=(treatment=fuelBaseTreatments[0].id)=>({...newFuelBase('g7d-'+treatment,'MZN'),treatment,description:'SYNTHETIC QA revenda de combustível A',period:'2026-12',operationDate:'2026-12-12',priceRef:'SYNTHETIC QA referência preço A / Autoridade Pública',priceScope:'SYNTHETIC QA combustível A, revenda A, Manica, dezembro2026, pressupostos',priceFrom:'2026-12-01',priceTo:'2026-12-31',documentRef:'SYNTHETIC QA G7D '+treatment,documentDate:'2026-12-12',consideration:'1500.25',fuelTax:'100.10',fuelTaxRef:'SYNTHETIC QA TC desta operação sem estimar taxa',compositionRef:'SYNTHETIC QA composição15(1)/(5)/(6),líquida de IVA',evidence:'SYNTHETIC QA pressupostos expressos,não documento real',conditions:Object.fromEntries(fuelBaseConditions.map(([key])=>[key,'yes']))});
|
||||
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:342',project_title:'SYNTHETIC QA — G7D / Bases de combustíveis'});
|
||||
|
||||
test('P095 own resale/price/scope/composition conditions; missing references and wrong date never become zero',()=>{
|
||||
for(const rule of fuelBaseTreatments){const input=row(rule.id),ok=evaluateFuelBase(input,'MZN');assert.equal(ok.status,'determined');assert.equal(ok.baseAmount,rule.id==='fuel_tax_included'?'1400.15':'1500.25');assert.equal(ok.vat,null);assert.equal(ok.rate,null);
|
||||
for(const[key]of fuelBaseConditions)for(const value of ['','unknown','no']){const r=evaluateFuelBase({...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 [{description:''},{priceRef:''},{priceScope:''},{documentRef:''},{documentDate:'2026-02-30'},{compositionRef:''},{evidence:''},{period:'2025-12'},{currency:'USD'},{treatment:'unknown'},{operationDate:'2026-11-12'},{operationDate:'2026-02-30'},{priceFrom:''},{priceTo:'invalid'},{priceFrom:'2026-12-20'},{priceTo:'2026-12-11'},{priceFrom:'2026-12-31',priceTo:'2026-12-01'}])assert.equal(evaluateFuelBase({...input,...patch},'MZN').baseAmount,null);
|
||||
assert.equal(evaluateFuelBase({...input,priceTo:''},'MZN').status,'determined');
|
||||
for(const operationDate of ['2026-12-01','2026-12-31'])assert.equal(evaluateFuelBase({...input,operationDate},'MZN').status,'determined');
|
||||
}
|
||||
});
|
||||
|
||||
test('TC subtracted exactly once only when included; zero,precision,excess and treatment changes preserve inputs',()=>{
|
||||
const input=row();for(const[patch,expected]of [[{consideration:'0',fuelTax:'0'},'0.00'],[{consideration:'1.000002',fuelTax:'0.000001'},'1.000001'],[{consideration:'999999999999999.999999',fuelTax:'0.000001'},'999999999999999.999998'],[{treatment:'fuel_tax_excluded',fuelTax:'invalid',fuelTaxRef:''},'1500.25']])assert.equal(evaluateFuelBase({...input,...patch},'MZN').baseAmount,expected);
|
||||
for(const patch of [{consideration:''},{fuelTax:''},{fuelTaxRef:''},{consideration:'-1'},{fuelTax:'NaN'},{fuelTax:'0.0000001'},{fuelTax:'1500.26'}])assert.equal(evaluateFuelBase({...input,...patch},'MZN').baseAmount,null);
|
||||
const switched=switchFuelTaxTreatment(input,'fuel_tax_excluded');assert.equal(switched.conditions.fuelTaxTreatment,'');for(const field of ['consideration','fuelTax','fuelTaxRef','priceRef','priceScope','priceFrom','priceTo','documentRef'])assert.equal(switched[field],input[field]);assert.equal(evaluateFuelBase(switched,'MZN').baseAmount,null);assert.equal(evaluateFuelBase({...switched,conditions:{...switched.conditions,fuelTaxTreatment:'yes'}},'MZN').baseAmount,'1500.25');
|
||||
});
|
||||
|
||||
test('save/recovery/history/report preserve all81 delivered books including P094/P091 and currentkeys',()=>{
|
||||
const d=draft(),prior=JSON.parse(d.modelo_a_snapshot);assert.deepEqual(fuelBaseBook(d),{rows:[]});assert.deepEqual(fuelBaseReportRows(d),[]);
|
||||
let book=recordFuelBases(d,{rows:fuelBaseTreatments.map(rule=>({...row(rule.id),conditions:{}}))});book=recordFuelBases(d,{...book,rows:fuelBaseTreatments.map(rule=>({...row(rule.id),conditions:{...row(rule.id).conditions,publicFixedPrice:'no'}}))});book=recordFuelBases(d,{...book,rows:fuelBaseTreatments.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(fuelBaseCurrent(d,book));
|
||||
for(const field of ['treatment','consideration','fuelTax','fuelTaxRef','priceRef','priceScope','priceFrom','priceTo','operationDate'])assert.ok(!fuelBaseCurrent(d,{...book,rows:book.rows.map(r=>({...r,[field]:'changed'}))}));for(const patch of [{context_opportunity:'opportunity:343'},{currency:'USD'},{selection_labels:{province_id:'Maputo'}}])assert.ok(!fuelBaseCurrent({...d,...patch},book));
|
||||
d.modelo_a_snapshot=saveFuelBaseBook(d,book);assert.deepEqual(fuelBaseBook(JSON.parse(JSON.stringify(d))),book);const next=JSON.parse(d.modelo_a_snapshot);const{regulatedFuelBases,...untouched}=next;assert.deepEqual(untouched,prior);for(const[scope,previous]of Object.entries(prior.actSaleBases))assert.ok(actSaleCurrent({...d,context_opportunity:scope},previous));for(const[scope,previous]of Object.entries(prior.commissionBases))assert.ok(commissionBaseCurrent({...d,context_opportunity:scope},previous));
|
||||
assert.equal(next.actSaleBases['opportunity:341'].result.rows[1].baseAmount,'2200.50');assert.equal(next.commissionBases['opportunity:340'].result.rows[0].baseAmount,'900.15');assert.equal(next.publicAuthorityBases['opportunity:339'].result.rows[0].vat,null);assert.equal(next.importExemptions['opportunity:338'].result.rows[1].importVat,'400.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 G7D'}));for(const token of ['Base P095','1400.15','1500.25','IVA: Indeterminado','opportunity:342','Manica','2026-12-12','preço A','15(2)(i)','100.10','2026-12-01','2026-12-31','campo11'])assert.ok(report.includes(token),token);assert.deepEqual(fuelBaseReportRows({...d,context_opportunity:'opportunity:343'}),[]);
|
||||
if(process.env.IVA_G7D_CALCULATED)fs.writeFileSync(process.env.IVA_G7D_CALCULATED,JSON.stringify(d));
|
||||
});
|
||||
|
||||
test('corrupt fuel rows/store cannot overwrite originals; duplicate operation identifiers cannot be recorded',()=>{
|
||||
const d=draft(),original=d.modelo_a_snapshot;for(const patch of [{fuelTax:4},{conditions:[]},{priceRef:null}])assert.throws(()=>saveFuelBaseBook(d,{rows:[{...row(),...patch}]}),/originais preservados/);assert.equal(d.modelo_a_snapshot,original);assert.throws(()=>recordFuelBases(d,{rows:[row(),row()]}),/distintos/);assert.throws(()=>recordFuelBases(d,{rows:[]}),/distintos/);assert.throws(()=>fuelBaseBook({...d,modelo_a_snapshot:JSON.stringify({...JSON.parse(original),regulatedFuelBases:[]})}),/originais preservados/);
|
||||
});
|
||||
Reference in New Issue
Block a user