feat(iva): classify G4D onboard supplies with explicit beverage exceptions
This commit is contained in:
@@ -4,6 +4,7 @@ import { pharmaAnnexEntries, pharmaAnnexEntry, pharmaAnnexReference } from '@/li
|
||||
import { annexIEntries, annexIEntry, annexIReference } from '@/lib/iva-annex-i';
|
||||
import { temporaryBenefit, temporaryBenefitNotice } from '@/lib/iva-temporary-benefits';
|
||||
import { isExportService } from '@/lib/iva-export-services';
|
||||
import { isOnboardSupply, supplyCategories, onboardSupplyIssue, type SupplyCategory } from '@/lib/iva-onboard-supplies';
|
||||
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
|
||||
import { referenceExportOperation, continueTemporaryOperation, continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
|
||||
|
||||
@@ -35,6 +36,11 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
|
||||
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
|
||||
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>temporaryBenefit(row.treatment)&&!temporaryBenefit(e.target.value)?continueTemporary(row.id,e.target.value):change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||||
</div>
|
||||
{isOnboardSupply(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Natureza do abastecimento a bordo</legend>
|
||||
<label>Categoria do artigo13(3)<select value={row.supplyCategory??''} onChange={e=>change(row.id,{supplyCategory:e.target.value as SupplyCategory,confirmed:[]})}><option value="">Por identificar</option>{supplyCategories.map(([value,label])=><option key={value} value={value}>{label}</option>)}</select></label>
|
||||
<p>A escolha conserva o preço, a moeda e o período; reveja as condições. Para fornecimento misto, discrimine as parcelas efectivas sem repetir o total em cada linha. Desconhecido não equivale a «sem bebidas».</p>
|
||||
{onboardSupplyIssue(row.treatment,row.supplyCategory)&&<p role="status">{onboardSupplyIssue(row.treatment,row.supplyCategory)}</p>}
|
||||
</fieldset>}
|
||||
{isExportService(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Referência à operação relacionada</legend>
|
||||
<p>Se a operação já consta desta oportunidade,pode referenciá-la sem preencher de novo a descrição. A escolha conserva o preço,moeda,período e fundamento deste serviço;limpa as confirmações para revisão. Não copia o valor dos bens para o preço do serviço nem comprova automaticamente a isenção. Para operação documentada fora deste livro,use o fundamento abaixo.</p>
|
||||
<label>Operação já preenchida<select value={row.relatedExport?.id??''} onChange={e=>run(()=>{if(!e.target.value){save({...book,rows:book.rows.map(item=>item.id===row.id?{...item,relatedExport:undefined,confirmed:[]}:item)});return;}const related=book.rows.find(item=>item.id===e.target.value);if(!related)throw new Error('Operação relacionada já não disponível; dados conservados.');save({...book,rows:book.rows.map(item=>item.id===row.id?referenceExportOperation(item,related):item)});})}><option value="">Sem referência interna — identificar no fundamento</option>{book.rows.filter(item=>item.id!==row.id).map(item=><option key={item.id} value={item.id}>{item.description||item.id} — {item.period}</option>)}</select></label>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { OperationTreatment } from './iva-operations.ts';
|
||||
|
||||
export type SupplyCategory = '' | 'provisions' | 'beverages' | 'fuel' | 'cargo_products';
|
||||
export const supplyCategories: Array<[Exclude<SupplyCategory, ''>, string]> = [
|
||||
['provisions', 'Provisões de bordo, excepto bebidas — consumo exclusivo da tripulação e passageiros'],
|
||||
['beverages', 'Bebidas como provisões de bordo — consumo exclusivo da tripulação e passageiros'],
|
||||
['fuel', 'Combustíveis, carburantes, lubrificantes ou outros produtos para propulsão ou aparelhos de uso técnico a bordo'],
|
||||
['cargo_products', 'Produtos acessórios para preparação, tratamento ou conservação das mercadorias transportadas a bordo'],
|
||||
];
|
||||
const source = 'CIVA republicado Lei 13/2016, BR156/10.º suplemento, art.13(1)(c/d/e/h),13(2–3), PDF12/p.1426(270);19(1)(b)(i), PDF15/p.1426(273). Lei22/2022 art.1, PDF3/p.2252(27), e Lei10/2025 art.1, PDF12–13/p.2180(12)–(13), não alteram13 e mantêm19(1)(b)(i). Âmbito2026/MZN; bebidas em c/d/h não têm isenção directa automática:13(2) prevê dedução ou restituição, sem aplicar20(1)(d) para esse efeito.';
|
||||
const make = (id:string,label:string,letter:string,conditions:string[]):OperationTreatment => ({id,label,field:'05',rate:0,article:`CIVA arts.13(1)(${letter}),13(2–3),19(1)(b)(i)`,source,logicVersion:'onboard-supplies/1',conditions:[
|
||||
'Transmissão de bens localizada em Moçambique, com territorialidade identificada; não importação nem serviço autónomo',
|
||||
'Bens efectivamente postos a bordo do meio identificado; natureza e destino correspondem à categoria escolhida do artigo13(3), sustentados pelo documento ou pressuposto expresso da projecção',
|
||||
...conditions,
|
||||
]});
|
||||
export const onboardSupplyTreatments:OperationTreatment[] = [
|
||||
make('onboard_seagoing_ship','Abastecimento de embarcação de navegação marítima em alto mar','c',[
|
||||
'Embarcação identificada que efectua navegação marítima em alto mar e assegura transporte remunerado de passageiros ou actividade comercial, industrial ou de pesca; utilização identificada, não qualquer barco de recreio',
|
||||
]),
|
||||
make('onboard_rescue_coastal_ship','Abastecimento de embarcação de salvamento, assistência marítima ou pesca costeira','d',[
|
||||
'Embarcação identificada de salvamento, assistência marítima ou pesca costeira; actividade identificada, não presumida pelo nome do operador',
|
||||
]),
|
||||
make('onboard_departing_warship','Abastecimento de embarcação de guerra com destino estrangeiro','e',[
|
||||
'Embarcação de guerra identificada que deixa o país com destino a porto ou ancoradouro situado no estrangeiro; saída e destino identificados, não simples utilização militar',
|
||||
]),
|
||||
make('onboard_international_aircraft','Abastecimento de avião de companhia principalmente internacional','h',[
|
||||
'Avião identificado utilizado por companhia de navegação aérea que se dedica principalmente ao tráfego internacional nos termos de13(1)(g); companhia e utilização identificadas, não apenas um voo internacional isolado',
|
||||
]),
|
||||
];
|
||||
export function isOnboardSupply(id:string):boolean { return onboardSupplyTreatments.some(rule=>rule.id===id); }
|
||||
export function onboardSupplyIssue(id:string,category:unknown):string|null {
|
||||
if(!isOnboardSupply(id))return null;
|
||||
if(!supplyCategories.some(([value])=>value===category))return 'Identifique a natureza do abastecimento segundo o artigo13(3). Informação em falta não significa ausência de bebidas ou provisões; nenhum montante fiscal foi presumido.';
|
||||
if(id==='onboard_rescue_coastal_ship'&&(category==='provisions'||category==='beverages'))return 'As provisões de bordo estão excluídas da isenção de13(1)(d), incluindo bebidas, nas embarcações de salvamento, assistência marítima e pesca costeira. Esta isenção não se aplica; o tratamento fiscal alternativo deve ser identificado, sem presumir taxa ou zero.';
|
||||
if(category==='beverages'&&id!=='onboard_departing_warship')return 'Bebidas em13(1)(c/h):13(2) prevê isenção por dedução ou restituição, não isenção directa deste fornecimento; para esse efeito não se aplica20(1)(d). O montante liquidado e o procedimento aplicável não são determinados neste incremento: sem crédito, restituição, taxa ou zero automáticos. Conserve o preço e identifique o tratamento antes do apuramento.';
|
||||
return null;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { temporaryBenefits, temporaryBenefit, temporaryBenefitNotice, temporaryB
|
||||
import { directExportTreatments } from './iva-export-operations.ts';
|
||||
import { internationalAssetTreatments } from './iva-international-assets.ts';
|
||||
import { exportServiceTreatments, isExportService } from './iva-export-services.ts';
|
||||
import { onboardSupplyTreatments, onboardSupplyIssue, supplyCategories, isOnboardSupply, type SupplyCategory } from './iva-onboard-supplies.ts';
|
||||
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
|
||||
|
||||
@@ -62,6 +63,7 @@ const listedGoodsTreatments:OperationTreatment[] = [
|
||||
].map(([id,label,condition])=>({id:`listed_${id}`,label:`Lista legal — ${label}`,field:'05',rate:0,article:'CIVA arts.9(10),19(1)(b)(v), redacção Lei22/2022',source:'Lei22/2022 art.1, PDF2, p.2252(26),9(10); CIVA republicado Lei13/2016, PDF15, p.1426(273),19(1)(b)(v). Âmbito técnico2026/MZN',conditions:[internalOperation,'Transmissão do próprio bem enumerado, identificado; não um serviço autónomo nem extensão por sector, marca ou destino',condition]}));
|
||||
export const operationTreatments:OperationTreatment[] = [
|
||||
...exportServiceTreatments,
|
||||
...onboardSupplyTreatments,
|
||||
...internationalAssetTreatments,
|
||||
...directExportTreatments,
|
||||
...temporaryBenefits.map(item=>({id:item.id,label:`Benefício terminado31/12/2025 — ${item.label}`,field:null,rate:null,article:`CIVA art.${item.article}`,source:`${item.source} CIVA republicado Lei13/2016,9(14),PDF10/p.1426(268),apenas para9(13)(b/f). Âmbito:cessação nas operações internas2026/MZN;sem cálculo histórico.`,conditions:[],logicVersion:temporaryBenefitVersion})),
|
||||
@@ -122,7 +124,7 @@ export const operationTreatments:OperationTreatment[] = [
|
||||
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
|
||||
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
|
||||
];
|
||||
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean;temporaryBenefitOrigin?:string;relatedExport?:{id:string;description:string;treatment:string}; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
|
||||
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean;temporaryBenefitOrigin?:string;relatedExport?:{id:string;description:string;treatment:string};supplyCategory?:SupplyCategory; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
|
||||
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
|
||||
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
|
||||
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
|
||||
@@ -182,6 +184,8 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
|
||||
if(currency!=='MZN'||row.sourceCurrency!==currency)return stop('Moeda fora de MZN ou divergente da origem; sem conversão silenciosa.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Período fora da cobertura de2026; sem cálculo histórico presumido.');
|
||||
if(temporaryBenefit(row.treatment))return stop(temporaryBenefitNotice(row.treatment,row.period));
|
||||
const supplyIssue=onboardSupplyIssue(row.treatment,row.supplyCategory);
|
||||
if(supplyIssue)return {...stop(supplyIssue),field:null,rate:null};
|
||||
if(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência, territorialidade e fundamento desta linha. A remissão do campo06 não constitui uma regra de classificação de todas as operações no estrangeiro.');
|
||||
if(n===null||!['net','included'].includes(row.basis))return stop('Montante ou indicação de IVA incluído/sem IVA em falta; vazio não significa zero.');
|
||||
if(!row.description.trim()||!row.establishment.trim())return stop('Indique a operação e o estabelecimento a que pertence; não inferidos da localização do investimento.');
|
||||
@@ -256,6 +260,7 @@ export function salesReportRows(draft:Record<string,unknown>):Array<[string,stri
|
||||
for(const row of r.inputs){const value=r.rows.find(v=>v.id===row.id)!;out.push(['IVA mensal',`${row.period} — ${row.description} — ${row.establishment}`,`Declarado ${row.amount||'em falta'} ${row.sourceCurrency}; ${row.basis==='included'?'IVA incluído':row.basis==='net'?'sem IVA':'base desconhecida'}; líquido ${value.net??'Indeterminado'}; IVA ${value.vat??'Indeterminado'}; bruto ${value.gross??'Indeterminado'}; campo ${value.field??'não determinado'}; ${value.reason}`,`${context}; ${value.article}; ${value.source}; origem: ${row.source}; pressuposto/documento: ${row.evidence||'em falta'}; condições confirmadas: ${row.confirmed.join(',')||'nenhuma'}`]);}
|
||||
for(const row of r.inputs.filter(v=>v.fundraiser))out.push(['IVA mensal',`Manifestação ocasional — ${row.description}`,`Entidade ${row.fundraiser!.entityReference}; ano ${row.period.slice(0,4)}; total anual declarado ${row.fundraiser!.annualEvents}; manifestação ${row.fundraiser!.eventNumber}; actividade ${row.fundraiser!.activity}`,`${context}; dados declarados para a entidade, incluindo manifestações fora deste dossier; não é contagem automática das vendas.`]);
|
||||
for(const row of r.inputs.filter(v=>isExportService(v.treatment)&&v.relatedExport))out.push(['IVA mensal',`Operação relacionada — ${row.description}`,`${row.relatedExport!.description}; ID ${row.relatedExport!.id}; tratamento ${row.relatedExport!.treatment}`,`${context}; referência escolhida expressamente nesta oportunidade,sem copiar valor para preço do serviço nem provar automaticamente o enquadramento subjacente`]);
|
||||
for(const row of r.inputs.filter(v=>isOnboardSupply(v.treatment)))out.push(['IVA mensal',`Natureza do abastecimento — ${row.description}`,supplyCategories.find(([value])=>value===row.supplyCategory)?.[1]??'Por identificar',`${context}; CIVA13(2–3); natureza conservada no cálculo/relatório, sem aplicar dedução ou restituição automaticamente`]);
|
||||
for(const value of r.rows.filter(v=>v.exact))out.push(['IVA mensal',`Precisão exacta — ${value.id}`,`Líquido ${fractionLabel(value.exact!.net)} MZN; IVA ${fractionLabel(value.exact!.vat)} MZN; bruto ${fractionLabel(value.exact!.gross)} MZN`,`${context}; apresentação decimal a6casas, aproximação quando a fracção não termina; não aplicar à declaração sem a sua regra própria.`]);
|
||||
for(const p of r.periods){out.push(['IVA mensal',`Operações L1 — ${p.period}`,Object.entries(p.fields).map(([f,v])=>`${f}: ${v??'Indeterminado'}`).join('; '),`${context}; ${p.reasons.join(' ')}`]);
|
||||
if(p.exactFields)out.push(['IVA mensal',`Precisão exacta do período — ${p.period}`,Object.entries(p.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),`${context}; totais em fracções MZN, calculados antes da apresentação decimal, sem regra declarativa presumida.`]);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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 IvaOnboardSupplyDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_onboard_supplies_recover_with_the_selected_opportunity_and_previous_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('outputs/iva-l1-g4-supplies-20261024/calculated.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:326', $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['salesOperations']['opportunity:326'];
|
||||
$this->assertSame('opportunity:326', $book['result']['opportunity']);
|
||||
$this->assertSame('MZN', $book['result']['currency']);
|
||||
$this->assertSame('2026-12', $book['result']['periods'][0]['period']);
|
||||
$this->assertSame('400.00', $book['result']['periods'][0]['fields']['05']);
|
||||
$this->assertCount(4, $book['rows']);
|
||||
$this->assertSame(['provisions','fuel','beverages','cargo_products'], array_column($book['rows'], 'supplyCategory'));
|
||||
foreach ($book['result']['rows'] as $row) {
|
||||
$this->assertSame('05', $row['field']);
|
||||
$this->assertSame('0.00', $row['vat']);
|
||||
}
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g4c-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
foreach (json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['salesOperations'] as $context => $original) {
|
||||
$this->assertSame($original, $store['salesOperations'][$context]);
|
||||
}
|
||||
file_put_contents(base_path('outputs/iva-l1-g4-supplies-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { onboardSupplyTreatments, supplyCategories, onboardSupplyIssue } from '../../resources/js/lib/iva-onboard-supplies.ts';
|
||||
import { newSalesOperation, evaluateSalesOperation, recordSales, saveSalesBook, salesBook, salesCurrent } from '../../resources/js/lib/iva-operations.ts';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g4c-inputs.json',import.meta.url),'utf8'));
|
||||
const positive=['provisions','fuel','beverages','cargo_products'];
|
||||
const row=(rule,i)=>({...newSalesOperation(`g4d-${i}`,'2026-12','MZN'),description:`SYNTHETIC QA — ${rule.label}`,establishment:'SYNTHETIC QA estabelecimento',amount:'100',basis:i%2?'included':'net',treatment:rule.id,supplyCategory:positive[i],confirmed:rule.conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — documento/pressuposto identifica bens postos a bordo, destino, embarcação/avião e actividade do operador; projecção, sem certificar despacho nem restituição'});
|
||||
function fixture(){
|
||||
const draft={...structuredClone(frozen),context_opportunity:'opportunity:326',project_title:'SYNTHETIC QA — G4D / Abastecimentos a bordo'};
|
||||
const book=recordSales(draft,{currency:'MZN',rows:onboardSupplyTreatments.map(row),periods:[{period:'2026-12',complete:true}]});draft.modelo_a_snapshot=saveSalesBook(draft,book);return{draft,book};
|
||||
}
|
||||
test('four supply branches require each legal condition and identification; values with/without included VAT use existing motor',()=>{
|
||||
assert.equal(onboardSupplyTreatments.length,4);
|
||||
for(const[i,rule]of onboardSupplyTreatments.entries()){
|
||||
const input=row(rule,i),result=evaluateSalesOperation(input,'MZN');assert.equal(result.field,'05');assert.equal(result.vat,'0.00');assert.equal(result.net,'100.00');assert.equal(result.gross,'100.00');
|
||||
for(const missing of input.confirmed)assert.equal(evaluateSalesOperation({...input,confirmed:input.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
|
||||
for(const patch of [{amount:''},{evidence:''},{sourceCurrency:'USD'},{period:'2027-01'},{basis:''},{description:''},{establishment:''}])assert.equal(evaluateSalesOperation({...input,...patch},'MZN').vat,null);
|
||||
}
|
||||
});
|
||||
test('article13 categories and exclusions are distinct: beverages do not cause automatic exemption or deduction',()=>{
|
||||
for(const[i,rule]of onboardSupplyTreatments.entries())for(const[category]of supplyCategories){
|
||||
const result=evaluateSalesOperation({...row(rule,i),supplyCategory:category},'MZN');
|
||||
const excluded=i===1&&(category==='provisions'||category==='beverages'),refund=category==='beverages'&&(i===0||i===3);
|
||||
if(excluded||refund){assert.equal(result.vat,null);assert.equal(result.net,null);assert.equal(result.field,null);assert.equal(result.rate,null);assert.match(result.reason,excluded?/excluídas/:/dedução ou restituição/);}
|
||||
else assert.equal(result.vat,'0.00');
|
||||
}
|
||||
assert.match(onboardSupplyIssue('onboard_international_aircraft','beverages'),/20\(1\)\(d\)/);
|
||||
assert.equal(onboardSupplyIssue('onboard_departing_warship','beverages'),null); // 13(2) lists c/d/h, not e.
|
||||
for(const category of [undefined,'',null,'unknown'])for(const rule of onboardSupplyTreatments){const result=evaluateSalesOperation({...row(rule,0),supplyCategory:category},'MZN');assert.equal(result.vat,null);assert.match(result.reason,/Identifique a natureza/);}
|
||||
});
|
||||
test('changing category preserves price and historical result; unresolved beverage/provisions never turn into zero totals',()=>{
|
||||
const{draft,book}=fixture();assert.equal(book.result.periods[0].fields['05'],'400.00');
|
||||
for(const[index,category]of [[0,'beverages'],[1,'provisions'],[3,'']]){
|
||||
const changed=recordSales(draft,{...book,rows:book.rows.map((r,i)=>i===index?{...r,supplyCategory:category,confirmed:[]}:r)});
|
||||
assert.equal(changed.rows[index].amount,'100');assert.equal(changed.rows[index].sourceCurrency,'MZN');assert.equal(changed.rows[index].period,'2026-12');
|
||||
assert.equal(changed.result.periods[0].fields['05'],null);assert.deepEqual(changed.history.at(-1),book.result);
|
||||
const edited={...draft,modelo_a_snapshot:saveSalesBook(draft,{...book,rows:changed.rows})};assert.equal(salesCurrent(edited,salesBook(edited)),false);
|
||||
}
|
||||
});
|
||||
test('QA snapshot and report retain category/context/source; all17 prior books stay identical/current without recalculation',()=>{
|
||||
const{draft,book}=fixture();assert.ok(salesCurrent(draft,book));assert.deepEqual(salesBook(JSON.parse(JSON.stringify(draft))),JSON.parse(JSON.stringify(book)));
|
||||
const before=JSON.parse(frozen.modelo_a_snapshot).salesOperations,after=JSON.parse(draft.modelo_a_snapshot).salesOperations;assert.equal(Object.keys(before).length,17);
|
||||
for(const[context,original]of Object.entries(before)){assert.deepEqual(after[context],original);assert.ok(salesCurrent({...draft,context_opportunity:context},after[context]));}
|
||||
const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance']),version:'QA G4D'}));
|
||||
for(const token of ['opportunity:326','Manica','2026-12','400.00','13(1)(c)','13(1)(d)','13(1)(e)','13(1)(h)','Natureza do abastecimento','Bebidas como provisões','QA G4D'])assert.ok(report.includes(token),token);
|
||||
if(process.env.IVA_G4D_CALCULATED)fs.writeFileSync(process.env.IVA_G4D_CALCULATED,JSON.stringify(draft));
|
||||
});
|
||||
Reference in New Issue
Block a user