57 lines
15 KiB
TypeScript
57 lines
15 KiB
TypeScript
import {closureBook,closureSources,closureFingerprint,type ClosureSource,type ClosurePayment} from './iva-closure.ts';
|
||
import {addFractions,subtractFractions,positiveFraction,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
|
||
|
||
export const substitutionSource='L3-C: VAT Return.pdf SHA2569c0b99a8,p2/quadro9;RCIVA15,Decreto7/2008 PDF3/p101.21=(17actual−18actual)−(17anterior−18anterior);devedor→22;credor só utilizável em declaração posterior em prazo após comunicação20. Sem repetir19/20 ou pagamentos anteriores. Exemplar recebido assinala substituiçãoModeloA;RCIVA15 refereModeloB:correspondência de formulário/submissão não automatizada.';
|
||
export type PriorDeclaration={kind:'AB'|'C';id:string;period:string;entityRef:string;origin:'projected'|'effective';calculatedAt:string;declarationDate:string;declarationReference:string;fingerprint:string;debit:FiscalFraction;credit:FiscalFraction;paid:FiscalFraction;outstanding:FiscalFraction;carry:FiscalFraction;paymentRefs:string[]};
|
||
export type SubstitutionInput={id:string;prior:PriorDeclaration|null;source:ClosureSource|null;priorReference:string;priorDate:string;date:string;reference:string;reason:string;stage:'preparation'|'presented';interest:''|'none'|'documented';interestAmount:string;interestRef:string;payments:ClosurePayment[]};
|
||
export type SubstitutionResult={id:string;status:'determined'|'indeterminate';reason:string;inputKey:string;fields:Record<string,FiscalFraction|null>;pendingCredit:FiscalFraction|null;paid:FiscalFraction|null;outstanding:FiscalFraction|null;overpayment:FiscalFraction|null;totalPaid:FiscalFraction|null;totalOutstanding:FiscalFraction|null;source:string};
|
||
export type SubstitutionCalculation={calculatedAt:string;key:string;inputs:SubstitutionInput[];rows:SubstitutionResult[]};
|
||
export type SubstitutionBook={version:1;rows:SubstitutionInput[];result?:SubstitutionCalculation;history?:SubstitutionCalculation[]};
|
||
const zero=()=>fraction(0n),scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
|
||
const date=(v:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(v)&&!Number.isNaN(Date.parse(v))&&new Date(v).toISOString().slice(0,10)===v;
|
||
function store(d:Record<string,unknown>):Record<string,unknown>{const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;}
|
||
function maps(d:Record<string,unknown>):Record<string,SubstitutionBook>{const s=store(d).ivaSubstitutions;if(s===undefined)return{};if(!s||typeof s!=='object'||Array.isArray(s))throw Error('Substituições não recuperáveis;originais preservados.');return s as Record<string,SubstitutionBook>;}
|
||
function validate(b:SubstitutionBook):void{if(!b||b.version!==1||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)))throw Error('Livro de substituições não recuperável;sem o substituir.');for(const r of b.rows)if(!r||!['id','priorReference','priorDate','date','reference','reason','interestAmount','interestRef'].every(k=>typeof r[k as keyof SubstitutionInput]==='string')||!['preparation','presented'].includes(r.stage)||!['','none','documented'].includes(r.interest)||!Array.isArray(r.payments)||r.payments.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof ClosurePayment]==='string')))throw Error('Entradas da substituição não recuperáveis;originais preservados.');}
|
||
export function substitutionBook(d:Record<string,unknown>):SubstitutionBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
|
||
export function saveSubstitutionBook(d:Record<string,unknown>,b:SubstitutionBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaSubstitutions:{...maps(d),[scope(d)]:b}});}
|
||
export function newSubstitution(id:string):SubstitutionInput{return{id,prior:null,source:null,priorReference:'',priorDate:'',date:'',reference:'',reason:'',stage:'preparation',interest:'',interestAmount:'',interestRef:'',payments:[]};}
|
||
export function previousDeclarations(d:Record<string,unknown>,b=substitutionBook(d)):PriorDeclaration[]{
|
||
const ab=closureBook(d),out:PriorDeclaration[]=[];
|
||
for(const calc of [...(ab.history??[]),...(ab.result?[ab.result]:[])])for(const r of calc.rows){const input=calc.inputs.find(i=>i.id===r.id);if(r.status!=='determined'||!input||!r.fields['17']||!r.fields['18']||!r.fields['26']||!r.paid||!r.outstanding)continue;out.push({kind:'AB',id:r.id,period:r.period,entityRef:r.entityRef,origin:r.origin,calculatedAt:calc.calculatedAt,declarationDate:input.submissionDate,declarationReference:'',fingerprint:closureFingerprint([input,r]),debit:r.fields['17'],credit:r.fields['18'],paid:r.paid,outstanding:r.outstanding,carry:r.fields['26'],paymentRefs:input.payments.map(p=>p.reference)});}
|
||
for(const calc of [...(b.history??[]),...(b.result?[b.result]:[])])for(const r of calc.rows){const input=calc.inputs.find(i=>i.id===r.id);if(r.status!=='determined'||!input?.prior||!input.source||!r.totalPaid||!r.totalOutstanding)continue;out.push({kind:'C',id:r.id,period:input.prior.period,entityRef:input.prior.entityRef,origin:input.prior.origin,calculatedAt:calc.calculatedAt,declarationDate:input.date,declarationReference:input.reference,fingerprint:closureFingerprint([input,r]),debit:input.source.debit!,credit:input.source.credit!,paid:r.totalPaid,outstanding:r.totalOutstanding,carry:input.prior.carry,paymentRefs:[...input.prior.paymentRefs,...input.payments.map(p=>p.reference)]});}
|
||
return out.filter((r,i,a)=>a.findIndex(s=>s.kind===r.kind&&s.id===r.id&&s.fingerprint===r.fingerprint)===i);
|
||
}
|
||
function available(d:Record<string,unknown>,b:SubstitutionBook,r:SubstitutionInput){return[previousDeclarations(d,b).some(p=>JSON.stringify(p)===JSON.stringify(r.prior)),closureSources(d).some(s=>JSON.stringify(s)===JSON.stringify(r.source))];}
|
||
function pastPaymentUses(d:Record<string,unknown>,b:SubstitutionBook):Array<{id:string;entity:string;origin:string;reference:string}>{return Object.entries({...maps(d),[scope(d)]:b}).flatMap(([context,book])=>[...(book.history??[]),...(book.result?[book.result]:[])].flatMap(calc=>calc.inputs.filter(r=>r.prior&&(context===scope(d)||r.prior.origin==='effective')&&calc.rows.some(v=>v.id===r.id&&v.status==='determined'&&v.paid)).flatMap(r=>r.payments.map(p=>({id:r.id,entity:norm(r.prior!.entityRef),origin:r.prior!.origin,reference:norm(p.reference)})))));}
|
||
function abPaymentKeys(d:Record<string,unknown>):string[]{const ab=store(d).ivaClosures as Record<string,ReturnType<typeof closureBook>>|undefined;return Object.entries(ab??{}).flatMap(([id,v])=>[...v.rows,...[...(v.history??[]),...(v.result?[v.result]:[])].flatMap(c=>c.inputs)].filter(r=>id===scope(d)||r.origin==='effective').flatMap(r=>r.payments.map(p=>JSON.stringify([norm(r.entityRef),r.origin,norm(p.reference)]))));}
|
||
function key(d:Record<string,unknown>,b:SubstitutionBook){const ab=abPaymentKeys(d),past=pastPaymentUses(d,b);return closureFingerprint([b.rows,b.rows.map(r=>available(d,b,r)),uses(d,b).map(r=>[r.prior?.entityRef,r.prior?.origin,r.prior?.kind,r.prior?.id,r.payments.map(p=>norm(p.reference))]),b.rows.map(r=>r.payments.map(p=>!!r.prior&&(ab.includes(JSON.stringify([norm(r.prior.entityRef),r.prior.origin,norm(p.reference)]))||past.some(v=>v.id!==r.id&&v.entity===norm(r.prior!.entityRef)&&v.origin===r.prior!.origin&&v.reference===norm(p.reference)))))]);}
|
||
|
||
export function substitutionCurrent(d:Record<string,unknown>,b:SubstitutionBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.some(s=>s.id===r.id&&s.inputKey===closureFingerprint(r)));}
|
||
function uses(d:Record<string,unknown>,b:SubstitutionBook):SubstitutionInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,v])=>{validate(v);return v.rows.filter(r=>r.prior?.origin==='effective');})];}
|
||
export function recordSubstitution(d:Record<string,unknown>,b:SubstitutionBook,now=new Date().toISOString()):SubstitutionBook{
|
||
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de substituição repetida.');
|
||
const all=uses(d,b),paymentsAB=abPaymentKeys(d);
|
||
const rows:SubstitutionResult[]=b.rows.map(row=>{
|
||
const fields:Record<string,FiscalFraction|null>=Object.fromEntries(['17','18','19','20','21','22','23','24','25','26','27'].map(k=>[k,null]));
|
||
const result:SubstitutionResult={id:row.id,status:'indeterminate',reason:'',inputKey:closureFingerprint(row),fields,pendingCredit:null,paid:null,outstanding:null,overpayment:null,totalPaid:null,totalOutstanding:null,source:substitutionSource};
|
||
const stop=(reason:string)=>({...result,reason});const prior=row.prior,source=row.source;
|
||
if(!prior||!source||!available(d,b,row).every(Boolean))return stop('Escolha declaração anterior e resultadoL2 congelados recuperáveis;sem fabricar ou recalcular origens.');
|
||
if(d.currency!=='MZN'||source.currency!=='MZN'||source.opportunity!==scope(d)||source.period!==prior.period||!/^2026-(0[1-9]|1[0-2])$/.test(prior.period)||!source.debit||!source.credit)return stop('A substituição exige mesmo âmbito e período2026/MZN,com17/18 determinados.');
|
||
if(!prior.entityRef.trim()||prior.origin==='effective'&&source.projected)return stop('Origem projectada não serve como declaração efectiva;conservar pressupostos identificados.');
|
||
const seen=new Set([row.id]);let ancestor:PriorDeclaration|null=prior;while(ancestor?.kind==='C'){if(seen.has(ancestor.id))break;seen.add(ancestor.id);ancestor=b.rows.find(r=>r.id===ancestor!.id)?.prior??null;}if(ancestor?.kind==='C'&&seen.has(ancestor.id))return stop('Sequência circular de substituições;ligue cada versão à anterior sem ciclos.');
|
||
if(all.filter(r=>r.prior&&norm(r.prior.entityRef)===norm(prior.entityRef)&&r.prior.origin===prior.origin&&r.prior.kind===prior.kind&&r.prior.id===prior.id).length>1)return stop('Declaração anterior já ligada a outra substituição;reconcilie a sequência sem duplicar o diferencial.');
|
||
if(!row.priorReference.trim()||row.priorDate!==prior.declarationDate||prior.declarationReference&&row.priorReference!==prior.declarationReference||!date(row.priorDate)||row.priorDate.slice(0,7)<prior.period||!date(row.date)||row.date<row.priorDate||!row.reference.trim()||!row.reason.trim())return stop('Reutilize a data/referência disponível da versão anterior;identifique referência/data da declaração anterior e da substituição,com motivo;preparação não equivale a submissão.');
|
||
if(BigInt(source.debit.numerator)<0n||BigInt(source.credit.numerator)<0n||BigInt(source.debit.numerator)>0n&&BigInt(source.credit.numerator)>0n)return stop('Sinais17/18 incompatíveis na origemL2.');
|
||
fields['17']=source.debit;fields['18']=source.credit;
|
||
fields['21']=subtractFractions(subtractFractions(source.debit,source.credit),subtractFractions(prior.debit,prior.credit));fields['22']=positiveFraction(fields['21']);result.pendingCredit=positiveFraction(subtractFractions(zero(),fields['21']));
|
||
if(row.interest==='none'&&row.interestRef.trim())fields['23']=zero();else if(row.interest==='documented'&&row.interestRef.trim())try{fields['23']=decimalFraction(row.interestAmount);}catch{/* unknown preserved */}
|
||
if(!fields['23'])return stop('Juros adicionais por identificar;sem copiar juros anteriores ou presumir taxa/prazo.');
|
||
fields['24']=addFractions([fields['22'],fields['23']]);
|
||
const paid:FiscalFraction[]=[];
|
||
for(const p of row.payments){const ident=JSON.stringify([norm(prior.entityRef),prior.origin,norm(p.reference)]);if(!p.reference.trim()||!date(p.date)||p.date<row.date||prior.paymentRefs.some(ref=>norm(ref)===norm(p.reference))||pastPaymentUses(d,b).some(v=>v.id!==row.id&&v.entity===norm(prior.entityRef)&&v.origin===prior.origin&&v.reference===norm(p.reference))||paymentsAB.includes(ident)||all.flatMap(r=>r.payments.filter(p2=>r.prior&&JSON.stringify([norm(r.prior.entityRef),r.prior.origin,norm(p2.reference)])===ident)).length>1)return stop('Pagamento já registado na anterior/outra declaração,sem referência/data aplicável;registe só pagamentos adicionais.');try{paid.push(decimalFraction(p.amount));}catch{return stop('Montante adicional não identificado;sem baixa presumida.');}}
|
||
result.paid=addFractions(paid);result.outstanding=positiveFraction(subtractFractions(fields['24'],result.paid));result.overpayment=positiveFraction(subtractFractions(result.paid,fields['24']));result.totalPaid=addFractions([prior.paid,result.paid]);result.totalOutstanding=addFractions([prior.outstanding,result.outstanding]);result.status='determined';result.reason='Diferencial21 separado da declaração anterior;19/20 não reutilizados,25/26/27 não atribuídos por esta substituição. Crédito credor aguarda comunicação20;sem compensar dívida anterior automaticamente. Pagamentos adicionais e saldo anterior separados;sem submissãoAT ou certificação de cobrança. '+(row.stage==='preparation'?'Preparação/simulação;referências e datas declaradas não comprovam entrega.':'Apresentação registada pelo utilizador fora do InvesteMoz.');return result;
|
||
});
|
||
const result={calculatedAt:now,key:key(d,b),inputs:structuredClone(b.rows),rows};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
|
||
}
|
||
export function substitutionReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=substitutionBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-C — substituições',substitutionCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origens',substitutionSource]];const fmt=(v:FiscalFraction|null|undefined)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const input=b.result!.inputs.find(i=>i.id===r.id),p=input?.prior;out.push(['IVA mensal',`Substituição — ${p?.period??'por identificar'}`,`21:${fmt(r.fields['21'])};22 adicional:${fmt(r.fields['22'])};23 adicional:${fmt(r.fields['23'])};24 adicional:${fmt(r.fields['24'])};crédito aguardando comunicação:${fmt(r.pendingCredit)}`,`${p?.entityRef??''};${scope(d)};MZN;${p?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};anterior ${input?.priorReference??''},${input?.priorDate??''};substituição ${input?.reference??''},${input?.date??''};${input?.reason??''};${r.reason}`]);out.push(['IVA mensal',`Reconciliação da substituição — ${p?.period??''}`,`Anterior:pago${fmt(p?.paid)},por liquidar${fmt(p?.outstanding)},reporte histórico26:${fmt(p?.carry)};adicional:pago${fmt(r.paid)},por liquidar${fmt(r.outstanding)},excedente${fmt(r.overpayment)};total registado${fmt(r.totalPaid)},total por liquidar sem compensação${fmt(r.totalOutstanding)}`,`Origem anterior${p?.kind}/${p?.id},versão${p?.calculatedAt};origemL2 revisão${input?.source?.revision},${input?.source?.calculatedAt};pagamentos adicionais:${input?.payments.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum'};juros adicionais:${input?.interestRef??''};histórico${b.result!.calculatedAt};${substitutionSource}`]);}return out;}catch{return[['IVA mensal','L3-C','Substituição não recuperável','Originais preservados;sem diferencial/crédito presumidos.']];}}
|