feat: preserve exact included VAT fractions and enumerate L1 remainder

This commit is contained in:
2026-10-24 13:20:32 +02:00
parent d89912e579
commit 689e7cbe8b
12 changed files with 1595 additions and 19 deletions
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,47 @@
# Rastreabilidade, utilização e formação
## 24/10/2026 — delta L1: precisão racional, cotejo do campo06 e inventário por operação
**Incremento em QA, sem integração/homologação.** Preservados candidata `544068e8`, registo `d89912e5` e as provas dos dez tratamentos. A Tempo apreciou o relatório comunicado; não lhe é atribuída conferência do manual ou da demonstração. L1 continua aberto. As referências abaixo actualizam os limites correntes, conservando o histórico anterior.
### IVA incluído a5% — pendência de cálculo resolvida, apresentação distinta da declaração
O bloqueio de um decimal periódico era uma escolha técnica inadequada, não uma restrição fiscal da Tempo. Retirado. Para preço brutoG, conserva-se exactamente base=G×20/21 e IVA=G/21, isto é, a identidade aritmética decorrente de contraprestação+5%IVA (CIVA arts.15(10)/17-A, Lei22/2022 art.1/2, PDF3–4, pp.2252(27)–(28)). Não se altera a taxa nem se cria um novo regime. O art.34 na redacção Lei10/2025 (PDF14, p.2180(14)) contém o divisor116: não é usado como fórmula para a taxa5%.
- **Cálculo/persistência:** fracções reduzidas com numerador/denominador inteiros em unidadesMZN, sem ponto flutuante. Exemplo1MZN→base20/21 e IVA1/21. Guardam-se nas linhas e nos totais por período. As parcelas condicionadas14–18 transportam as fracções, sem somar aproximações das linhas.
- **Apresentação:** seis casas, metade para cima, identificadas como apresentação técnica. Mostram-se também as fracções exactas. Exemplo1/21→0.047619; não se perde o resto no valor calculado.21linhas de1MZN produzem IVA total1MZN, não0.999999. A resolução decimal não é precisão limite do cálculo.
- **Regra declarativa:** não determinada por escolher seis casas de apresentação. Esta implementação não afirma como a AT arredonda o campo declarativo a5%, nem bloqueia por isso a simulação. Cálculo, recuperação e relatório estão disponíveis; preenchimento/submissão oficial requerem a regra específica, no recorte declarativo próprio.
- **Preservação:** nova versão `iva-operations-l1/2`; resultados/entradas v1 permanecem guardados e legíveis. Não há recálculo automático dos dez tratamentos aceites. Outra oportunidade conserva a sua versão original, sem alteração. Origem alterada/incompleta não conserva indevidamente uma fracção apresentada como resultado actual.
### Campo06 — cotejo concluído no acervo acessível; correspondência executável ainda pendente
1. **Exemplares recebidos:** DOCX SHA62c198e9/63d45fa8 e `VAT Return.pdf` SHA9c0b99a8, p.1/quadro8:05 «Vendas isentas com direito a dedução»;06 remete para18(1)(b);07 isentas sem direito;08–10 dedução de compras. As instruções/rodapés existentes nos exemplares não discriminam os conjuntos05/06. Originais inalterados; não se repetiu a prova dos restantes campos.
2. **Lei:** CIVA republicado2016 PDF14, p.1426(272),18(1)(b)=imposto devido na importação. PDF15, p.1426(273),19(1)(b)(i–v)=operações que permitem o exercício do direito, com múltiplos ramos; operações no estrangeiro são apenas o subponto(ii). Lei10/2025 PDF13 mantém a estrutura de19(1) e altera19(2). Não se deduz daqui uma regra «todo o estrangeiro vai para06», nem se coloca IVA de importação numa base de vendas.
3. **Formulário antigo da AT:** [e-declaração, Modelo A](https://edeclaracao.at.gov.mz/formularios/iva/view/formulario_iva_ma.aspx), conteúdo oficial indexado consultado24/10:03 contém a remissão18(1)(b),04 isentas sem direito,05 imobilizado,06 existências,07 outros bens/serviços; taxa17%/remissão16. Portanto, a instrução antiga para «campo06» não corresponde ao campo06 da edição recebida. A consulta directa da página terminou em timeout; a leitura é identificada como conteúdo indexado, não sessão preenchida/submetida.
4. **Publicação localizada:** BR72 de15/04/2019, despacho/art.1(a), identificado no índice primário com aprovação do Modelo A e exemplar a17%. O ficheiro integral em `archive.gazettes.africa` respondeu **HTTP403**; não se declara leitura integral nem se usa o índice como instrução inexistente de05/06 da edição recebida. Não se contornou a restrição.
**Conclusão delimitada:** os números pertencem a layouts distintos e a remissão recebida não coincide com o texto legal consultado. A associação do antigo03 ao actual06 por simples deslocamento numérico continua insuficiente, porque existe também05 na edição recebida. Falta a instrução dessa edição que distinga os conjuntos05/06 ou uma correspondência administrativa expressa aplicável. Essa é a dependência de `AUD-SIM-L1-P001`. **Não se declara campo06 resolvido/activado.** A interface explica o cotejo; a localização da operação estrangeira é analisada independentemente desse número. O cálculo das linhas já delimitadas continua disponível; totais que dependem de uma classificação não determinada permanecem indeterminados.
Fontes locais verificáveis: [CIVA2016](../simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf), [Lei22/2022](../simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf), [Lei10/2025 no BR248](../simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf). A fonte oficial é aceite; a pendência é a correspondência concreta/edição, não nova validação da sua credibilidade nem aprovação jurídica genérica.
### Restante cobertura — operações individualizadas na mesma matriz
A linha-mãeAUD-SIM-INT02 conserva os fechos e passa a remeter para **133 linhas de cobertura `AUD-SIM-L1-P001`–`AUD-SIM-L1-P133` na matriz CSV existente**, sem outro backlog. São ramos legais/operacionais do saldo anteriormente agregado, não133 novos impostos ou lotes. Cada linha identifica operação, artigo/fonte, dependência exacta, dados/condições em falta, próximo incremento e critério de percurso completo; nenhum desses ramos é declarado implementado por este inventário.
Sequência dos incrementos do saldo: **R1** incidência/territorialidade e momento; **R2** isenções internas por natureza/entidade e listas; **R2T** limites temporais/cessação; **R3E** exportações/assimiladas; **R3Z** regimes/benefícios especiais; **R4** bases e composição da contraprestação. Os serviços transfronteiriços/digitais e operações assimiladas constam individualmente emR1D/R1S, não numa linha «outros». **R3I** delimita isenções na importação, conservando os cálculos/documentos/dedução deL2 e os direitos deL13 nos lotes próprios. A dependência06 éD06; o arredondamento declarativo está separado do cálculo racional disponível e do futuro fecho declarativoL3.
As linhas discriminam as alíneas ainda não implementadas dos arts.9,12,13,14,15 e as operações/localizações específicas dos arts.3/4/6/7/8. Produtos abrangidos por anexos exigem posição identificada; açúcar/óleo/sabões e solar têm dependência temporal concreta, sem prorrogação presumida para2026. Fontes antigas com regimes/benefícios especiais têm a alteração aplicável indicada como dependência antes de activar esse ramo. SISA/IPRA permanecemL15/L16 por analisar, esforço separado; não foram implementados nem somados à estimativa preliminar.
### Provas deste delta e estado da entrega
**Seis testes frontend novos aprovados**, exclusivamente no delta: fracções periódicas, total antes da apresentação, gravação/recuperação e preservação do snapshot aceite da primeira oportunidade, totais14–18/relatório, informação insuficiente/origem alterada e não associação automática ao campo06. Não executada novamente a suite dos dez tratamentos aceites.
**API real QA:1teste/10asserções**, base nominal isolada `simulator_fiscal_delta_20261021_test:55441`, guarda de identidade e transacções revertidas. Preenchimento/calculadora real com três preços periódicos1/100/0.02MZN, na segunda oportunidade310→gravação privada→recuperação exacta das fracções/resultados e snapshot309 original→relatório HTML/CSV sem recálculo. Interface distingue fracções e decimais, incluindo total04=5051/1050MZN, apresentado4.810476. Prova visual dirigida apenas ao delta. Sem novo PDF exigido ou regeneração dos PDFs aceites.
TypeScript/build conferidos; código frontend e representação racional reutilizam a API/rascunho e exportador existentes. Nenhum motor/parametrização fiscal publicado, alteração de schema/permissões/dados activos ou entrega na homologação. O recibo integrado `iva_l1_followup_qa_20261024` regista a candidata, artefactos e dependência documental06 ainda aberta. As544–912h continuam preliminares; este refinamento não é medição de esforço nem orçamento adicional aprovado.
## 24/10/2026 — L1 executável em QA: operações de IVA, cobertura e saldo explícitos
**Estado: incremento executado em QA, não entregue na homologação; não é fecho integral de L1 nem do IVA.** Base `e8c39a1b`, descendente da entrega `1655f57e`. Ramo `codex/iva-l1-20261024`; candidata executavel `544068e8`. Preservados os fechos S1–S3, aquisições08–10/totais14–18 e referência `276d833a`. Mantém-se o plano único abaixo; o inventário admite omissões e recebe SISA/IPRA para análise antes do futuro Imobiliário. Nenhum imposto/regra publicado ou activado no destino, schema/configuração/permissões/dados activos alterados.
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
import { modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
@@ -12,6 +12,7 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
<h4 className="font-semibold">Operações de IVA — enquadramento das vendas</h4>
<p>Oportunidade {String(draft.context_opportunity||'entrada geral')}; moeda original {book.currency||'por indicar'}. Cobertura2026/MZN. Reutilize as vendas e complete apenas o enquadramento. Este bloco classifica as operações; não substitui nem soma novamente a execução mensal geral acima.</p>
<p>Uma empresa pode ter operações com tratamentos diferentes. Os valores são projecções sob pressupostos declarados, sem certificar facturas, reconhecer créditos ou submeter uma declaração à AT.</p>
<details><summary>Correspondência do campo06 e limites do formulário</summary><p>{modeloA06Correspondence}</p></details>
{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>run(()=>save(reuseSales(draft,book)))}>Reutilizar vendas e períodos existentes</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>run(()=>save({...book,rows:[...book.rows,newSalesOperation(crypto.randomUUID(),book.periods[0].period,book.currency)]}))}>Acrescentar operação em falta</button>
@@ -33,7 +34,7 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
<button type="button" className="underline" onClick={()=>run(()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id),periods:book.periods.map(p=>p.period===row.period?{...p,complete:false}:p)}))}>Remover esta linha do preenchimento (resultado anterior conservado)</button>
</details>;})}
{book.periods.map(p=><label className="block" key={p.period}><input type="checkbox" checked={p.complete} onChange={e=>run(()=>save({...book,periods:book.periods.map(v=>v.period===p.period?{...v,complete:e.target.checked}:v)}))}/> {p.period}: incluí todas as operações deste período; lista vazia significa ausência expressamente confirmada.</label>)}
<p>Limites: outras isenções/bases especiais e correspondência do campo06 permanecem por delimitar. IVA incluído a5% só é calculado quando a inversão é exacta até6casas; nos restantes casos falta delimitar o arredondamento e não se usa divisor116. O desconhecido nunca é assumido como zero.</p>
<p>Limites: outras isenções/bases especiais e correspondência do campo06 permanecem por delimitar. IVA incluído a5% conserva a fracção exacta e apresenta um decimal a6casas; a aproximação de apresentação não constitui regra declarativa. O desconhecido nunca é assumido como zero.</p>
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordSales(draft,book)))}>Calcular e guardar operações IVA</button>
{book.result&&<><p role="status">{salesCurrent(draft,book)?'Resultado correspondente às entradas.':'Resultado histórico conservado; reveja as alterações antes de recalcular.'}</p><dl>{salesReportRows(draft).map(([,label,value,origin],index)=><div className="break-inside-avoid border-t py-2" key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
+22
View File
@@ -0,0 +1,22 @@
/** Exact amounts in currency units; decimal formatting is presentation, never a tax rule. */
export type FiscalFraction = { numerator:string; denominator:string };
function parts(value:FiscalFraction):[bigint,bigint] {
if(!/^-?\d{1,80}$/.test(value.numerator)||!/^\d{1,80}$/.test(value.denominator)||BigInt(value.denominator)===0n)throw new Error('Representação racional inválida; sem substituição por zero.');
return [BigInt(value.numerator),BigInt(value.denominator)];
}
export function fraction(n:bigint,d=1n):FiscalFraction {
if(d===0n)throw new Error('Denominador nulo.');if(d<0n){n=-n;d=-d;}
let a=n<0n?-n:n,b=d;while(b){const r=a%b;a=b;b=r;}return {numerator:String(n/(a||1n)),denominator:String(d/(a||1n))};
}
export function decimalFraction(raw:string):FiscalFraction {
if(!/^\d{1,18}(\.\d{1,6})?$/.test(raw))throw new Error('Montante decimal inválido.');const [a,b='']=raw.split('.');return fraction(BigInt(a+b),10n**BigInt(b.length));
}
export function addFractions(values:FiscalFraction[]):FiscalFraction {return values.reduce((a,b)=>{const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd+bn*ad,ad*bd);},fraction(0n));}
export function subtractFractions(a:FiscalFraction,b:FiscalFraction):FiscalFraction {const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd-bn*ad,ad*bd);}
export function positiveFraction(value:FiscalFraction):FiscalFraction {return parts(value)[0]>0n?value:fraction(0n);}
export function fractionPresentation(value:FiscalFraction):{decimal:string;approximate:boolean} {
const [raw,d]=parts(value),n=raw<0n?-raw:raw,scaled=n*1000000n;
const q=(scaled+d/2n)/d,dec=(q%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0');
return {decimal:`${raw<0n?'-':''}${q/1000000n}.${dec}`,approximate:scaled%d!==0n};
}
export function fractionLabel(value:FiscalFraction):string {parts(value);return `${value.numerator}/${value.denominator}`;}
+14 -1
View File
@@ -2,6 +2,7 @@ import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts';
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts';
export const acquisitionVersion = 'modelo-a-internal-acquisitions/1';
export const acquisitionBasis = 'CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5),19(1)(a),19(2),20 (Lei22/2022),21(1–3); âmbito interno, afectação exclusiva, mesmo período, sem exclusões ou dedução parcial.';
@@ -19,7 +20,7 @@ export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gr
type PeriodInput = {period:string; complete:boolean; absent:string[]};
export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation};
export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string;
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; reasons:string[]}>};
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; exactFields?:Record<string,FiscalFraction|null>; reasons:string[]}>};
type Store = {version:1; books:Record<string,AcquisitionBook>};
const excluded = ['03','04','05','06','07','11','12','13'];
export const acquisitionExcludedFields = excluded;
@@ -165,6 +166,17 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
const state=amount(fields['16']),taxpayer=amount(fields['15']);
fields['17']=state===null||taxpayer===null?null:money(state>taxpayer?state-taxpayer:0n);
fields['18']=state===null||taxpayer===null?null:money(taxpayer>state?taxpayer-state:0n);
const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null;
if(classified?.exactFields) {
const exactFields:Record<string,FiscalFraction|null>={};
for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified.exactFields[f]??(fields[f]===null?null:decimalFraction(fields[f]!));
for(const [f,parts] of [['14',['01','03','05','06','07']],['15',['08','09','10','11','12']],['16',['02','04','13']]] as const)exactFields[f]=parts.some(k=>exactFields[k]===null)?null:addFractions(parts.map(k=>exactFields[k]!));
exactFields['17']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['16'],exactFields['15'])):null;
exactFields['18']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['15'],exactFields['16'])):null;
for(const f of Object.keys(exactFields))fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
reasons.push('Totais derivados das fracções exactas, sem somar aproximações das linhas; decimais a6casas são apresentação técnica, não arredondamento declarativo.');
return {period,fields,exactFields,reasons};
}
return {period,fields,reasons};
});
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources,declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
@@ -188,6 +200,7 @@ export function acquisitionReportRows(draft:Record<string,unknown>):Array<[strin
const declarationOrigin=declaration?`Aquisições08–10 ${declaration.complete?'declaradas completas':'por completar'}; ausência de operações expressamente declarada nos campos ${declaration.absent.join(', ')||'nenhum'}.`:'Declarações detalhadas não recuperáveis nesta versão histórica.';
out.push(['IVA mensal',`Modelo A — ${period.period} — parcelas01–13`,entries.filter(([field])=>Number(field)<=13).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),`${context}; ${declarationOrigin}`]);
out.push(['IVA mensal',`Modelo A — ${period.period} — totais14–18`,entries.filter(([field])=>Number(field)>=14).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),context]);
if(period.exactFields)out.push(['IVA mensal',`Modelo A — ${period.period} — precisão exacta`,Object.entries(period.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),'Fracções em MZN; apresentação decimal técnica, sem regra declarativa presumida.']);
if(period.reasons.length)out.push(['IVA mensal',`Modelo A — ${period.period} — pendências`,period.reasons.join(' '),result.sources]);
}
return out;
+26 -13
View File
@@ -1,10 +1,12 @@
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
export const operationVersion = 'iva-operations-l1/1';
export const operationVersion = 'iva-operations-l1/2';
const source2022 = 'Lei22/2022, BR250, 2.º suplemento, PDF1–4 (2252(25)–(28)); arts.1/2/6; efeitos01/01/2023';
const source2016 = 'CIVA republicado Lei13/2016, BR156, 10.º suplemento, PDF9–15 (1426(267)–(273))';
const source2025 = 'Lei10/2025, BR248, suplemento1, PDF14–15 (2180(14)–(15)); arts.1/2/3/5; efeitos01/01/2026';
export const modeloA06Correspondence = 'Exemplar recebido:06 é base de operações isentas, com remissão18(1)(b); nessa lei18(1)(b) trata IVA de importações, enquanto19(1)(b) enumera operações com direito. O mesmo exemplar já tem05 para vendas isentas com direito. O formulário antigo da AT usa03 para a remissão e06 para existências. Não há correspondência unívoca nem instrução aplicável que separe05/06 no acervo conferido: não atribuir06 automaticamente a operações no estrangeiro ou importar instruções antigas por número.';
export type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[]};
export const operationTreatments:OperationTreatment[] = [
{id:'general',label:'Operação interna geral',field:'01',rate:16,article:'CIVA arts.1,6,15(1),17(1–2); art.34 (Lei10/2025)',source:`${source2016}; ${source2022}; ${source2025}`,conditions:['Operação localizada e tributável em Moçambique, no regime normal','Contraprestação completa; sem base especial do art.15(2), isenção ou taxa reduzida']},
@@ -17,13 +19,13 @@ export const operationTreatments:OperationTreatment[] = [
{id:'public_health',label:'Serviços médicos e sanitários públicos',field:'07',rate:0,article:'CIVA arts.9(1)(a),10(1),19(1)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Estabelecimento hospitalar público, dispensário ou similar; natureza pública confirmada']},
{id:'public_education',label:'Ensino público reconhecido',field:'07',rate:0,article:'CIVA arts.9(3)(a),10(1),19(1)',source:source2022,conditions:['Ensino ou fornecimentos conexos pelo estabelecimento','Estabelecimento público integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
{id:'public_training',label:'Formação profissional pública',field:'07',rate:0,article:'CIVA arts.9(3)(b),10(1),19(1)',source:source2022,conditions:['Formação profissional ou fornecimentos conexos pelo prestador','Prestador público']},
{id:'foreign_operation',label:'Operação no estrangeiro — correspondência documental pendente',field:null,rate:null,article:'CIVA arts.6 e19(1)(b)(ii); remissão do campo06 do exemplar para18(1)(b) divergente',source:source2016,conditions:[]},
{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; 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};
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>;reasons:string[]}>;rounding:string};
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};
type Store = ReturnType<typeof readAcquisitionStore> & {salesOperations?:Record<string,SalesBook>};
export function newSalesOperation(id:string,period:string,currency:string):SalesOperation {return {id,period,description:'',establishment:'',amount:'',basis:'',treatment:'other',confirmed:[],evidence:'',sourceAmount:null,sourcePeriod:period,sourceCurrency:currency,source:'Introdução directa; projecção, não declaração submetida'};}
export function salesBook(draft:Record<string,unknown>):SalesBook {
@@ -63,15 +65,17 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
const stop=(reason:string)=>({...result,reason});
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(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência e fundamento; campo06 mantém divergência de remissão, sem impedir outras linhas.');
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.');
if(rule.conditions.some((_,i)=>!row.confirmed.includes(i))||!row.evidence.trim())return stop('Condições da operação ou fundamento/pressuposto em falta; o sector não prova o tratamento fiscal.');
if(row.sourceAmount!==null&&amount(row.sourceAmount)!==n)return stop('Divergência com o valor reutilizado: conserve a origem e prepare a repartição explicitamente; não substituir silenciosamente.');
if(row.basis==='included'&&rule.rate===5&&(n*100n)%105n!==0n)return stop('Preço com IVA a5%: a inversão exacta excede6casas; falta delimitar o arredondamento aplicável. Art.34, redacção2026, explicita divisor116; não reutilizar esse divisor nem inventar arredondamento para105. Pode indicar base líquida documentada.');
if(row.basis==='included'&&rule.rate===5){
const exact={net:fraction(n*20n,21000000n),vat:fraction(n,21000000n),gross:fraction(n,1000000n)};
return {...result,net:fractionPresentation(exact.net).decimal,vat:fractionPresentation(exact.vat).decimal,gross:money(n),exact,status:'calculated',reason:'Base e IVA conservados como fracções exactas (preço×20/21 e preço/21). Decimal a6casas é apresentação técnica, metade para cima; não é regra de arredondamento da declaração nem imposto final a pagar.'};
}
let net=n,vat=0n;
if(rule.rate===16&&row.basis==='included') {net=((n*100n+58000000n)/116000000n)*1000000n;vat=n-net;if(vat<0n)return stop('Arredondamento à unidade produz IVA negativo para este montante; reveja a linha e a documentação.');}
else if(rule.rate===5&&row.basis==='included'){net=n*100n/105n;vat=n-net;}
else vat=(n*BigInt(rule.rate)+50n)/100n;
return {...result,net:money(net),vat:money(vat),gross:money(row.basis==='included'?n:net+vat),status:rule.rate===0?'exempt':'calculated',reason:rule.rate===0?(rule.field==='05'?'Isenção com direito à dedução nas condições legais; não é taxa zero nem dedução automática de compras.':'Isenção sem direito à dedução para esta operação; não é isenção geral da entidade.'):'IVA da operação projectada; não é imposto final a pagar nem declaração submetida.'};
}
@@ -87,31 +91,40 @@ export function calculateSales(draft:Record<string,unknown>,book:SalesBook,now=n
const result=evaluateSalesOperation(r,book.currency);
if(r.splitSource) {
const source=r.splitSource,group=book.rows.filter(v=>v.splitSource?.id===source.id),index=Number(source.period.slice(5))-Number(start.slice(5));
if(group.some(v=>v.period!==source.period||v.basis!==source.basis||v.sourceCurrency!==source.currency||JSON.stringify(v.splitSource)!==JSON.stringify(source))||sum(group.map(v=>v.amount))!==(amount(source.amount)===null?null:money(amount(source.amount)!))||!live||live.amounts[index]!==source.amount)return {...result,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'Repartição não reconciliada com o total, período, moeda e representação do preço da origem; nenhuma parcela foi presumida.'};
if(group.some(v=>v.period!==source.period||v.basis!==source.basis||v.sourceCurrency!==source.currency||JSON.stringify(v.splitSource)!==JSON.stringify(source))||sum(group.map(v=>v.amount))!==(amount(source.amount)===null?null:money(amount(source.amount)!))||!live||live.amounts[index]!==source.amount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'Repartição não reconciliada com o total, período, moeda e representação do preço da origem; nenhuma parcela foi presumida.'};
}
if(r.sourceAmount!==null||r.source.startsWith('Vendas por produto')||r.source.startsWith('Receita mensal')) {
const index=Number(r.sourcePeriod.slice(5))-Number(start.slice(5));
if(!live||!/^2026-/.test(start)||r.sourcePeriod!==r.period||live.amounts[index]!==r.sourceAmount)return {...result,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'A origem/período das vendas mudou. Reveja e confirme a nova origem; o resultado anterior foi conservado.'};
if(!live||!/^2026-/.test(start)||r.sourcePeriod!==r.period||live.amounts[index]!==r.sourceAmount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'A origem/período das vendas mudou. Reveja e confirme a nova origem; o resultado anterior foi conservado.'};
}
return result;
});
const periods=book.periods.map(p=>{
const matching=book.rows.map((r,i)=>({input:r,result:rows[i]})).filter(r=>r.input.period===p.period),fields:Record<string,string|null>={},reasons=matching.filter(r=>r.result.status==='indeterminate').map(r=>`${r.input.description||r.input.id}: ${r.result.reason}`);
const matching=book.rows.map((r,i)=>({input:r,result:rows[i]})).filter(r=>r.input.period===p.period),fields:Record<string,string|null>={},exactFields:Record<string,FiscalFraction|null>={},reasons=matching.filter(r=>r.result.status==='indeterminate').map(r=>`${r.input.description||r.input.id}: ${r.result.reason}`);
const known=p.complete&&!matching.some(r=>r.result.status==='indeterminate');
for(const f of ['01','03','05','07'])fields[f]=known?sum(matching.filter(r=>r.result.field===f).map(r=>r.result.net)):null;
for(const [f,base] of [['02','01'],['04','03']])fields[f]=known?sum(matching.filter(r=>r.result.field===base).map(r=>r.result.vat)):null;
fields['06']=known?'0.00':null;
if(matching.some(r=>r.result.exact)) {
for(const [f,base,part] of [['01','01','net'],['02','01','vat'],['03','03','net'],['04','03','vat'],['05','05','net'],['06','06','net'],['07','07','net']] as const) {
exactFields[f]=known?addFractions(matching.filter(r=>r.result.field===base).map(r=>r.result.exact?.[part]??decimalFraction(r.result[part]!))):null;
fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
}
}
if(!p.complete)reasons.push('Confirme que todas as operações do período foram incluídas; uma lista vazia só é zero após essa confirmação.');
return {period:p.period,fields,reasons};
return {period:p.period,fields,...(Object.keys(exactFields).length?{exactFields}:{}),reasons};
});
const labels=draft.selection_labels as Record<string,unknown>|undefined;
return {version:operationVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:acquisitionScope(draft),location:['province_id','district_id'].filter(k=>draft[k]).map(k=>String(labels?.[k]??draft[k])).join(' / '),currency:book.currency,key:key(draft,book),inputs:structuredClone(book.rows),rows,periods,rounding:'Base líquida: precisão técnica6decimais, metade para cima, sem arredondamento final da declaração. Preço geral com IVA: base=preço×100/116, arredondada à unidade mais próxima (metade para cima), IVA=preço−base, CIVA art.34 redacção Lei10/2025. Preço incluído5%: apenas inversão exacta da contraprestação×1,05 (arts.15(10)/17-A), sem arredondar; quando exige mais de6casas permanece indeterminado.'};
return {version:operationVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:acquisitionScope(draft),location:['province_id','district_id'].filter(k=>draft[k]).map(k=>String(labels?.[k]??draft[k])).join(' / '),currency:book.currency,key:key(draft,book),inputs:structuredClone(book.rows),rows,periods,rounding:'Base líquida: precisão técnica6decimais, metade para cima, sem arredondamento final da declaração. Preço geral com IVA: base=preço×100/116, arredondada à unidade mais próxima (metade para cima), IVA=preço−base, CIVA art.34 redacção Lei10/2025. Preço incluído5%: fracções exactas preço×20/21 e preço/21 conservadas; decimal a6casas/metade para cima apenas na apresentação. A regra declarativa não é presumida.'};
}
export function salesReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]> {
try {const book=salesBook(draft),r=book.result;if(!r)return [];
const context=`${r.currency}; oportunidade ${r.opportunity}; ${r.location||'localização não indicada'}; ${r.version}; revisão ${r.revision}; ${r.calculatedAt}`;
const out:Array<[string,string,string,string]>=[['IVA mensal','Operações IVA L1',salesCurrent(draft,book)?'Resultado correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Método e limites L1',r.rounding,'Sem apuramento integral, dedução automática ou submissão AT']];
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 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(' ')}`]);return out;
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.`]);
}return out;
}catch{return [['IVA mensal','Operações IVA L1','Não foi possível recuperar as operações guardadas','Originais preservados; sem substituição por zero']];}
}
+38
View File
@@ -0,0 +1,38 @@
<?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 IvaRationalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_periodic_results_and_accepted_first_opportunity_round_trip_exactly(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$path = getenv('IVA_RATIONAL_INPUT') ?: base_path('tests/frontend/fixtures/iva-rational.json');
$draft = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
$store = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$first = $store['salesOperations']['opportunity:309'];
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$economic = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $economic['modelo_a_snapshot']);
$saved = json_decode($economic['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($first, $saved['salesOperations']['opportunity:309']);
$second = $saved['salesOperations']['opportunity:310']['result'];
$this->assertSame('iva-operations-l1/2', $second['version']);
$this->assertSame(['numerator' => '20', 'denominator' => '21'], $second['rows'][0]['exact']['net']);
$this->assertSame(['numerator' => '1', 'denominator' => '21'], $second['rows'][0]['exact']['vat']);
$this->assertSame('0.047619', $second['rows'][0]['vat']);
if (getenv('IVA_RATIONAL_RECOVERED')) file_put_contents(getenv('IVA_RATIONAL_RECOVERED'), json_encode($economic, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,9 @@
import fs from 'node:fs';
import {newSalesOperation,recordSales,saveSalesBook} from '../../../resources/js/lib/iva-operations.ts';
export function l1DeltaFixture(){
const draft=JSON.parse(fs.readFileSync(new URL('iva-operations.json',import.meta.url),'utf8'));
draft.context_opportunity='opportunity:310';draft.project_title='SYNTHETIC QA — L1 precisão racional';
const context=JSON.parse(draft.context_applicability_snapshot);context.opportunity_choices.push({opportunity:{key:'opportunity:310',title:'SYNTHETIC QA — segunda oportunidade racional'}});draft.context_applicability_snapshot=JSON.stringify(context);
let book={currency:'MZN',rows:['1','100','0.02'].map((amount,i)=>({...newSalesOperation(`rational-${i}`,'2026-01','MZN'),amount,description:`SYNTHETIC QA periódico ${i+1}`,establishment:'SYNTHETIC QA estabelecimento',basis:'included',treatment:'private_health',confirmed:[0,1],evidence:'SYNTHETIC QA — pressupostos já delimitados do tratamento, preço incluído'})),periods:[{period:'2026-01',complete:true}]};
book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};
}
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {l1DeltaFixture} from './fixtures/iva-l1-delta-fixture.mjs';
import {calculateSales,evaluateSalesOperation,modeloA06Correspondence,salesBook,salesCurrent,saveSalesBook} from '../../resources/js/lib/iva-operations.ts';
import {calculateAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {addFractions,decimalFraction,fraction,fractionLabel,fractionPresentation} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
test('periodic included5% retains exact rational amounts; only presentation is rounded',()=>{
const {book}=l1DeltaFixture(),r=book.result.rows[0];
assert.deepEqual(r.exact.net,{numerator:'20',denominator:'21'});assert.deepEqual(r.exact.vat,{numerator:'1',denominator:'21'});
assert.equal(r.net,'0.952381');assert.equal(r.vat,'0.047619');assert.equal(r.status,'calculated');assert.match(r.reason,/apresentação técnica/);
assert.deepEqual(addFractions([r.exact.net,r.exact.vat]),r.exact.gross);
assert.deepEqual(book.result.rows[1].exact.vat,{numerator:'100',denominator:'21'});
});
test('aggregation uses exact fractions, not21 rounded values',()=>{
const {draft,book}=l1DeltaFixture();book.rows=Array.from({length:21},(_,i)=>({...book.rows[0],id:`q${i}`}));const r=calculateSales(draft,book);
assert.equal(r.rows.every(row=>row.vat==='0.047619'),true);assert.equal(r.periods[0].fields['04'],'1.00');assert.deepEqual(r.periods[0].exactFields['04'],{numerator:'1',denominator:'1'});assert.equal(r.periods[0].fields['03'],'20.00');
});
test('new version round trips by opportunity, keeping accepted first snapshot untouched',()=>{
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-operations.json',import.meta.url),'utf8'));const original=JSON.parse(accepted.modelo_a_snapshot).salesOperations['opportunity:309'];
const {draft,book}=l1DeltaFixture();const saved=JSON.parse(JSON.stringify({...draft,modelo_a_snapshot:saveSalesBook(draft,book)}));
assert.deepEqual(JSON.parse(saved.modelo_a_snapshot).salesOperations['opportunity:309'],original);assert.equal(salesCurrent(saved,salesBook(saved)),true);assert.deepEqual(salesBook(saved).result.rows[2].exact,book.result.rows[2].exact);
});
test('conditional14-18 and report carry exact precision rather than rounded line amounts',()=>{
const {draft,book}=l1DeltaFixture();const a={currency:'MZN',rows:[],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:['03','04','05','06','07','11','12','13']}))};
const result=calculateAcquisitions(draft,a);assert.equal(fractionLabel(result.periods[0].exactFields['17']),'5051/1050');assert.equal(result.periods[0].fields['17'],'4.810476');
a.result=result;draft.modelo_a_snapshot=saveAcquisitionBook(draft,a);const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance'])}));assert.match(report,/5051\/1050/);assert.match(report,/20\/21/);assert.match(report,/opportunity:310/);
assert.match(report,/apresentação técnica/);assert.equal(book.result.rows.length,3);
});
test('missing inputs still block only the affected result; invalid fractions do not become zero',()=>{
const {book}=l1DeltaFixture();assert.equal(evaluateSalesOperation({...book.rows[0],amount:''},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...book.rows[0],basis:''},'MZN').vat,null);assert.equal(evaluateSalesOperation({...book.rows[0],amount:'0'},'MZN').vat,'0.00');
const {draft}=l1DeltaFixture();book.rows[0].sourceAmount='1';const stale=calculateSales(draft,book).rows[0];assert.equal(stale.vat,null);assert.equal(stale.exact,undefined);
assert.throws(()=>fractionPresentation({numerator:'1',denominator:'0'}));assert.equal(fractionPresentation(fraction(1n,21n)).approximate,true);assert.equal(fractionPresentation(decimalFraction('105')).approximate,false);
});
test('form06 is not automatically inferred from a foreign operation or old stock instructions',()=>{
const {book}=l1DeltaFixture();const r=evaluateSalesOperation({...book.rows[0],treatment:'foreign_operation'},'MZN');assert.equal(r.field,null);assert.equal(r.vat,null);assert.match(modeloA06Correspondence,/existências/);assert.match(modeloA06Correspondence,/separe05\/06/);
});
if(process.env.IVA_L1_DELTA_OUT)fs.writeFileSync(process.env.IVA_L1_DELTA_OUT,JSON.stringify(l1DeltaFixture().draft,null,2));
+2 -2
View File
@@ -20,13 +20,13 @@ test('each enumerated treatment calculates only with its conditions, article and
}
const by=Object.fromEntries(book.rows.map(r=>[r.treatment,evaluateSalesOperation(r,'MZN')]));assert.equal(by.private_health.vat,'5.00');assert.equal(by.export_goods.field,'05');assert.match(by.export_goods.reason,/não é taxa zero/);assert.equal(by.housing_rent.field,'07');
});
test('VAT-included general uses statutory unit rounding, conserves gross; 5 percent included is explicitly unresolved',()=>{
test('VAT-included general uses statutory unit rounding, conserves gross; included5percent presentation is covered by the rational delta',()=>{
const row=operationsFixture().book.rows[0];const r=evaluateSalesOperation(row,'MZN');assert.equal(r.net,'1001.00');assert.equal(r.vat,'160.00');assert.equal(r.gross,'1161.00');
assert.equal(evaluateSalesOperation({...row,amount:'1160'},'MZN').net,'1000.00');
assert.equal(evaluateSalesOperation({...row,basis:'net',amount:'1000.03'},'MZN').vat,'160.0048');
assert.equal(evaluateSalesOperation({...row,basis:'',amount:'0'},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,amount:'0'},'MZN').vat,'0.00');
const reduced=operationsFixture().book.rows[1];assert.match(evaluateSalesOperation({...reduced,basis:'included'},'MZN').reason,/arredondamento/);
const reduced=operationsFixture().book.rows[1];assert.equal(evaluateSalesOperation({...reduced,basis:'included'},'MZN').vat,'4.761905');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').net,'100.00');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').vat,'5.00');
});