feat(iva): add own-production exemptions and dated tax options in QA
This commit is contained in:
@@ -6446,5 +6446,65 @@
|
||||
"installed-public-delta.json": "69250defa1f92d6a2c6e034d99650b967c22b15b35f18ca64e00fefa56a36913",
|
||||
"manifest-served.json": "4018026aac013855f2c9baf9258ed96e1b1bf808d996e8fa8eec9458ea6b35c7"
|
||||
}
|
||||
},
|
||||
"iva_l1_g2_production_qa_20261024": {
|
||||
"candidate": "CANDIDATE_G2I",
|
||||
"base_delivered": "d4eae917",
|
||||
"base_receipt": "7696af68",
|
||||
"status": "demonstrated_QA_not_installed",
|
||||
"group": "G2I within G2",
|
||||
"branches": {
|
||||
"AUD-SIM-L1-P019": [
|
||||
"production_agriculture"
|
||||
],
|
||||
"AUD-SIM-L1-P020": [
|
||||
"production_forestry",
|
||||
"production_forestry_opted"
|
||||
],
|
||||
"AUD-SIM-L1-P021": [
|
||||
"production_livestock",
|
||||
"production_livestock_opted"
|
||||
],
|
||||
"AUD-SIM-L1-P022": [
|
||||
"production_fishing",
|
||||
"production_fishing_opted"
|
||||
]
|
||||
},
|
||||
"branch_count": 4,
|
||||
"treatment_count": 7,
|
||||
"option_logic_version": "production-option/1",
|
||||
"scope": "2026/MZN;9(7)a-d;11;19(1);05/07exempt;01/02opted",
|
||||
"tests": {
|
||||
"frontend": 3,
|
||||
"api_tests": 1,
|
||||
"api_assertions": 14,
|
||||
"UI_to_API_to_report": true,
|
||||
"types": "passed",
|
||||
"build": "passed"
|
||||
},
|
||||
"preservation": "Nine frozen books unchanged/current without recalculation;29delivered matrix rows and prior receipt objects unchanged",
|
||||
"counts": {
|
||||
"delivered": 29,
|
||||
"demonstrated_QA": 4,
|
||||
"functional_pending": 99,
|
||||
"documentary_pending": 1,
|
||||
"total": 133
|
||||
},
|
||||
"limits": [
|
||||
"Startup29Feb/returnDeclaration28Febinanniversaryyear5indeterminate;noinventedcalendarconvention;doesnotblockotherdates",
|
||||
"NohistoricalVAT/noautomaticdeduction/noATsubmission"
|
||||
],
|
||||
"homologation_unchanged": true,
|
||||
"P001_scope": "05/06only;notresolved",
|
||||
"L1_complete": false,
|
||||
"artifacts": {
|
||||
"api-ui-tests.xml": "006dfe35025130c9324aa9a976bebca3fbf5e3b9e1033b13e23bff72db205a8b",
|
||||
"production-model.json": "607c11588600d2813ba5082d7f222d453d6012254369fefd54a3ba84567e7f14",
|
||||
"production-report.html": "6b6ad322545f1e13300c0cc8144bef827edba8a236a8ae1dc56cb5424e8db826",
|
||||
"recovered.json": "3ba8fa877c5faf7e16f056389bdfda4dd6f9bee449e88f8ce3bf0b4f6196106f",
|
||||
"report-qa.png": "5fb20c08fe19621ab91a217f34498b96b3a0d699b91f07bcb10e272b30c1f57d",
|
||||
"ui-calculated.json": "2ae7bdbb601b3f3707a7189f2e95170721cf206dd178fa2335eabe485e95a3f3"
|
||||
},
|
||||
"explicit_option_reuse": "Source chosen and confirmed;operationDate target20Oct preserved vs source15Oct;origin in recovered snapshot/report;revision5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# Rastreabilidade, utilização e formação
|
||||
|
||||
## 24/10/2026 — G2I produção própria e opção tributada em QA
|
||||
|
||||
**Candidata CANDIDATE_G2I; quatro ramos P019–P022, sete tratamentos.** Base instalada d4eae917/recibo7696af68;29ramosentreguespreservadossemreensaios. Homologação inalterada nesta passagem;L1aberto.
|
||||
|
||||
| Ramo | Operações / campos | Fonte exacta |
|
||||
| --- | --- | --- |
|
||||
| P019 | Produção agrícola,incluindoavicultura/apicultura;05,isençãocomdireitonascondiçõeslegais;semopção11 | CIVA9(7)(a),19(1)(b)(v) |
|
||||
| P020 | Produçãosilvícola;07isençãoou01/02poropçãoeficaz | CIVA9(7)(b),11,19(1) |
|
||||
| P021 | Produçãopecuária;07isençãoou01/02poropçãoeficaz | CIVA9(7)(c),11,19(1) |
|
||||
| P022 | Pesca;07isençãoou01/02poropçãoeficaz | CIVA9(7)(d),11,19(1) |
|
||||
|
||||
CIVA republicado Lei13/2016PDF10,p.1426(268),9(7):própriaprodução,recursospróprios;transformaçãoapenasacessória,pelopróprioprodutorsobreprodutospróprios,pormeiosgeralmenteutilizadosnasexplorações.Nãoinferirisençãoapartirdosectorouaplicararevenda. PDF11,p.1426(269),11(1)limitaopçãoàs alíneasb/c/d;11(2)define declaração àDirecçãodaÁreaFiscalcompetente,efeitos1Janeiroanoseguinteouiníciodaactividadesenarespectivadeclaração;11(3)impõeperíodomínimodecincoanos,continuidadeatédeclaraçãoadequadaderegressocom efeitosno1Janeiroanoseguinte.PDF15,p.1426(273),19(1)(b)(v)enumeraapenas9(7)(a)paradireitonaisençãodestegrupo. Lei22/2022art1PDF2,p.2252(26),mantém9(7),taxanormal16%no17(1);Lei10/2025art34paraIVAincluídogeral,semalterar11.Reutilizadoocotejoposteriorregistado;nenhumaautenticaçãogenéricadafonterepetida.
|
||||
|
||||
**Dados e regras executáveis:**reutilizamoportunidade,localização,moeda,valor,representaçãodopreço,período,condiçõese fundamento.Sónaopçãotributada aparecem tipo/data da declaração,data de início quando aplicável,data da operação e estado explícito do regresso(nãoexiste/existe/aindadesconhecido);dataderegressoseexistir.Desconhecidonãoéausência;estadonegativocomdataguardadaédivergênciaa resolverexplicitamente.Nãoapagarvaloresocultosnemrepartirmontantesautomaticamente.Daopçãoanualderiva1Janeiroseguinte;opçãonadeclaraçãodeinícioderivadata de início comdeclaraçãonãoposterior;datasfora doperíodo/opçãoineficaz/regressoantesdoscincoanosoujáeficazdãolinhaindeterminada,semreclassificarautomaticamente.Terpassadocincoanosnãocessaopção.Semregressodeclarado,continuaatributação.Data/resultadodaopçãoeficazseguemno relatório e snapshot/histórico. Móduloderegra`production-option/1`,versãoespecíficasónostrêsnovostratamentosoptantes;chavesdosanterioresinalteradas.
|
||||
|
||||
**Reutilização da opção:**a origem pode ser escolhida entre as linhas do dossier; não se assume automaticamente a primeira nem a equivalência dos sujeitos passivos. Antes de substituir dados deliberados, a interface explica o impacto e pede confirmação. A data específica da operação de destino é conservada; a origem copiada segue no snapshot e no relatório. Prova dirigida: reutilizar de g2i-2(data da operação15/10) para g2i-4 preservou20/10, com declaração/estado comuns e origem identificada; revisão final5 recuperada pela API.
|
||||
|
||||
**Limitação delimitada:**inícioem29/02edeclaraçãoderegressoem28/02doquintoano,semcorrespondênciadeaniversárionocalendário,nãoéresolvidopelasfontesfiscaisconferidas.Permaneceindeterminadonestecaso,sem inventar convenção fiscal;declarar em data inequivocamente posterior ou confirmar a regra de contagem aplicável. Élimitaçãointerna às opções P020–P022,nãonovoIDnemalargamentodeP001;asrestantesdatas/operaçõesavançam.P001continuaexclusivamente05/06.
|
||||
|
||||
**Provas dirigidas:**3testesfrontendcobrem7tratamentos,datasanuais/início,efeitosfuturos,regressodeclaradoeficaz,cincoanos,ausênciaexplícita,estadodesconhecido,divergênciadata/estado,calendárioinválido,29/02→28/02indeterminado,regressoposterior,nãocaducidadeealteraçãoqueconservaresultadohistórico.FixturecongeladaG2Hpreservanovecontextosanteriorescomentradas/resultados/chavesactuais,semrecálculo.Naamostra:01=300,00;02=48,00;05=100,00;07=300,00MZN,preçolíquido/incluído.APIprivadaPOST→GET,QAisolada `simulator_fiscal_delta_20261021_test:55441`,guardadedestino/transacçãorevertida,1teste/14asserções;snapshotintegral,datas e campos recuperados.Interface real:opçãoanualde2026→efeitos2027/indeterminado;dataválidareposta→cálculo;regressodesconhecido→indeterminado;ausênciaexplicitada→revisão3guardada;payloadreal→API→recuperação→relatórioHTMLcomdatas/efeitos/versão/valores/contexto318Manica/2026-10.MoedaMZN. Tipos/buildpassaram. Renderizadoresinalterados,provasWord/PDF/fracçõesaceitesreutilizadas,semnovaafirmaçãodepaginação.
|
||||
|
||||
**Saldo:**133IDs=29entregues+4QA+99funcionaispendentes+P001documental.G2total32=25entregues+4QA+3pendentes(P008/P016/P035).Motor70tratamentoscondicionados(10iniciais+60dosgrupos),mais2opçõesindeterminadas.Sete tratamentos correspondem a quatro ramos.29fechospreservados;semnovaentreganestapassagem.Estatísticas/IAadiadas.Artefactosprivados `outputs/iva-l1-g2-production-20261024/`.
|
||||
|
||||
|
||||
## 24/10/2026 — G2G/G2H entregues na homologação d4eae917
|
||||
|
||||
**Cinco ramos entregues:**P010(segurança por entidade pública),P027(fornecimentos oficiais elegíveis),P031(operações sujeitas aSISA),P032(alienação com três fundamentos) eP026(produtos enumerados9(10)). São24tratamentos(G2G6/G2H18),não24IDs. Revisão efectivamente servidad4eae917,integrada por fast-forward9a56d982→d4eae917apósconsulta fresca do remoto e destino SSH autenticado,semforce. Provas G2G9dbfdd67/2440cb44 eG2H48146089/b297058d reutilizadas;os24anteriores mantêm os fechos. ProcessoGit/webhook/instalador existente;zero migrações/importações/alterações de acesso ou cálculo de outro imposto.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
|
||||
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('');
|
||||
const [reuseOption,setReuseOption]=useState<{rowId:string;sourceId:string}|null>(null);
|
||||
let book:SalesBook;
|
||||
try{book=salesBook(draft);}catch(cause){return <p role="alert">{String(cause)}</p>;}
|
||||
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
|
||||
@@ -26,6 +28,14 @@ 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=>change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
|
||||
</div>
|
||||
{row.treatment.startsWith('production_')&&row.treatment.endsWith('_opted')&&<fieldset className="space-y-2 rounded border p-2"><legend>Opção de tributação da produção — artigo11</legend>
|
||||
<p>Complete apenas as datas em falta. A opção não cessa automaticamente ao fim de cinco anos. Uma declaração de regresso deve respeitar o prazo mínimo e produz efeitos no ano seguinte.</p>
|
||||
{book.rows.some(source=>source.id!==row.id&&source.taxOption)&&<label className="block">Reutilizar opção já preenchida neste dossier<select value={reuseOption?.rowId===row.id?reuseOption.sourceId:''} onChange={e=>setReuseOption(e.target.value?{rowId:row.id,sourceId:e.target.value}:null)}><option value="">Escolha a origem, se aplicável ao mesmo sujeito passivo</option>{book.rows.filter(source=>source.id!==row.id&&source.taxOption).map(source=><option key={source.id} value={source.id}>{source.description} — {source.establishment}</option>)}</select></label>}
|
||||
{reuseOption?.rowId===row.id&&<div><p>Ao confirmar, substituirá o enquadramento e as datas da opção desta linha pelos da origem escolhida, conservando a data desta operação. Confirme que se aplicam ao mesmo sujeito passivo; o resultado anterior permanece no histórico.</p><button type="button" onClick={()=>run(()=>{const source=book.rows.find(r=>r.id===reuseOption.sourceId);if(!source?.taxOption)throw new Error('Origem da opção já não disponível; dados conservados.');change(row.id,{taxOption:{...structuredClone(source.taxOption),operationDate:row.taxOption?.operationDate??'',origin:{rowId:source.id,description:source.description,establishment:source.establishment}}});setReuseOption(null);})}>Confirmar reutilização da opção escolhida</button><button type="button" onClick={()=>setReuseOption(null)}>Conservar os dados desta linha</button></div>}
|
||||
<label className="block">Tipo de declaração de opção<select value={row.taxOption?.mode??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,mode:e.target.value as ProductionTaxOption['mode']}})}><option value="">Por indicar</option><option value="annual">Opção com efeitos no ano seguinte</option><option value="startup">Opção na declaração de início da actividade</option></select></label>
|
||||
<label className="block">Declaração de regresso à isenção<select value={row.taxOption?.returnStatus??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,returnStatus:e.target.value as ProductionTaxOption['returnStatus']}})}><option value="">Ainda desconhecido</option><option value="none">Não existe declaração de regresso</option><option value="declared">Existe declaração de regresso</option></select></label>
|
||||
{([['declarationDate','Data da declaração de opção'],...(row.taxOption?.mode==='startup'?[['startupDate','Data de início da actividade']]:[]),['operationDate','Data da operação nesta linha'],...(row.taxOption?.returnStatus==='declared'||row.taxOption?.returnDeclarationDate?[['returnDeclarationDate','Data da declaração de regresso à isenção']]:[])] as Array<['declarationDate'|'startupDate'|'operationDate'|'returnDeclarationDate',string]>).map(([field,label])=><label className="block" key={field}>{label}<input type="date" value={row.taxOption?.[field]??''} onChange={e=>change(row.id,{taxOption:{...emptyProductionTaxOption(),...row.taxOption,[field]:e.target.value}})}/></label>)}
|
||||
</fieldset>}
|
||||
<p>{rule?.article}; {rule?.source}. Aplicação nesta versão: Janeiro–Dezembro2026, sujeita às condições seguintes.</p>
|
||||
{rule?.conditions.map((condition,index)=><label className="block" key={index}><input type="checkbox" checked={row.confirmed.includes(index)} onChange={e=>change(row.id,{confirmed:e.target.checked?[...row.confirmed,index]:row.confirmed.filter(i=>i!==index)})}/> {condition}</label>)}
|
||||
<label className="block">Documento ou pressuposto que sustenta estas condições<textarea className="block w-full rounded border p-2" value={row.evidence} onChange={e=>change(row.id,{evidence:e.target.value})}/></label>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||||
import { productionOptionCheck, productionOptionVersion, type ProductionTaxOption } from './iva-production-option.ts';
|
||||
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
|
||||
|
||||
@@ -12,7 +13,7 @@ const source2025 = 'Lei10/2025, BR248, suplemento1, PDF14–15 (2180(14)–(15))
|
||||
const healthGoodsSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(1)(b–f); PDF15, p.1426(273), art.19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(1)(b–f). Âmbito técnico2026/MZN';
|
||||
const internalOperation = 'Operação interna localizada em Moçambique; não é importação nem operação localizada no estrangeiro';
|
||||
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 type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[];logicVersion?:string};
|
||||
const nonprofitConditions = [
|
||||
'Não distribui nem disponibiliza lucros; os corpos gerentes não têm interesse directo ou indirecto nos resultados, por si ou interposta pessoa',
|
||||
'Escrituração abrange todas as actividades e está à disposição dos serviços fiscais para comprovação',
|
||||
@@ -53,6 +54,13 @@ const listedGoodsTreatments:OperationTreatment[] = [
|
||||
['insecticides','Insecticidas','Bem transmitido identificado como insecticida; não qualquer produto químico'],
|
||||
].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[] = [
|
||||
...(['agriculture','forestry','livestock','fishing'] as const).flatMap((kind,index)=>{
|
||||
const label=['Produção agrícola, avicultura e apicultura','Produção silvícola','Produção pecuária','Pesca'][index],letter=['a','b','c','d'][index];
|
||||
const conditions=[internalOperation,'Operação no âmbito da própria produção identificada, utilizando os próprios recursos; não mera revenda/comércio','Se houver transformação, é acessória, pelo próprio produtor sobre produtos da sua produção, por meios geralmente utilizados nas respectivas explorações'];
|
||||
const source=`CIVA republicado Lei13/2016, PDF10, p.1426(268),9(7)(${letter}); PDF11, p.1426(269),11; PDF15, p.1426(273),19(1)(b)(v); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(7). Âmbito técnico2026/MZN`;
|
||||
const exempt:OperationTreatment={id:`production_${kind}`,label:`${label} — isenção`,field:kind==='agriculture'?'05':'07',rate:0,article:`CIVA arts.9(7)(${letter}),19(1)${kind==='agriculture'?'(b)(v)':',11'}`,source,conditions:[...conditions,...(kind==='agriculture'?[]:['Regime de isenção aplicável nesta operação: não existe opção de tributação eficaz do art.11 no período; fundamento/histórico identificado'])]};
|
||||
return kind==='agriculture'?[exempt]:[exempt,{id:`production_${kind}_opted`,logicVersion:productionOptionVersion,label:`${label} — opção de tributação eficaz`,field:'01',rate:16,article:`CIVA arts.9(7)(${letter}),11,15(1),17(1),19(1)(a),34`,source:`${source}; ${source2022}; ${source2025}`,conditions:[...conditions,'Opção declarada à Direcção da Área Fiscal competente, na declaração adequada; se opção no início, consta da declaração de início da actividade','Operação no regime normal por opção eficaz; contraprestação completa, sem base especial do art.15(2) ou outro tratamento próprio']}];
|
||||
}),
|
||||
...listedGoodsTreatments,
|
||||
{...socialTreatment('public_security','Bens e serviços ligados à segurança por entidade pública','b',['Transmissão de bens ou prestação ligada à segurança identificada e efectuada pela própria entidade pública; não presumir segurança social nem extensão a prestador privado contratado'],false),source:'CIVA republicado Lei13/2016, PDF9, p.1426(267),9(2)(b); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(2)(b); PDF2, p.2252(26),10(1). Âmbito técnico2026/MZN'},
|
||||
{id:'official_military_supplies',label:'Fornecimentos oficiais militares/paramilitares elegíveis',field:'07',rate:0,article:'CIVA arts.9(11),19(1)',source:'CIVA republicado Lei13/2016, PDF10, p.1426(268),9(11); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(11). Âmbito técnico2026/MZN',conditions:[internalOperation,'Bem/serviço no fornecimento de material de guerra e de aquartelamento ou fardamento militar/paramilitar, identificado','Destino de utilização oficial das Forças de Defesa e Segurança Nacional; actividade efectuada exclusivamente para aqueles serviços','Estabelecimento reconhecido pelo Ministério competente, com reconhecimento identificado; não basta o cliente ser militar']},
|
||||
@@ -100,7 +108,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; 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; 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};
|
||||
@@ -148,6 +156,8 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
|
||||
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.');
|
||||
const option=row.treatment.startsWith('production_')&&row.treatment.endsWith('_opted')?productionOptionCheck(row.taxOption,row.period):{};
|
||||
if(option.error)return stop(option.error);
|
||||
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.'};
|
||||
@@ -155,7 +165,7 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
|
||||
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 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.'};
|
||||
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.')+(option.summary?' '+option.summary:'')};
|
||||
}
|
||||
function key(draft:Record<string,unknown>,book:SalesBook,catalog=operationTreatments.filter(t=>book.rows.some(r=>r.treatment===t.id))):string {return JSON.stringify([operationVersion,catalog,acquisitionScope(draft),draft.currency,draft.province_id,draft.district_id,draft.business_plan_snapshot,draft.cashflow_snapshot,draft.monthly_iva_settings,book.currency,book.rows,book.periods]);}
|
||||
export function salesCurrent(draft:Record<string,unknown>,book:SalesBook):boolean {return !!book.result&&book.result.version===operationVersion&&book.result.opportunity===acquisitionScope(draft)&&(book.result.key===key(draft,book)||book.result.key===key(draft,book,operationTreatments.filter(t=>legacyV2TreatmentIds.includes(t.id))));}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export type ProductionTaxOption = {mode:''|'annual'|'startup';declarationDate:string;startupDate:string;returnStatus:''|'none'|'declared';returnDeclarationDate:string;operationDate:string;origin?:{rowId:string;description:string;establishment:string}};
|
||||
export const productionOptionVersion='production-option/1';
|
||||
export const emptyProductionTaxOption = ():ProductionTaxOption=>({mode:'',declarationDate:'',startupDate:'',returnStatus:'',returnDeclarationDate:'',operationDate:''});
|
||||
export function productionOptionCheck(option:ProductionTaxOption|undefined,period:string):{error?:string;summary?:string} {
|
||||
const valid=(value:unknown):value is string=>typeof value==='string'&&/^\d{4}-\d{2}-\d{2}$/.test(value)&&Number(value.slice(0,4))>=1&&Number.isFinite(new Date(value+'T00:00:00Z').valueOf())&&new Date(value+'T00:00:00Z').toISOString().slice(0,10)===value;
|
||||
if(!option||!['annual','startup'].includes(option.mode)||!valid(option.declarationDate)||!valid(option.operationDate))return {error:'Indique o tipo/data da declaração de opção e a data da operação; não se presume renúncia eficaz.'};
|
||||
if(option.operationDate.slice(0,7)!==period)return {error:'A data da operação deve pertencer ao período desta linha; não repartir montantes automaticamente.'};
|
||||
let effective=String(Number(option.declarationDate.slice(0,4))+1).padStart(4,'0')+'-01-01';
|
||||
if(option.mode==='startup'){
|
||||
if(!valid(option.startupDate)||option.declarationDate>option.startupDate)return {error:'Na opção constante da declaração de início, indique a data de início e uma declaração que não lhe seja posterior.'};
|
||||
effective=option.startupDate;
|
||||
}
|
||||
if(option.operationDate<effective)return {error:`Opção ainda sem efeitos nesta operação; início de efeitos ${effective}.`};
|
||||
if(!['none','declared'].includes(option.returnStatus))return {error:'Indique expressamente se existe declaração de regresso à isenção; informação desconhecida não significa ausência de declaração.'};
|
||||
if(option.returnStatus==='none'&&option.returnDeclarationDate)return {error:'Foi indicada ausência de declaração de regresso, mas existe uma data guardada; resolva a divergência explicitamente.'};
|
||||
let end='';
|
||||
if(option.returnStatus==='declared'){
|
||||
if(!valid(option.returnDeclarationDate))return {error:'Data da declaração de regresso à isenção em falta ou inválida; não presumir uma data.'};
|
||||
const anniversary=String(Number(effective.slice(0,4))+5).padStart(4,'0')+effective.slice(4);
|
||||
if(effective.endsWith('-02-29')&&option.returnDeclarationDate===anniversary.slice(0,4)+'-02-28')return {error:'Regresso no aniversário de cinco anos de início em29/02: o acervo conferido não define este caso de calendário; não presumir a data limite.'};
|
||||
if(option.returnDeclarationDate<anniversary)return {error:'A declaração de regresso indicada antecede o período mínimo de cinco anos; reveja o histórico, sem presumir regresso eficaz.'};
|
||||
end=String(Number(option.returnDeclarationDate.slice(0,4))+1).padStart(4,'0')+'-01-01';
|
||||
if(option.operationDate>=end)return {error:`Opção já cessou na data desta operação; regresso à isenção com efeitos ${end}. Reveja o tratamento.`};
|
||||
}
|
||||
return {summary:`Art.11 (${productionOptionVersion}): declaração ${option.declarationDate} (${option.mode==='startup'?'opção no início da actividade':'opção anual'}); efeitos ${effective}; operação ${option.operationDate}; ${end?`declaração de regresso ${option.returnDeclarationDate}, efeitos ${end}`:'ausência de declaração de regresso expressamente indicada; tributação continua, sem caducidade automática ao fim de cinco anos'}.${option.origin?` Opção reutilizada de ${option.origin.description} (${option.origin.rowId}), estabelecimento ${option.origin.establishment}; escolha confirmada nesta linha.`:''}`};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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 IvaProductionDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
public function test_production_context_preserves_accepted_opportunities(): 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(getenv('IVA_G2I_INPUT') ?: base_path('outputs/iva-l1-g2-production-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']);
|
||||
$book = json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:318'];
|
||||
$this->assertCount(7, $book['result']['rows']);
|
||||
$this->assertSame('300.00', $book['result']['periods'][0]['fields']['07']);
|
||||
$this->assertSame('100.00', $book['result']['periods'][0]['fields']['05']);
|
||||
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:310'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:310']);
|
||||
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:309'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:309']);
|
||||
$this->assertSame('300.00', $book['result']['periods'][0]['fields']['01']);
|
||||
$this->assertSame('48.00', $book['result']['periods'][0]['fields']['02']);
|
||||
$this->assertSame('2024-12-15', $book['rows'][2]['taxOption']['declarationDate']);
|
||||
$this->assertSame('2026-10-15', $book['rows'][2]['taxOption']['operationDate']);
|
||||
if (getenv('IVA_G2I_RECOVERED')) file_put_contents(getenv('IVA_G2I_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import fs from 'node:fs';
|
||||
import {operationTreatments,newSalesOperation,evaluateSalesOperation,recordSales,saveSalesBook,salesBook,salesCurrent} from '../../resources/js/lib/iva-operations.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const ids=['production_agriculture', 'production_forestry', 'production_forestry_opted', 'production_livestock', 'production_livestock_opted', 'production_fishing', 'production_fishing_opted'];
|
||||
function seed(){return JSON.parse(fs.readFileSync(new URL('./fixtures/iva-listed.json',import.meta.url),'utf8'));}
|
||||
function fixture(){const draft=seed();draft.context_opportunity='opportunity:318';draft.project_title='SYNTHETIC QA — G2I social';
|
||||
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g2i-${i}`,'2026-10','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA equipamento Manica',amount:id.endsWith('_opted')&&i%3===2?'116':'100',basis:i%3===2?'included':'net',treatment:id,...(id.endsWith('_opted')?{taxOption:{mode:'annual',declarationDate:'2024-12-15',startupDate:'',returnStatus:'none',returnDeclarationDate:'',operationDate:'2026-10-15'}}:{}),confirmed:operationTreatments.find(t=>t.id===id).conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — natureza, entidade, reconhecimento e condições cumulativas pressupostos documentados; sem certificação'})),periods:[{period:'2026-10',complete:true}]};book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};}
|
||||
test('G2I four production branches with seven exemption or opted treatments require their own operation and eligibility conditions',()=>{
|
||||
const {book}=fixture();for(const row of book.rows){const rule=operationTreatments.find(t=>t.id===row.treatment),r=evaluateSalesOperation(row,'MZN');assert.equal(r.field,row.treatment.endsWith('_opted')?'01':row.treatment==='production_agriculture'?'05':'07');assert.equal(r.net,'100.00');assert.equal(r.vat,row.treatment.endsWith('_opted')?'16.00':'0.00');assert.equal(r.gross,row.treatment.endsWith('_opted')?'116.00':'100.00');assert.match(r.article,/9\(7\)/);
|
||||
assert.ok(rule.conditions.length>=3);
|
||||
for(const missing of row.confirmed)assert.equal(evaluateSalesOperation({...row,confirmed:row.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
|
||||
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);assert.equal(evaluateSalesOperation({...row,treatment:'listed_banana'},'MZN').vat,null); assert.equal(evaluateSalesOperation({...row,period:'2025-10'},'MZN').vat,null); assert.equal(evaluateSalesOperation(row,'USD').vat,null);
|
||||
}
|
||||
assert.equal(book.result.periods[0].fields['07'],'300.00');assert.equal(book.result.periods[0].fields['05'],'100.00');assert.equal(book.result.periods[0].fields['01'],'300.00');assert.equal(book.result.periods[0].fields['02'],'48.00');
|
||||
});
|
||||
test('new production context round-trips and reports only its operations, preserving nine accepted books without recalculation',()=>{
|
||||
const {draft,book}=fixture(),original=seed();const old=JSON.parse(original.modelo_a_snapshot).salesOperations;
|
||||
const recovered=JSON.parse(JSON.stringify(draft)),store=JSON.parse(recovered.modelo_a_snapshot).salesOperations;
|
||||
for(const id of ['opportunity:309','opportunity:310','opportunity:311','opportunity:312','opportunity:313','opportunity:314','opportunity:315','opportunity:316','opportunity:317']){assert.deepEqual(store[id],old[id]);assert.equal(salesCurrent({...recovered,context_opportunity:id},store[id]),true);}
|
||||
assert.equal(salesCurrent(recovered,salesBook(recovered)),true);
|
||||
const model=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));
|
||||
for(const token of [...ids,'48.00','opportunity:318','Manica','2026-10','MZN'])assert.ok(model.includes(token),token);
|
||||
for(const token of ['financial_stamp','patient_transport','social_assistance_public','statutory_membership','author_rights','industrial_feed','special_tax_games','public_security','listed_maize'])assert.equal(model.includes(token),false);
|
||||
assert.equal(book.result.rows.length,7);assert.equal(draft.currency,original.currency);assert.deepEqual(draft.selection_labels,original.selection_labels);
|
||||
});
|
||||
test('article11 distinguishes annual effects, startup, five-year minimum and effective return without automatic expiry',()=>{
|
||||
const {draft,book}=fixture(),row=book.rows.find(r=>r.treatment==='production_forestry_opted');
|
||||
const check=(patch)=>evaluateSalesOperation({...row,taxOption:{...row.taxOption,...('returnDeclarationDate' in patch?{returnStatus:patch.returnDeclarationDate?'declared':'none'}:{}),...patch}},'MZN');
|
||||
assert.equal(check({declarationDate:'2026-01-01'}).vat,null);
|
||||
assert.match(check({declarationDate:'2026-01-01'}).reason,/2027-01-01/);
|
||||
assert.equal(check({mode:'startup',declarationDate:'2026-09-20',startupDate:'2026-10-01'}).vat,'16.00');
|
||||
assert.equal(check({mode:'startup',declarationDate:'2026-10-02',startupDate:'2026-10-01'}).vat,null);
|
||||
assert.equal(check({declarationDate:'2020-01-01',returnDeclarationDate:'2025-12-31'}).vat,null);
|
||||
assert.equal(check({declarationDate:'2019-12-31',returnDeclarationDate:'2025-01-01'}).vat,null);
|
||||
assert.match(check({declarationDate:'2019-12-31',returnDeclarationDate:'2025-01-01'}).reason,/2026-01-01/);
|
||||
assert.equal(check({declarationDate:'2020-01-01',returnDeclarationDate:'2026-01-01'}).vat,'16.00');
|
||||
assert.equal(check({declarationDate:'2019-12-31',returnDeclarationDate:''}).vat,'16.00');
|
||||
for(const patch of [{mode:''},{returnStatus:''},{returnStatus:'declared',returnDeclarationDate:''},{returnStatus:'none',returnDeclarationDate:'2026-01-01'},{declarationDate:''},{declarationDate:'2026-99-99'},{declarationDate:'2024-02-30'},{operationDate:'2026-11-01'},{operationDate:''},{returnDeclarationDate:'not-a-date'}])assert.equal(check(patch).vat,null);
|
||||
assert.equal(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2025-02-28'}).vat,null);
|
||||
assert.match(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2025-03-01'}).reason,/regresso.*2026-01-01/);
|
||||
assert.equal(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2026-03-01'}).vat,'16.00');
|
||||
assert.equal(operationTreatments.some(t=>t.id==='production_agriculture_opted'),false);
|
||||
const changed={...book,rows:book.rows.map(r=>r.id===row.id?{...r,taxOption:{...r.taxOption,declarationDate:'2025-12-31'}}:r)};
|
||||
assert.equal(salesCurrent(draft,changed),false);assert.deepEqual(changed.result,book.result);
|
||||
const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance'])}));
|
||||
for(const token of ['2024-12-15','2025-01-01','2026-10-15','ausência de declaração de regresso'])assert.ok(report.includes(token),token);
|
||||
});
|
||||
if(process.env.IVA_G2I_OUT)fs.writeFileSync(process.env.IVA_G2I_OUT,JSON.stringify(fixture().draft,null,2));
|
||||
Reference in New Issue
Block a user