Files
Investidor-V2/resources/js/lib/ispc-residuals.ts
T
2026-10-25 18:18:03 +02:00

87 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {acquisitionScope} from './iva-acquisitions.ts';
import {ispcEligibilityBook,ispcEligibilityCurrent} from './ispc-eligibility.ts';
import {ispcWithholdingBook} from './ispc-withholding.ts';
import {ispcQuarterBook,ispcQuarterCurrent} from './ispc-quarter.ts';
import {addFractions,decimalFraction,fraction,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export type ResidualKind='occasional'|'acquisitions'|'formalities';
export type ResidualResult={key:string;scope:string;calculatedAt:string;status:'calculated'|'settled_by_withholding'|'partial'|'indeterminate'|'not_applicable';reason:string;missing:string[];input:Record<string,string>;amounts:Record<string,FiscalFraction|null>;details:Record<string,string>;source:string};
export type ResidualBook={version:1;kind:ResidualKind;input:Record<string,string>;result?:ResidualResult;history?:ResidualResult[]};
export const residualSources={occasional:'ISPC Lei9/2025 arts3(3–4),4,5,8,9,10-A,12(3);BR248 PDF10–11. IRPS republicação Lei19/2017,BR202/7.ºsuplemento,art8(3h,4),9;PDF4/p2602(128). Lei11/2025 não altera estes artigos. Operação imprevisível/não reiterada, pessoa singular sem actividade habitual e famílias restritas; imposto próprio distinto de retenção liberatória documentada.',acquisitions:'ISPC12(3–4):limite global anual2.500.000MZN para factura por conta/retencão5%. Parcela acima do limite não adquire taxa por ultrapassagem. IRPS57(1,2k,4),republicação19/2017 PDF13/p2602(137),e65(1)naLei11/2025 PDF17:acto isolado qualificado e sem opçãoISPC aplicável,20%sobre rendimento ilíquido declarado; não confundir com20%do excesso de vendasISPC8(1)d.',formalities:'AT Comunicado9/2026,06/10/2026,p1:ModeloB–Geral enquanto se ajusta sistema;pagamentos trimestrais emJaneiro/Abril/Julho/Outubro. AT ModeloBGeral2p:quadros1/2/3/5/6/8/10. Modelo30 disponível contém75.000/3%antigos,não usado para calcular2026. Sem submissãoAT automática.'};
const blank=(kind:ResidualKind):Record<string,string>=>kind==='occasional'?{personType:'',currency:'',origin:'projected',operationDate:'',quarter:'',noHabitual:'',territory:'',sporadic:'',activity:'',reference:'',amount:'',allQuarterOperations:'',priorYearTurnover:'',priorYearRef:'',organizedRequired:'',option:'preparation',optionRef:'',optionDate:'',settlement:'',annual2026:'',annualOrigin:'',annualRef:'',priorInYear:'',buyerIspc:'',unregistered:'',buyerAnnualPurchases:'',withheldActual:'',withheldRef:''}:kind==='acquisitions'?{route:'unknown',noIspcOnExcess:'',isolatedAct:'',incomeInMoz:'',grossExcess:'',grossRef:'',supplierOptionRef:'',reference:''}:{sourceKind:'quarter',name:'',nuit:'',fiscalArea:'',address:'',declaration:'initial',formMonth:'',formYear:'2026',formPeriodRef:'',interestSituation:'',interestAmount:'',interestRef:'',externalStatus:'preparation',externalRef:'',externalDate:''};
function store(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 maps(d:Record<string,unknown>):Record<string,Record<string,ResidualBook>>{const v=store(d).ispcResiduals;if(v===undefined)return{};if(!v||typeof v!=='object'||Array.isArray(v))throw Error('Mapa residualISPC inválido; originais preservados.');return v;}
function validate(b:ResidualBook,kind:ResidualKind){
const enums:Record<string,string[]>=kind==='occasional'?{personType:['','singular','collective'],origin:['projected','effective'],noHabitual:['','yes','no'],territory:['','yes','no'],sporadic:['','yes','no'],activity:['','forestry','fishery','livestock','agriculture','poultry','beekeeping','other'],allQuarterOperations:['','yes','no'],organizedRequired:['','yes','no'],option:['preparation','opted','not_opted'],settlement:['','self','withheld'],annualOrigin:['','forecast','closed_year'],buyerIspc:['','yes','no'],unregistered:['','yes','no']}:kind==='acquisitions'?{route:['unknown','ispc_supplier','irps_isolated'],noIspcOnExcess:['','yes','no'],isolatedAct:['','yes','no'],incomeInMoz:['','yes','no']}:{sourceKind:['quarter','occasional','acquisitions'],declaration:['initial','substitution'],interestSituation:['','none','amount'],externalStatus:['preparation','submitted','received','paid']};
if(b?.input&&Object.entries(enums).some(([k,v])=>!v.includes(b.input[k])))throw Error('Opções residuais não recuperáveis;entradas preservadas.');
if(!b||b.version!==1||b.kind!==kind||!b.input||Object.keys(blank(kind)).some(k=>typeof b.input[k]!=='string')||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!b.result.input||!b.result.amounts||!Array.isArray(b.result.missing)))throw Error('Livro residualISPC não recuperável; sem substituir por vazio.');}
export function ispcResidualBook(d:Record<string,unknown>,kind:ResidualKind):ResidualBook{const m=maps(d)[acquisitionScope(d)];if(m!==undefined&&(!m||typeof m!=='object'||Array.isArray(m)))throw Error('Contexto residual inválido.');let b=m?.[kind];if(!b){const input=blank(kind);if(kind==='occasional'){const i=ispcEligibilityBook(d).input;Object.assign(input,{personType:i.personType,currency:i.currency,origin:i.origin,operationDate:i.assessmentDate,territory:i.territory,option:i.option,optionRef:i.optionReference,optionDate:i.optionDate});}if(kind==='formalities'){if(ispcEligibilityBook(d).input.mode==='occasional'&&m?.occasional?.result)input.sourceKind='occasional';else if(!ispcQuarterBook(d).result&&m?.acquisitions?.result)input.sourceKind='acquisitions';Object.assign(input,{name:String(d.legal_name??d.organization_name??''),nuit:String(d.nuit??''),address:String(d.address??'')});}b={version:1,kind,input};}validate(b,kind);return b;}
export function saveIspcResidualBook(d:Record<string,unknown>,b:ResidualBook):string{validate(b,b.kind);const scope=acquisitionScope(d);return JSON.stringify({...store(d),ispcResiduals:{...maps(d),[scope]:{...maps(d)[scope],[b.kind]:b}}});}
const cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
const rate=(a:FiscalFraction,n:number)=>fraction(BigInt(a.numerator)*BigInt(n),BigInt(a.denominator)*100n);
const min=(a:FiscalFraction,b:FiscalFraction)=>cmp(a,b)<0n?a:b;
const positive=(a:FiscalFraction)=>cmp(a,fraction(0n))>0n?a:fraction(0n);
const validDate=(s:string)=>/^2026-\d{2}-\d{2}$/.test(s)&&!Number.isNaN(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
const amount=(s:string,label:string,r:ResidualResult)=>{try{return decimalFraction(s);}catch{r.missing.push(label);return null;}};
function dependency(d:Record<string,unknown>,kind:ResidualKind,i:Record<string,string>):unknown{if(kind==='acquisitions'){const e=ispcEligibilityBook(d);return[ispcWithholdingBook(d).input,e.input,e.result?.key];}if(kind==='formalities'){if(i.sourceKind==='quarter'){const b=ispcQuarterBook(d);return[b.input,b.result?.key,ispcQuarterCurrent(d,b)];}const b=ispcResidualBook(d,i.sourceKind==='occasional'?'occasional':'acquisitions');return[b.input,b.result?.key,b.result?.calculatedAt,ispcResidualCurrent(d,b)];}return null;}
const key=(d:Record<string,unknown>,b:ResidualBook)=>JSON.stringify(['ispc-residual/1',acquisitionScope(d),b.kind,b.input,dependency(d,b.kind,b.input)]);
export const ispcResidualCurrent=(d:Record<string,unknown>,b:ResidualBook)=>!!b.result&&b.result.key===key(d,b);
function occasional(i:Record<string,string>,r:ResidualResult){
if(i.personType==='collective'||i.noHabitual==='no'||i.territory==='no'||i.sporadic==='no'||i.activity==='other'){r.status='not_applicable';r.reason='Os factos excluem o recorte ocasional3(3–4); conserve o percurso habitual ou o imposto próprio aplicável, sem presumir isenção.';return;}
if(i.territory!=='yes')r.missing.push('Operação em território nacional,sem inferir da moeda ou nacionalidade');
if(!i.personType)r.missing.push('Pessoa singular por identificar');
if(i.currency!=='MZN'||!validDate(i.operationDate)||!['1','2','3','4'].includes(i.quarter)||validDate(i.operationDate)&&Math.ceil(Number(i.operationDate.slice(5,7))/3)!==Number(i.quarter))r.missing.push('Data/trimestre de2026 coerentes e moedaMZN');
if(i.noHabitual!=='yes'||i.sporadic!=='yes'||!['forestry','fishery','livestock','agriculture','poultry','beekeeping'].includes(i.activity)||!i.reference.trim())r.missing.push('Pessoa sem actividade habitual,acto imprevisível/não reiterado,família3(4)e suporte da operação');
const base=amount(i.amount,'Volume das operações ocasionais deste trimestre, incluindo zero expresso',r);r.amounts.base=base;
r.details.IRPS='Acto isolado8(3h)referente a8(2a),não previsível/reiterado8(4);família3(4)ISPC. Não se infere pelo nome do sector.';
if(i.settlement==='withheld'){
const purchases=amount(i.buyerAnnualPurchases,'Aquisições globais anuais do adquirente, incluindo estas operações',r),actual=amount(i.withheldActual,'Montante efectivamente retido',r);
if(i.buyerIspc!=='yes'||i.unregistered!=='yes'||!i.withheldRef.trim())r.missing.push('AdquirenteISPC,fornecedor não inscrito e documento de retenção liberatória12(3)');
if(purchases&&base&&(cmp(purchases,base)<0n||cmp(purchases,fraction(2500000n))>0n))r.missing.push('Reconciliar aquisições globais do adquirente dentro do limite2.500.000;fora dele usar tratamento próprio');
if(!r.missing.length&&base&&actual){r.amounts.tax=rate(base,5);r.amounts.withheld=actual;if(cmp(actual,r.amounts.tax)!==0n){r.missing.push('Retenção documentada diverge de5%da base;reconciliar diferença sem presumir quitação');return;}r.status='settled_by_withholding';r.details.rate='5';r.reason='Retenção liberatória documentada para estas operações; não somar nova liquidação própria3/4/5%nem usar a retenção como crédito duplicado. Pagamento próprio adicional não é gerado.';}return;
}
if(i.settlement!=='self'){r.missing.push('Via de liquidação própria ou retenção liberatória documentada');return;}
if(i.organizedRequired==='yes'){r.status='not_applicable';r.reason='Obrigação de contabilidade organizada declarada:fora da liquidação própriaISPC4(1b);entradas e documentos conservados,sem presumir isenção noutro imposto.';return;}
if(i.allQuarterOperations!=='yes')r.missing.push('Volume de todas as operações ocasionais do sujeito passivo no trimestre');
const access=amount(i.priorYearTurnover,'Volume de referência2025, sem inferir zero por ausência de actividade habitual',r);if(!i.priorYearRef.trim()||i.organizedRequired!=='no')r.missing.push('Suporte da referência anual e inexistência de obrigação de contabilidade organizada4(1)');if(access&&cmp(access,fraction(4000000n))>0n){r.status='not_applicable';r.reason='Volume de acesso4(1)superior4.000.000MZN declarado:fora da liquidação própriaISPC;sem atribuir taxa de outro regime por presunção.';return;}
if(i.option==='not_opted'){r.status='not_applicable';r.reason='Sem opçãoISPC para esta operação; não aplicar exclusãoIRPS5nem taxaISPC automaticamente.';return;}
if(!['preparation','opted'].includes(i.option)||i.option==='opted'&&(!i.optionRef.trim()||!validDate(i.optionDate)||i.optionDate>i.operationDate)||i.origin==='effective'&&i.option!=='opted')r.missing.push('Opção documentada aplicável à operação efectiva;preparação apenas permite cenário');
const annual=amount(i.annual2026,'Volume anual2026 para escalão, distinto do acesso2025',r),prior=amount(i.priorInYear,'Volume anterior no ano2026',r);if(!['forecast','closed_year'].includes(i.annualOrigin)||!i.annualRef.trim())r.missing.push('Referência anual suportada e previsão identificada como cenário');
if(base&&prior&&annual&&(cmp(annual,addFractions([base,prior]))<0n||i.annualOrigin==='closed_year'&&i.quarter==='4'&&cmp(annual,addFractions([base,prior]))!==0n))r.missing.push('Reconciliar volume anual com acumulado anterior e trimestre');
if(r.missing.length||!base||!prior||!annual)return;
const n=cmp(annual,fraction(1000000n))<=0n?3:cmp(annual,fraction(2500000n))<=0n?4:5,ordinary=min(base,positive(subtractFractions(fraction(4000000n),prior))),excess=subtractFractions(base,ordinary),tax=addFractions([rate(ordinary,n),rate(excess,20)]);r.amounts={...r.amounts,ordinary,excess,tax,payment:cmp(tax,fraction(500n))<0n?fraction(0n):tax};r.details.projected=i.origin==='projected'||i.option==='preparation'||i.annualOrigin==='forecast'?'yes':'no';r.details.rate=String(n);r.details.declaration='Mantida mesmo quando prestação inferior500MZN';r.status='calculated';r.reason=`${r.details.projected==='yes'?'Cenário declarado;sem adesão automática.':'Opção e factos efectivos declarados.'} Operações ocasionais próprias3(3–4),base trimestral9,taxa${n}%e20%somente no excesso de vendas acima4.000.000. Não há retenção presumida nem dedução de custos/IVA.`;
}
function acquisitions(d:Record<string,unknown>,i:Record<string,string>,r:ResidualResult){
const w=ispcWithholdingBook(d).input,e=ispcEligibilityBook(d);if(!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.missing.push('Enquadramento/opção actual do adquirenteISPC');return;}
if(w.applicable!=='yes'||w.naturalOccasional!=='yes'||w.unregistered!=='yes'||w.related!=='yes'||!['forestry','fishery','livestock','agriculture','poultry','beekeeping'].includes(w.activity)||!w.reference.trim()){r.missing.push('Condições específicas12(3–4)e suporte no livro das aquisições');return;}
const base=amount(w.base,'Base das aquisições',r),prior=amount(w.priorAnnualPurchases,'Aquisições globais anteriores do ano',r);if(!base||!prior)return;const covered=min(base,positive(subtractFractions(fraction(2500000n),prior))),excess=subtractFractions(base,covered);r.amounts={base,prior,covered,excess,coveredTax:rate(covered,5),excessTax:null,total:null};r.details.quarter=w.quarter;r.details.acquisitionRef=w.reference;
if(cmp(excess,fraction(0n))<=0n){r.status='not_applicable';r.reason='Sem parcela acima do limite; o cálculo5%já disponível permanece no seu livro, sem segunda liquidação.';return;}
r.status='partial';r.reason='Parcela até ao limite separada a5%; parcela excedente quantificada, sem presumir5%ou20%pelo simples excesso. Montante total continua indeterminado até identificar tratamento próprio.';
if(i.route==='irps_isolated'){
const net=amount(i.grossExcess,'Rendimento ilíquido da parcela excedente, distinto de rendimento líquido e do preço líquido de IVA',r);if(i.noIspcOnExcess!=='yes'||i.isolatedAct!=='yes'||i.incomeInMoz!=='yes'||!i.grossRef.trim()||!i.reference.trim())r.missing.push('Acto isoladoIRPS8(3h,4),rendimento emMoçambique,sem opçãoISPC5aplicável à parcela,e referências da classificação/base');
if(net&&cmp(net,excess)!==0n)r.missing.push('Base ilíquida57(4)não coincide com a parcela bruta identificada;reconciliar sem deduzir custos/IVA automaticamente');
if(!r.missing.length&&net){r.amounts.excessTax=rate(net,20);r.amounts.total=addFractions([r.amounts.coveredTax!,r.amounts.excessTax]);r.status='calculated';r.reason='Correspondência positiva independente:IRPS57(2k)acto isolado,20%sobre rendimento ilíquido57(4)da parcela identificada,sem opçãoISPCaplicável. Não se usa taxa20%do excesso de vendasISPC. Retenções5%e20%separadas,não são crédito do imposto próprio do adquirente.';}
}else if(i.route==='ispc_supplier'){if(!i.supplierOptionRef.trim())r.missing.push('Referência da opção/registoISPCdo fornecedor aplicável a estas operações');r.reason='Fornecedor com opçãoISPC declarada:art5impede aplicarIRPSporpresunção. A parcela fora12(3)exige factura/registo do fornecedor e liquidação própria correspondente; o comprador não atribui5%ou20%automaticamente. Sem total de retenção presumido.';}
else r.missing.push('Enquadramento específico da parcela excedente:opçãoISPCdo fornecedor ou acto isoladoIRPS,base e suporte');
}
function formalities(d:Record<string,unknown>,i:Record<string,string>,r:ResidualResult){
let src:ResidualResult|undefined;if(i.sourceKind==='quarter'){const b=ispcQuarterBook(d),v=b.result;if(!v||!ispcQuarterCurrent(d,b)||v.status!=='calculated'){r.missing.push('Apuramento trimestral actual e calculado');return;}r.amounts={base:v.base,tax:v.tax,payment:v.payment,ordinary:v.ordinary,excess:v.excess};r.details={quarter:v.input.quarter,rate:String(v.rate),sourceDate:v.calculatedAt};}
else{const b=ispcResidualBook(d,i.sourceKind==='occasional'?'occasional':'acquisitions');src=b.result;if(!src||!ispcResidualCurrent(d,b)){r.missing.push('Resultado actual do percurso seleccionado');return;}if(src.status==='settled_by_withholding'){r.status='not_applicable';r.reason='Operação própria já com retenção liberatória documentada:conservar comprovativo,sem emitir liquidação própria duplicada.';return;}if(src.status!=='calculated'){r.missing.push('Cálculo completo do percurso seleccionado;as partes determinadas permanecem consultáveis');return;}r.amounts={...src.amounts,tax:src.amounts.total??src.amounts.tax,payment:src.amounts.total??src.amounts.payment};r.details={quarter:src.input.quarter||src.details.quarter,rate:src.details.rate||'retenções separadas',sourceDate:src.calculatedAt};}
const first=r.amounts.covered??r.amounts.ordinary??r.amounts.base,second=r.amounts.excess??fraction(0n),firstRate=i.sourceKind==='acquisitions'?5:Number(r.details.rate);
if(first&&[3,4,5,12,15].includes(firstRate)){const firstTax=r.amounts.coveredTax??rate(first,firstRate),secondTax=r.amounts.excessTax??rate(second,20);r.amounts.q8base01=first;r.amounts.q8tax05=firstTax;r.amounts.q8base02=second;r.amounts.q8tax06=secondTax;r.details.q8rate01=String(firstRate);r.details.q8rate02='20';if(r.amounts.tax&&cmp(addFractions([firstTax,secondTax]),r.amounts.tax)!==0n)r.missing.push('Parcelas do quadro8divergem do cálculo guardado;conferir apenas essa correspondência');}
if(i.sourceKind==='acquisitions'&&!(i.retentionFormRef??'').trim())r.missing.push('Modelo/código de entrega da retençãoISPC12(3)e da retençãoIRPS57(2k),distintos do imposto próprio;comunicado9não resolve isoladamente este modelo');
if(r.amounts.tax&&r.amounts.payment&&cmp(r.amounts.tax,fraction(0n))>0n&&cmp(r.amounts.tax,fraction(500n))<0n&&cmp(r.amounts.payment,fraction(0n))===0n)r.missing.push('Representação da dispensa de pagamento10-A noB–Geral:declarar imposto apurado sem presumir zero no campo de montante apurado');
r.details.model='Modelo B–Geral;comunicadoAT9/2026';r.details.paymentMonth=['Abril2026','Julho2026','Outubro2026','Janeiro2027'][Number(r.details.quarter)-1]||'Trimestre por identificar';r.details.formPeriod=i.formMonth?`${i.formMonth}/${i.formYear}`:'Indeterminado:quadro2pede mês/ano;comunicado identifica trimestre/mês de pagamento,sem resolver a codificação do período';
if(!/^[0-9]{9}$/.test(i.nuit)||!i.name.trim()||!i.fiscalArea.trim()||!i.address.trim())r.missing.push('NUIT9dígitos,nome,área fiscal/UGC e domicílio para formalização;preparação do cálculo não exige esses campos');
if(!/^(0[1-9]|1[0-2])$/.test(i.formMonth)||!['2026','2027'].includes(i.formYear)||!i.formPeriodRef.trim())r.missing.push('Mês/ano a inscrever no quadro2e instrução/correspondência utilizada; não preencher automaticamente com mês de pagamento');
if(!['none','amount'].includes(i.interestSituation)||!i.interestRef.trim())r.missing.push('Situação dos juros compensatórios e suporte;desconhecido não é zero');
if(i.interestSituation==='amount')r.amounts.interest=amount(i.interestAmount,'Juros documentados,não calculados por falta de taxa/período regulamentar',r);else if(i.interestSituation==='none')r.amounts.interest=fraction(0n);
r.amounts.finalPayment=r.amounts.payment&&r.amounts.interest?addFractions([r.amounts.payment,r.amounts.interest]):null;
if(i.externalStatus!=='preparation'&&(!i.externalRef.trim()||!i.externalDate.trim()))r.missing.push('Referência e data da submissão/recepção/pagamento declarado;sem confirmação automáticaAT');
r.status=r.missing.length?'partial':'calculated';r.reason='Preparação dos campos identificados do B–Geral,com cálculo e prestação separados. O Modelo30antigo não recebe taxas2026. Submissão,recepção e pagamento são actos externos;este registo conserva referências e histórico,sem os efectuar ou certificar.';
}
export function priorOccasionalSettlement(b:ResidualBook):ResidualResult|undefined{if(b.kind!=='occasional'||b.input.settlement!=='self')return;return[b.result,...(b.history??[])].find(r=>r?.status==='settled_by_withholding'&&r.input.reference.trim()===b.input.reference.trim()&&r.input.operationDate===b.input.operationDate&&r.input.quarter===b.input.quarter&&r.input.amount===b.input.amount);}
export function recordIspcResidual(d:Record<string,unknown>,b:ResidualBook,now=new Date().toISOString()):ResidualBook{validate(b,b.kind);const r:ResidualResult={key:key(d,b),scope:acquisitionScope(d),calculatedAt:now,status:'indeterminate',reason:'Dados específicos em falta;sem presumir zero.',missing:[],input:structuredClone(b.input),amounts:{},details:{},source:residualSources[b.kind]};if(b.kind==='occasional'){occasional(b.input,r);const prior=priorOccasionalSettlement(b);if(prior&&r.status==='calculated'){r.details.priorWithholdingRef=prior.input.withheldRef;r.details.priorWithholdingDate=prior.calculatedAt;if(b.input.withholdingCorrected==='yes'&&(b.input.withholdingCorrectionRef??'').trim()){r.details.withholdingCorrectionRef=b.input.withholdingCorrectionRef;r.reason+=' Correcção da retenção anterior declarada segundo referência externa,sem certificar esse acto;histórico preservado.';}else{r.status='partial';r.amounts.hypotheticalOwnTax=r.amounts.tax;r.amounts.tax=null;r.amounts.payment=null;r.amounts.withheld=prior.amounts.withheld??prior.amounts.tax;r.missing.push('Retenção liberatória anterior para a mesma operação;reconciliar eventual correcção documentada antes de nova prestação própria');r.reason='A via foi alterada,mas a retenção liberatória documentada permanece no histórico. Não é gerada segunda prestação;os dados da operação e as duas versões ficam preservados.';}}}else if(b.kind==='acquisitions')acquisitions(d,b.input,r);else formalities(d,b.input,r);return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result:r};}
const labels:Record<string,string>={base:'Base',prior:'Acumulado global anterior',ordinary:'Parcela ordinária',excess:'Parcela excedente',tax:'Imposto',payment:'Prestação própria',withheld:'Retenção documentada',hypotheticalOwnTax:'Cálculo próprio apenas hipotético,sem prestação gerada',covered:'Parcela abrangida pelo limite',coveredTax:'Retenção5%da parcela abrangida',excessTax:'RetençãoIRPS20%da parcela qualificada',total:'Total de retenções',interest:'Juros documentados',finalPayment:'Preparação do montante a entregar'};
export function ispcResidualReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{const rows:Array<[string,string,string,string]>=[];for(const kind of ['occasional','acquisitions','formalities']as const){try{const b=ispcResidualBook(d,kind),r=b.result;if(!r)continue;const title={occasional:'Operações ocasionais próprias',acquisitions:'Aquisições acima do limite',formalities:'Formalidades2026—B–Geral'}[kind];rows.push(['ISPC',title,`${({calculated:'Calculado',settled_by_withholding:'Retenção liberatória documentada',partial:'Parcial',indeterminate:'Indeterminado',not_applicable:'Não aplicável ao recorte'} as Record<string,string>)[r.status]};${r.reason};falta:${r.missing.join('; ')||'nenhum dado deste recorte'}`,`${ispcResidualCurrent(d,b)?'Entradas correspondentes':'Histórico preservado;rever entradas'};${r.calculatedAt};${r.source}`]);for(const[k,v]of Object.entries(r.amounts).filter(([k])=>!k.startsWith('q8')))rows.push(['ISPC',`${title}—${kind==='formalities'&&r.input.sourceKind==='acquisitions'&&k==='payment'?'Retenções a entregar pelo adquirente':labels[k]||k}`,v?`${fractionPresentation(v).decimal}MZN (${v.numerator}/${v.denominator})`:'Indeterminado',`${r.input.reference||r.details.acquisitionRef||r.input.externalRef||'Pressupostos/documentos declarados'};${r.scope}`]);}catch(e){rows.push(['ISPC','Residual não recuperável',String(e),'Originais preservados.']);}}return rows;}
export function ispcFormalPreparationHtml(d:Record<string,unknown>):string{const b=ispcResidualBook(d,'formalities'),r=b.result;if(!r||!ispcResidualCurrent(d,b)||!Object.keys(r.amounts).length)throw Error('Guarde primeiro preparação actual com cálculo identificado.');const esc=(v:unknown)=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));const v=(n:string)=>r.amounts[n]?fractionPresentation(r.amounts[n]!).decimal:'Indeterminado';const fields=[['Quadro1—tipo',b.input.declaration==='initial'?'Inicial':'Substituição'],['Quadro2—mês/ano',r.details.formPeriod],['Contexto—trimestre e mês de pagamento',`2026/T${r.details.quarter};${r.details.paymentMonth} (não copiado automaticamente para quadro2)`],['Quadro3—NUIT/ÁreaFiscal',`${b.input.nuit}/${b.input.fiscalArea}`],['Quadro5—nome',b.input.name],['Quadro6—domicílio',b.input.address],['Quadro8—linha1:base01/taxa/montante05',`${b.input.sourceKind==='acquisitions'?'ISPC—retenção12(3)':'ISPC—parcela ordinária'};base01:${v('q8base01')};taxa:${r.details.q8rate01}%;montante05:${v('q8tax05')}`],['Quadro8—linha2:base02/taxa/montante06',r.amounts.q8base02&&cmp(r.amounts.q8base02,fraction(0n))>0n?`${b.input.sourceKind==='acquisitions'?'IRPS—acto isolado57(2k)':'ISPC—excesso de vendas8(1)d'};base02:${v('q8base02')};taxa20%;montante06:${v('q8tax06')}`:'Não aplicável ao cálculo seleccionado;sem segunda parcela'],['Quadro8—subtotal1campo09',v('tax')],['Quadro10—montante/juros/importância',`Imposto calculado:${v('tax')};prestação legal:${v('payment')};juros:${v('interest')};totalpreparado:${v('finalPayment')}`],['Modelo/código das retenções',b.input.retentionFormRef||'Por corresponder se aplicável'],['Estado externo declarado',`${({preparation:'Em preparação',submitted:'Submetido segundo referência declarada',received:'Recebido segundo referência declarada',paid:'Pago segundo referência declarada'} as Record<string,string>)[b.input.externalStatus]};${b.input.externalRef};${b.input.externalDate}`],['Falta',r.missing.join('; ')||'Nenhum dado deste recorte']];return`<!doctype html><html lang="pt"><meta charset="utf-8"><title>PreparaçãoISPC—B–Geral</title><style>body{font:16px system-ui;margin:32px}td,th{border:1px solid #777;padding:10px;vertical-align:top}table{border-collapse:collapse;width:100%}</style><h1>Preparação de campos ISPC—Modelo B–Geral</h1><p>Correspondência de apoio; não é formulário oficial emitido, declaração submetida ou comprovativo de pagamento. Original e instrução AT preservados,sem os alterar.</p><table>${fields.map(([a,c])=>`<tr><th>${esc(a)}</th><td>${esc(c)}</td></tr>`).join('')}</table><p>${esc(r.source)}</p><p>Quadros8e10têm numeração própria;campos homónimos não são somados entre quadros. Não preencher área de serviços nem assinatura automaticamente.</p></html>`;}