62 lines
12 KiB
TypeScript
62 lines
12 KiB
TypeScript
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
|
|
|
export const serviceNormalVersion='assimilated-service-normal/1';
|
|
export const serviceNormalSource='CIVA republicado Lei13/2016,15(2)(c),PDF13/p1426(271):operações4(2),base=valor normal do serviço definido15(4),PDF14/p1426(272).4(2)(a),PDF7/p1426(265):serviços gratuitos pela própria empresa para necessidades particulares do titular/pessoal ou fins alheios;4(2)(b):utilização de bens da empresa para uso próprio/titular/pessoal/fins alheios ou sectores isentos,quando houve dedução total/parcial nesses bens ou elementos constitutivos. A condição de dedução anterior é de4(2)(b),não de4(2)(a). Composição15(4)/(5)/(6),sem próprioIVA. Lei22/2022mantém15c/4por remissão,não altera4;Lei10/2025não altera4/15. Recorte2026/MZN;base apenas,sem calcularIVA/dedução/campo11/regularizações/totaisModeloA,nem confundir uso com transmissão permanenteP088.';
|
|
export const serviceNormalTreatments=[{id:'free_company_service',label:'Serviço gratuito pela própria empresa para necessidades particulares ou fins alheios'},{id:'private_company_asset_use',label:'Utilização de bens da empresa para uso próprio,particular ou fins alheios'},{id:'exempt_sector_asset_use',label:'Utilização de bens da empresa em sector de actividade isento'}] as const;
|
|
export const serviceNormalConditions=[
|
|
['domesticOperation','A operação e o âmbito territorial aplicável estão identificados para esta base'],
|
|
['normalValueBasis','O valor normal do serviço corresponde15(4):concorrência normal,fornecedor independente,estádio de comercialização,tempo/lugar da operação ou mais próximos,com fundamento e âmbito identificados'],
|
|
['netComposition','O montante exclui o próprio IVA e reflecte15(4)/(5)/(6),sem omissões ou dupla contagem'],
|
|
] as const;
|
|
export const serviceOwnConditions={
|
|
free_company_service:[['freeByCompany','A prestação é gratuita e efectuada pela própria empresa'],['nonBusinessPurpose','Destina-se a necessidades particulares do titular/pessoal ou,em geral,a fins alheios à empresa']],
|
|
private_company_asset_use:[['companyAssetUse','É utilização de bens da empresa,não afectação/transmissão permanente de bens'],['privatePurpose','Destina-se a uso próprio do titular/pessoal ou,em geral,a fins alheios à empresa'],['priorDeduction','Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem']],
|
|
exempt_sector_asset_use:[['companyAssetUse','É utilização de bens da empresa,não afectação/transmissão permanente de bens'],['exemptSector','A utilização corresponde a sector de actividade isento identificado'],['priorDeduction','Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem']],
|
|
} as const;
|
|
export function ownServiceConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return serviceOwnConditions[treatment as keyof typeof serviceOwnConditions]??[];}
|
|
type Choice=''|'yes'|'no';type Condition=typeof serviceNormalConditions[number][0];
|
|
export type ServiceNormalRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationRef:string;purposeRef:string;assetRef:string;deductionRef:string;documentRef:string;documentDate:string;normalValue:string;valuationRef:string;valuationScope:string;compositionRef:string;evidence:string;conditions:Partial<Record<string,Choice>>};
|
|
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
|
|
export type ServiceNormalCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ServiceNormalRow[];rows:LineResult[]};
|
|
export type ServiceNormalBook={rows:ServiceNormalRow[];result?:ServiceNormalCalculation;history?:ServiceNormalCalculation[]};
|
|
type Store=ReturnType<typeof readAcquisitionStore>&{serviceNormals?:Record<string,ServiceNormalBook>};
|
|
export function newServiceNormal(id:string,currency:string):ServiceNormalRow{return{id,treatment:'',description:'',period:'',origin:'projected',currency,operationRef:'',purposeRef:'',assetRef:'',deductionRef:'',documentRef:'',documentDate:'',normalValue:'',valuationRef:'',valuationScope:'',compositionRef:'',evidence:'',conditions:{}};}
|
|
export function switchServiceTreatment(row:ServiceNormalRow,treatment:string):ServiceNormalRow{return{...row,treatment,conditions:Object.fromEntries(serviceNormalConditions.map(([key])=>[key,row.conditions[key]??'']))};}
|
|
|
|
function readStore(draft:Record<string,unknown>):Store{
|
|
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
|
if(store.serviceNormals!==undefined&&(!store.serviceNormals||typeof store.serviceNormals!=='object'||Array.isArray(store.serviceNormals)))throw new Error('Bases de contraprestação não recuperáveis;originais preservados.');
|
|
const fields=Object.keys(newServiceNormal('','')).filter(key=>key!=='conditions');
|
|
for(const book of Object.values(store.serviceNormals??{}))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 ServiceNormalRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de contraprestação não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
|
return store;
|
|
}
|
|
export function serviceNormalBook(draft:Record<string,unknown>):ServiceNormalBook{return readStore(draft).serviceNormals?.[acquisitionScope(draft)]??{rows:[]};}
|
|
export function saveServiceNormalBook(draft:Record<string,unknown>,book:ServiceNormalBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,serviceNormals:{...current.serviceNormals,[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 evaluateServiceNormal(row:ServiceNormalRow,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});
|
|
const rule=serviceNormalTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique serviço gratuito,utilização particular/alheia ou em sector isento;desconhecido não permite presumir aplicação.');
|
|
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 expressos de projecção.');
|
|
const required=[...serviceNormalConditions,...ownServiceConditions(row.treatment)];const failed=required.filter(([key])=>row.conditions[key]==='no');if(failed.length)return stop('Não se aplica esta base4(2)/15(2)(c): '+failed.map(([,label])=>label).join('; ')+'. Não é isenção nem IVA zero.','not_applicable');
|
|
if(required.some(([key])=>row.conditions[key]!=='yes'))return stop('Confirme as condições próprias deste percurso;desconhecido não é zero,não aplicável ou dedução presumida.');
|
|
if(!row.description.trim()||!row.operationRef.trim()||!row.purposeRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.valuationRef.trim()||!row.valuationScope.trim()||!row.compositionRef.trim()||!row.evidence.trim())return stop('Identifique serviço/operação,finalidade,documento/data,fundamento e âmbito do valor normal e composição,ou pressupostos expressos;reutilize dados existentes,sem certificado/avaliador/aprovador novo.');
|
|
if(row.treatment!=='free_company_service'&&(!row.assetRef.trim()||!row.deductionRef.trim()))return stop('Identifique os bens/elementos e a referência da dedução anterior total/parcial,ou pressupostos expressos;esta referência não calcula nem concede dedução/campo11.');
|
|
const normal=decimal(row.normalValue);if(normal===null)return stop('Indique o valor normal do serviço;desconhecido não é zero nem custo/preço de aquisição dos bens presumido.');
|
|
return{...result,status:'determined',baseAmount:money(normal),reason:`Base15(2)(c),${rule.label}:valor normal do serviço ${money(normal)}MZN,${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. ${row.treatment==='free_company_service'?'4(2)(a),sem impor a condição de dedução anterior da alínea b).':'4(2)(b),bens/elementos e dedução anterior total/parcial referenciados;uso não é transmissão permanente.'} Valor normal com fundamento/âmbito,sem avaliação automática. Base não é imposto;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não determinados por este cálculo.`};
|
|
}
|
|
|
|
function key(draft:Record<string,unknown>,book:ServiceNormalBook):string{return JSON.stringify({version:serviceNormalVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
|
export function serviceNormalCurrent(draft:Record<string,unknown>,book:ServiceNormalBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
|
export function recordServiceNormals(draft:Record<string,unknown>,book:ServiceNormalBook):ServiceNormalBook{
|
|
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:ServiceNormalCalculation={version:serviceNormalVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:serviceNormalSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateServiceNormal(row,draft.currency))};
|
|
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveServiceNormalBook(draft,next);return next;
|
|
}
|
|
export function serviceNormalReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
|
try{const book=serviceNormalBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base P089 — ${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};${serviceNormalCurrent(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'};${serviceNormalTreatments.find(t=>t.id===row.treatment)?.label??'Percurso por indicar'};operação ${row.operationRef};finalidade ${row.purposeRef};${row.treatment==='free_company_service'?'Dedução anterior não é condição de4(2)(a)':`bens/elementos ${row.assetRef},dedução anterior ${row.deductionRef}`};valor normal ${row.normalValue} ${row.currency};fundamento ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${[...serviceNormalConditions,...ownServiceConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento factual ${row.evidence};não preenche totaisModeloA ou campo11.`]);}
|
|
catch{return[['IVA mensal','Valor normal de serviços assimilados','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
|
|
}
|