78 lines
14 KiB
TypeScript
78 lines
14 KiB
TypeScript
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.']];}
|
||
}
|