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

351 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 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.',
],
];
}
}