Files
Investidor-V2/resources/js/lib/iva-fuel-base.ts
T

370 lines
14 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 fuelBaseVersion = 'regulated-fuel-base/1';
export const fuelBaseSource =
'CIVA republicado Lei13/2016,15(2)(i),PDF13/p1426(271): nas transmissões de combustíveis cujo preço é fixado por Autoridade Pública,efectuadas por revendedores,o valor da contraprestação determinado nos termos dos números1 e5 não inclui a Taxa sobre os Combustíveis. Art15(5)/(6),PDF14/p1426(272),composição/exclusões,sem próprioIVA. Lei22/2022PDF3mantém15(2)(i)/(1)/(5)/(6)por remissão;Lei10/2025não altera15. Recorte2026/MZN;base apenas,sem presumir preço regulado,valorTC,incidência,taxaIVA,IVA,dedução/campo11 ou totaisModeloA.';
export const fuelBaseTreatments = [
{
id: 'fuel_tax_included',
label: 'Montante sem IVA ainda inclui a Taxa sobre Combustíveis',
},
{
id: 'fuel_tax_excluded',
label: 'Montante sem IVA já exclui a Taxa sobre Combustíveis',
},
] as const;
export const fuelBaseConditions = [
[
'taxableFuelResale',
'Está identificada uma transmissão de combustíveis sujeita a IVA,efectuada por revendedor; não é qualquer aquisição ou prestação de serviços',
],
[
'publicFixedPrice',
'O preço deste combustível é fixado pela Autoridade Pública identificada na referência aplicável',
],
[
'priceScopeMatches',
'A referência do preço e o respectivo âmbito abrangem este combustível,operação,local e data; não se presume um preço actual para outro período',
],
[
'netComposition',
'O montante indicado exclui o próprio IVA e reflecte a composição aplicável do artigo15(1)/(5)/(6),sem omissões ou duplicações',
],
[
'fuelTaxTreatment',
'O tratamento escolhido corresponde à composição do montante:TC ainda incluída ou já excluída; a mesma TC não será subtraída duas vezes',
],
] as const;
type Choice = '' | 'yes' | 'no';
type Condition = (typeof fuelBaseConditions)[number][0];
export type FuelBaseRow = {
id: string;
treatment: string;
description: string;
period: string;
origin: 'projected' | 'effective';
currency: string;
operationDate: string;
priceRef: string;
priceScope: string;
priceFrom: string;
priceTo: string;
documentRef: string;
documentDate: string;
consideration: string;
fuelTax: string;
fuelTaxRef: string;
compositionRef: string;
evidence: string;
conditions: Partial<Record<Condition, Choice>>;
};
type LineResult = {
id: string;
status: 'determined' | 'not_applicable' | 'indeterminate';
baseAmount: string | null;
vat: null;
rate: null;
reason: string;
};
export type FuelBaseCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: FuelBaseRow[];
rows: LineResult[];
};
export type FuelBaseBook = {
rows: FuelBaseRow[];
result?: FuelBaseCalculation;
history?: FuelBaseCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
regulatedFuelBases?: Record<string, FuelBaseBook>;
};
export function newFuelBase(id: string, currency: string): FuelBaseRow {
return {
id,
treatment: '',
description: '',
period: '',
origin: 'projected',
currency,
operationDate: '',
priceRef: '',
priceScope: '',
priceFrom: '',
priceTo: '',
documentRef: '',
documentDate: '',
consideration: '',
fuelTax: '',
fuelTaxRef: '',
compositionRef: '',
evidence: '',
conditions: {},
};
}
export function switchFuelTaxTreatment(
row: FuelBaseRow,
treatment: string,
): FuelBaseRow {
return {
...row,
treatment,
conditions: { ...row.conditions, fuelTaxTreatment: '' },
};
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.regulatedFuelBases !== undefined &&
(!store.regulatedFuelBases ||
typeof store.regulatedFuelBases !== 'object' ||
Array.isArray(store.regulatedFuelBases))
)
throw new Error(
'Bases de combustíveis não recuperáveis;originais preservados.',
);
const fields = Object.keys(newFuelBase('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.regulatedFuelBases ?? {}))
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 FuelBaseRow] !== 'string',
) ||
!row.conditions ||
typeof row.conditions !== 'object' ||
Array.isArray(row.conditions),
)
)
throw new Error(
'Linhas de combustíveis não recuperáveis;originais preservados,sem substituição por lista vazia.',
);
return store;
}
export function fuelBaseBook(draft: Record<string, unknown>): FuelBaseBook {
return (
readStore(draft).regulatedFuelBases?.[acquisitionScope(draft)] ?? {
rows: [],
}
);
}
export function saveFuelBaseBook(
draft: Record<string, unknown>,
book: FuelBaseBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
regulatedFuelBases: {
...current.regulatedFuelBases,
[acquisitionScope(draft)]: book,
},
});
readStore({ ...draft, modelo_a_snapshot: encoded });
return encoded;
}
function decimal(raw: unknown): bigint | null {
if (typeof raw !== 'string' || !/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))
return null;
const [whole, fraction = ''] = raw.trim().split('.');
return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, '0'));
}
function money(n: bigint): string {
return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')}`;
}
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 evaluateFuelBase(
row: FuelBaseRow,
currency: unknown,
): LineResult {
const result: LineResult = {
id: row.id,
status: 'indeterminate',
baseAmount: null,
vat: null,
rate: null,
reason: '',
};
const stop = (
reason: string,
status: LineResult['status'] = 'indeterminate',
) => ({ ...result, status, reason });
if (!fuelBaseTreatments.some((item) => item.id === row.treatment))
return stop(
'Indique se o montante sem IVA ainda inclui a TC ou já a exclui;não se presume a composição.',
);
if (currency !== 'MZN' || row.currency !== currency)
return stop(
'Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa.',
);
if (
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
!['projected', 'effective'].includes(row.origin)
)
return stop(
'Indique mês em2026 e dados efectivos declarados ou pressupostos de projecção;sem inferir do orçamento.',
);
const failed = fuelBaseConditions.filter(
([key]) => row.conditions[key] === 'no',
);
if (failed.length)
return stop(
'Não se aplica esta base especial de combustíveis: ' +
failed.map(([, label]) => label).join('; ') +
'. Não é isenção nem IVA zero.',
'not_applicable',
);
const missing = fuelBaseConditions.filter(
([key]) => row.conditions[key] !== 'yes',
);
if (missing.length)
return stop(
'Condições por indicar: ' +
missing.map(([, label]) => label).join('; ') +
'. Desconhecido não significa não aplicável ou zero.',
);
if (
!row.description.trim() ||
!row.priceRef.trim() ||
!row.priceScope.trim() ||
!row.documentRef.trim() ||
!row.compositionRef.trim() ||
!row.evidence.trim() ||
!validDate(row.documentDate)
)
return stop(
'Identifique combustível/transmissão,referência e âmbito do preço,documento/data,composição e fundamento ou pressupostos expressos;reutilize referências existentes,sem novo certificado/aprovador.',
);
if (
!validDate(row.operationDate) ||
row.operationDate.slice(0, 7) !== row.period ||
!validDate(row.priceFrom) ||
row.operationDate < row.priceFrom ||
(row.priceTo !== '' &&
(!validDate(row.priceTo) ||
row.priceFrom > row.priceTo ||
row.operationDate > row.priceTo))
)
return stop(
'Identifique a data da transmissão no mês escolhido e o intervalo aplicável do preço que a abrange. Sem aplicar preço de outro período;na projecção indique pressupostos temporais expressos. O fim pode ficar vazio quando não existe termo fixado,com a correspondência temporal confirmada.',
);
const amount = decimal(row.consideration);
if (amount === null)
return stop(
'Indique o montante da contraprestação sem IVA;valor desconhecido ou inválido não é zero.',
);
let base = amount,
formula = 'montante já sem TC,sem nova subtracção';
if (row.treatment === 'fuel_tax_included') {
const tc = decimal(row.fuelTax);
if (tc === null || !row.fuelTaxRef.trim())
return stop(
'Identifique o montante exacto e a referência da TC incluída nesta contraprestação,ou pressuposto expresso;sem taxa TC presumida ou valor desconhecido igual a zero.',
);
if (tc > amount)
return stop(
'A TC indicada excede a contraprestação sem IVA;reveja a composição e os montantes,sem base negativa ou zero substituto.',
);
base = amount - tc;
formula = `${money(amount)} − ${money(tc)} de TC`;
}
return {
...result,
status: 'determined',
baseAmount: money(base),
reason: `Base15(2)(i): ${formula} = ${money(base)}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Preço/âmbito/período e composição identificados;sem determinar automaticamente preço ou taxaTC. Base não é imposto;IVA/taxa,dedução/campo11 e totaisModeloA não são determinados por este cálculo.`,
};
}
function key(draft: Record<string, unknown>, book: FuelBaseBook): string {
return JSON.stringify({
version: fuelBaseVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function fuelBaseCurrent(
draft: Record<string, unknown>,
book: FuelBaseBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordFuelBases(
draft: Record<string, unknown>,
book: FuelBaseBook,
): FuelBaseBook {
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: FuelBaseCalculation = {
version: fuelBaseVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: fuelBaseSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) => evaluateFuelBase(row, draft.currency)),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveFuelBaseBook(draft, next);
return next;
}
export function fuelBaseReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = fuelBaseBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base P095 — ${row.period} — ${row.id}`,
`${row.description};base tributável: ${result.rows[index].baseAmount ?? 'Indeterminada'} ${row.currency};IVA: Indeterminado;${result.rows[index].reason}`,
`${result.opportunity};${result.location};${fuelBaseCurrent(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'};transmissão ${row.operationDate};preço ${row.priceRef};âmbito ${row.priceScope};intervalo ${row.priceFrom} a ${row.priceTo || 'sem termo fixado,correspondência temporal confirmada'};${fuelBaseTreatments.find((item) => item.id === row.treatment)?.label ?? 'Composição por indicar'};contraprestação ${row.consideration} ${row.currency};${row.treatment === 'fuel_tax_included' ? `TC incluída ${row.fuelTax} ${row.currency},referência ${row.fuelTaxRef}` : 'TC já excluída,sem segunda subtracção'};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${fuelBaseConditions.map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento ${row.evidence};não preenche totaisModeloA ou campo11.`,
]);
} catch {
return [
[
'IVA mensal',
'Base de combustíveis',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}