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

350 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 actSaleBaseVersion = 'act-goods-base/1';
export const actSaleBaseSource =
'CIVA republicado Lei13/2016,15(2)(h),PDF13/p1426(271): nas transmissões de bens resultantes de actos de arrematação ou venda judicial ou administrativa,de conciliação ou contratos de transacção,a base é o valor por que as arrematações ou vendas foram efectuadas ou,se for caso,o valor normal dos bens. Art15(4)/(5)/(6),PDF14/p1426(272),valor normal,composição e exclusões,sem próprioIVA. Lei22/2022PDF3mantém15(2)(h)/(4–6)por remissão;Lei10/2025não altera15. Recorte2026/MZN;determinação da base,sem inferir incidência,taxa,IVA,dedução/campo11 ou totaisModeloA;sem substituir regime especial15(2)(g)de leilão em comissão.';
export const actSaleKinds = [
{ id: 'auction', label: 'Arrematação' },
{ id: 'judicial_sale', label: 'Venda judicial' },
{ id: 'administrative_sale', label: 'Venda administrativa' },
{ id: 'conciliation', label: 'Conciliação' },
{ id: 'settlement', label: 'Contrato de transacção' },
] as const;
export const actSaleTreatments = [
{ id: 'act_sale_amount', label: 'Valor da arrematação ou venda efectuada' },
{ id: 'act_normal_value', label: 'Valor normal dos bens,quando aplicável' },
] as const;
export const actSaleConditions = [
[
'taxableDomesticGoods',
'Está identificada uma transmissão de bens sujeita a IVA no território nacional; não é qualquer pagamento,indemnização ou prestação de serviços',
],
[
'goodsByAct',
'Estes bens são transmitidos em resultado do acto de arrematação,venda judicial/administrativa,conciliação ou contrato de transacção identificado',
],
[
'valuationApplies',
'O valor escolhido corresponde ao fundamento aplicável nesta transmissão:valor por que a arrematação/venda foi efectuada ou,se for caso,valor normal; não é uma opção automática pelo menor valor',
],
[
'netComposition',
'O montante indicado exclui o próprio IVA e reflecte a composição aplicável do artigo15(5)/(6),identificada no fundamento,sem omissões ou duplicações',
],
[
'notAuctionCommissionRegime',
'Esta transmissão corresponde ao artigo15(2)(h),não a uma venda abrangida pela regra especial do organizador de leilão em comissão do artigo15(2)(g)',
],
] as const;
type Choice = '' | 'yes' | 'no';
type Condition = (typeof actSaleConditions)[number][0];
export type ActSaleRow = {
id: string;
treatment: string;
actKind: string;
description: string;
period: string;
origin: 'projected' | 'effective';
currency: string;
actRef: string;
documentRef: string;
documentDate: string;
actValue: string;
normalValue: string;
valuationRef: string;
valuationScope: 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 ActSaleCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: ActSaleRow[];
rows: LineResult[];
};
export type ActSaleBook = {
rows: ActSaleRow[];
result?: ActSaleCalculation;
history?: ActSaleCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
actSaleBases?: Record<string, ActSaleBook>;
};
export function newActSaleBase(id: string, currency: string): ActSaleRow {
return {
id,
treatment: '',
actKind: '',
description: '',
period: '',
origin: 'projected',
currency,
actRef: '',
documentRef: '',
documentDate: '',
actValue: '',
normalValue: '',
valuationRef: '',
valuationScope: '',
compositionRef: '',
evidence: '',
conditions: {},
};
}
export function switchActValuation(
row: ActSaleRow,
treatment: string,
): ActSaleRow {
return {
...row,
treatment,
conditions: { ...row.conditions, valuationApplies: '' },
};
}
export function switchActKind(row: ActSaleRow, actKind: string): ActSaleRow {
return {
...row,
actKind,
conditions: { ...row.conditions, goodsByAct: '', valuationApplies: '' },
};
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.actSaleBases !== undefined &&
(!store.actSaleBases ||
typeof store.actSaleBases !== 'object' ||
Array.isArray(store.actSaleBases))
)
throw new Error(
'Bases de transmissão por acto não recuperáveis;originais preservados.',
);
const fields = Object.keys(newActSaleBase('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.actSaleBases ?? {}))
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 ActSaleRow] !== 'string',
) ||
!row.conditions ||
typeof row.conditions !== 'object' ||
Array.isArray(row.conditions),
)
)
throw new Error(
'Linhas de transmissão por acto não recuperáveis;originais preservados,sem substituição por lista vazia.',
);
return store;
}
export function actSaleBook(draft: Record<string, unknown>): ActSaleBook {
return (
readStore(draft).actSaleBases?.[acquisitionScope(draft)] ?? { rows: [] }
);
}
export function saveActSaleBook(
draft: Record<string, unknown>,
book: ActSaleBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
actSaleBases: {
...current.actSaleBases,
[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 evaluateActSale(
row: ActSaleRow,
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 });
const rule = actSaleTreatments.find((item) => item.id === row.treatment),
kind = actSaleKinds.find((item) => item.id === row.actKind);
if (!rule || !kind)
return stop(
'Identifique o acto e o fundamento do valor aplicável; acto ou valorização desconhecidos não determinam base.',
);
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 o período em2026 e dados efectivos declarados ou pressupostos de projecção; não se infere o período do orçamento.',
);
const failed = actSaleConditions.filter(
([key]) => row.conditions[key] === 'no',
);
if (failed.length)
return stop(
'Não se aplica esta base de transmissão por acto: ' +
failed.map(([, label]) => label).join('; ') +
'. Não é isenção nem IVA zero.',
'not_applicable',
);
const missing = actSaleConditions.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.actRef.trim() ||
!row.documentRef.trim() ||
!row.valuationRef.trim() ||
!row.valuationScope.trim() ||
!row.compositionRef.trim() ||
!row.evidence.trim() ||
!validDate(row.documentDate)
)
return stop(
'Identifique bens,acto,documento/data,fundamento e âmbito do valor escolhido e composição,ou pressupostos expressos;reutilize referências existentes,sem novo certificado ou avaliador imposto pelo portal.',
);
const value = decimal(
row.treatment === 'act_sale_amount' ? row.actValue : row.normalValue,
);
if (value === null)
return stop(
'Indique o montante sem IVA correspondente ao fundamento escolhido; valor desconhecido ou inválido não é zero. Não se escolhe automaticamente o menor valor.',
);
const base = money(value);
return {
...result,
status: 'determined',
baseAmount: base,
reason: `Base15(2)(h),${kind.label}: ${rule.label} = ${base}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Valor sem IVA,com fundamento e âmbito identificados;valor normal não é avaliado automaticamente pelo portal. 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: ActSaleBook): string {
return JSON.stringify({
version: actSaleBaseVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function actSaleCurrent(
draft: Record<string, unknown>,
book: ActSaleBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordActSaleBases(
draft: Record<string, unknown>,
book: ActSaleBook,
): ActSaleBook {
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: ActSaleCalculation = {
version: actSaleBaseVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: actSaleBaseSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) => evaluateActSale(row, draft.currency)),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveActSaleBook(draft, next);
return next;
}
export function actSaleReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = actSaleBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base P094 — ${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};${actSaleCurrent(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'};${actSaleKinds.find((item) => item.id === row.actKind)?.label ?? 'Acto por indicar'};acto ${row.actRef};${actSaleTreatments.find((item) => item.id === row.treatment)?.label ?? 'Valorização por indicar'};montante ${row.treatment === 'act_sale_amount' ? row.actValue : row.normalValue} ${row.currency};fundamento da valorização ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${actSaleConditions.map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento factual ${row.evidence};não preenche totaisModeloA ou campo11.`,
]);
} catch {
return [
[
'IVA mensal',
'Base de transmissão por acto',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}