Files
Investidor-V2/resources/js/lib/iva-currency-events.ts
T

641 lines
25 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 {
selectFx,
fxSelectionKey,
type FxSelection,
} from './iva-fx-selection.ts';
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
import { convertCents as convertScaledAmount } from './business-financial-plan.ts';
export const currencyEventVersion = 'currency-events/1';
export const currencyEventSource =
'CIVA Lei13/2016,7(1)–(7),PDF8/p1426(266);8,PDF9/p1426(267);15(8),PDF14/p1426(272).Lei22/2022 eLei10/2025 não alteram7/8,15(8)mantido.RIVA21/2008PDF4:regra histórica5dias úteis,não prazo automático vigente;usar prazo e disposição aplicáveis identificados. P104:orientação operacional da Tempo25/10/2026 compra/venda/médiaBM conforme natureza e perspectiva;data de submissão indicada;excepções fiscais identificadas. Cenários históricos permanecem condicionados;sem base/IVA/dedução automaticamente activados.';
export const currencyEventTreatments = [
{
id: 'foreign_scenario',
branch: 'P104',
label: 'Moeda estrangeira — selecção operacional / cenário histórico',
},
{
id: 'goods_available',
branch: 'P131',
label: 'Bens postos à disposição7(1)a',
},
{
id: 'goods_transport',
branch: 'P131',
label: 'Início do transporte7(2)',
},
{
id: 'goods_installation',
branch: 'P131',
label: 'Instalação/montagem concluída7(2)',
},
{
id: 'service_completed',
branch: 'P131',
label: 'Serviço realizado7(1)b',
},
{
id: 'service_early_charge',
branch: 'P131',
label: 'Serviço cobrado/debitado antes da realização7(1)b',
},
{
id: 'import_clearance',
branch: 'P131',
label: 'Desembaraço aduaneiro7(1)c',
},
{
id: 'import_auction',
branch: 'P131',
label: 'Arrematação/venda na importação7(1)c',
},
{
id: 'continuous_period',
branch: 'P131',
label: 'Termo do período de pagamento sucessivo7(3)',
},
{
id: 'contract_effects',
branch: 'P131',
label: 'Efeitos translativos posteriores7(7)',
},
{
id: 'own_goods',
branch: 'P132',
label: 'Afectação/transmissão assimilada3(3)e/f e7(4)',
},
{
id: 'own_services',
branch: 'P132',
label: 'Serviço assimilado4(2) e7(4)',
},
{
id: 'commission_delivery',
branch: 'P132',
label: 'Comissário põe bens à disposição7(5)',
},
{
id: 'consignment_deadline',
branch: 'P132',
label: 'Termo de180dias da consignação7(6)',
},
{
id: 'advance_payment',
branch: 'P132',
label: 'Pagamento anterior à factura8c',
},
] as const;
type Choice = '' | 'yes' | 'no';
export type CurrencyEventRow = {
fxSelection?: FxSelection;
id: string;
treatment: string;
description: string;
period: string;
origin: 'projected' | 'effective';
operationRef: string;
documentRef: string;
documentDate: string;
scope: string;
evidence: string;
eventDate: string;
eventRef: string;
otherDate: string;
reviewDate: string;
factsConfirmed: Choice;
invoiceRequired: Choice;
invoiceState: '' | 'issued' | 'not_issued';
invoiceDate: string;
invoiceRef: string;
legalDeadline: string;
deadlineRef: string;
paymentState: '' | 'none' | 'received';
paymentDate: string;
paymentRef: string;
amount: string;
paymentAmount: string;
currency: string;
sourceCurrency: string;
fxRate: string;
fxDate: string;
fxSource: string;
fxRuleRef: string;
fxDirection: '' | 'mzn_per_unit' | 'units_per_mzn';
};
type DueEvent = { date: string; amount: string | null; reason: string };
type LineResult = {
id: string;
status: 'determined' | 'not_applicable' | 'indeterminate' | 'conditional';
eventDate: string | null;
dueEvents: DueEvent[];
remainingPending: boolean;
scenarioAmount: string | null;
baseAmount: null;
vat: null;
rate: null;
reason: string;
};
export type CurrencyEventCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: CurrencyEventRow[];
rows: LineResult[];
};
export type CurrencyEventBook = {
rows: CurrencyEventRow[];
result?: CurrencyEventCalculation;
history?: CurrencyEventCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
currencyEvents?: Record<string, CurrencyEventBook>;
};
export function newCurrencyEvent(
id: string,
currency: string,
): CurrencyEventRow {
return {
id,
treatment: '',
description: '',
period: '',
origin: 'projected',
operationRef: '',
documentRef: '',
documentDate: '',
scope: '',
evidence: '',
eventDate: '',
eventRef: '',
otherDate: '',
reviewDate: '',
factsConfirmed: '',
invoiceRequired: '',
invoiceState: '',
invoiceDate: '',
invoiceRef: '',
legalDeadline: '',
deadlineRef: '',
paymentState: '',
paymentDate: '',
paymentRef: '',
amount: '',
paymentAmount: '',
currency,
sourceCurrency: '',
fxRate: '',
fxDate: '',
fxSource: '',
fxRuleRef: '',
fxDirection: '',
};
}
export function switchCurrencyEvent(
row: CurrencyEventRow,
treatment: string,
): CurrencyEventRow {
return { ...row, treatment, factsConfirmed: '', invoiceRequired: '' };
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.currencyEvents !== undefined &&
(!store.currencyEvents ||
typeof store.currencyEvents !== 'object' ||
Array.isArray(store.currencyEvents))
)
throw new Error(
'Moeda/eventos não recuperáveis;originais preservados.',
);
const fields = Object.keys(newCurrencyEvent('', ''));
for (const b of Object.values(store.currencyEvents ?? {}))
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 CurrencyEventRow] !== 'string',
),
)
)
throw new Error(
'Linhas de moeda/eventos não recuperáveis;sem lista vazia por defeito.',
);
return store;
}
export function currencyEventBook(
draft: Record<string, unknown>,
): CurrencyEventBook {
return (
readStore(draft).currencyEvents?.[acquisitionScope(draft)] ?? {
rows: [],
}
);
}
export function saveCurrencyEventBook(
draft: Record<string, unknown>,
book: CurrencyEventBook,
): string {
const store = readStore(draft),
encoded = JSON.stringify({
...store,
currencyEvents: {
...store.currencyEvents,
[acquisitionScope(draft)]: book,
},
});
readStore({ ...draft, modelo_a_snapshot: encoded });
return encoded;
}
function date(s: string): number | null {
const n = Date.parse(s + 'T00:00:00Z');
return /^\d{4}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(n) &&
new Date(n).toISOString().slice(0, 10) === s
? n
: null;
}
function decimal(s: string, places = 6): bigint | null {
if (!new RegExp('^\\d{1,15}(\\.\\d{1,' + places + '})?$').test(s.trim()))
return null;
const [w, f = ''] = s.trim().split('.');
return BigInt(w) * 10n ** BigInt(places) + BigInt(f.padEnd(places, '0'));
}
function money(n: bigint): string {
return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')}`;
}
export function evaluateCurrencyEvent(row: CurrencyEventRow): LineResult {
const result: LineResult = {
id: row.id,
status: 'indeterminate',
eventDate: null,
dueEvents: [],
remainingPending: false,
scenarioAmount: null,
baseAmount: null,
vat: null,
rate: null,
reason: '',
};
const stop = (
reason: string,
status: LineResult['status'] = 'indeterminate',
) => ({ ...result, status, reason });
if (!currencyEventTreatments.some((t) => t.id === row.treatment))
return stop('Identifique percurso de moeda/evento.');
if (
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
!['projected', 'effective'].includes(row.origin) ||
date(row.documentDate) === null ||
![
row.description,
row.operationRef,
row.documentRef,
row.scope,
row.evidence,
].every((v) => !!v.trim())
)
return stop(
'Identifique mês de contexto2026,operação,documento/data,âmbito e factos ou pressupostos expressos.',
);
if (row.factsConfirmed === 'no')
return stop(
'Os factos declarados afastam este percurso;sem isenção ou IVA zero por defeito.',
'not_applicable',
);
if (row.factsConfirmed !== 'yes')
return stop(
'Factos do percurso por esclarecer;não usar datas ou câmbio por defeito.',
);
if (row.treatment === 'foreign_scenario') {
const amount = decimal(row.amount),
fx = decimal(row.fxRate, 8);
if (
row.currency !== 'MZN' ||
![
'USD',
'EUR',
'ZAR',
'GBP',
'CNY',
'BRL',
'JPY',
'CHF',
'AUD',
'CAD',
].includes(row.sourceCurrency) ||
amount === null ||
fx === null ||
fx === 0n ||
date(row.fxDate) === null ||
![row.fxSource, row.fxRuleRef].every((v) => !!v.trim()) ||
!['mzn_per_unit', 'units_per_mzn'].includes(row.fxDirection)
)
return stop(
'Indique moeda original,valor,taxa positiva,direcção,data,fonte e regra/pressuposto expresso.Sem substituir moeda original porMZN ou reutilizar taxa de apresentação como regra fiscal.',
);
result.scenarioAmount = money(
row.fxDirection === 'mzn_per_unit'
? convertScaledAmount(amount, fx, 100000000n)
: (amount * 100000000n + fx / 2n) / fx,
);
if (row.fxSelection) {
const converted = result.scenarioAmount;
result.scenarioAmount = null;
try {
const selected = selectFx(row.fxSelection),
applied = row.fxSelection.applied;
if (
!applied ||
applied.selectionKey !== fxSelectionKey(row.fxSelection) ||
applied.rate !== selected.rate ||
applied.date !== selected.date ||
row.sourceCurrency !== row.fxSelection.sourceCurrency ||
row.fxDirection !== row.fxSelection.direction ||
row.fxRate !== selected.rate ||
row.fxDate !== selected.date ||
row.fxSource !== applied.source
)
return stop(
'Selecção cambial alterada ou não aplicada explicitamente;histórico preservado,sem recálculo automático.',
);
result.scenarioAmount = converted;
const columnLabel = (
{
buy: 'Compra',
sell: 'Venda',
mean: 'Média de referência BM',
exception: 'Excepção fiscal identificada',
} as Record<string, string>
)[selected.column];
return stop(
`Conversão operacional segundo a orientação da Tempo de25/10/2026:${row.amount} ${row.sourceCurrency} → ${result.scenarioAmount} MZN. Coluna ${columnLabel};taxa ${row.fxRate};${row.fxDirection === 'mzn_per_unit' ? 'MZN por unidade da moeda original' : 'unidades da moeda original por MZN'};data ${row.fxDate};boletim ${row.fxSelection.bulletinRef}. Sem cálculo de IVA ou dedução e sem alteração dos livros fiscais anteriores.`,
'determined',
);
} catch (error) {
return stop(String(error));
}
}
return stop(
`P104:cenário condicionado=${result.scenarioAmount}MZN,origem${row.sourceCurrency},taxa${row.fxRate},${row.fxDirection},data${row.fxDate}. CotejoLei2/2006 concluido:106(5)nao define taxa/data;regra complementar15(8) ainda por demonstrar;referência indicada é pressuposto,não validação da taxa legal.Base fiscal permanece indeterminada.Sem IVA ou activação nos livros anteriores.`,
`conditional`,
);
}
const review = date(row.reviewDate),
event = date(row.eventDate);
if (review === null || !row.eventRef.trim())
return stop(
'Identifique data de conferência e documento/âmbito do evento específico.',
);
if (row.treatment === 'consignment_deadline') {
const delivery = date(row.otherDate);
if (delivery === null)
return stop(
'Identifique entrega das mercadorias à consignação3(3)d;7(6) exige termo180dias,não embalagensP102.',
);
result.eventDate = new Date(delivery + 180 * 86400000)
.toISOString()
.slice(0, 10);
} else if (row.treatment === 'advance_payment') {
if (date(row.paymentDate) === null)
return stop('Identifique recebimento anterior à factura para8c.');
result.eventDate = null;
} else {
if (event === null)
return stop(
'Identifique a data própria do evento7,sem inferir da emissão do documento.',
);
result.eventDate = row.eventDate;
if (
row.treatment === 'service_early_charge' &&
(date(row.otherDate) === null || event >= date(row.otherDate)!)
)
return stop(
'Indique realização do serviço posterior à cobrança/débito;aplique o evento apenas ao montante respectivo.',
);
if (
row.treatment === 'contract_effects' &&
(date(row.otherDate) === null || event < date(row.otherDate)!)
)
return stop(
'Indique disponibilização anterior ou simultânea aos efeitos translativos;este7(7) exclui3(3)a/b,que têm percursos próprios.',
);
}
if (['import_clearance', 'import_auction'].includes(row.treatment)) {
if (date(result.eventDate!)! > review)
return stop(
'Evento aduaneiro futuro na conferência;sem exigibilidade declarada.',
);
result.dueEvents = [
{
date: result.eventDate!,
amount:
decimal(row.amount) === null
? null
: money(decimal(row.amount)!),
reason: '7(1)c:desembaraço/arrematação/venda na importação;factura comercial não desloca o evento aduaneiro',
},
];
return stop(
'Importação7(1)c:facto gerador/exigibilidade pelo evento aduaneiro identificado;sem aplicar8 por factura comercial,nem calcular base/IVA.',
'determined',
);
}
if (row.invoiceRequired === '')
return stop(
'Identifique se esta operação dá lugar à obrigação de factura25;7 e8 são estados separados.',
);
if (row.invoiceRequired === 'no') {
if (result.eventDate && date(result.eventDate)! > review)
return stop(
'Evento futuro na conferencia;sem exigibilidade declarada.',
);
if (row.treatment === 'advance_payment')
return stop(
'Percurso8c exige operação com obrigação de factura;conferir enquadramento.',
'not_applicable',
);
result.dueEvents = [
{
date: result.eventDate!,
amount:
decimal(row.amount) === null
? null
: money(decimal(row.amount)!),
reason: 'Exigibilidade segundo evento7,sem obrigação de factura declarada',
},
];
return stop(
'Facto gerador e exigibilidade7 identificados no âmbito declarado;sem calcular IVA,base ou dedução.',
'determined',
);
}
const deadline = date(row.legalDeadline);
if (
deadline === null ||
!row.deadlineRef.trim() ||
(result.eventDate && deadline < date(result.eventDate)!)
)
return stop(
'Facto gerador identificado;indique prazo legal de emissão e disposição/calendário aplicáveis.Sem presumir automaticamente cinco dias úteis2008 ou feriados.',
);
if (
!['issued', 'not_issued'].includes(row.invoiceState) ||
!['none', 'received'].includes(row.paymentState)
)
return stop(
'Identifique emissão e recebimentos;desconhecido não é ausência de pagamento.',
);
const invoice =
row.invoiceState === 'issued' ? date(row.invoiceDate) : null;
if (
row.invoiceState === 'issued' &&
(invoice === null || invoice > review || !row.invoiceRef.trim())
)
return stop(
'Identifique factura efectivamente emitida/data/referência até à conferência.',
);
const normalDate =
invoice !== null && invoice <= deadline
? row.invoiceDate
: row.legalDeadline;
const total = decimal(row.amount);
let paid = 0n;
if (row.paymentState === 'received') {
const payment = date(row.paymentDate),
amount = decimal(row.paymentAmount);
if (
payment === null ||
payment > review ||
amount === null ||
total === null ||
amount > total ||
!row.paymentRef.trim()
)
return stop(
'Identifique recebimento,parcela e total do âmbito na mesma moeda;pagamento não pode exceder o total.Sem duplicar parcelas nem substituir montante desconhecido porzero.',
);
if (
(invoice === null || payment < invoice) &&
payment < date(normalDate)!
) {
paid = amount;
result.dueEvents.push({
date: row.paymentDate,
amount: money(amount),
reason: '8c:montante recebido antes da factura,sem prejuízo do termo do prazo8b',
});
} else if (row.treatment === 'advance_payment')
return stop(
'Pagamento não é anterior à factura/termo aplicável;não antecipar por8c.',
'not_applicable',
);
}
if (row.treatment === 'advance_payment' && row.paymentState !== 'received')
return stop('Identifique o recebimento anterior à factura.');
const remainder = total === null ? null : total - paid;
if (date(normalDate)! <= review) {
if (remainder === null || remainder > 0n)
result.dueEvents.push({
date: normalDate,
amount: remainder === null ? null : money(remainder),
reason:
invoice !== null && invoice <= deadline
? '8a:factura emitida dentro do prazo'
: '8b:termo do prazo de emissão',
});
} else result.remainingPending = remainder === null || remainder > 0n;
return stop(
'Exigibilidade separada em parcelas8a/b/c,sem dupla contagem do recebimento e restante. Facto gerador7 conservado;prazo legal parametrizado com referência aplicável.Sem IVA,base fiscal,nova dedução ou totaisModeloA.',
'determined',
);
}
function key(draft: Record<string, unknown>, book: CurrencyEventBook): string {
return JSON.stringify({
version: currencyEventVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
rows: book.rows,
});
}
export function currencyEventCurrent(
draft: Record<string, unknown>,
book: CurrencyEventBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordCurrencyEvents(
draft: Record<string, unknown>,
book: CurrencyEventBook,
): CurrencyEventBook {
if (
!book.rows.length ||
new Set(book.rows.map((r) => r.id)).size !== book.rows.length
)
throw new Error(
'Indique operações com identidades distintas;originais conservados.',
);
const result: CurrencyEventCalculation = {
version: currencyEventVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: currencyEventSource,
inputs: structuredClone(book.rows),
rows: book.rows.map(evaluateCurrencyEvent),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveCurrencyEventBook(draft, next);
return next;
}
export function currencyEventReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = currencyEventBook(draft),
r = book.result;
if (!r) return [];
return r.inputs.map((row, i): [string, string, string, string] => {
if (row.treatment === 'foreign_scenario' && row.fxSelection)
return [
'IVA mensal',
`P104 — ${row.period} — ${row.id}`,
`${row.description};${r.rows[i].reason};Original:${row.amount} ${row.sourceCurrency};Convertido:${r.rows[i].scenarioAmount ?? 'Indeterminado'} MZN`,
`${r.opportunity};${currencyEventCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};documento:${row.documentRef},${row.documentDate};âmbito:${row.scope};boletim:${row.fxSelection.bulletinRef};fonte:${row.fxSelection.source};natureza:${row.fxSelection.nature};perspectiva:${row.fxSelection.bulletinPerspective};relação:${row.fxSelection.relation};taxa:${row.fxRate};data:${row.fxDate};sentido:${row.fxDirection};regra/excepção:${row.fxRuleRef};sem base/IVA/dedução activados.`,
];
return [
'IVA mensal',
`${currencyEventTreatments.find((t) => t.id === row.treatment)?.branch ?? 'Evento'} — ${row.period} — ${row.id}`,
`${row.description};${r.rows[i].reason};facto gerador:${r.rows[i].eventDate ?? 'Indeterminado/não apurado'};parcelas:${r.rows[i].dueEvents.map((p) => p.date + ': ' + (p.amount ?? 'Montante indeterminado') + ' ' + row.currency + ' (' + p.reason + ')').join('; ') || 'Nenhuma apurada'};restante pendente:${r.rows[i].status === 'indeterminate' ? 'Indeterminado' : r.rows[i].remainingPending ? 'Sim' : 'Não'};${row.treatment === 'foreign_scenario' && r.rows[i].status === 'determined' ? 'conversão operacional' : 'cenário condicionado'}:${r.rows[i].scenarioAmount ?? 'Indeterminado'};base/IVA:Indeterminados`,
`${r.opportunity};${r.location};${currencyEventCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};${row.origin};documento ${row.documentRef},${row.documentDate};ambito ${row.scope};${row.treatment === 'foreign_scenario' ? `origem ${row.amount} ${row.sourceCurrency};taxa ${row.fxRate},${row.fxDirection},${row.fxDate};fonte ${row.fxSource};pressuposto ${row.fxRuleRef}` : `evento ${row.eventDate},${row.eventRef};outra data ${row.otherDate};conferencia ${row.reviewDate};factura ${row.invoiceRequired},${row.invoiceState},${row.invoiceDate},${row.invoiceRef};prazo ${row.legalDeadline},${row.deadlineRef};recebimento ${row.paymentState},${row.paymentDate},${row.paymentAmount},${row.paymentRef};total do ambito ${row.amount} ${row.currency}`};fundamento ${row.evidence};sem nova dedução/campo11/IVA ou activação cambial nos livros anteriores.`,
];
});
} catch {
return [
[
'IVA mensal',
'Moeda/eventos',
'Dados não recuperáveis',
'Originais preservados;sem substituição porzero.',
],
];
}
}