393 lines
16 KiB
TypeScript
393 lines
16 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
|
||
export const goodsLocationVersion = 'goods-territoriality/1';
|
||
export const goodsLocationSource =
|
||
'CIVA republicado Lei13/2016,6(1)/(2),PDF7/p1426(265):localização dos bens no território nacional ao iniciar transporte/expedição para o adquirente ou,sem transporte/expedição,ao pôr à sua disposição. Não obstante6(1),são também tributáveis transmissões pelo importador e subsequentes de bens transportados/expedidos do estrangeiro,antes da importação. Lei10/2025PDF12mantém6(1)/(2),altera6(7–9)quanto a digitais;recorte2026,bens corpóreos,não digitais. Endereço/nacionalidade do cliente não substituem localização dos bens. Classificação territorial apenas,não isenção/taxa/IVA/dedução/campo11/direitos aduaneiros ou totaisModeloA.';
|
||
export const goodsLocationTreatments = [
|
||
{
|
||
id: 'goods_with_transport',
|
||
label: 'Bens com transporte ou expedição para o adquirente',
|
||
},
|
||
{
|
||
id: 'goods_without_transport',
|
||
label: 'Bens sem transporte ou expedição',
|
||
},
|
||
{
|
||
id: 'goods_before_import',
|
||
label: 'Transmissão pelo importador ou subsequente,antes da importação',
|
||
},
|
||
] as const;
|
||
export const goodsLocationConditions = [
|
||
[
|
||
'goodsSupply',
|
||
'Está identificada uma transmissão de bens corpóreos,não uma prestação de serviços ou bens digitais abrangidos por regras próprias',
|
||
],
|
||
[
|
||
'factsCorrespond',
|
||
'Os factos,referências e âmbito correspondem a esta operação e período,não apenas ao endereço ou nacionalidade do adquirente',
|
||
],
|
||
] as const;
|
||
export const goodsOwnConditions = {
|
||
goods_with_transport: [
|
||
[
|
||
'transportToBuyer',
|
||
'Os bens são transportados ou expedidos para o adquirente',
|
||
],
|
||
[
|
||
'notBeforeImport',
|
||
'Não corresponde à transmissão pré-importação abrangida pelo artigo6(2)',
|
||
],
|
||
],
|
||
goods_without_transport: [
|
||
[
|
||
'noTransport',
|
||
'Não há expedição ou transporte dos bens para esta transmissão',
|
||
],
|
||
[
|
||
'notBeforeImport',
|
||
'Não corresponde à transmissão pré-importação abrangida pelo artigo6(2)',
|
||
],
|
||
],
|
||
goods_before_import: [
|
||
['fromAbroad', 'Os bens são transportados ou expedidos do estrangeiro'],
|
||
[
|
||
'nationalImport',
|
||
'Está identificada a importação desses bens para o território nacional',
|
||
],
|
||
[
|
||
'importerChain',
|
||
'A transmissão é feita pelo importador ou é subsequente na cadeia desses bens',
|
||
],
|
||
[
|
||
'beforeImport',
|
||
'Esta transmissão ocorre antes da importação identificada',
|
||
],
|
||
],
|
||
} as const;
|
||
export function ownGoodsConditions(
|
||
treatment: string,
|
||
): ReadonlyArray<readonly [string, string]> {
|
||
return (
|
||
goodsOwnConditions[treatment as keyof typeof goodsOwnConditions] ?? []
|
||
);
|
||
}
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type GoodsLocationRow = {
|
||
id: string;
|
||
treatment: string;
|
||
description: string;
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
operationDate: string;
|
||
operationRef: string;
|
||
documentRef: string;
|
||
documentDate: string;
|
||
goodsPlace: string;
|
||
placeCountry: string;
|
||
placeRef: string;
|
||
transportRef: string;
|
||
importRef: string;
|
||
chainRef: string;
|
||
scope: string;
|
||
evidence: string;
|
||
conditions: Partial<Record<string, Choice>>;
|
||
};
|
||
type LineResult = {
|
||
id: string;
|
||
status: 'determined' | 'not_applicable' | 'indeterminate';
|
||
territory: 'national' | 'outside' | null;
|
||
baseAmount: null;
|
||
vat: null;
|
||
rate: null;
|
||
reason: string;
|
||
};
|
||
export type GoodsLocationCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
location: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: GoodsLocationRow[];
|
||
rows: LineResult[];
|
||
};
|
||
export type GoodsLocationBook = {
|
||
rows: GoodsLocationRow[];
|
||
result?: GoodsLocationCalculation;
|
||
history?: GoodsLocationCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
goodsLocations?: Record<string, GoodsLocationBook>;
|
||
};
|
||
export function newGoodsLocation(id: string): GoodsLocationRow {
|
||
return {
|
||
id,
|
||
treatment: '',
|
||
description: '',
|
||
period: '',
|
||
origin: 'projected',
|
||
operationDate: '',
|
||
operationRef: '',
|
||
documentRef: '',
|
||
documentDate: '',
|
||
goodsPlace: '',
|
||
placeCountry: '',
|
||
placeRef: '',
|
||
transportRef: '',
|
||
importRef: '',
|
||
chainRef: '',
|
||
scope: '',
|
||
evidence: '',
|
||
conditions: {},
|
||
};
|
||
}
|
||
export function switchGoodsTreatment(
|
||
row: GoodsLocationRow,
|
||
treatment: string,
|
||
): GoodsLocationRow {
|
||
return {
|
||
...row,
|
||
treatment,
|
||
conditions: Object.fromEntries(
|
||
goodsLocationConditions.map(([key]) => [
|
||
key,
|
||
row.conditions[key] ?? '',
|
||
]),
|
||
),
|
||
};
|
||
}
|
||
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
store.goodsLocations !== undefined &&
|
||
(!store.goodsLocations ||
|
||
typeof store.goodsLocations !== 'object' ||
|
||
Array.isArray(store.goodsLocations))
|
||
)
|
||
throw new Error(
|
||
'Bases de contraprestação não recuperáveis;originais preservados.',
|
||
);
|
||
const fields = Object.keys(newGoodsLocation('')).filter(
|
||
(key) => key !== 'conditions',
|
||
);
|
||
for (const book of Object.values(store.goodsLocations ?? {}))
|
||
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 GoodsLocationRow] !==
|
||
'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 goodsLocationBook(
|
||
draft: Record<string, unknown>,
|
||
): GoodsLocationBook {
|
||
return (
|
||
readStore(draft).goodsLocations?.[acquisitionScope(draft)] ?? {
|
||
rows: [],
|
||
}
|
||
);
|
||
}
|
||
export function saveGoodsLocationBook(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsLocationBook,
|
||
): string {
|
||
const current = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...current,
|
||
goodsLocations: {
|
||
...current.goodsLocations,
|
||
[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 evaluateGoodsLocation(row: GoodsLocationRow): 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 = goodsLocationTreatments.find((t) => t.id === row.treatment);
|
||
if (!rule)
|
||
return stop(
|
||
'Identifique transporte,ausência de transporte ou transmissão antes da importaçã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ês em2026 e data da operação correspondentes,com dados efectivos declarados ou pressupostos expressos de projecção.',
|
||
);
|
||
const required = [
|
||
...goodsLocationConditions,
|
||
...ownGoodsConditions(row.treatment),
|
||
];
|
||
const failed = required.filter(([key]) => row.conditions[key] === 'no');
|
||
if (failed.length)
|
||
return stop(
|
||
'Este percurso não se aplica: ' +
|
||
failed.map(([, label]) => label).join('; ') +
|
||
'. Rever o percurso,não classificar automaticamente como fora do território ou isento.',
|
||
'not_applicable',
|
||
);
|
||
if (required.some(([key]) => row.conditions[key] !== 'yes'))
|
||
return stop(
|
||
'Confirme os factos próprios deste percurso;desconhecido não é fora do território,isenção ou IVA zero.',
|
||
);
|
||
if (
|
||
!row.description.trim() ||
|
||
!row.operationRef.trim() ||
|
||
!row.documentRef.trim() ||
|
||
!validDate(row.documentDate) ||
|
||
!row.scope.trim() ||
|
||
!row.evidence.trim()
|
||
)
|
||
return stop(
|
||
'Identifique bens/operação,documento/data,âmbito e factos ou pressupostos expressos;reutilize referências existentes,sem certificado/aprovador novo.',
|
||
);
|
||
if (row.treatment === 'goods_before_import') {
|
||
if (
|
||
!row.transportRef.trim() ||
|
||
!row.importRef.trim() ||
|
||
!row.chainRef.trim()
|
||
)
|
||
return stop(
|
||
'Identifique transporte/expedição do estrangeiro,importação nacional e posição da transmissão na cadeia,ou pressupostos expressos;não inferir a condição pelo endereço do adquirente.',
|
||
);
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
territory: 'national',
|
||
reason: `Art6(2):transmissão pelo importador ou subsequente de bens transportados/expedidos do estrangeiro,antes da importação nacional identificada;abrangida territorialmente emMoçambique,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. A localização física anterior no estrangeiro não afasta por si esta regra. Classificação não determina isenção,base,taxa,IVA,dedução/campo11,direitos ou totaisModeloA.`,
|
||
};
|
||
}
|
||
if (
|
||
!row.goodsPlace.trim() ||
|
||
!row.placeRef.trim() ||
|
||
!['MZ', 'foreign'].includes(row.placeCountry)
|
||
)
|
||
return stop(
|
||
'Identifique onde os bens se encontram no momento relevante e respectiva referência:início do transporte/expedição ou colocação à disposição sem transporte;endereço do cliente não substitui esse facto.',
|
||
);
|
||
if (row.treatment === 'goods_with_transport' && !row.transportRef.trim())
|
||
return stop(
|
||
'Identifique o transporte/expedição correspondente ao início referido,ou pressuposto expresso.',
|
||
);
|
||
const territory = row.placeCountry === 'MZ' ? 'national' : 'outside';
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
territory,
|
||
reason: `Art6(1):bens ${territory === 'national' ? 'situados emMoçambique' : 'situados fora deMoçambique'} ${row.treatment === 'goods_with_transport' ? 'ao iniciar o transporte/expedição para o adquirente' : 'ao serem postos à disposição do adquirente,sem transporte/expedição'},${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Factos de6(2)conferidos como não aplicáveis a este percurso. ${territory === 'outside' ? 'Fora deste âmbito territorial não equivale a isenção ou taxa/IVA zero.' : 'Abrangência territorial não confirma isenção,taxa ou IVA.'} Classificação não determina base,dedução/campo11,direitos ou totaisModeloA.`,
|
||
};
|
||
}
|
||
|
||
function key(draft: Record<string, unknown>, book: GoodsLocationBook): string {
|
||
return JSON.stringify({
|
||
version: goodsLocationVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
rows: book.rows,
|
||
});
|
||
}
|
||
export function goodsLocationCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsLocationBook,
|
||
): boolean {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
}
|
||
export function recordGoodsLocations(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsLocationBook,
|
||
): GoodsLocationBook {
|
||
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: GoodsLocationCalculation = {
|
||
version: goodsLocationVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
location: JSON.stringify(draft.selection_labels ?? {}),
|
||
sourceKey: key(draft, book),
|
||
source: goodsLocationSource,
|
||
inputs: structuredClone(book.rows),
|
||
rows: book.rows.map((row) => evaluateGoodsLocation(row)),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
saveGoodsLocationBook(draft, next);
|
||
return next;
|
||
}
|
||
export function goodsLocationReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const book = goodsLocationBook(draft),
|
||
result = book.result;
|
||
if (!result) return [];
|
||
return result.inputs.map((row, index) => [
|
||
'IVA mensal',
|
||
`Localização P105 — ${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' : 'Indeterminada'};IVA: Indeterminado;${result.rows[index].reason}`,
|
||
`${result.opportunity};${result.location};${goodsLocationCurrent(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'};${goodsLocationTreatments.find((t) => t.id === row.treatment)?.label ?? 'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};${row.treatment === 'goods_before_import' ? `transporte ${row.transportRef};importação ${row.importRef};cadeia ${row.chainRef}` : `local dos bens ${row.goodsPlace},${row.placeCountry};referência ${row.placeRef}${row.treatment === 'goods_with_transport' ? `;transporte ${row.transportRef}` : ''}`};âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...goodsLocationConditions, ...ownGoodsConditions(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 determina isenção/IVA/campo11/totaisModeloA.`,
|
||
]);
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'Localização de bens',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados;sem classificar por defeito.',
|
||
],
|
||
];
|
||
}
|
||
}
|