489 lines
18 KiB
TypeScript
489 lines
18 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
import {
|
||
goodsLocationBook,
|
||
goodsLocationCurrent,
|
||
} from './iva-goods-location.ts';
|
||
import {
|
||
serviceLocationBook,
|
||
serviceLocationCurrent,
|
||
} from './iva-service-location.ts';
|
||
import {
|
||
listedServiceBook,
|
||
listedServiceCurrent,
|
||
} from './iva-listed-services.ts';
|
||
import {
|
||
currencyEventBook,
|
||
currencyEventCurrent,
|
||
} from './iva-currency-events.ts';
|
||
import { digitalBook, digitalCurrent } from './iva-digital.ts';
|
||
export const foreignOperationVersion = 'foreign-operations/1';
|
||
export const foreignOperationSource =
|
||
'CIVA6,Lei13/2016PDF7–8 e redacção2025:reutilizar localização já apurada.19(1)bii,2016PDF15/p1426(273),mantidoLei10/2025PDF13:operações no estrangeiro que seriam tributáveis se efectuadas no território nacional;19(2)alterado2025 mantém exclusões próprias. Enquadramento potencial,não dedução concedida/campo11.Não confundir fora do território com isenção/IVAzero,moeda estrangeira,cliente estrangeiro ou caixa06.';
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type ForeignOperationRow = {
|
||
id: string;
|
||
sourceKind: '' | 'goods' | 'services' | 'listed' | 'digital';
|
||
sourceRowId: string;
|
||
territorialPart: '' | 'outside' | 'national';
|
||
eventRowId: string;
|
||
hypotheticalTaxable: Choice;
|
||
hypotheticalRef: string;
|
||
excluded19_2: Choice;
|
||
scope: string;
|
||
evidence: string;
|
||
};
|
||
type Source = {
|
||
id: string;
|
||
description: string;
|
||
period: string;
|
||
operationRef: string;
|
||
scope: string;
|
||
status: string;
|
||
territory: string | null;
|
||
current: boolean;
|
||
revision: number;
|
||
source: string;
|
||
reason: string;
|
||
};
|
||
type LineResult = {
|
||
id: string;
|
||
status: 'determined' | 'not_applicable' | 'indeterminate';
|
||
territory: string | null;
|
||
relation19:
|
||
| 'potential'
|
||
| 'not_demonstrated'
|
||
| 'excluded_19_2'
|
||
| 'indeterminate';
|
||
source: Source | null;
|
||
event: Record<string, unknown> | null;
|
||
deduction: null;
|
||
baseAmount: null;
|
||
vat: null;
|
||
rate: null;
|
||
reason: string;
|
||
};
|
||
export type ForeignOperationCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: ForeignOperationRow[];
|
||
rows: LineResult[];
|
||
};
|
||
export type ForeignOperationBook = {
|
||
rows: ForeignOperationRow[];
|
||
result?: ForeignOperationCalculation;
|
||
history?: ForeignOperationCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
foreignOperations?: Record<string, ForeignOperationBook>;
|
||
};
|
||
export function newForeignOperation(id: string): ForeignOperationRow {
|
||
return {
|
||
id,
|
||
sourceKind: '',
|
||
sourceRowId: '',
|
||
territorialPart: '',
|
||
eventRowId: '',
|
||
hypotheticalTaxable: '',
|
||
hypotheticalRef: '',
|
||
excluded19_2: '',
|
||
scope: '',
|
||
evidence: '',
|
||
};
|
||
}
|
||
export function switchForeignSource(
|
||
row: ForeignOperationRow,
|
||
sourceKind: ForeignOperationRow['sourceKind'],
|
||
sourceRowId = '',
|
||
): ForeignOperationRow {
|
||
return {
|
||
...row,
|
||
sourceKind,
|
||
sourceRowId,
|
||
territorialPart: '',
|
||
eventRowId: '',
|
||
hypotheticalTaxable: '',
|
||
excluded19_2: '',
|
||
};
|
||
}
|
||
export function foreignSources(
|
||
draft: Record<string, unknown>,
|
||
kind: ForeignOperationRow['sourceKind'],
|
||
): Source[] {
|
||
if (kind === 'digital') {
|
||
const b = digitalBook(draft),
|
||
r = b.result;
|
||
return r
|
||
? r.inputs.map((input, i) => ({
|
||
id: input.id,
|
||
description: input.description,
|
||
period: input.period,
|
||
operationRef: input.operationRef,
|
||
scope: input.scope,
|
||
status: r.rows[i].status,
|
||
territory: r.rows[i].territory,
|
||
current: digitalCurrent(draft, b),
|
||
revision: r.revision,
|
||
source: r.source,
|
||
reason: r.rows[i].reason,
|
||
}))
|
||
: [];
|
||
}
|
||
if (kind === 'goods') {
|
||
const b = goodsLocationBook(draft),
|
||
r = b.result;
|
||
return r
|
||
? r.inputs.map((input, i) => ({
|
||
id: input.id,
|
||
description: input.description,
|
||
period: input.period,
|
||
operationRef: input.operationRef,
|
||
scope: input.scope,
|
||
status: r.rows[i].status,
|
||
territory: r.rows[i].territory,
|
||
current: goodsLocationCurrent(draft, b),
|
||
revision: r.revision,
|
||
source: r.source,
|
||
reason: r.rows[i].reason,
|
||
}))
|
||
: [];
|
||
}
|
||
if (kind === 'services') {
|
||
const b = serviceLocationBook(draft),
|
||
r = b.result;
|
||
return r
|
||
? r.inputs.map((input, i) => ({
|
||
id: input.id,
|
||
description: input.description,
|
||
period: input.period,
|
||
operationRef: input.operationRef,
|
||
scope: input.scope,
|
||
status: r.rows[i].status,
|
||
territory: r.rows[i].territory,
|
||
current: serviceLocationCurrent(draft, b),
|
||
revision: r.revision,
|
||
source: r.source,
|
||
reason: r.rows[i].reason,
|
||
}))
|
||
: [];
|
||
}
|
||
if (kind === 'listed') {
|
||
const b = listedServiceBook(draft),
|
||
r = b.result;
|
||
return r
|
||
? r.inputs.map((input, i) => ({
|
||
id: input.id,
|
||
description: input.description,
|
||
period: input.period,
|
||
operationRef: input.operationRef,
|
||
scope: input.scope,
|
||
status: r.rows[i].status,
|
||
territory: r.rows[i].territory,
|
||
current: listedServiceCurrent(draft, b),
|
||
revision: r.revision,
|
||
source: r.source,
|
||
reason: r.rows[i].reason,
|
||
}))
|
||
: [];
|
||
}
|
||
return [];
|
||
}
|
||
export function foreignEvents(draft: Record<string, unknown>): Array<{
|
||
id: string;
|
||
description: string;
|
||
operationRef: string;
|
||
period: string;
|
||
current: boolean;
|
||
revision: number;
|
||
status: string;
|
||
eventDate: string | null;
|
||
dueEvents: unknown[];
|
||
remainingPending: boolean;
|
||
}> {
|
||
const b = currencyEventBook(draft),
|
||
r = b.result;
|
||
return r
|
||
? r.inputs
|
||
.filter((x) => x.treatment !== 'foreign_scenario')
|
||
.map((input) => {
|
||
const line =
|
||
r.rows[r.inputs.findIndex((x) => x.id === input.id)];
|
||
return {
|
||
id: input.id,
|
||
description: input.description,
|
||
operationRef: input.operationRef,
|
||
period: input.period,
|
||
current: currencyEventCurrent(draft, b),
|
||
revision: r.revision,
|
||
status: line.status,
|
||
eventDate: line.eventDate,
|
||
dueEvents: line.dueEvents,
|
||
remainingPending: line.remainingPending,
|
||
};
|
||
})
|
||
: [];
|
||
}
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const s = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
s.foreignOperations !== undefined &&
|
||
(!s.foreignOperations ||
|
||
typeof s.foreignOperations !== 'object' ||
|
||
Array.isArray(s.foreignOperations))
|
||
)
|
||
throw new Error(
|
||
'Operações no estrangeiro não recuperáveis;originais conservados.',
|
||
);
|
||
const fields = Object.keys(newForeignOperation(''));
|
||
for (const b of Object.values(s.foreignOperations ?? {}))
|
||
if (
|
||
!b ||
|
||
!Array.isArray(b.rows) ||
|
||
(b.history !== undefined && !Array.isArray(b.history)) ||
|
||
b.rows.some(
|
||
(r) =>
|
||
!r ||
|
||
fields.some(
|
||
(k) =>
|
||
typeof r[k as keyof ForeignOperationRow] !==
|
||
'string',
|
||
),
|
||
)
|
||
)
|
||
throw new Error(
|
||
'Linhas de operações no estrangeiro não recuperáveis;sem lista vazia por defeito.',
|
||
);
|
||
return s;
|
||
}
|
||
export function foreignOperationBook(
|
||
draft: Record<string, unknown>,
|
||
): ForeignOperationBook {
|
||
return (
|
||
readStore(draft).foreignOperations?.[acquisitionScope(draft)] ?? {
|
||
rows: [],
|
||
}
|
||
);
|
||
}
|
||
export function saveForeignOperationBook(
|
||
draft: Record<string, unknown>,
|
||
book: ForeignOperationBook,
|
||
): string {
|
||
const s = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...s,
|
||
foreignOperations: {
|
||
...s.foreignOperations,
|
||
[acquisitionScope(draft)]: book,
|
||
},
|
||
});
|
||
readStore({ ...draft, modelo_a_snapshot: encoded });
|
||
return encoded;
|
||
}
|
||
export function evaluateForeignOperation(
|
||
draft: Record<string, unknown>,
|
||
row: ForeignOperationRow,
|
||
): LineResult {
|
||
const result: LineResult = {
|
||
id: row.id,
|
||
status: 'indeterminate',
|
||
territory: null,
|
||
relation19: 'indeterminate',
|
||
source: null,
|
||
event: null,
|
||
deduction: null,
|
||
baseAmount: null,
|
||
vat: null,
|
||
rate: null,
|
||
reason: '',
|
||
};
|
||
const stop = (
|
||
reason: string,
|
||
status: LineResult['status'] = 'indeterminate',
|
||
) => ({ ...result, status, reason });
|
||
try {
|
||
const source = foreignSources(draft, row.sourceKind).find(
|
||
(s) => s.id === row.sourceRowId,
|
||
);
|
||
if (!source)
|
||
return stop(
|
||
'Escolha uma classificação de localização já guardada nesta oportunidade. Prepare-a nas secções existentes;sem nova classificação por endereço,nacionalidade ou moeda.',
|
||
);
|
||
result.source = source;
|
||
if (
|
||
!source.current ||
|
||
source.status !== 'determined' ||
|
||
!source.territory
|
||
)
|
||
return stop(
|
||
'Localização alterada ou indeterminada;resultado anterior conservado,sem o tratar como classificação actual.',
|
||
);
|
||
result.territory = source.territory;
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(source.period) ||
|
||
!row.scope.trim() ||
|
||
!row.evidence.trim()
|
||
)
|
||
return stop(
|
||
'Identifique âmbito da análise e factos/pressupostos;reutilize operação/período/documentos da localização.',
|
||
);
|
||
if (source.territory === 'national')
|
||
return stop(
|
||
'Localização reutilizada aponta para Moçambique;P002operação no estrangeiro não se aplica neste âmbito.Não equivale a isenção nem decide tributação interna.',
|
||
'not_applicable',
|
||
);
|
||
if (source.territory === 'partial') {
|
||
if (!['outside', 'national'].includes(row.territorialPart))
|
||
return stop(
|
||
'Transporte tem parcela nacional e exterior;delimite a parcela desta análise,sem transformar todo o trajecto em estrangeiro.',
|
||
);
|
||
if (row.territorialPart === 'national') {
|
||
result.territory = 'national';
|
||
return stop(
|
||
'Análise limitada à parcela nacional do trajecto;P002estrangeiro não se aplica a essa parcela.',
|
||
'not_applicable',
|
||
);
|
||
}
|
||
result.territory = 'outside_portion';
|
||
}
|
||
if (row.eventRowId) {
|
||
const event = foreignEvents(draft).find(
|
||
(e) => e.id === row.eventRowId,
|
||
);
|
||
if (
|
||
!event ||
|
||
!event.current ||
|
||
event.status !== 'determined' ||
|
||
event.operationRef !== source.operationRef ||
|
||
event.period !== source.period
|
||
)
|
||
return stop(
|
||
'Evento escolhido não está actual ou não corresponde à mesma operação/período;conserve localização e corrija apenas a associação.Não inferir evento pela data do documento.',
|
||
);
|
||
result.event = event;
|
||
}
|
||
if (
|
||
!row.hypotheticalRef.trim() ||
|
||
!['yes', 'no'].includes(row.hypotheticalTaxable)
|
||
)
|
||
return stop(
|
||
'Operação/parcela estrangeira identificada;fundamente se seria tributável caso efectuada em Moçambique19(1)bii.Não presumir esse facto pela localização ou pela taxa estrangeira.',
|
||
);
|
||
if (row.hypotheticalTaxable === 'no') {
|
||
result.relation19 = 'not_demonstrated';
|
||
return stop(
|
||
'Operação no estrangeiro identificada;pressuposto de tributabilidade interna19(1)bii não demonstrado pelos factos declarados.Isto não decide outros enquadramentos de dedução.',
|
||
'determined',
|
||
);
|
||
}
|
||
if (!['yes', 'no'].includes(row.excluded19_2))
|
||
return stop(
|
||
'Tributabilidade interna hipotética indicada;conferir exclusões19(2),incluindo17-A e pagamentos15(6)c.Sem presumir dedução.',
|
||
);
|
||
if (row.excluded19_2 === 'yes') {
|
||
result.relation19 = 'excluded_19_2';
|
||
return stop(
|
||
'Operação no estrangeiro identificada;exclusão19(2) declarada e fundamentada afasta este enquadramento potencial.Não calcula ou regulariza dedução.',
|
||
'determined',
|
||
);
|
||
}
|
||
result.relation19 = 'potential';
|
||
return stop(
|
||
'Operação/parcela no estrangeiro reutilizada da localização;seria tributável em Moçambique segundo fundamento declarado,sem exclusão19(2) indicada.Enquadramento potencial19(1)bii,dependente das demais condições18/20 e âmbito dos bens/serviços utilizados.Não é concessão/cálculo de dedução,campo11,isenção ou IVA zero.',
|
||
'determined',
|
||
);
|
||
} catch {
|
||
return stop(
|
||
'Localização/eventos de origem não recuperáveis;originais preservados,sem inferir classificação ou valores.',
|
||
);
|
||
}
|
||
}
|
||
function key(
|
||
draft: Record<string, unknown>,
|
||
book: ForeignOperationBook,
|
||
): string {
|
||
return JSON.stringify({
|
||
version: foreignOperationVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
rows: book.rows,
|
||
dependencies: book.rows.map((row) => ({
|
||
source: foreignSources(draft, row.sourceKind).find(
|
||
(s) => s.id === row.sourceRowId,
|
||
),
|
||
event: row.eventRowId
|
||
? foreignEvents(draft).find((e) => e.id === row.eventRowId)
|
||
: null,
|
||
})),
|
||
});
|
||
}
|
||
export function foreignOperationCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: ForeignOperationBook,
|
||
): boolean {
|
||
try {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
export function recordForeignOperations(
|
||
draft: Record<string, unknown>,
|
||
book: ForeignOperationBook,
|
||
): ForeignOperationBook {
|
||
if (
|
||
!book.rows.length ||
|
||
new Set(book.rows.map((r) => r.id)).size !== book.rows.length
|
||
)
|
||
throw new Error('Indique identidades distintas;originais conservados.');
|
||
const result: ForeignOperationCalculation = {
|
||
version: foreignOperationVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
sourceKey: key(draft, book),
|
||
source: foreignOperationSource,
|
||
inputs: structuredClone(book.rows),
|
||
rows: book.rows.map((r) => evaluateForeignOperation(draft, r)),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
saveForeignOperationBook(draft, next);
|
||
return next;
|
||
}
|
||
export function foreignOperationReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const b = foreignOperationBook(draft),
|
||
r = b.result;
|
||
if (!r) return [];
|
||
return r.inputs.map((row, i) => {
|
||
const line = r.rows[i];
|
||
return [
|
||
'IVA mensal',
|
||
`P002 — ${line.source?.period ?? 'Período indeterminado'} — ${row.id}`,
|
||
`${line.source?.description ?? 'Origem por escolher'};${line.reason};dedução/IVA:Indeterminados`,
|
||
`${r.opportunity};${foreignOperationCurrent(draft, b) ? 'Resultado actual' : 'Origens/entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};localização${row.sourceKind}/${row.sourceRowId},versão${line.source?.revision ?? 'indeterminada'};${line.source?.reason ?? ''};operação${line.source?.operationRef ?? ''};âmbito origem${line.source?.scope ?? ''};parcela${row.territorialPart};evento${row.eventRowId || 'Sem associação necessária'},${line.event ? JSON.stringify(line.event) : 'Não apurado por esta análise'};hipótese interna${row.hypotheticalTaxable},fundamento${row.hypotheticalRef};exclusão19(2)${row.excluded19_2};âmbito${row.scope};factos${row.evidence};sem base/IVA/dedução/campo11/totaisModeloA.`,
|
||
];
|
||
});
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'P002',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados;sem classificação por defeito.',
|
||
],
|
||
];
|
||
}
|
||
}
|