351 lines
14 KiB
TypeScript
351 lines
14 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
|
||
export const noncashBaseVersion = 'noncash-consideration-base/1';
|
||
export const noncashBaseSource =
|
||
'CIVA republicado Lei13/2016,15(3)/(4),PDF14/p1426(272): contraprestação não definida,no todo ou em parte,em dinheiro;valor tributável=montante recebido ou a receber acrescido do valor normal dos bens ou serviços dados em troca. Valor normal:preço em concorrência normal,a fornecedor independente,no mesmo estádio de comercialização,tempo e lugar da operação ou mais próximos,aumentado dos elementos15(5)na medida não incluídos. Composição/exclusões15(5)/(6),sem próprioIVA. Lei22/2022PDF3mantém15(3–6)por remissão;Lei10/2025não altera15. Recorte2026/MZN,sem conversão cambial;base apenas,sem avaliação automática,incidência,taxaIVA,IVA,dedução/campo11 ou totaisModeloA.';
|
||
export const noncashBaseTreatments = [
|
||
{ id: 'noncash_only', label: 'Contraprestação inteiramente não monetária' },
|
||
{
|
||
id: 'cash_and_noncash',
|
||
label: 'Contraprestação em parte monetária e em parte não monetária',
|
||
},
|
||
] as const;
|
||
export const noncashBaseConditions = [
|
||
[
|
||
'taxableOperation',
|
||
'Está identificada a operação sujeita a IVA cuja contraprestação não é definida no todo ou em parte em dinheiro',
|
||
],
|
||
[
|
||
'exchangeIdentified',
|
||
'Estão identificados os bens ou serviços dados em troca como contraprestação desta operação; não se somam o valor da própria operação e a respectiva contraprestação',
|
||
],
|
||
[
|
||
'cashComposition',
|
||
'A composição escolhida corresponde à operação:inteiramente não monetária,sem parte em dinheiro,ou com montante monetário recebido/a receber identificado',
|
||
],
|
||
[
|
||
'normalValueBasis',
|
||
'O valor normal corresponde ao artigo15(4):concorrência normal,fornecedor independente,mesmo estádio de comercialização,tempo/lugar da operação ou mais próximos,com fundamento identificado',
|
||
],
|
||
[
|
||
'netComposition',
|
||
'Os montantes excluem o próprio IVA e incluem a composição aplicável15(5)/(6),sem omissões ou dupla contagem dos mesmos elementos',
|
||
],
|
||
] as const;
|
||
type Choice = '' | 'yes' | 'no';
|
||
type Condition = (typeof noncashBaseConditions)[number][0];
|
||
export type NoncashBaseRow = {
|
||
id: string;
|
||
treatment: string;
|
||
description: string;
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
currency: string;
|
||
exchangeDescription: string;
|
||
agreementRef: string;
|
||
documentRef: string;
|
||
documentDate: string;
|
||
cashAmount: string;
|
||
cashRef: 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 NoncashBaseCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
location: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: NoncashBaseRow[];
|
||
rows: LineResult[];
|
||
};
|
||
export type NoncashBaseBook = {
|
||
rows: NoncashBaseRow[];
|
||
result?: NoncashBaseCalculation;
|
||
history?: NoncashBaseCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
noncashBases?: Record<string, NoncashBaseBook>;
|
||
};
|
||
export function newNoncashBase(id: string, currency: string): NoncashBaseRow {
|
||
return {
|
||
id,
|
||
treatment: '',
|
||
description: '',
|
||
period: '',
|
||
origin: 'projected',
|
||
currency,
|
||
exchangeDescription: '',
|
||
agreementRef: '',
|
||
documentRef: '',
|
||
documentDate: '',
|
||
cashAmount: '',
|
||
cashRef: '',
|
||
normalValue: '',
|
||
valuationRef: '',
|
||
valuationScope: '',
|
||
compositionRef: '',
|
||
evidence: '',
|
||
conditions: {},
|
||
};
|
||
}
|
||
export function switchNoncashComposition(
|
||
row: NoncashBaseRow,
|
||
treatment: string,
|
||
): NoncashBaseRow {
|
||
return {
|
||
...row,
|
||
treatment,
|
||
conditions: { ...row.conditions, cashComposition: '' },
|
||
};
|
||
}
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
store.noncashBases !== undefined &&
|
||
(!store.noncashBases ||
|
||
typeof store.noncashBases !== 'object' ||
|
||
Array.isArray(store.noncashBases))
|
||
)
|
||
throw new Error(
|
||
'Bases de contraprestação não recuperáveis;originais preservados.',
|
||
);
|
||
const fields = Object.keys(newNoncashBase('', '')).filter(
|
||
(key) => key !== 'conditions',
|
||
);
|
||
for (const book of Object.values(store.noncashBases ?? {}))
|
||
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 NoncashBaseRow] !==
|
||
'string',
|
||
) ||
|
||
!row.conditions ||
|
||
typeof row.conditions !== 'object' ||
|
||
Array.isArray(row.conditions),
|
||
)
|
||
)
|
||
throw new Error(
|
||
'Linhas de contraprestação não recuperáveis;originais preservados,sem substituição por lista vazia.',
|
||
);
|
||
return store;
|
||
}
|
||
export function noncashBaseBook(
|
||
draft: Record<string, unknown>,
|
||
): NoncashBaseBook {
|
||
return (
|
||
readStore(draft).noncashBases?.[acquisitionScope(draft)] ?? { rows: [] }
|
||
);
|
||
}
|
||
export function saveNoncashBaseBook(
|
||
draft: Record<string, unknown>,
|
||
book: NoncashBaseBook,
|
||
): string {
|
||
const current = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...current,
|
||
noncashBases: {
|
||
...current.noncashBases,
|
||
[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 evaluateNoncashBase(
|
||
row: NoncashBaseRow,
|
||
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 (!noncashBaseTreatments.some((item) => item.id === row.treatment))
|
||
return stop(
|
||
'Identifique a composição não monetária total ou parcial;parte monetária desconhecida não equivale a ausência de dinheiro.',
|
||
);
|
||
if (currency !== 'MZN' || row.currency !== currency)
|
||
return stop(
|
||
'Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa ou taxa de câmbio presumida.',
|
||
);
|
||
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 = noncashBaseConditions.filter(
|
||
([key]) => row.conditions[key] === 'no',
|
||
);
|
||
if (failed.length)
|
||
return stop(
|
||
'Não se aplica esta determinação da base: ' +
|
||
failed.map(([, label]) => label).join('; ') +
|
||
'. Não é isenção nem IVA zero.',
|
||
'not_applicable',
|
||
);
|
||
const missing = noncashBaseConditions.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.exchangeDescription.trim() ||
|
||
!row.agreementRef.trim() ||
|
||
!row.documentRef.trim() ||
|
||
!row.valuationRef.trim() ||
|
||
!row.valuationScope.trim() ||
|
||
!row.compositionRef.trim() ||
|
||
!row.evidence.trim() ||
|
||
!validDate(row.documentDate)
|
||
)
|
||
return stop(
|
||
'Identifique operação,contraprestação em bens/serviços,acordo,documento/data,fundamento e âmbito do valor normal e composição,ou pressupostos expressos;reutilize referências existentes,sem novo certificado/avaliador/aprovador.',
|
||
);
|
||
const normal = decimal(row.normalValue);
|
||
if (normal === null)
|
||
return stop(
|
||
'Indique o valor normal dos bens/serviços dados em troca;desconhecido não é zero nem valor avaliado automaticamente pelo portal.',
|
||
);
|
||
let base = normal,
|
||
formula = `valor normal ${money(normal)},sem parte monetária expressamente confirmada`;
|
||
if (row.treatment === 'cash_and_noncash') {
|
||
const cash = decimal(row.cashAmount);
|
||
if (cash === null || !row.cashRef.trim())
|
||
return stop(
|
||
'Identifique montante monetário recebido/a receber e referência ou pressuposto expresso;um montante desconhecido não é zero.',
|
||
);
|
||
base = cash + normal;
|
||
formula = `montante monetário ${money(cash)} + valor normal ${money(normal)}`;
|
||
}
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
baseAmount: money(base),
|
||
reason: `Base15(3)/(4): ${formula} = ${money(base)}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Valor normal/fundamento/âmbito e composição identificados,sem avaliação automática. 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: NoncashBaseBook): string {
|
||
return JSON.stringify({
|
||
version: noncashBaseVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
currency: draft.currency,
|
||
rows: book.rows,
|
||
});
|
||
}
|
||
export function noncashBaseCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: NoncashBaseBook,
|
||
): boolean {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
}
|
||
export function recordNoncashBases(
|
||
draft: Record<string, unknown>,
|
||
book: NoncashBaseBook,
|
||
): NoncashBaseBook {
|
||
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: NoncashBaseCalculation = {
|
||
version: noncashBaseVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
location: JSON.stringify(draft.selection_labels ?? {}),
|
||
sourceKey: key(draft, book),
|
||
source: noncashBaseSource,
|
||
inputs: structuredClone(book.rows),
|
||
rows: book.rows.map((row) => evaluateNoncashBase(row, draft.currency)),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
saveNoncashBaseBook(draft, next);
|
||
return next;
|
||
}
|
||
export function noncashBaseReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const book = noncashBaseBook(draft),
|
||
result = book.result;
|
||
if (!result) return [];
|
||
return result.inputs.map((row, index) => [
|
||
'IVA mensal',
|
||
`Base P100 — ${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};${noncashBaseCurrent(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'};${noncashBaseTreatments.find((item) => item.id === row.treatment)?.label ?? 'Composição por indicar'};bens/serviços dados em troca ${row.exchangeDescription};acordo ${row.agreementRef};${row.treatment === 'cash_and_noncash' ? `montante monetário ${row.cashAmount} ${row.currency},referência ${row.cashRef}` : 'Sem parte monetária,expressamente confirmado'};valor normal ${row.normalValue} ${row.currency};fundamento ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${noncashBaseConditions.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 contraprestação não monetária',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados;sem substituir por zero.',
|
||
],
|
||
];
|
||
}
|
||
}
|