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