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