Files
Investidor-V2/resources/js/lib/iva-service-location.ts
T

460 lines
18 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 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';
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.',
],
];
}
}