Implement ISPC quarterly calculation and demonstrate private recovery and financial report

This commit is contained in:
2026-10-25 17:06:28 +02:00
parent 479b66815c
commit 1b7ccfee23
17 changed files with 250 additions and 6 deletions
@@ -27279,5 +27279,27 @@
"Demonstração visual integrada e gravação privada deste incremento antes de propor entregaL4."
],
"active_changes": false
},
"ispc_l4_quarterly_qa_20261025": {
"status": "QA_demonstrated_eligibility_option_quarter_calculation_private_recovery_and_report_not_installed",
"report": "docs/investmoz/simulations/ispc-l4-quarterly-qa-20261025.json",
"example": {
"context": "opportunity:375;SYNTHETIC QA",
"option": "documentada2026-01-01",
"quarter": "2026/T2",
"prior_turnover_MZN": "100000",
"quarter_base_MZN": "200000",
"rate_percent": 12,
"tax_MZN": "24000",
"payment_MZN": "24000",
"declaration": "mantida",
"other_fiscal_books": "exactamente preservados"
},
"remaining_specific": [
"Operações ocasionais próprias3(3–4)enquanto sujeito passivo,distintas da retenção do adquirente;mantidas indeterminadas no percurso específico.",
"Aquisições acima limiteglobal2.500.000:correspondência do tratamento próprio;nãoliquidar5%porpresunção.",
"Formulário/modelo,instruções eprocedimentos da regulamentação2026;relatório depreparação não édeclaraçãosubmetida. Aviso primário do Governo confirma aprovação de novo regulamento,mas não contém articulado/modelos: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . Sem impor segunda validação jurídica ou repetir pesquisa sem nova evidência."
],
"active_changes": false
}
}
@@ -0,0 +1,64 @@
{
"status": "QA_demonstrated_eligibility_option_quarter_calculation_private_recovery_and_report_not_installed",
"matrix_anchor": "AUD-SIM-INT02",
"scope": "Actividade habitual ISPC2026/MZN, enquadramento/opção, cálculo trimestral e retenção12(3–4) delimitada. Não é encerramento de operações ocasionais próprias ou formalidades sem correspondência de modelo.",
"source": {
"file": "docs/investmoz/simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf",
"sha256": "ccf2a8deeca820f2dd076b4b2141b1efe3f6a806568c3a587ca59a8e92f4c251",
"citation": "Lei9/2025,republicaçãoISPC,arts8–12;PDF11/p2180(11),enquadramento2–5PDF10",
"application": "8(1)a selecciona taxa3/4/5 pelo volume anual2026 declarado e9 aplica-a à base trimestral; não repartir limites anuais por4 nem copiar acesso2025. Serviços12/15; cumulação usa taxa maior8(2).20%somenteparcela acima limite anual4.000.000 com acumulado anterior; cálculos racionais exactos."
},
"new_implementation": [
"Livro trimestral por contexto; receitas de cada trimestre e suporte explícitos,volume global do sujeito passivo confirmado; previsão anual distinta de ano encerrado reconciliado.",
"Elegibilidade actual e opção documentada reutilizadas; preparação apenas simula; opção posterior ao trimestre não retroage automaticamente.",
"Imposto/prestação/obrigação declarativa separados; inferior500 dispensa pagamento, não declaração;igual500 mantém pagamento. Não acumular dispensas nem deduzir custos/IVA.",
"Retenção5%separada para aquisições ocasionais a pessoa singular não inscrita3(3–4),ligadasàactividade,limiteglobalanual2.500.000;acima limiteindeterminado,semtruncar. Sem crédito contraimpostopróprio,mínimo500não aplicadoàretenção.",
"Gravação/recuperação mantém outros livros e históricos;alterações conservam resultado congelado até nova versão;relatório financeiro inclui grupoISPC."
],
"verification": {
"prior_proofs": "L4-A12testes eL1/L2/L3/P001não repetidos;inventário eaceitações reutilizados",
"new_frontend": "13distinctpassed:10quarter/3withholding;frontend-final.txt",
"private_API": "1test/21assertions passed;loopback55441/simulator_fiscal_delta_20261021_test,DatabaseTransactions rollback;otheraccount404;api.txt",
"UI": "Componentesreais:avaliar/opçãoguardada→calcular→guardarficheiroQA→recuperar→relatório;persistênciaprivadaconferidaemAPIseparadamente;ui-recovered-report.txt/png",
"report_fix": "GrupoISPCmapeadoàsecçãofinanceira;testeactualdoexportadorincluiimposto/opção;ausênciaidentificadanademonstraçãoecorrigidaantesdeconcluir.",
"types": "exit0;types-final.txt",
"build": "exit0;build-final.txt",
"inventory": "outputs/ispc-l4b-20261025/preservation-before-tests.json;reutiliza8143ficheirosinventariadoseL1fechados"
},
"example": {
"context": "opportunity:375;SYNTHETIC QA",
"option": "documentada2026-01-01",
"quarter": "2026/T2",
"prior_turnover_MZN": "100000",
"quarter_base_MZN": "200000",
"rate_percent": 12,
"tax_MZN": "24000",
"payment_MZN": "24000",
"declaration": "mantida",
"other_fiscal_books": "exactamente preservados"
},
"remaining_specific": [
"Operações ocasionais próprias3(3–4)enquanto sujeito passivo,distintas da retenção do adquirente;mantidas indeterminadas no percurso específico.",
"Aquisições acima limiteglobal2.500.000:correspondência do tratamento próprio;nãoliquidar5%porpresunção.",
"Formulário/modelo,instruções eprocedimentos da regulamentação2026;relatório depreparação não édeclaraçãosubmetida. Aviso primário do Governo confirma aprovação de novo regulamento,mas não contém articulado/modelos: https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/ . Sem impor segunda validação jurídica ou repetir pesquisa sem nova evidência."
],
"active_changes": false,
"schema_changes": false,
"rules_published_or_activated": 0,
"residuals": "P001/P058nãointerrompem;P084/P085fechados;L3residuaisapenasbloqueiamcasosdependentes;estatísticas/IAadiadas.",
"runtime_hashes": {
"resources/js/lib/ispc-quarter.ts": "f66bd52e9cdfe1ddaa6b00a931465862b3d890b9be216b7a056740f027f24129",
"resources/js/lib/ispc-withholding.ts": "6c31b240299edd3262efa28f7f3d5aee21123f94e30ef4ab90e26c35777cb6e4",
"resources/js/components/simulation-ispc-quarter.tsx": "0b950091e9204b2b20823694ea16dc94e4863914080542253966367a5580d62d",
"resources/js/components/simulation-ispc-withholding.tsx": "e688a8d6a2e1e20c1e10a85d33152d58a5c14867672cf4ffb834980ec1732f08",
"resources/js/lib/project-dossier-report.ts": "0db9f5fcb2929fd8340894bd13b74ee7c400af25e483ddfe5651002fd7160f49"
},
"proof_hashes": {
"outputs/ispc-l4b-20261025/frontend-final.txt": "2b0ec25bb83505ec9ce8ace02e0860faeeac4ce233083859f9a502069944f49f",
"outputs/ispc-l4b-20261025/api.txt": "2a770d07561a1475973795d5bac01ce5d94ee8a91be1d57d9b0702c621b8284d",
"outputs/ispc-l4b-20261025/ui-recovered-report.txt": "234bae0f17ef84dd9928197bfa38ad4a8a05c2dae18051e1ea9bd88852bb84fa",
"outputs/ispc-l4b-20261025/build-final.txt": "9683e783fd6e457c89a747d4a1a6deb10ba00a034f7891cf69e4c1a0cb1f0073",
"outputs/ispc-l4b-20261025/types-final.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
"tests/frontend/fixtures/ispc-quarter-preserved.json": "30266584de9cacb0e4d771a9cc9c38838b62a3472583792e13effcd094247c39"
}
}
@@ -5770,3 +5770,11 @@ Implementado enquadramento preliminar2026/MZN da actividade habitual pelos arts2
Provas novas:12testesfrontend,tipos/build exit0. Não foram repetidos os testes aceites deL1/L2/L3/P001. Serialização/recuperação cobertas nos novos testes;transporte privado existente reutilizado. Demonstração visual integrada e gravação privada deste incremento continuam antes de propor entregaL4;nenhuma escrita activa. Recibo:ispc-l4a-eligibility-qa-20261025.json.
L4 continua aberto:apuramento trimestral,escalões e actividades mistas/excesso,mínimo500,retenção5%,percurso ocasional e formalidades próprias. As correspondências concretas serão tratadas no incremento seguinte,sem aprovação jurídica genérica ou bloqueio pelos residuaisP001/P058. L3 permanece aberto com candidata0f4df8a4 preparada e execução autenticada/backup-restauro por realizar. P084/P085 fechados;estatísticas/IA adiadas.
## 25/10/2026 — L4 ISPC: apuramento trimestral e percurso demonstrado em QA
O enquadramento e a opção guardados alimentam o apuramento trimestral de 2026 em MZN. O volume anual de acesso anterior, o volume anual do escalão e a base trimestral são distintos. Calculam-se as taxas de 3/4/5%, os serviços a 12/15%, a cumulação pela taxa maior e o excesso a 20%, com valores racionais exactos. O acumulado anterior impede repetir o excesso. O ano encerrado reconcilia os quatro trimestres; uma previsão permanece cenário. A prestação inferior a 500 MZN dispensa pagamento e conserva a declaração; exactamente 500 MZN mantém pagamento. A retenção de 5% nas aquisições ocasionais do artigo 12(3–4) tem livro e limite global de 2.500.000 MZN próprios, sem reduzir o imposto do adquirente.
Percurso demonstrado com componentes reais: elegibilidade/opção → cálculo → gravação → recuperação → relatório financeiro. Exemplo do T2: base de 200.000 MZN, taxa de 12%, imposto/prestação de 24.000 MZN e opção documentada em 01/01/2026. A API privada em QA passou 1 teste com 21 asserções, rollback e recusa de acesso a outra conta (404); os livros anteriores foram preservados exactamente. Passaram 13 testes novos (10 trimestrais e 3 de retenção), tipos e compilação. As fontes, provas aceites de L1/L2/L3/P001 e os 12 testes de L4-A foram reutilizados. Recibo: ispc-l4-quarterly-qa-20261025.json; provas: outputs/ispc-l4b-20261025.
O âmbito habitual trimestral está demonstrado em QA, ainda não instalado. O saldo de L4 fica delimitado às operações ocasionais próprias do artigo 3(3–4), às aquisições acima do limite global e à correspondência de formulários, instruções e regulamentação de 2026. O aviso primário do Governo sobre a aprovação de novo regulamento não fornece o articulado nem o modelo. Não se inventam formulários, prazos ou aprovadores; essas dependências não bloqueiam o cálculo habitual demonstrado. P001/P058 permanecem residuais e não interrompem a sequência; P084/P085 permanecem fechados.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
import {SimulationIspcWithholding} from '@/components/simulation-ispc-withholding';
import {SimulationIspcQuarter} from '@/components/simulation-ispc-quarter';
import {SimulationIspcEligibility} from './simulation-ispc-eligibility';
import { simulationDraftEtag } from '@/lib/simulation-draft-etag';
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
@@ -3503,6 +3505,8 @@ export function SimulationForm() {
/></details>
)}
{(current.id === 'financial' || current.id === 'tax') && <SimulationIspcEligibility draft={draft} onChange={change} />}
{(current.id === 'financial' || current.id === 'tax') && <SimulationIspcQuarter draft={draft} onChange={change} />}
{(current.id === 'financial' || current.id === 'tax') && <SimulationIspcWithholding draft={draft} onChange={change} />}
{(current.id === 'financial' || current.id === 'tax') && <SimulationMonthlyIva draft={draft} busy={isCalculating} onCalculate={runSimulation} onChange={change} />}
{current.id === 'financial' &&
(draft.detail_level === 'advanced' ||
@@ -1,4 +1,4 @@
import {useState} from 'react';
import {useState} from 'react';
import {ispcActivities,ispcEligibilityBook,saveIspcEligibilityBook,recordIspcEligibility,ispcEligibilityCurrent,ispcReuseDiff,ispcEligibilityReportRows,type IspcEligibilityInput,type IspcEligibilityBook} from '@/lib/ispc-eligibility';
const factLabels:Partial<Record<keyof IspcEligibilityInput,string>>={territory:'Exerce a actividade em território nacional',organizedRequired:'Está obrigado a possuir contabilidade organizada para os impostos sobre o rendimento',sharesInIspc:'Tem participações noutras sociedades comerciais em ISPC',sharesAnonymous:'Tem participações em sociedades anónimas ou sociedades cujos sócios não são identificáveis',otherActivities:'Exerce outras actividades fora dos âmbitos identificados',unlistedServices:'Presta serviços fora dos previstos na Lei',forecastConfirmed:'Previsão da declaração de início confirmada pela AT (ou pressuposto expresso na simulação)',generalRegimeChosen:'Optou pela aplicação de IVA/IRPS/IRPC com permanência mínima de três anos',generalThreeYearsCompleted:'Já cumpriu o período mínimo de três anos dessa opção'};
export function SimulationIspcEligibility({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
@@ -8,7 +8,7 @@ export function SimulationIspcEligibility({draft,onChange}:{draft:Record<string,
const fact=(field:keyof typeof factLabels)=><label className="block" key={field}>{factLabels[field]}<select className="block rounded border p-2" value={String(i[field])} onChange={e=>change({[field]:e.target.value})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>;
const text=(field:keyof IspcEligibilityInput,label:string,type='text')=><label className="block" key={field}>{label}<input className="block rounded border p-2" type={type} value={String(i[field])} onChange={e=>change({[field]:e.target.value})}/></label>;
return<section aria-label="ISPC — enquadramento e opção" className="space-y-3 rounded border p-4"><h4 className="font-semibold">ISPC — enquadramento e opção</h4>
<p>Avaliação preliminar para 2026, em MZN, segundo a Lei 9/2025. Distingue condições de acesso, opção e cálculo do imposto. Este primeiro incremento prepara o enquadramento; o apuramento trimestral fica por realizar.</p>
<p>Avaliação preliminar para 2026, em MZN, segundo a Lei 9/2025. Distingue condições de acesso, opção e cálculo do imposto. Guarde esta avaliação e prossiga no apuramento trimestral abaixo.</p>
{error&&<p role="alert">{error}</p>}
{diff.length>0&&<button type="button" className="underline" onClick={()=>setReusePreview(diff)}>Conferir dados do perfil e da actividade para reutilizar</button>}
{reusePreview&&<aside className="rounded border p-3"><p>A adopção altera as entradas deste cenário. O resultado guardado passa a histórico até novo cálculo; os outros livros e a opção registada são preservados.</p>{reusePreview.map(v=><p key={v.field}>{({personType:'Tipo de pessoa',establishments:'Estabelecimentos',activityDescription:'Actividade',entityRef:'Entidade'} as Record<string,string>)[v.field]??v.field}: {v.before||'Por preencher'} → {v.after}</p>)}<button type="button" onClick={()=>{change(Object.fromEntries(reusePreview.map(v=>[v.field,v.after])));setReusePreview(null);}}>Confirmar adopção destes dados</button><button type="button" onClick={()=>setReusePreview(null)}>Manter as minhas entradas</button></aside>}
@@ -0,0 +1,17 @@
import {useState} from 'react';
import {ispcEligibilityBook,ispcEligibilityCurrent} from '@/lib/ispc-eligibility';
import {ispcQuarterBook,saveIspcQuarterBook,recordIspcQuarter,ispcQuarterReportRows,type IspcQuarterInput,type IspcQuarterBook} from '@/lib/ispc-quarter';
export function SimulationIspcQuarter({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');if(draft.fiscal_component!=='ispc')return null;
let book:IspcQuarterBook;try{book=ispcQuarterBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const i=book.input,save=(b:IspcQuarterBook)=>{try{onChange('modelo_a_snapshot',saveIspcQuarterBook(draft,b));setError('');}catch(e){setError(String(e));}},change=(patch:Partial<IspcQuarterInput>)=>save({...book,input:{...i,...patch}});
let service=false,ready=false;try{const e=ispcEligibilityBook(draft);ready=ispcEligibilityCurrent(draft,e)&&e.result?.status==='compatible';service=e.input.activities.some(a=>['services12','liberal15'].includes(a));}catch{/* Show explicit preparation issue below, never fabricate eligibility. */}
return<section aria-label="Apuramento trimestral ISPC" className="space-y-3 rounded border p-4"><h4>ISPC — apuramento trimestral</h4><p>Reutiliza o enquadramento e a opção guardados. Declare o volume de todas as actividades do sujeito passivo em MZN; custos e IVA de aquisições não reduzem esta base. Dados em falta permanecem indeterminados.</p>{!ready&&<p>Reveja e guarde primeiro a avaliação de enquadramento acima. As entradas trimestrais e o histórico ficam conservados.</p>}{error&&<p role="alert">{error}</p>}
<label className="block">Ano<input className="block rounded border p-2" value={i.year} onChange={e=>change({year:e.target.value})}/></label><label className="block">Trimestre<select className="block rounded border p-2" value={i.quarter} onChange={e=>change({quarter:e.target.value})}>{['1','2','3','4'].map(q=><option key={q} value={q}>{q} — {['Janeiro a Março','Abril a Junho','Julho a Setembro','Outubro a Dezembro'][Number(q)-1]}</option>)}</select></label>
<label className="block">Âmbito do volume<select className="block rounded border p-2" value={i.coverage} onChange={e=>change({coverage:e.target.value as IspcQuarterInput['coverage']})}><option value="">Por confirmar</option><option value="all_taxpayer_activity">Todas as actividades do sujeito passivo, sem repetir oportunidades</option></select></label>
<div className="grid gap-3 md:grid-cols-2">{i.sales.map((v,n)=>(n<Number(i.quarter)||i.annualBandOrigin==='closed_year'&&!service)&&<fieldset key={n} className="space-y-2 rounded border p-2"><legend>Trimestre {n+1}{n+1===Number(i.quarter)?' — a apurar':' — acumulado anterior'}</legend><label className="block">Volume T{n+1} em MZN<input className="block rounded border p-2" inputMode="decimal" value={v} onChange={e=>{const a=[...i.sales] as IspcQuarterInput['sales'];a[n]=e.target.value;change({sales:a});}}/></label><label className="block">Suporte ou pressuposto T{n+1}<input className="block rounded border p-2" value={i.salesRefs[n]} onChange={e=>{const a=[...i.salesRefs] as IspcQuarterInput['salesRefs'];a[n]=e.target.value;change({salesRefs:a});}}/></label></fieldset>)}</div>
{!service&&<fieldset className="space-y-2 rounded border p-3"><legend>Escalão anual da actividade — distinto do volume de acesso</legend><p>Os limites de 1.000.000 / 2.500.000 / 4.000.000 MZN são anuais. A taxa do escalão é aplicada à base do trimestre; não dividir os limites por quatro. O excesso anual tem taxa própria de 20%.</p><label className="block">Volume anual 2026 para o escalão<input className="block rounded border p-2" value={i.annualBandVolume} onChange={e=>change({annualBandVolume:e.target.value})}/></label><label className="block">Origem da referência anual<select className="block rounded border p-2" value={i.annualBandOrigin} onChange={e=>change({annualBandOrigin:e.target.value as IspcQuarterInput['annualBandOrigin']})}><option value="">Por identificar</option><option value="forecast">Previsão expressa — simulação</option><option value="closed_year">Ano encerrado — reconciliar quatro trimestres</option></select></label><label className="block">Suporte da referência anual<input className="block rounded border p-2" value={i.annualBandRef} onChange={e=>change({annualBandRef:e.target.value})}/></label></fieldset>}
<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordIspcQuarter(draft,book));}catch(e){setError(String(e));}}}>Calcular e guardar apuramento trimestral ISPC</button>
{ispcQuarterReportRows(draft).map((r,n)=><p key={n}><strong>{r[1]}:</strong> {r[2]}<small className="block">{r[3]}</small></p>)}
<p>Preparação para a declaração periódica dos artigos 10/11. Este cálculo não submete formulários nem efectua pagamentos. Retenção na fonte em aquisições ocasionais tem registo e base próprios.</p></section>;
}
@@ -0,0 +1,5 @@
import {ispcWithholdingBook,saveIspcWithholdingBook,recordIspcWithholding,ispcWithholdingReportRows,type IspcWithholdingInput} from '@/lib/ispc-withholding';
export function SimulationIspcWithholding({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
if(draft.fiscal_component!=='ispc')return null;
try{const b=ispcWithholdingBook(draft),i=b.input,change=(p:Partial<IspcWithholdingInput>)=>onChange('modelo_a_snapshot',saveIspcWithholdingBook(draft,{...b,input:{...i,...p}}));return<section aria-label="Retenções ISPC" className="space-y-3 rounded border p-4"><h4>ISPC — retenção em aquisições ocasionais</h4><label className="block">Existem aquisições abrangidas pelo artigo 12(3–4)?<select value={i.applicable} onChange={e=>change({applicable:e.target.value as IspcWithholdingInput['applicable']})}><option value="">Por esclarecer</option><option value="no">Não</option><option value="yes">Sim — conferir condições</option></select></label>{i.applicable==='yes'&&<><p>Registe apenas aquisições do mesmo trimestre. O acumulado anterior cobre todas as aquisições desta via no ano e evita reutilizar o limite por fornecedor ou oportunidade.</p>{(['quarter','base','priorAnnualPurchases','reference'] as const).map(k=><label className="block" key={k}>{{quarter:'Trimestre de 2026',base:'Base de aquisições deste trimestre em MZN',priorAnnualPurchases:'Aquisições globais anteriores de 2026 em MZN',reference:'Suporte das aquisições e condições'}[k]}<input className="block rounded border p-2" value={i[k]} onChange={e=>change({[k]:e.target.value})}/></label>)}{(['naturalOccasional','unregistered','related']as const).map(k=><label className="block" key={k}>{{naturalOccasional:'Fornecedor pessoa singular ocasional abrangido por 3(3–4)',unregistered:'Fornecedor não inscrito para tributação',related:'Bens relacionados com a actividade do adquirente'}[k]}<select value={i[k]} onChange={e=>change({[k]:e.target.value})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}<label className="block">Actividade da operação ocasional<select value={i.activity} onChange={e=>change({activity:e.target.value as IspcWithholdingInput['activity']})}>{[['','Por identificar'],['forestry','Silvícola'],['fishery','Pesqueira'],['livestock','Pecuária'],['agriculture','Agrícola'],['poultry','Avícola'],['beekeeping','Apícola'],['other','Outra']].map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></label></>}<button type="button" className="rounded border p-2" onClick={()=>onChange('modelo_a_snapshot',saveIspcWithholdingBook(draft,recordIspcWithholding(draft,b)))}>Conferir e guardar retenção ISPC</button>{ispcWithholdingReportRows(draft).map((r,n)=><p key={n}><strong>{r[1]}:</strong> {r[2]}<small className="block">{r[3]}</small></p>)}</section>;}catch(e){return<p role="alert">{String(e)}</p>;}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import {acquisitionScope} from './iva-acquisitions.ts';
import {decimalFraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
import {decimalFraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const ispcEligibilitySource='L4-A:Lei9/2025,BR248/1.ºsuplemento,republicaçãoISPC,arts2–5,9-A/13;PDF10–11/p2180(10–11);vigência2026art6/PDF9. Enquadramento preliminar de actividade habitual;opção separada;sem liquidação trimestral,retenção,submissão ou alteração de IVA/IRPC/IRPS.';
export const ispcActivities=[['forestry','Silvícola'],['fishery','Pesqueira'],['livestock','Pecuária'],['agriculture','Agrícola'],['poultry','Avícola'],['beekeeping','Apícola'],['industry','Industrial'],['commerce','Comercial'],['craft','Artesanato'],['services12','Serviços do artigo8(1)b'],['liberal15','Profissão liberal do artigo8(1)c'],['other','Outra actividade,fora dos âmbitos identificados']] as const;
@@ -54,10 +54,10 @@ function assess(i:IspcEligibilityInput):Pick<IspcEligibilityResult,'status'|'rea
if(liberal&&(!/^\d{1,3}$/.test(i.liberalDaysSameEntity)||Number(i.liberalDaysSameEntity)>366||!i.liberalClientRef.trim()))missing.push('maior duração anual por mesma entidade e suporte declarado');
if(i.generalRegimeChosen==='yes'&&(i.generalThreeYearsCompleted!=='yes'||!i.generalRegimeRef.trim()))missing.push('cumprimento dos três anos5(3)e referência da opção/registo');
if(missing.length)return stop('indeterminate','Condições específicas por esclarecer;desconhecido não significa não aplicável nemzero.',missing);
return stop('compatible','Condições de acesso da actividade habitual compatíveis com os dados de referência declarados;opção separada. Volume do ano emcurso,trimestre,excesso,taxas,mínimo e retenções não são apurados neste incremento.'+(liberal?' Serviçosliberais:limite5anos9-A,contagemtransitória13a partir da vigência2026;sem presumir permanência fora da cobertura.':''));
return stop('compatible','Condições de acesso da actividade habitual compatíveis com os dados de referência declarados;opção separada. Volume do ano em curso, trimestre, excesso e mínimo têm apuramento trimestral próprio; retenções têm registo separado.'+(liberal?' Serviçosliberais:limite5anos9-A,contagemtransitória13a partir da vigência2026;sem presumir permanência fora da cobertura.':''));
}
const key=(d:Record<string,unknown>,b:IspcEligibilityBook)=>JSON.stringify(['ispc-eligibility/1',scope(d),b.input]);
export function ispcEligibilityCurrent(d:Record<string,unknown>,b:IspcEligibilityBook):boolean{return!!b.result&&b.result.key===key(d,b);}
export function recordIspcEligibility(d:Record<string,unknown>,b:IspcEligibilityBook,now=new Date().toISOString()):IspcEligibilityBook{validate(b);const result:IspcEligibilityResult={calculatedAt:now,key:key(d,b),scope:scope(d),input:structuredClone(b.input),...assess(b.input),...option(b.input),source:ispcEligibilitySource};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};}
export function ispcReuseDiff(d:Record<string,unknown>,b:IspcEligibilityBook):Array<{field:keyof IspcEligibilityInput;before:string;after:string}>{return Object.entries(ispcReuseValues(d)).filter(([field,value])=>b.input[field as keyof IspcEligibilityInput]!==value).map(([field,value])=>({field:field as keyof IspcEligibilityInput,before:String(b.input[field as keyof IspcEligibilityInput]),after:String(value)}));}
export function ispcEligibilityReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=ispcEligibilityBook(d),r=b.result;if(!r)return[];const i=r.input;return[['ISPC','L4-A — enquadramento2026',`${r.status==='compatible'?'Compatível com os dados de referência':r.status==='incompatible'?'Incompatível':'Indeterminado'};${r.reason};falta:${r.missing.join('; ')||'nenhum dado deste recorte'}`,`${r.scope};${ispcEligibilityCurrent(d,b)?'Entradas correspondentes':'Histórico preservado;rever entradas'};${r.calculatedAt};${r.source}`],['ISPC','Origem e âmbito',`${i.entityRef||'Entidade por identificar'};${i.personType};${i.origin==='projected'?'Pressupostos de simulação':'Dados efectivos declarados'};${i.activities.map(a=>ispcActivities.find(([kind])=>kind===a)?.[1]??a).join('; ')};${i.activityDescription};estabelecimentos${i.establishments};territórionacional${i.territory}`,`Referênciaanual:${i.basis}/${i.referenceYear};${r.annualReference?fractionPresentation(r.annualReference).decimal:'Indeterminado'}MZN;${i.annualReferenceRef};previsãoAT${i.forecastConfirmed}/${i.forecastConfirmationRef};liberal${i.liberalDaysSameEntity}dias/${i.liberalClientRef};factos:${JSON.stringify({organizedRequired:i.organizedRequired,sharesInIspc:i.sharesInIspc,sharesAnonymous:i.sharesAnonymous,otherActivities:i.otherActivities,unlistedServices:i.unlistedServices,generalRegimeChosen:i.generalRegimeChosen,generalThreeYearsCompleted:i.generalThreeYearsCompleted,generalRegimeRef:i.generalRegimeRef})}`],['ISPC','Opção e próximo passo',`${r.optionReason};referência${i.optionReference};data${i.optionDate}`,`Apuramento trimestral ainda pendente;sem imposto ou opção automáticos. ${r.source}`]];}catch(error){return[['ISPC','Enquadramento','Dados não recuperáveis;originais preservados',String(error)]];}}
export function ispcEligibilityReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=ispcEligibilityBook(d),r=b.result;if(!r)return[];const i=r.input;return[['ISPC','L4-A — enquadramento2026',`${r.status==='compatible'?'Compatível com os dados de referência':r.status==='incompatible'?'Incompatível':'Indeterminado'};${r.reason};falta:${r.missing.join('; ')||'nenhum dado deste recorte'}`,`${r.scope};${ispcEligibilityCurrent(d,b)?'Entradas correspondentes':'Histórico preservado;rever entradas'};${r.calculatedAt};${r.source}`],['ISPC','Origem e âmbito',`${i.entityRef||'Entidade por identificar'};${i.personType};${i.origin==='projected'?'Pressupostos de simulação':'Dados efectivos declarados'};${i.activities.map(a=>ispcActivities.find(([kind])=>kind===a)?.[1]??a).join('; ')};${i.activityDescription};estabelecimentos${i.establishments};territórionacional${i.territory}`,`Referênciaanual:${i.basis}/${i.referenceYear};${r.annualReference?fractionPresentation(r.annualReference).decimal:'Indeterminado'}MZN;${i.annualReferenceRef};previsãoAT${i.forecastConfirmed}/${i.forecastConfirmationRef};liberal${i.liberalDaysSameEntity}dias/${i.liberalClientRef};factos:${JSON.stringify({organizedRequired:i.organizedRequired,sharesInIspc:i.sharesInIspc,sharesAnonymous:i.sharesAnonymous,otherActivities:i.otherActivities,unlistedServices:i.unlistedServices,generalRegimeChosen:i.generalRegimeChosen,generalThreeYearsCompleted:i.generalThreeYearsCompleted,generalRegimeRef:i.generalRegimeRef})}`],['ISPC','Opção e próximo passo',`${r.optionReason};referência${i.optionReference};data${i.optionDate}`,`Prossiga no apuramento trimestral; esta avaliação não adopta regime nem calcula imposto. ${r.source}`]];}catch(error){return[['ISPC','Enquadramento','Dados não recuperáveis;originais preservados',String(error)]];}}
+45
View File
@@ -0,0 +1,45 @@
import {acquisitionScope} from './iva-acquisitions.ts';
import {ispcEligibilityBook,ispcEligibilityCurrent,type IspcEligibilityResult} from './ispc-eligibility.ts';
import {addFractions,decimalFraction,fraction,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export const ispcQuarterSource='Lei 9/2025, republicação ISPC, arts 8, 9, 10, 10-A e 11; BR248/2025, PDF11/p2180(11). Taxa do escalão anual aplicada à base trimestral (8(1)a e 9); 20% apenas na parcela em excesso de 4.000.000; actividades cumuladas: taxa mais alta (8(2)). Sem fraccionar os limites anuais por quatro nem copiar o volume do ano anterior. Preparação do apuramento; sem submissão ou pagamento AT.';
export type IspcQuarterInput={year:string;quarter:string;sales:[string,string,string,string];salesRefs:[string,string,string,string];annualBandVolume:string;annualBandOrigin:''|'forecast'|'closed_year';annualBandRef:string;coverage:''|'all_taxpayer_activity';};
export type IspcQuarterResult={key:string;calculatedAt:string;scope:string;input:IspcQuarterInput;eligibility:IspcEligibilityResult|null;status:'calculated'|'indeterminate'|'not_applicable';reason:string;missing:string[];base:FiscalFraction|null;prior:FiscalFraction|null;ordinary:FiscalFraction|null;excess:FiscalFraction|null;rate:number|null;tax:FiscalFraction|null;payment:FiscalFraction|null;declaration:boolean;projected:boolean;source:string};
export type IspcQuarterBook={version:1;input:IspcQuarterInput;result?:IspcQuarterResult;history?:IspcQuarterResult[]};
export const newIspcQuarterInput=():IspcQuarterInput=>({year:'2026',quarter:'1',sales:['','','',''],salesRefs:['','','',''],annualBandVolume:'',annualBandOrigin:'',annualBandRef:'',coverage:''});
function snapshot(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros não recuperáveis; originais preservados.');return s;}
function validate(b:IspcQuarterBook){if(!b||b.version!==1||!b.input||['year','quarter','annualBandVolume','annualBandOrigin','annualBandRef','coverage'].some(k=>typeof b.input[k as keyof IspcQuarterInput]!=='string')||!['','forecast','closed_year'].includes(b.input.annualBandOrigin)||!['','all_taxpayer_activity'].includes(b.input.coverage)||!Array.isArray(b.input.sales)||b.input.sales.length!==4||b.input.sales.some(v=>typeof v!=='string')||!Array.isArray(b.input.salesRefs)||b.input.salesRefs.length!==4||b.input.salesRefs.some(v=>typeof v!=='string')||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!b.result.input||!Array.isArray(b.result.missing)))throw Error('Apuramento ISPC não recuperável; sem substituir por zero ou apagar histórico.');}
function books(d:Record<string,unknown>):Record<string,IspcQuarterBook>{const v=snapshot(d).ispcQuarters;if(v===undefined)return{};if(!v||typeof v!=='object'||Array.isArray(v))throw Error('Mapa trimestral ISPC inválido; originais preservados.');return v;}
export function ispcQuarterBook(d:Record<string,unknown>):IspcQuarterBook{const b=books(d)[acquisitionScope(d)]??{version:1,input:newIspcQuarterInput()};validate(b);return b;}
export function saveIspcQuarterBook(d:Record<string,unknown>,b:IspcQuarterBook):string{validate(b);return JSON.stringify({...snapshot(d),ispcQuarters:{...books(d),[acquisitionScope(d)]:b}});}
const cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
const minimum=(a:FiscalFraction,b:FiscalFraction)=>cmp(a,b)<=0n?a:b;
const positive=(a:FiscalFraction)=>cmp(a,fraction(0n))>0n?a:fraction(0n);
const atRate=(v:FiscalFraction,r:number)=>fraction(BigInt(v.numerator)*BigInt(r),BigInt(v.denominator)*100n);
function eligibility(d:Record<string,unknown>){const b=ispcEligibilityBook(d);return ispcEligibilityCurrent(d,b)?b.result??null:null;}
const key=(d:Record<string,unknown>,b:IspcQuarterBook)=>JSON.stringify(['ispc-quarter/1',acquisitionScope(d),b.input,eligibility(d)]);
export const ispcQuarterCurrent=(d:Record<string,unknown>,b:IspcQuarterBook)=>!!b.result&&b.result.key===key(d,b);
export function recordIspcQuarter(d:Record<string,unknown>,b:IspcQuarterBook,now=new Date().toISOString()):IspcQuarterBook{
validate(b);const i=b.input,e=eligibility(d),r:IspcQuarterResult={key:key(d,b),calculatedAt:now,scope:acquisitionScope(d),input:structuredClone(i),eligibility:e?structuredClone(e):null,status:'indeterminate',reason:'',missing:[],base:null,prior:null,ordinary:null,excess:null,rate:null,tax:null,payment:null,declaration:false,projected:e?.input.origin==='projected'||i.annualBandOrigin==='forecast',source:ispcQuarterSource};
const finish=()=>({...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result:r});
if(!e){r.missing.push('Avaliação de elegibilidade actual correspondente às entradas');r.reason='Reveja e guarde o enquadramento; a avaliação anterior continua no histórico.';return finish();}
if(e.status==='incompatible'||e.optionStatus==='not_opted'){r.status='not_applicable';r.reason='Os factos guardados não permitem aplicar ISPC ou indicam que não optou. Outros regimes mantêm os seus percursos.';return finish();}
if(e.status!=='compatible'){r.missing.push(...e.missing);r.reason='Enquadramento indeterminado; não produzir imposto zero.';return finish();}
// A preparation scenario may estimate tax, but never asserts an effective election.
if(e.optionStatus==='indeterminate'||e.input.origin==='effective'&&e.optionStatus!=='recorded'){r.missing.push('Opção documentada para apuramento efectivo; a preparação permite somente simulação');}
if(e.optionStatus==='preparation')r.projected=true;
if(i.year!=='2026'||!['1','2','3','4'].includes(i.quarter))r.missing.push('Ano 2026 e trimestre entre 1 e 4');
if(i.coverage!=='all_taxpayer_activity')r.missing.push('Confirmar volume de todas as actividades do sujeito passivo, não apenas desta oportunidade');
const q=Number(i.quarter),amounts:FiscalFraction[]=[],allFour=i.annualBandOrigin==='closed_year'&&!e.input.activities.some(a=>['liberal15','services12'].includes(a));
if(e.input.origin==='effective'&&e.input.optionDate&&['1','2','3','4'].includes(i.quarter)&&e.input.optionDate>`2026-${['03-31','06-30','09-30','12-31'][q-1]}`)r.missing.push('A opção registada é posterior ao trimestre; esclarecer operações abrangidas sem retroagir automaticamente');
if(['1','2','3','4'].includes(i.quarter))for(let n=0;n<(allFour?4:q);n++){try{amounts[n]=decimalFraction(i.sales[n]);}catch{r.missing.push(`Volume do trimestre ${n+1}, incluindo zero expresso`);}if(!i.salesRefs[n].trim())r.missing.push(`Suporte ou pressuposto do trimestre ${n+1}`);}
const liberal=e.input.activities.includes('liberal15'),service=e.input.activities.includes('services12');
let band:FiscalFraction|null=null;
if(!liberal&&!service){try{band=decimalFraction(i.annualBandVolume);}catch{r.missing.push('Volume anual de 2026 para identificar o escalão; distinto do volume de acesso 2025');}if(!i.annualBandOrigin||!i.annualBandRef.trim())r.missing.push('Origem (previsão ou ano encerrado) e suporte do escalão anual');}
if(r.missing.length){r.reason='Faltam dados específicos para este apuramento; os restantes percursos mantêm-se disponíveis.';return finish();}
r.base=amounts[q-1];r.prior=addFractions(amounts.slice(0,q-1));const cumulative=addFractions(amounts.slice(0,q)),annualTotal=addFractions(amounts);
if(band&&(cmp(band,cumulative)<0n||i.annualBandOrigin==='closed_year'&&cmp(band,annualTotal)!==0n)){r.reason='Volume anual do escalão diverge dos trimestres declarados; reveja a divergência antes de calcular.';r.missing.push('Reconciliar referência anual 2026 com o acumulado trimestral');return finish();}
r.rate=liberal?15:service?12:band&&cmp(band,fraction(1000000n))<=0n?3:band&&cmp(band,fraction(2500000n))<=0n?4:5;
r.ordinary=minimum(r.base,positive(subtractFractions(fraction(4000000n),r.prior)));r.excess=subtractFractions(r.base,r.ordinary);r.tax=addFractions([atRate(r.ordinary,r.rate),atRate(r.excess,20)]);r.payment=cmp(r.tax,fraction(500n))<0n?fraction(0n):r.tax;r.declaration=true;r.status='calculated';r.reason=`${r.projected?'Simulação com pressupostos explícitos; sem adesão automática.':'Apuramento segundo factos e opção documentados.'} Taxa ${r.rate}% sobre a parcela ordinária trimestral e 20% apenas sobre o excesso anual. ${cmp(r.tax,fraction(500n))<0n?'Prestação inferior a 500 MZN: sem pagamento; obrigação declarativa preservada.':'Prestação igual ou superior a 500 MZN: montante a preparar para pagamento.'} Sem dedução de custos, retenções sofridas presumidas, compensação ou acumulação de prestações dispensadas.`;return finish();
}
const amount=(v:FiscalFraction|null)=>v?`${fractionPresentation(v).decimal} MZN (${v.numerator}/${v.denominator})`:'Indeterminado';
export function ispcQuarterReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=ispcQuarterBook(d),r=b.result;if(!r)return[];return[['ISPC',`Apuramento ${r.input.year}/T${r.input.quarter}`,`${r.status==='calculated'?'Calculado':r.status==='not_applicable'?'Não aplicável':'Indeterminado'}; ${r.reason}; falta: ${r.missing.join('; ')||'nenhum dado deste recorte'}`,`${r.scope}; ${ispcQuarterCurrent(d,b)?'Entradas correspondentes':'Histórico preservado; rever entradas'}; ${r.calculatedAt}; ${r.source}`],['ISPC','Base e taxa trimestral',`Base: ${amount(r.base)}; anterior no ano: ${amount(r.prior)}; ordinária: ${amount(r.ordinary)} a ${r.rate??'indeterminada'}%; excesso: ${amount(r.excess)} a 20%`,`Volume anual de escalão: ${r.input.annualBandVolume||'não necessário para taxa de serviços'}; ${r.input.annualBandOrigin}; ${r.input.annualBandRef}; suportes: ${r.input.salesRefs.join('; ')}`],['ISPC','Imposto, pagamento e declaração',`Imposto: ${amount(r.tax)}; prestação a pagar: ${amount(r.payment)}; obrigação declarativa: ${r.declaration?'mantida':'por determinar neste percurso'}; ${r.projected?'simulação':'factos efectivos'}`,`Opção: ${r.eligibility?.optionStatus??'indeterminada'}; referência: ${r.eligibility?.input.optionReference??''}; histórico: ${b.history?.length??0} versão(ões) anterior(es). Sem submissão AT.`]];}catch(error){return[['ISPC','Apuramento não recuperável',String(error),'Originais preservados.']];}}
+24
View File
@@ -0,0 +1,24 @@
import {ispcEligibilityBook,ispcEligibilityCurrent} from './ispc-eligibility.ts';
import {acquisitionScope} from './iva-acquisitions.ts';
import {addFractions,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export type IspcWithholdingInput={applicable:''|'no'|'yes';quarter:string;base:string;priorAnnualPurchases:string;naturalOccasional:''|'yes'|'no';unregistered:''|'yes'|'no';related:''|'yes'|'no';activity:''|'forestry'|'fishery'|'livestock'|'agriculture'|'poultry'|'beekeeping'|'other';reference:string;};
export type IspcWithholdingResult={input:IspcWithholdingInput;key:string;status:'calculated'|'indeterminate'|'not_applicable';reason:string;withheld:FiscalFraction|null;calculatedAt:string;source:string};
export type IspcWithholdingBook={version:1;input:IspcWithholdingInput;result?:IspcWithholdingResult;history?:IspcWithholdingResult[]};
export const ispcWithholdingSource='Lei9/2025,republicaçãoISPC,arts3(3–4),12(3–4);PDF10–11/p2180(10–11). Retenção liberatória5% sobre aquisição a pessoa singular ocasional não inscrita, ligada à actividade, até limite global anual2.500.000MZN; factura por conta do fornecedor e entrega trimestral em modelo próprio. Não é crédito contra o imposto próprio nem se aplica o mínimo500 desta prestação à retenção.';
const blank=():IspcWithholdingInput=>({applicable:'',quarter:'1',base:'',priorAnnualPurchases:'',naturalOccasional:'',unregistered:'',related:'',activity:'',reference:''});
function snapshot(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros não recuperáveis; originais preservados.');return s;}
function validate(b:IspcWithholdingBook){if(!b||b.version!==1||!b.input||Object.keys(blank()).some(k=>typeof b.input[k as keyof IspcWithholdingInput]!=='string')||['applicable','naturalOccasional','unregistered','related'].some(k=>!['','yes','no'].includes(b.input[k as keyof IspcWithholdingInput]))||!['','forestry','fishery','livestock','agriculture','poultry','beekeeping','other'].includes(b.input.activity)||b.history!==undefined&&!Array.isArray(b.history))throw Error('RetençõesISPC não recuperáveis; originais preservados.');}
function map(d:Record<string,unknown>):Record<string,IspcWithholdingBook>{const v=snapshot(d).ispcWithholdings;if(v===undefined)return{};if(!v||typeof v!=='object'||Array.isArray(v))throw Error('Mapa de retençõesISPC inválido.');return v;}
export function ispcWithholdingBook(d:Record<string,unknown>):IspcWithholdingBook{const b=map(d)[acquisitionScope(d)]??{version:1,input:blank()};validate(b);return b;}
export function saveIspcWithholdingBook(d:Record<string,unknown>,b:IspcWithholdingBook):string{validate(b);return JSON.stringify({...snapshot(d),ispcWithholdings:{...map(d),[acquisitionScope(d)]:b}});}
export function recordIspcWithholding(d:Record<string,unknown>,b:IspcWithholdingBook,now=new Date().toISOString()):IspcWithholdingBook{validate(b);const i=b.input,e=ispcEligibilityBook(d),r:IspcWithholdingResult={input:structuredClone(i),key:JSON.stringify([acquisitionScope(d),i]),status:'indeterminate',reason:'Identifique se existem aquisições abrangidas; não presumir retenção zero.',withheld:null,calculatedAt:now,source:ispcWithholdingSource};
if(i.applicable==='no'){r.status='not_applicable';r.reason='Sem aquisições abrangidas, segundo declaração expressa; entradas anteriores preservadas.';}
else if(i.applicable==='yes'&&(!ispcEligibilityCurrent(d,e)||e.result?.status!=='compatible'||e.result.optionStatus==='not_opted'||e.result.optionStatus==='indeterminate'||e.input.origin==='effective'&&e.result.optionStatus!=='recorded')){r.reason='Guarde o enquadramento e a opção aplicáveis do adquirenteISPC antes de calcular retenção efectiva; sem inferir adesão.';}
else if(i.applicable==='yes'){
if(i.naturalOccasional==='no'||i.unregistered==='no'||i.related==='no'||i.activity==='other'){r.status='not_applicable';r.reason='Aquisição fora do recorte12(3–4) declarado; outros regimes de retenção mantêm análise própria,sem presumir isenção.';}
else if(i.naturalOccasional==='yes'&&i.unregistered==='yes'&&i.related==='yes'&&i.activity&&i.reference.trim()&&['1','2','3','4'].includes(i.quarter)){
try{const base=decimalFraction(i.base),prior=decimalFraction(i.priorAnnualPurchases),total=addFractions([base,prior]);if(BigInt(total.numerator)>2500000n*BigInt(total.denominator))r.reason='Total global anual superior2.500.000MZN: tratamento da aquisição acima do limite por corresponder; não reter automaticamente5% nem truncar a base.';else{r.status='calculated';r.withheld=fraction(BigInt(base.numerator)*5n,BigInt(base.denominator)*100n);r.reason='Retenção5% calculada separadamente do imposto próprio; emitir factura por conta do fornecedor e preparar entrega trimestral em modelo próprio. Sem deduzir esta retenção da prestaçãoISPC.';}}catch{r.reason='Base e aquisições globais anteriores do ano necessários,incluindo zero expresso; não presumir montantes.';}
}else r.reason='Identifique pessoa singular ocasional3(3–4),actividade abrangida,não inscrição,ligação à actividade,trimestre e suporte; dados desconhecidos não viram não aplicáveis.';
}
return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result:r};}
export function ispcWithholdingReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=ispcWithholdingBook(d),r=b.result;if(!r)return[];return[['ISPC','Retenção em aquisições ocasionais',`${r.status}; ${r.withheld?fractionPresentation(r.withheld).decimal+' MZN':'Indeterminado/não aplicado'}; ${r.reason}`,`2026/T${r.input.quarter}; base:${r.input.base}; anteriores globais:${r.input.priorAnnualPurchases}; suporte:${r.input.reference}; ${r.key===JSON.stringify([acquisitionScope(d),b.input])?'Entradas correspondentes':'Histórico preservado;rever entradas'}; ${r.calculatedAt}; ${r.source}`]];}catch(e){return[['ISPC','Retenção não recuperável',String(e),'Originais preservados.']];}}
@@ -175,6 +175,7 @@ const rowSection: Record<string, DossierReportSectionId> = {
Orçamento: 'finance',
Tesouraria: 'finance',
'IVA mensal': 'finance',
ISPC: 'finance',
Equipa: 'team',
'Necessidade de negócio': 'resources',
Financiamento: 'resources',
+4
View File
@@ -1,3 +1,5 @@
import {ispcWithholdingReportRows} from './ispc-withholding.ts';
import {ispcQuarterReportRows} from './ispc-quarter.ts';
import {ispcEligibilityReportRows} from './ispc-eligibility.ts';
import {fiscalCalculationGapReportRows} from './iva-calculation-gaps.ts';
import {l3FormPreparationReportRows} from './l3-form-preparation.ts';
@@ -687,6 +689,8 @@ export function buildProjectDossierRows(
rows.push(...travelEventReportRows(draft));
rows.push(...offerPreparationReportRows(draft));
rows.push(...ispcEligibilityReportRows(draft));
rows.push(...ispcQuarterReportRows(draft));
rows.push(...ispcWithholdingReportRows(draft));
rows.push(...fiscalCalculationGapReportRows(draft));
rows.push(...l3FormPreparationReportRows(draft));
const operationRows = salesReportRows(draft);
+24
View File
@@ -0,0 +1,24 @@
<?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 IspcQuarterDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_quarter_and_option_recover_privately_without_changing_accepted_fiscal_books(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$actor=User::factory()->create(['email_verified_at'=>now()]);$this->actingAs($actor);
$draft=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-quarter-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
$personal=['person_type'=>'collective'];unset($draft['person_type']);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'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']);
$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$r=$s['ispcQuarters']['opportunity:375']['result'];$this->assertSame('calculated',$r['status']);$this->assertSame(['numerator'=>'24000','denominator'=>'1'],$r['tax']);$this->assertSame($r['tax'],$r['payment']);$this->assertTrue($r['declaration']);$this->assertFalse($r['projected']);$this->assertSame('recorded',$r['eligibility']['optionStatus']);$this->assertSame('SYNTHETIC QA opção ISPC2026',$r['eligibility']['input']['optionReference']);$this->assertNotEmpty($s['ispcQuarters']['opportunity:375']['history']);
$seed=json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l3e-preserved.json')),true,flags:JSON_THROW_ON_ERROR);foreach(json_decode($seed['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR)as$key=>$value)$this->assertSame($value,$s[$key]);
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$this->getJson('/api/v1/simulation-drafts/'.$id)->assertNotFound();$this->actingAs($actor);$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
if(is_dir(base_path('outputs/ispc-l4b-20261025')))file_put_contents(base_path('outputs/ispc-l4b-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {newIspcEligibilityInput,recordIspcEligibility,saveIspcEligibilityBook} from '../../resources/js/lib/ispc-eligibility.ts';
import {newIspcQuarterInput,ispcQuarterBook,recordIspcQuarter,saveIspcQuarterBook,ispcQuarterCurrent,ispcQuarterReportRows} from '../../resources/js/lib/ispc-quarter.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3e-preserved.json',import.meta.url),'utf8'));
function fixture(ep={},qp={}){let d={...structuredClone(seed),context_opportunity:'opportunity:375',project_title:'SYNTHETIC QA — L4 ISPC',fiscal_component:'ispc',currency:'MZN',person_type:'collective',fiscal_activity:'SYNTHETIC QA canalização',fiscal_establishments:'1'};const input={...newIspcEligibilityInput(d),activities:['services12'],territory:'yes',basis:'previous_year',referenceYear:'2025',annualReference:'1000000',annualReferenceRef:'SYNTHETIC QA acesso2025',organizedRequired:'no',sharesInIspc:'no',sharesAnonymous:'no',otherActivities:'no',generalRegimeChosen:'no',unlistedServices:'no',...ep};const e=recordIspcEligibility(d,{version:1,input},'2026-10-25T12:00:00Z');d.modelo_a_snapshot=saveIspcEligibilityBook(d,e);const b={version:1,input:{...newIspcQuarterInput(),sales:['100000','','',''],salesRefs:['SYNTHETIC QA T1','','',''],coverage:'all_taxpayer_activity',...qp}};return{d,b};}
const calc=(ep={},qp={})=>{const{d,b}=fixture(ep,qp);return recordIspcQuarter(d,b,'2026-10-25T12:01:00Z').result;};
const decimal=x=>Number(x.numerator)/Number(x.denominator);
test('quarterly services12 andliberal15;highest mixed rate applied to entire quarterly base;no costs or VAT deduction',()=>{assert.equal(decimal(calc().tax),12000);assert.equal(decimal(calc({activities:['commerce','services12']}).tax),12000);assert.equal(decimal(calc({activities:['services12','liberal15'],liberalDaysSameEntity:'100',liberalClientRef:'QA'}).tax),15000);assert.equal(calc().projected,true);});
test('annual goods tiers applied to quarter without splitting annual limits or using previous-year accessvolume',()=>{for(const[annual,rate]of [['1000000',3],['1000000.000001',4],['2500000',4],['2500000.000001',5],['4000000',5]]){const r=calc({activities:['commerce']},{annualBandVolume:annual,annualBandOrigin:'forecast',annualBandRef:'QAannual2026'});assert.equal(r.status,'calculated',r.reason);assert.equal(r.rate,rate);assert.equal(decimal(r.tax),1000*rate);}assert.equal(calc({activities:['commerce']}).status,'indeterminate');});
test('20percent only newly crossed excess;prior quarters prevent repeating same excess;no threshold divided byfour',()=>{const q={quarter:'3',sales:['2000000','1900000','200000',''],salesRefs:['QA1','QA2','QA3','']};const r=calc({},q);assert.equal(decimal(r.ordinary),100000);assert.equal(decimal(r.excess),100000);assert.equal(decimal(r.tax),32000);const next=calc({},{...q,quarter:'4',sales:['2000000','1900000','200000','100000'],salesRefs:['QA1','QA2','QA3','QA4']});assert.equal(decimal(next.ordinary),0);assert.equal(decimal(next.tax),20000);});
test('minimum500:below no payment,declaration stays;exact500 payable;explicitzero distinct from missing',()=>{let r=calc({activities:['liberal15'],liberalDaysSameEntity:'100',liberalClientRef:'QA'},{sales:['3000','','','']});assert.equal(decimal(r.tax),450);assert.equal(decimal(r.payment),0);assert.equal(r.declaration,true);r=calc({activities:['commerce']},{sales:['16666.666667','','',''],annualBandVolume:'1000000',annualBandOrigin:'forecast',annualBandRef:'QA'});assert.ok(decimal(r.payment)>500);r=calc({}, {sales:['0','','','']});assert.equal(r.status,'calculated');assert.equal(decimal(r.tax),0);assert.equal(calc({}, {sales:['','','','']}).status,'indeterminate');const exact=calc({activities:['commerce']},{sales:['12500','','',''],annualBandVolume:'2000000',annualBandOrigin:'forecast',annualBandRef:'QA'});assert.equal(decimal(exact.payment),500);});
test('effective tax needs recorded option;notopted and incompatible are notapplicable;preparation remains scenario only',()=>{assert.equal(calc({origin:'effective'}).status,'indeterminate');assert.equal(calc({option:'not_opted'}).status,'not_applicable');assert.equal(calc({organizedRequired:'yes'}).status,'not_applicable');const r=calc({origin:'effective',option:'opted',optionDate:'2026-01-01',optionReference:'QA opção'});assert.equal(r.status,'calculated');assert.equal(r.projected,false);assert.equal(calc({origin:'effective',assessmentDate:'2026-10-25',option:'opted',optionDate:'2026-10-01',optionReference:'QA opção'}).status,'indeterminate');});
test('missing prior quarter,scope,year,source andannual mismatch do not produce zero',()=>{for(const p of [{year:'2025'},{quarter:'5'},{coverage:''},{salesRefs:['','','','']},{quarter:'2',sales:['','1000','','']}])assert.equal(calc({},p).status,'indeterminate');assert.equal(calc({activities:['commerce']},{annualBandVolume:'99999',annualBandOrigin:'forecast',annualBandRef:'QA'}).status,'indeterminate');});
test('closed-yearannual mustequalallfourquarters;quarter amount is not annual base',()=>{const q={quarter:'4',sales:['100000','200000','300000','400000'],salesRefs:['QA1','QA2','QA3','QA4'],annualBandVolume:'1000000',annualBandOrigin:'closed_year',annualBandRef:'QA2026'};assert.equal(decimal(calc({activities:['commerce']},q).tax),12000);assert.equal(calc({activities:['commerce']},{...q,annualBandVolume:'1000001'}).status,'indeterminate');assert.equal(decimal(calc({activities:['commerce']},{...q,quarter:'1'}).tax),3000);});
test('eligibility edits invalidate dependent calculation;save recovery freezesinput andretainsotherbooks/history',()=>{let{d,b}=fixture();const old=JSON.parse(d.modelo_a_snapshot);b=recordIspcQuarter(d,b);d.modelo_a_snapshot=saveIspcQuarterBook(d,b);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books,old.books);assert.equal(ispcQuarterCurrent(d,ispcQuarterBook(d)),true);const recovered=ispcQuarterBook(JSON.parse(JSON.stringify(d)));recovered.input.sales[0]='200000';assert.equal(ispcQuarterCurrent(d,recovered),false);assert.equal(decimal(recovered.result.tax),12000);const next=recordIspcQuarter(d,recovered);assert.equal(decimal(next.result.tax),24000);assert.equal(next.history.length,1);assert.equal(decimal(next.history[0].tax),12000);const s=JSON.parse(d.modelo_a_snapshot);s.ispcEligibility['opportunity:375'].input.activityDescription='edited';d.modelo_a_snapshot=JSON.stringify(s);assert.equal(recordIspcQuarter(d,next).result.status,'indeterminate');});
test('invalidstoredmaps andambiguouscontext remainpreservederrors',()=>{let{d,b}=fixture();const s=JSON.parse(d.modelo_a_snapshot);s.ispcQuarters=[];assert.throws(()=>ispcQuarterBook({...d,modelo_a_snapshot:JSON.stringify(s)}));assert.throws(()=>saveIspcQuarterBook(d,{...b,input:{...b.input,sales:['1']}}));});
test('export completeeligibilityoptioncalculationfixture andreport from recovereddata',()=>{let{d,b}=fixture({origin:'effective',assessmentDate:'2026-10-25',option:'opted',optionDate:'2026-01-01',optionReference:'SYNTHETIC QA opção ISPC2026'},{quarter:'2',sales:['100000','200000','',''],salesRefs:['SYNTHETIC QA T1','SYNTHETIC QA T2','','']});b=recordIspcQuarter(d,b,'2026-10-25T12:01:00Z');d.modelo_a_snapshot=saveIspcQuarterBook(d,b);const recovered=JSON.parse(JSON.stringify(d));assert.equal(decimal(ispcQuarterBook(recovered).result.tax),24000);assert.equal(ispcQuarterBook(recovered).result.projected,false);assert.match(JSON.stringify(ispcQuarterReportRows(recovered)),/24000.00/);const report=createProjectDossierReport(recovered,{selectedSections:new Set(['finance']),version:'QA ISPC',generatedAt:'2026-10-25T12:02:00Z'});assert.match(JSON.stringify(report.sections),/24000.00/);assert.match(JSON.stringify(report.sections),/opção ISPC2026/);if(process.env.L4_ISPC_OUTPUT)fs.writeFileSync(process.env.L4_ISPC_OUTPUT,JSON.stringify(recovered,null,2));});
+7
View File
@@ -0,0 +1,7 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {ispcWithholdingBook,saveIspcWithholdingBook,recordIspcWithholding,ispcWithholdingReportRows} from '../../resources/js/lib/ispc-withholding.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/ispc-quarter-preserved.json',import.meta.url),'utf8'));
function evaluate(p={}){const d=structuredClone(seed),b=ispcWithholdingBook(d);b.input={...b.input,applicable:'yes',base:'10000',priorAnnualPurchases:'0',naturalOccasional:'yes',unregistered:'yes',related:'yes',activity:'agriculture',reference:'SYNTHETIC QA fornecedor e aquisição',...p};return{d,b,r:recordIspcWithholding(d,b)};}
test('fivepercent withholding notownquartertax;small withholding notexempt via500minimum',()=>{let{r}=evaluate();assert.deepEqual(r.result.withheld,{numerator:'500',denominator:'1'});r=evaluate({base:'100'}).r;assert.deepEqual(r.result.withheld,{numerator:'5',denominator:'1'});});
test('globalannual2.5minclusive,exceedingnottruncated;missingnotzero;scopeexclusionsspecific',()=>{assert.equal(evaluate({priorAnnualPurchases:'2490000'}).r.result.status,'calculated');assert.equal(evaluate({priorAnnualPurchases:'2490000.000001'}).r.result.status,'indeterminate');assert.equal(evaluate({priorAnnualPurchases:''}).r.result.status,'indeterminate');for(const p of [{naturalOccasional:'no'},{unregistered:'no'},{related:'no'},{activity:'other'},{applicable:'no'}])assert.equal(evaluate(p).r.result.status,'not_applicable');assert.equal(evaluate({naturalOccasional:''}).r.result.status,'indeterminate');});
test('withholdingrecoverhistoryandotherbookspreserved;reportneveroffsetsowntax',()=>{let{d,r}=evaluate();const prior=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveIspcWithholdingBook(d,r);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).ispcQuarters,prior.ispcQuarters);const b=ispcWithholdingBook(d);assert.equal(b.result.status,'calculated');b.input.base='20000';r=recordIspcWithholding(d,b);assert.equal(r.history.length,1);assert.equal(r.history[0].withheld.numerator,'500');d.modelo_a_snapshot=saveIspcWithholdingBook(d,r);assert.match(JSON.stringify(ispcWithholdingReportRows(d)),/1000.00/);});