362 lines
14 KiB
TypeScript
362 lines
14 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
|
||
export const publicBaseVersion = 'public-authority-base/1';
|
||
export const publicBaseSource =
|
||
'CIVA republicado Lei13/2016,15(2)(d),PDF13/p1426(271): base das transmissões de bens e prestações de serviços resultantes de actos de autoridades públicas é a indemnização ou qualquer outra compensação. Art15(3)/(4),PDF14/p1426(272): componente não monetária pelo valor normal;15(5)/(6),composição/exclusões,incluindo exclusão do próprio IVA e indemnização judicial por incumprimento de contratos. Lei22/2022PDF3 mantém15(2)(d)/(3–6)por remissão;Lei10/2025não altera15. Recorte2026/MZN,determinação da base;sem inferir incidência,taxa,isenção,IVA,dedução/campo11 ou totaisModeloA.';
|
||
export const publicBaseTreatments = [
|
||
{
|
||
id: 'public_authority_indemnity',
|
||
label: 'Indemnização por acto de autoridade pública',
|
||
},
|
||
{
|
||
id: 'public_authority_compensation',
|
||
label: 'Outra compensação por acto de autoridade pública',
|
||
},
|
||
] as const;
|
||
export const publicBaseConditions = [
|
||
[
|
||
'taxableDomesticSupply',
|
||
'Está identificada uma transmissão de bens ou prestação de serviços sujeita a IVA no território nacional; não basta existir um pagamento ou uma entidade pública',
|
||
],
|
||
[
|
||
'authorityAct',
|
||
'A transmissão ou prestação resulta do acto da autoridade pública identificado',
|
||
],
|
||
[
|
||
'compensationLink',
|
||
'O montante e as componentes indicadas correspondem à indemnização ou compensação desta operação',
|
||
],
|
||
[
|
||
'notJudicialContractBreach',
|
||
'Não se trata de quantia recebida a título de indemnização declarada judicialmente por incumprimento total ou parcial de contrato, excluída pelo artigo15(6)(a)',
|
||
],
|
||
[
|
||
'netComposition',
|
||
'Os valores indicados excluem o próprio IVA e reflectem a composição aplicável dos números5/6 do artigo15, sem omissões nem duplicações; a referência explica a composição',
|
||
],
|
||
] as const;
|
||
type Condition = (typeof publicBaseConditions)[number][0];
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type PublicBaseRow = {
|
||
id: string;
|
||
treatment: string;
|
||
description: string;
|
||
operationNature: '' | 'goods' | 'services';
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
currency: string;
|
||
authority: string;
|
||
actRef: string;
|
||
documentRef: string;
|
||
documentDate: string;
|
||
compensationForm: '' | 'money' | 'non_money' | 'mixed';
|
||
moneyAmount: string;
|
||
nonMoneyValue: string;
|
||
valuationRef: 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 PublicBaseCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
location: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: PublicBaseRow[];
|
||
rows: LineResult[];
|
||
};
|
||
export type PublicBaseBook = {
|
||
rows: PublicBaseRow[];
|
||
result?: PublicBaseCalculation;
|
||
history?: PublicBaseCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
publicAuthorityBases?: Record<string, PublicBaseBook>;
|
||
};
|
||
export function newPublicBase(id: string, currency: string): PublicBaseRow {
|
||
return {
|
||
id,
|
||
treatment: '',
|
||
description: '',
|
||
operationNature: '',
|
||
period: '',
|
||
origin: 'projected',
|
||
currency,
|
||
authority: '',
|
||
actRef: '',
|
||
documentRef: '',
|
||
documentDate: '',
|
||
compensationForm: '',
|
||
moneyAmount: '',
|
||
nonMoneyValue: '',
|
||
valuationRef: '',
|
||
compositionRef: '',
|
||
evidence: '',
|
||
conditions: {},
|
||
};
|
||
}
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
store.publicAuthorityBases !== undefined &&
|
||
(!store.publicAuthorityBases ||
|
||
typeof store.publicAuthorityBases !== 'object' ||
|
||
Array.isArray(store.publicAuthorityBases))
|
||
)
|
||
throw new Error(
|
||
'Bases de actos públicos não recuperáveis; originais preservados.',
|
||
);
|
||
const fields = Object.keys(newPublicBase('', '')).filter(
|
||
(key) => key !== 'conditions',
|
||
);
|
||
for (const book of Object.values(store.publicAuthorityBases ?? {})) {
|
||
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 PublicBaseRow] !== 'string',
|
||
) ||
|
||
!row.conditions ||
|
||
typeof row.conditions !== 'object' ||
|
||
Array.isArray(row.conditions),
|
||
)
|
||
)
|
||
throw new Error(
|
||
'Linhas de base tributável não recuperáveis; originais preservados,sem substituição por lista vazia.',
|
||
);
|
||
}
|
||
return store;
|
||
}
|
||
export function publicBaseBook(draft: Record<string, unknown>): PublicBaseBook {
|
||
return (
|
||
readStore(draft).publicAuthorityBases?.[acquisitionScope(draft)] ?? {
|
||
rows: [],
|
||
}
|
||
);
|
||
}
|
||
export function savePublicBaseBook(
|
||
draft: Record<string, unknown>,
|
||
book: PublicBaseBook,
|
||
): string {
|
||
const current = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...current,
|
||
publicAuthorityBases: {
|
||
...current.publicAuthorityBases,
|
||
[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 evaluatePublicBase(
|
||
row: PublicBaseRow,
|
||
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 (!publicBaseTreatments.some((item) => item.id === row.treatment))
|
||
return stop(
|
||
'Escolha indemnização ou outra compensação por acto de autoridade pública; um pagamento genérico não determina esta base.',
|
||
);
|
||
if (currency !== 'MZN' || row.currency !== currency)
|
||
return stop(
|
||
'Moeda fora do âmbito MZN ou divergente do projecto; originais conservados,sem conversão.',
|
||
);
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
||
!['projected', 'effective'].includes(row.origin)
|
||
)
|
||
return stop(
|
||
'Indique o período em2026 e dados efectivos declarados ou pressupostos de projecção; não são inferidos do orçamento.',
|
||
);
|
||
const failed = publicBaseConditions.filter(
|
||
([key]) => row.conditions[key] === 'no',
|
||
);
|
||
if (failed.length)
|
||
return stop(
|
||
'Não se aplica esta determinação de base: ' +
|
||
failed.map(([, label]) => label).join('; ') +
|
||
'. Não é isenção nem IVA zero; conserve o enquadramento próprio da operação.',
|
||
'not_applicable',
|
||
);
|
||
const missing = publicBaseConditions.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 (
|
||
!['goods', 'services'].includes(row.operationNature) ||
|
||
!row.description.trim() ||
|
||
!row.authority.trim() ||
|
||
!row.actRef.trim() ||
|
||
!row.documentRef.trim() ||
|
||
!row.compositionRef.trim() ||
|
||
!row.evidence.trim() ||
|
||
!validDate(row.documentDate)
|
||
)
|
||
return stop(
|
||
'Identifique a operação,natureza,autoridade,acto,documento/data e fundamento da composição,ou pressupostos expressos; reutilize referências existentes,sem novo certificado.',
|
||
);
|
||
if (!['money', 'non_money', 'mixed'].includes(row.compensationForm))
|
||
return stop(
|
||
'Indique se a compensação é em dinheiro,em espécie ou mista; ausência de resposta não elimina uma componente.',
|
||
);
|
||
const cash =
|
||
row.compensationForm === 'non_money'
|
||
? 0n
|
||
: decimal(row.moneyAmount),
|
||
kind =
|
||
row.compensationForm === 'money' ? 0n : decimal(row.nonMoneyValue);
|
||
if (cash === null || kind === null)
|
||
return stop(
|
||
'Indique os valores sem IVA das componentes aplicáveis; montante em falta ou inválido não é zero.',
|
||
);
|
||
if (row.compensationForm !== 'money' && !row.valuationRef.trim())
|
||
return stop(
|
||
'Identifique o fundamento do valor normal da componente não monetária,conforme15(3)/(4),ou o pressuposto expresso; não é avaliado pelo portal.',
|
||
);
|
||
const base = money(cash + kind);
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
baseAmount: base,
|
||
reason: `Base15(2)(d): componente monetária ${money(cash)} + valor normal não monetário ${money(kind)} = ${base}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Componentes sem IVA,não duplicadas,composição15(5)/(6) identificada. Base não é imposto; incidência/taxa/IVA/dedução/campo11 não são determinados por este cálculo.`,
|
||
};
|
||
}
|
||
function key(draft: Record<string, unknown>, book: PublicBaseBook): string {
|
||
return JSON.stringify({
|
||
version: publicBaseVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
currency: draft.currency,
|
||
rows: book.rows,
|
||
});
|
||
}
|
||
function natureLabel(value: string): string {
|
||
return value === 'goods'
|
||
? 'Transmissão de bens'
|
||
: value === 'services'
|
||
? 'Prestação de serviços'
|
||
: 'Por indicar';
|
||
}
|
||
function formLabel(value: string): string {
|
||
return value === 'money'
|
||
? 'Em dinheiro'
|
||
: value === 'non_money'
|
||
? 'Em espécie'
|
||
: value === 'mixed'
|
||
? 'Mista — dinheiro e espécie'
|
||
: 'Por indicar';
|
||
}
|
||
export function publicBaseCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: PublicBaseBook,
|
||
): boolean {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
}
|
||
export function recordPublicBases(
|
||
draft: Record<string, unknown>,
|
||
book: PublicBaseBook,
|
||
): PublicBaseBook {
|
||
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: PublicBaseCalculation = {
|
||
version: publicBaseVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
location: JSON.stringify(draft.selection_labels ?? {}),
|
||
sourceKey: key(draft, book),
|
||
source: publicBaseSource,
|
||
inputs: structuredClone(book.rows),
|
||
rows: book.rows.map((row) => evaluatePublicBase(row, draft.currency)),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
savePublicBaseBook(draft, next);
|
||
return next;
|
||
}
|
||
export function publicBaseReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const book = publicBaseBook(draft),
|
||
result = book.result;
|
||
if (!result) return [];
|
||
return result.inputs.map((row, index) => [
|
||
'IVA mensal',
|
||
`Base P090 — ${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}; ${publicBaseCurrent(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'}; ${publicBaseTreatments.find((item) => item.id === row.treatment)?.label ?? row.treatment}; natureza ${natureLabel(row.operationNature)}; autoridade ${row.authority}; acto ${row.actRef}; documento ${row.documentRef},${row.documentDate}; forma ${formLabel(row.compensationForm)}; ${row.compensationForm !== 'non_money' ? `dinheiro ${row.moneyAmount} ${row.currency}; ` : ''}${row.compensationForm !== 'money' ? `valor normal não monetário ${row.nonMoneyValue} ${row.currency},fundamento ${row.valuationRef}; ` : ''}composição ${row.compositionRef}; condições ${publicBaseConditions.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 actos públicos',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados;sem substituir por zero.',
|
||
],
|
||
];
|
||
}
|
||
}
|