67 lines
14 KiB
TypeScript
67 lines
14 KiB
TypeScript
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||
|
||
export const serviceLocationVersion='service-territoriality/1';
|
||
export const serviceLocationSource='CIVA6(3),6(4)/(5)(a–d),6(6),Lei13/2016PDF7–8/p1426(265–266);AvisoBR89-I,08/06/2017,PDF1 rectifica6(5)c/d:actividades materialmente nacionais e transporte pela distância nacional;Lei10/2025PDF12mantém6(3–6),altera7–9digitais. Recorte2026:regra geral sem regras especiais aplicáveis;imóveis,trabalhos/peritagens em móveis e actividades presenciais pelos factos próprios. Sem taxa/IVA/isenção/dedução/campo11/totaisModeloA;digital e outros6(7)/(8)não classificados por estes percursos.';
|
||
export const serviceLocationTreatments=[{id:'general_service',label:'P106 — Regra geral dos serviços',branch:'P106'},{id:'immovable_service',label:'P107 — Serviços relacionados com imóveis',branch:'P107'},{id:'movable_work',label:'P108 — Trabalhos/peritagens em bens móveis',branch:'P108'},{id:'activity_service',label:'P109 — Actividades presenciais e serviços acessórios',branch:'P109'},{id:'transport_service',label:'P110 — Transporte por distância e percurso',branch:'P110'}] as const;
|
||
export const serviceLocationConditions=[['serviceSupply','Está identificada uma prestação de serviços e a operação/período correspondentes'],['otherSpecialRulesExcluded','Foi conferida a natureza da operação: não está abrangida neste caso por regime digital ou regras6(7)/(8)que exijam outro percurso']] as const;
|
||
export const serviceOwnConditions={
|
||
general_service:[['noPlaceException','Não se aplica nenhuma excepção de localização6(4)/(5):imóveis,trabalhos/peritagens em móveis,actividades materiais ou transporte'],['providerEstablishment','Identificada a sede,estabelecimento estável ou domicílio do prestador a partir do qual este serviço é prestado']],
|
||
immovable_service:[['propertyRelation','O serviço relaciona-se com o imóvel identificado,incluindo preparação/coordenação de trabalhos imobiliários,peritos ou agentes imobiliários']],
|
||
movable_work:[['corporealMovable','São trabalhos em bens móveis corpóreos ou peritagens a eles referentes'],['totalEssential','O local indicado corresponde à execução total ou essencial dos trabalhos/peritagens']],
|
||
activity_service:[['activityNature','Serviço artístico,científico,desportivo,recreativo,de ensino ou similar,incluindo organizadores e serviços acessórios dessa actividade'],['materialActivity','A actividade ou serviço acessório tem materialmente lugar no local indicado']],
|
||
transport_service:[['transportNature','Prestação de transporte com percurso e distâncias identificados'],['routeCorresponds','Distâncias e pontos de partida/chegada correspondem a este trajecto;ida e volta são trajectos distintos6(6)']],
|
||
} 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 serviceLocationConditions[number][0];
|
||
export type ServiceLocationRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationDate:string;operationRef:string;documentRef:string;documentDate:string;servicePlace:string;placeCountry:string;placeRef:string;totalDistance:string;nationalDistance:string;departure:string;arrival:string;routeRef:string;providerRef:string;providerPlace:string;providerCountry:string;subjectRef:string;scope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
|
||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';territory:'national'|'outside'|'partial'|null;baseAmount:null;vat:null;rate:null;reason:string};
|
||
export type ServiceLocationCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ServiceLocationRow[];rows:LineResult[]};
|
||
export type ServiceLocationBook={rows:ServiceLocationRow[];result?:ServiceLocationCalculation;history?:ServiceLocationCalculation[]};
|
||
type Store=ReturnType<typeof readAcquisitionStore>&{serviceLocations?:Record<string,ServiceLocationBook>};
|
||
export function newServiceLocation(id:string):ServiceLocationRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',servicePlace:'',placeCountry:'',placeRef:'',totalDistance:'',nationalDistance:'',departure:'',arrival:'',routeRef:'',providerRef:'',providerPlace:'',providerCountry:'',subjectRef:'',scope:'',evidence:'',conditions:{}};}
|
||
export function switchServiceTreatment(row:ServiceLocationRow,treatment:string):ServiceLocationRow{return{...row,treatment,conditions:Object.fromEntries(serviceLocationConditions.map(([key])=>[key,row.conditions[key]??'']))};}
|
||
|
||
function readStore(draft:Record<string,unknown>):Store{
|
||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if(store.serviceLocations!==undefined&&(!store.serviceLocations||typeof store.serviceLocations!=='object'||Array.isArray(store.serviceLocations)))throw new Error('Localizações de serviços não recuperáveis;originais preservados.');
|
||
const fields=Object.keys(newServiceLocation('')).filter(key=>key!=='conditions');
|
||
for(const book of Object.values(store.serviceLocations??{}))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 ServiceLocationRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de localização de serviços não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
||
return store;
|
||
}
|
||
export function serviceLocationBook(draft:Record<string,unknown>):ServiceLocationBook{return readStore(draft).serviceLocations?.[acquisitionScope(draft)]??{rows:[]};}
|
||
export function saveServiceLocationBook(draft:Record<string,unknown>,book:ServiceLocationBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,serviceLocations:{...current.serviceLocations,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||
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 evaluateServiceLocation(row:ServiceLocationRow):LineResult{
|
||
const result:LineResult={id:row.id,status:'indeterminate',territory:null,baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||
const rule=serviceLocationTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique o percurso do serviço;desconhecido não determina território.');
|
||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique mês2026,data correspondente e natureza efectiva/projectada dos dados.');
|
||
const conditions=[...serviceLocationConditions,...ownServiceConditions(row.treatment)];
|
||
if(conditions.some(([key])=>row.conditions[key]==='no'))return stop('Este percurso não se aplica aos factos declarados;rever a regra própria,sem presumir fora do território,isenção ou IVAzero.','not_applicable');
|
||
if(conditions.some(([key])=>row.conditions[key]!=='yes'))return stop('Factos ou regra própria por esclarecer;localização indeterminada,não isenção ou IVAzero.');
|
||
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.scope.trim()||!row.evidence.trim())return stop('Identifique serviço/operação,documento/data,âmbito e fundamento factual ou pressupostos expressos;reutilize dados sem certificado/aprovador novo.');
|
||
if(row.treatment==='transport_service'){
|
||
const parse=(v:string):bigint|null=>/^\d{1,18}(\.\d{1,6})?$/.test(v)?BigInt(v.split('.')[0])*1000000n+BigInt((v.split('.')[1]??'').padEnd(6,'0')):null;
|
||
const total=parse(row.totalDistance),national=parse(row.nationalDistance);
|
||
if(total===null||total===0n||national===null||national>total||!['MZ','foreign'].includes(row.departure)||!['MZ','foreign'].includes(row.arrival)||!row.routeRef.trim())return stop('Identifique percurso,pontos de partida/chegada e distâncias na mesma unidade,total positiva e nacional entre zero e total;desconhecido não é zero.');
|
||
const deemed=row.departure==='MZ'&&row.arrival==='MZ';const covered=deemed?total:national;const territory=covered===0n?'outside':covered===total?'national':'partial';
|
||
return{...result,status:'determined',territory,reason:`P110:artigo6(4)d/6(5)d rectificado pelo AvisoBR89-I,08/06/2017,PDF1,e6(6). ${deemed?'Partida e chegada no território nacional:inclui o percurso efectuado fora dos mesmos.':`Parcela nacional:distância ${row.nationalDistance} de ${row.totalDistance},na mesma unidade,sem arredondamento ou apuramento monetário.`} Ida e volta são trajectos distintos:recolher uma linha por trajecto. ${row.origin==='projected'?'Pressupostos expressos.':'Dados efectivos declarados.'} Fora do território não equivale a isenção/IVAzero;sem base,taxa,IVA,dedução/campo11 ou totaisModeloA.`};
|
||
}
|
||
const general=row.treatment==='general_service';const country=general?row.providerCountry:row.placeCountry;
|
||
if(!['MZ','foreign'].includes(country)||!(general?row.providerPlace:row.servicePlace).trim()||!(general?row.providerRef:row.placeRef).trim()||(!general&&!row.subjectRef.trim()))return stop(general?'Identifique sede/EP/domicílio do prestador a partir do qual o serviço é prestado,país e referência;endereço do cliente não substitui esse estabelecimento.':'Identifique local,país,referência e imóvel/bens/actividade a que respeita;endereço do cliente ou sede do prestador não substituem esses factos.');
|
||
const territory=country==='MZ'?'national':'outside';const clause=general?'6(3)':`6(${territory==='national'?'5':'4'})(${row.treatment==='immovable_service'?'a':row.treatment==='movable_work'?'b':'c'})`;
|
||
const fact=general?'estabelecimento do prestador a partir do qual o serviço é prestado':row.treatment==='immovable_service'?'local do imóvel':row.treatment==='movable_work'?'local de execução total ou essencial dos trabalhos/peritagens':'local material da actividade ou serviço acessório';
|
||
return{...result,status:'determined',territory,reason:`${rule.branch}:artigo${clause},${fact} ${territory==='national'?'emMoçambique':'fora deMoçambique'},${row.origin==='projected'?'sob pressupostos expressos':'segundo dados efectivos declarados'}. ${general?'Regras especiais conferidas como não aplicáveis.':'Regra territorial própria,independente da sede nacional ou estrangeira do prestador.'} Fora deste âmbito não equivale a isenção/IVAzero;não calcula base,taxa,IVA,dedução/campo11 ou totaisModeloA.`};
|
||
}
|
||
|
||
function key(draft:Record<string,unknown>,book:ServiceLocationBook):string{return JSON.stringify({version:serviceLocationVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},rows:book.rows});}
|
||
export function serviceLocationCurrent(draft:Record<string,unknown>,book:ServiceLocationBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||
export function recordServiceLocations(draft:Record<string,unknown>,book:ServiceLocationBook):ServiceLocationBook{
|
||
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:ServiceLocationCalculation={version:serviceLocationVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:serviceLocationSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateServiceLocation(row))};
|
||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveServiceLocationBook(draft,next);return next;
|
||
}
|
||
export function serviceLocationReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||
try{const book=serviceLocationBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Localização ${serviceLocationTreatments.find(t=>t.id===row.treatment)?.branch??'serviço'} — ${row.period} — ${row.id}`,`${row.description};classificação territorial: ${result.rows[index].territory==='national'?'Moçambique':result.rows[index].territory==='outside'?'Fora do território nacional':result.rows[index].territory==='partial'?'Parcela nacional do transporte':'Indeterminada'};IVA: Indeterminado;${result.rows[index].reason}`,`${result.opportunity};${result.location};${serviceLocationCurrent(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'};${serviceLocationTreatments.find(t=>t.id===row.treatment)?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};${row.treatment==='transport_service'?`trajecto ${row.routeRef};partida ${row.departure};chegada ${row.arrival};distâncias nacional/total ${row.nationalDistance}/${row.totalDistance}`:row.treatment==='general_service'?`estabelecimento prestador ${row.providerPlace},${row.providerCountry};referência ${row.providerRef}`:`local ${row.servicePlace},${row.placeCountry};referência ${row.placeRef};imóvel/bens/actividade ${row.subjectRef}`};âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...serviceLocationConditions,...ownServiceConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};não determina isenção/IVA/campo11/totaisModeloA.`]);}
|
||
catch{return[['IVA mensal','Localização de serviços','Dados não recuperáveis','Originais preservados;sem classificar por defeito.']];}
|
||
}
|