Files
Investidor-V2/resources/js/lib/iva-exclusion-base.ts
T

78 lines
14 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,readAcquisitionStore} from './iva-acquisitions.ts';
export const exclusionBaseVersion='consideration-exclusions-base/1';
export const exclusionBaseSource='CIVA republicado Lei13/2016,15(6)(a–d),PDF14/p1426(272):exclui indemnizações declaradas judicialmente por incumprimento total/parcial de contratos;descontos,abatimentos ou bónus concedidos;quantias pagas em nome e por conta do adquirente/destinatário registadas em adequadas contas de terceiros;embalagens não efectivamente transaccionadas com elementos27(5)(b)na factura/documento equivalente.27(5)(b),PDF17/p1426(275):indicação separada e menção expressa da devolução acordada. Lei22/2022PDF3mantém15(6);Lei10/2025PDF14mantém27(5)(b),altera27(10),não15. Recorte2026/MZN,base apenas,sem IVA/dedução/campo11/regularizações ou totaisModeloA.';
export const exclusionTreatments=[{id:'judicial_indemnity',label:'Indemnização declarada judicialmente por incumprimento contratual'},{id:'granted_discount',label:'Desconto,abatimento ou bónus concedido'},{id:'third_party_payment',label:'Quantia paga em nome e por conta do adquirente/destinatário'},{id:'returnable_packaging',label:'Embalagem não efectivamente transaccionada,com devolução acordada'}] as const;
export const exclusionConditions=[
['operationIdentified','A operação e os montantes relacionados estão identificados'],
['initialNet','O montante inicial exclui o próprio IVA e tem a composição aplicável15(5)identificada,antes das exclusões ainda não consideradas'],
['completeComposition','As exclusões aplicáveis estão reconciliadas com o montante inicial,sem omissões ou dupla subtracção'],
['specialRules','Esta composição respeita a regra aplicável da operação,sem substituir bases especiais ou regularizações de outros períodos'],
] as const;
export const exclusionPartConditions={
judicial_indemnity:[['judicialDeclaration','A indemnização recebida foi declarada judicialmente'],['contractBreach','Corresponde ao incumprimento total ou parcial de contrato']],
granted_discount:[['granted','O desconto,abatimento ou bónus está concedido e corresponde a esta operação']],
third_party_payment:[['paidInName','A quantia foi paga em nome do adquirente/destinatário identificado'],['paidForAccount','A quantia foi paga por conta desse adquirente/destinatário'],['thirdPartyAccounts','Está registada pelo contribuinte em adequadas contas de terceiros']],
returnable_packaging:[['notTransacted','As embalagens não foram efectivamente transaccionadas'],['separateIndication','Constam separadamente na factura ou documento equivalente'],['returnMention','Consta menção expressa de que foi acordada a devolução das embalagens']],
} as const;
export function partConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return exclusionPartConditions[treatment as keyof typeof exclusionPartConditions]??[];}
type Choice=''|'yes'|'no';type Condition=typeof exclusionConditions[number][0];
export type ExclusionPart={id:string;treatment:string;description:string;amount:string;included:Choice;reference:string;scope:string;accountRef:string;beneficiaryRef:string;documentExcerpt:string;conditions:Partial<Record<string,Choice>>};
export type ExclusionRow={id:string;description:string;period:string;origin:'projected'|'effective';currency:string;initialAmount:string;initialRef:string;operationRef:string;documentRef:string;documentDate:string;exclusionRef:string;evidence:string;conditions:Partial<Record<Condition,Choice>>;parts:ExclusionPart[]};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
export type ExclusionCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ExclusionRow[];rows:LineResult[]};
export type ExclusionBook={rows:ExclusionRow[];result?:ExclusionCalculation;history?:ExclusionCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{considerationExclusions?:Record<string,ExclusionBook>};
export function newExclusionPart(id:string):ExclusionPart{return{id,treatment:'',description:'',amount:'',included:'',reference:'',scope:'',accountRef:'',beneficiaryRef:'',documentExcerpt:'',conditions:{}};}
export function newExclusionBase(id:string,currency:string):ExclusionRow{return{id,description:'',period:'',origin:'projected',currency,initialAmount:'',initialRef:'',operationRef:'',documentRef:'',documentDate:'',exclusionRef:'',evidence:'',conditions:{},parts:[]};}
export function switchExclusionPart(part:ExclusionPart,treatment:string):ExclusionPart{return{...part,treatment,conditions:{}};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.considerationExclusions!==undefined&&(!store.considerationExclusions||typeof store.considerationExclusions!=='object'||Array.isArray(store.considerationExclusions)))throw new Error('Exclusões não recuperáveis;originais preservados.');
const fields=Object.keys(newExclusionBase('','')).filter(key=>!['conditions','parts'].includes(key));const partFields=Object.keys(newExclusionPart('')).filter(key=>key!=='conditions');
for(const book of Object.values(store.considerationExclusions??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof ExclusionRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)||!Array.isArray(row.parts)||row.parts.some(part=>!part||partFields.some(key=>typeof part[key as keyof ExclusionPart]!=='string')||!part.conditions||typeof part.conditions!=='object'||Array.isArray(part.conditions))))throw new Error('Linhas de exclusão não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function exclusionBook(draft:Record<string,unknown>):ExclusionBook{return readStore(draft).considerationExclusions?.[acquisitionScope(draft)]??{rows:[]};}
export function saveExclusionBook(draft:Record<string,unknown>,book:ExclusionBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,considerationExclusions:{...current.considerationExclusions,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
function decimal(raw:unknown):bigint|null{if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[whole,fraction='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(fraction.padEnd(6,'0'));}
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
function validDate(value:string):boolean{const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
export function evaluateExclusion(row:ExclusionRow,currency:unknown):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin))return stop('Indique mês em2026 e dados efectivos declarados ou pressupostos de projecção.');
const failed=exclusionConditions.filter(([key])=>row.conditions[key]==='no');if(failed.length)return stop('Não se aplica esta composição: '+failed.map(([,label])=>label).join('; ')+'. Não é isenção nem IVA zero.','not_applicable');
const missing=exclusionConditions.filter(([key])=>row.conditions[key]!=='yes');if(missing.length)return stop('Condições por indicar: '+missing.map(([,label])=>label).join('; ')+'. Desconhecido não é zero.');
if(!row.description.trim()||!row.initialRef.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!row.exclusionRef.trim()||!row.evidence.trim()||!validDate(row.documentDate))return stop('Identifique operação,montante inicial,documento/data,composição e fundamento ou pressupostos expressos;reutilize referências existentes,sem certificado/aprovador novo.');
const initial=decimal(row.initialAmount);if(initial===null)return stop('Indique o montante inicial sem próprio IVA,com as inclusões aplicáveis consideradas e antes das exclusões ainda não realizadas;desconhecido não é zero.');
if(!row.parts.length||new Set(row.parts.map(p=>p.id)).size!==row.parts.length||row.parts.some(p=>!p.id.trim()))return stop('Identifique as parcelas desta operação com identificadores distintos;sem subtracção de linhas vazias ou duplicadas.');
let exclusions=0n;const formulas:string[]=[];
for(const part of row.parts){
const rule=exclusionTreatments.find(t=>t.id===part.treatment);if(!rule)return stop('Identifique o tratamento de cada parcela;uma descrição genérica não demonstra exclusão.');
const own=partConditions(part.treatment),failed=own.filter(([key])=>part.conditions[key]==='no');
if(failed.length)return stop('Não se aplica a exclusão da parcela '+part.id+': '+failed.map(([,label])=>label).join('; ')+'. Sem subtracção automática ou IVA zero.','not_applicable');
if(own.some(([key])=>part.conditions[key]!=='yes')||!['yes','no'].includes(part.included))return stop('Confirme as condições próprias da parcela '+part.id+' e se ainda está incluída no montante inicial;desconhecido não permite presumir exclusão ou zero.');
if(!part.description.trim()||!part.reference.trim()||!part.scope.trim())return stop('Identifique descrição,referência e âmbito da parcela '+part.id+',ou pressuposto expresso. Na indemnização,a referência identifica a declaração judicial e o contrato;no desconto,a concessão.');
if(part.treatment==='third_party_payment'&&(!part.accountRef.trim()||!part.beneficiaryRef.trim()))return stop('Identifique as adequadas contas de terceiros e o adquirente/destinatário em nome e por conta de quem se pagou,ou pressupostos expressos;despesa própria não equivale a pagamento por conta alheia.');
if(part.treatment==='returnable_packaging'&&!part.documentExcerpt.trim())return stop('Identifique na referência da factura/documento a indicação separada e a menção expressa da devolução acordada,ou pressuposto expresso;sem exigir devolução já realizada.');
const amount=decimal(part.amount);if(amount===null)return stop('Indique o montante exacto da parcela '+part.id+';desconhecido não é zero.');
if(part.included==='yes')exclusions+=amount;
formulas.push(`${part.id}:${rule.label},${money(amount)}MZN,${part.included==='yes'?'excluída uma vez':'já excluída,sem segunda subtracção'}`);
}
if(exclusions>initial)return stop('As exclusões ainda incluídas excedem o montante inicial;reveja a composição,sem base negativa ou piso zero presumido.');
const base=initial-exclusions;return{...result,status:'determined',baseAmount:money(base),reason:`Base15(6):montante inicial ${money(initial)} − exclusões ainda incluídas ${money(exclusions)} = ${money(base)}MZN;${formulas.join('; ')},${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. Base única por operação. Base não é imposto;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não são determinados por esta composição.`};
}
function key(draft:Record<string,unknown>,book:ExclusionBook):string{return JSON.stringify({version:exclusionBaseVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
export function exclusionCurrent(draft:Record<string,unknown>,book:ExclusionBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordExclusions(draft:Record<string,unknown>,book:ExclusionBook):ExclusionBook{
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
const result:ExclusionCalculation={version:exclusionBaseVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:exclusionBaseSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateExclusion(row,draft.currency))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveExclusionBook(draft,next);return next;
}
export function exclusionReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=exclusionBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base P102 — ${row.period} — ${row.id}`,`${row.description};base tributável: ${result.rows[index].baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;${result.rows[index].reason}`,`${result.opportunity};${result.location};${exclusionCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};operação ${row.operationRef};montante inicial ${row.initialAmount} ${row.currency},referência ${row.initialRef};parcelas ${row.parts.map(p=>`${p.id}/${p.treatment},${p.description},${p.amount} ${row.currency},já incluída:${p.included==='yes'?'Sim':p.included==='no'?'Não':'Por indicar'},referência ${p.reference},âmbito ${p.scope}${p.treatment==='third_party_payment'?`,contas ${p.accountRef},adquirente/destinatário ${p.beneficiaryRef}`:''}${p.treatment==='returnable_packaging'?`,menção documental ${p.documentExcerpt}`:''},condições ${partConditions(p.treatment).map(([key,label])=>label+': '+(p.conditions[key]==='yes'?'Sim':p.conditions[key]==='no'?'Não':'Por indicar')).join('; ')}`).join('; ')};composição ${row.exclusionRef};documento ${row.documentRef},${row.documentDate};condições ${exclusionConditions.map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};não preenche totaisModeloA ou campo11.`]);}
catch{return[['IVA mensal','Composição da contraprestação','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
}