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