Files
Investidor-V2/resources/js/lib/iva-reduced-bases.ts
T

441 lines
16 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 reducedBaseVersion = 'proportional-bases/1';
export const reducedBaseSource =
'CIVA15(2)j/k/l/m,15(1)/(5)/(6)/(9),Lei13/2016PDF13–14/p1426(271–272);Lei22/2022PDF3mantém15(2)j–m;Lei10/2025não altera15 eadita18(6),PDF13:dedução do imposto suportado relativo a estas operações na mesma proporção da base reduzida. Recorte2026/MZN;base apenas,sem taxaIVA/IVA/dedução/campo11 ou totaisModeloA. Energia62%,taxas aeronáuticas85%,obras redução60%(base40%),água75%. Percentagem de base não é taxaIVA.';
export const reducedBaseTreatments = [
{
id: 'regulated_energy',
branch: 'P096',
clause: 'j',
label: 'P096 — Energia com preço fixado por Autoridade Pública',
percent: 62,
},
{
id: 'aeronautical_fees',
branch: 'P097',
clause: 'k',
label: 'P097 — Serviços com preço fixado por taxas aeronáuticas',
percent: 85,
},
{
id: 'public_works',
branch: 'P098',
clause: 'l',
label: 'P098 — Obras públicas abrangidas',
percent: 40,
},
{
id: 'public_water',
branch: 'P099',
clause: 'm',
label: 'P099 — Água potável por rede pública e preço fixado',
percent: 75,
},
] as const;
export const reducedBaseConditions = [
[
'taxableOperation',
'Está identificada a operação sujeita a IVA e a correspondência com este tratamento',
],
[
'fullNetInvoice',
'O montante é a contraprestação total da operação/factura antes desta redução,sem o próprioIVA ecom composição15(1)/(5)/(6)conferida',
],
[
'scopeMatches',
'Documentos/referências ou pressupostos expressos correspondem ao objecto,local,operação eperíodo',
],
] as const;
export const reducedOwnConditions = {
regulated_energy: [
['energySupply', 'Transmissão de energia identificada'],
[
'publicFixedPrice',
'Preço fixado pela Autoridade Pública identificada na referência aplicável',
],
],
aeronautical_fees: [
[
'aeronauticalPrice',
'Preço do serviço fixado através de taxas aeronáuticas;uma descrição de comunicações,saneamento ou telecomunicações não basta',
],
],
public_works: [
[
'publicWorks',
'Construção ou reabilitação de obras públicas abrangidas15(2)l/15(9)',
],
[
'workCategoryMatches',
'A categoria eâmbito da obra correspondem ao projecto público identificado',
],
],
public_water: [
['potableWater', 'Fornecimento de água potável'],
['publicNetwork', 'Fornecimento através de rede pública'],
[
'publicFixedPrice',
'Preço fixado pela Autoridade Pública identificada na referência aplicável',
],
],
} as const;
export const publicWorkCategories = [
{ id: 'roads', label: 'Estradas' },
{ id: 'bridges', label: 'Pontes' },
{
id: 'water_infrastructure',
label: 'Infra-estruturas de abastecimento de água/saneamento15(9)a',
},
{
id: 'rural_electricity',
label: 'Electrificação rural em projectos públicos15(9)b',
},
{
id: 'agricultural_hydraulics',
label: 'Hidráulica agrícola no âmbito definido15(9)c',
},
] as const;
export function ownReducedConditions(
treatment: string,
): ReadonlyArray<readonly [string, string]> {
return (
reducedOwnConditions[treatment as keyof typeof reducedOwnConditions] ??
[]
);
}
type Choice = '' | 'yes' | 'no';
export type ReducedBaseRow = {
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;
projectRef: string;
workCategory: string;
compositionRef: string;
evidence: string;
conditions: Partial<Record<string, Choice>>;
};
type LineResult = {
id: string;
status: 'determined' | 'not_applicable' | 'indeterminate';
baseAmount: string | null;
deductionProportion: string | null;
vat: null;
rate: null;
reason: string;
};
export type ReducedBaseCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: ReducedBaseRow[];
rows: LineResult[];
};
export type ReducedBaseBook = {
rows: ReducedBaseRow[];
result?: ReducedBaseCalculation;
history?: ReducedBaseCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
regulatedReducedBases?: Record<string, ReducedBaseBook>;
};
export function newReducedBase(id: string, currency: string): ReducedBaseRow {
return {
id,
treatment: '',
description: '',
period: '',
origin: 'projected',
currency,
operationDate: '',
priceRef: '',
priceScope: '',
priceFrom: '',
priceTo: '',
documentRef: '',
documentDate: '',
consideration: '',
projectRef: '',
workCategory: '',
compositionRef: '',
evidence: '',
conditions: {},
};
}
export function switchReducedTreatment(
row: ReducedBaseRow,
treatment: string,
): ReducedBaseRow {
return {
...row,
treatment,
conditions: Object.fromEntries(
reducedBaseConditions.map(([key]) => [
key,
row.conditions[key] ?? '',
]),
),
};
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.regulatedReducedBases !== undefined &&
(!store.regulatedReducedBases ||
typeof store.regulatedReducedBases !== 'object' ||
Array.isArray(store.regulatedReducedBases))
)
throw new Error(
'Bases reduzidas não recuperáveis;originais preservados.',
);
const fields = Object.keys(newReducedBase('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.regulatedReducedBases ?? {}))
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 ReducedBaseRow] !==
'string',
) ||
!row.conditions ||
typeof row.conditions !== 'object' ||
Array.isArray(row.conditions),
)
)
throw new Error(
'Linhas de bases reduzidas não recuperáveis;originais preservados,sem substituição por lista vazia.',
);
return store;
}
export function reducedBaseBook(
draft: Record<string, unknown>,
): ReducedBaseBook {
return (
readStore(draft).regulatedReducedBases?.[acquisitionScope(draft)] ?? {
rows: [],
}
);
}
export function saveReducedBaseBook(
draft: Record<string, unknown>,
book: ReducedBaseBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
regulatedReducedBases: {
...current.regulatedReducedBases,
[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 / 100000000n}.${(n % 100000000n).toString().padStart(8, '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 evaluateReducedBase(
row: ReducedBaseRow,
currency: unknown,
): LineResult {
const result: LineResult = {
id: row.id,
status: 'indeterminate',
baseAmount: null,
deductionProportion: null,
vat: null,
rate: null,
reason: '',
};
const stop = (
reason: string,
status: LineResult['status'] = 'indeterminate',
) => ({ ...result, status, reason });
const rule = reducedBaseTreatments.find((t) => t.id === row.treatment);
if (!rule)
return stop(
'Identifique energia,taxas aeronáuticas,obras públicas ou água;sem percentagem por defeito.',
);
if (currency !== 'MZN' || row.currency !== currency)
return stop(
'Moeda fora do âmbitoMZN 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ês2026 e dados efectivos ou pressupostos expressos de projecção.',
);
const conditions = [
...reducedBaseConditions,
...ownReducedConditions(row.treatment),
];
if (conditions.some(([key]) => row.conditions[key] === 'no'))
return stop(
'Este tratamento de base reduzida não se aplica aos factos declarados;rever natureza/composição/âmbito,sem isenção ou IVAzero presumido.',
'not_applicable',
);
if (conditions.some(([key]) => row.conditions[key] !== 'yes'))
return stop(
'Condições por esclarecer;desconhecido não é não aplicável ou zero. Confirme o montante total antes da redução,sem próprioIVA.',
);
if (
!row.description.trim() ||
!row.documentRef.trim() ||
!row.compositionRef.trim() ||
!row.evidence.trim() ||
!row.priceScope.trim() ||
!validDate(row.documentDate) ||
!validDate(row.operationDate) ||
row.operationDate.slice(0, 7) !== row.period
)
return stop(
'Identifique operação,data/mês,documento/data,âmbito e composição ou pressupostos expressos;reutilize referências sem certificado/aprovador novo.',
);
if (row.treatment === 'public_works') {
if (
!row.projectRef.trim() ||
!publicWorkCategories.some((c) => c.id === row.workCategory)
)
return stop(
'Identifique projecto/contrato público,categoria eâmbito da obra15(2)l/15(9);não aplicar a qualquer obra privada.',
);
} else if (
!row.priceRef.trim() ||
!validDate(row.priceFrom) ||
row.operationDate < row.priceFrom ||
(row.priceTo !== '' &&
(!validDate(row.priceTo) ||
row.priceFrom > row.priceTo ||
row.operationDate > row.priceTo))
)
return stop(
'Identifique referência aplicável do preço/taxa e intervalo que abrange a operação;fim pode ficar vazio sem termo fixado,com correspondência temporal confirmada. Sem preço actual presumido para outro período.',
);
const amount = decimal(row.consideration);
if (amount === null)
return stop(
'Indique contraprestação total antes da redução,sem próprioIVA;desconhecido/inválido não é zero. Se só existe total comIVA,separe a composição no percurso aplicável antes de usar este campo.',
);
const base = money(amount * BigInt(rule.percent));
return {
...result,
status: 'determined',
baseAmount: base,
deductionProportion: String(rule.percent) + '%',
reason: `${rule.branch}:15(2)${rule.clause},base=${row.consideration}×${rule.percent}%=${base}MZN,${row.origin === 'projected' ? 'sob pressupostos expressos' : 'nos dados efectivos declarados'}. ${row.treatment === 'public_works' ? 'Redução de60% significa base40%,não base60%.' : 'Percentagem aplicada uma única vez ao montante total antes da redução,sem próprioIVA.'} Art18(6),Lei10/2025:dedução relativa a estas operações na mesma proporção(${rule.percent}%),sujeita às condições próprias;isto não calcula nem concede dedução/campo11. Base não é imposto;sem taxaIVA,IVA ou totaisModeloA. Decimal exacto,sem arredondamento fiscal presumido.`,
};
}
function key(draft: Record<string, unknown>, book: ReducedBaseBook): string {
return JSON.stringify({
version: reducedBaseVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function reducedBaseCurrent(
draft: Record<string, unknown>,
book: ReducedBaseBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordReducedBases(
draft: Record<string, unknown>,
book: ReducedBaseBook,
): ReducedBaseBook {
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: ReducedBaseCalculation = {
version: reducedBaseVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: reducedBaseSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) => evaluateReducedBase(row, draft.currency)),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveReducedBaseBook(draft, next);
return next;
}
export function reducedBaseReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = reducedBaseBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base ${reducedBaseTreatments.find((t) => t.id === row.treatment)?.branch ?? 'reduzida'} — ${row.period} — ${row.id}`,
`${row.description};base tributável: ${result.rows[index].baseAmount ?? 'Indeterminada'} ${row.currency};IVA: Indeterminado;proporção18(6):${result.rows[index].deductionProportion ?? 'Indeterminada'}(sem dedução calculada);${result.rows[index].reason}`,
`${result.opportunity};${result.location};${reducedBaseCurrent(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'};operação ${row.operationDate};âmbito ${row.priceScope};${row.treatment === 'public_works' ? `projecto/contrato ${row.projectRef};categoria ${row.workCategory}` : `preço/taxa ${row.priceRef};intervalo ${row.priceFrom} a ${row.priceTo || 'sem termo fixado,correspondência confirmada'}`};contraprestação total antes redução ${row.consideration} ${row.currency};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${[...reducedBaseConditions, ...ownReducedConditions(row.treatment)].map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento ${row.evidence};sem dedução/campo11/totaisModeloA.`,
]);
} catch {
return [
[
'IVA mensal',
'Bases reduzidas',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}