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

372 lines
15 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 commissionBaseVersion = 'commission-goods-base/1';
export const commissionBaseSource =
'CIVA republicado Lei13/2016,15(2)(e),PDF13/p1426(271): nas transmissões de bens entre comitente e comissário,preço de venda acordado pelo comissário diminuído da comissão;entre comissário e comitente,preço de compra acordado pelo comissário aumentado da comissão. Art3(3)(c),PDF6/p1426(264): transferências em execução de contrato de comissão,incluindo consignação;na comissão de venda considera-se comprador o comissário,na de compra o comitente. Art15(5)/(6),PDF14,p1426(272),composição/exclusões,sem próprioIVA;regra especial de leilão15(2)(g)não substituída. Lei22/2022PDF3mantém15(2)(e);Lei10/2025PDF12mantém3(3)(c)por remissão e não altera15. Recorte2026/MZN,base de bens;sem incidência,taxa,IVA,serviço autónomo,dedução/campo11 ou totaisModeloA presumidos.';
export const commissionBaseTreatments = [
{
id: 'commission_sale_transfer',
label: 'Comitente → comissário — comissão de venda',
direction: 'Comitente para comissário',
priceLabel:
'Preço de venda acordado pelo comissário,antes da dedução da comissão,sem IVA',
operator: '−',
},
{
id: 'commission_purchase_transfer',
label: 'Comissário → comitente — comissão de compra',
direction: 'Comissário para comitente',
priceLabel:
'Preço de compra acordado pelo comissário,antes do acréscimo da comissão,sem IVA',
operator: '+',
},
] as const;
export const commissionBaseConditions = [
[
'taxableDomesticGoods',
'Está identificada uma transmissão de bens sujeita a IVA no território nacional; não é um serviço autónomo de intermediação nem qualquer pagamento de comissão',
],
[
'commissionContract',
'A transferência destes bens entre comitente e comissário executa o contrato de comissão identificado,conforme3(3)(c),incluindo consignação quando aplicável',
],
[
'directionConfirmed',
'A operação e os papéis correspondem ao sentido escolhido:comissão de venda(comitente para comissário)ou de compra(comissário para comitente)',
],
[
'netComposition',
'O preço acordado pelo comissário e a comissão desta operação estão identificados sem o próprio IVA,com composição15(5)/(6)aplicável; o preço ainda não sofreu o ajuste da comissão efectuado por esta fórmula',
],
[
'notSpecialAuction',
'Esta operação não está abrangida pela regra especial das vendas em leilão do artigo15(2)(g); essa base não é substituída pela comissão genérica',
],
] as const;
type Condition = (typeof commissionBaseConditions)[number][0];
type Choice = '' | 'yes' | 'no';
export type CommissionBaseRow = {
id: string;
treatment: string;
description: string;
period: string;
origin: 'projected' | 'effective';
currency: string;
principal: string;
agent: string;
agreementRef: string;
agreedPrice: string;
priceRef: string;
commissionAmount: string;
commissionRef: string;
compositionRef: string;
documentRef: string;
documentDate: 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 CommissionBaseCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: CommissionBaseRow[];
rows: LineResult[];
};
export type CommissionBaseBook = {
rows: CommissionBaseRow[];
result?: CommissionBaseCalculation;
history?: CommissionBaseCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
commissionBases?: Record<string, CommissionBaseBook>;
};
export function newCommissionBase(
id: string,
currency: string,
): CommissionBaseRow {
return {
id,
treatment: '',
description: '',
period: '',
origin: 'projected',
currency,
principal: '',
agent: '',
agreementRef: '',
agreedPrice: '',
priceRef: '',
commissionAmount: '',
commissionRef: '',
compositionRef: '',
documentRef: '',
documentDate: '',
evidence: '',
conditions: {},
};
}
export function switchCommissionDirection(
row: CommissionBaseRow,
treatment: string,
): CommissionBaseRow {
return {
...row,
treatment,
conditions: { ...row.conditions, directionConfirmed: '' },
};
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.commissionBases !== undefined &&
(!store.commissionBases ||
typeof store.commissionBases !== 'object' ||
Array.isArray(store.commissionBases))
)
throw new Error(
'Bases de comissão não recuperáveis;originais preservados.',
);
const fields = Object.keys(newCommissionBase('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.commissionBases ?? {}))
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 CommissionBaseRow] !==
'string',
) ||
!row.conditions ||
typeof row.conditions !== 'object' ||
Array.isArray(row.conditions),
)
)
throw new Error(
'Linhas de comissão não recuperáveis;originais preservados,sem substituição por lista vazia.',
);
return store;
}
export function commissionBaseBook(
draft: Record<string, unknown>,
): CommissionBaseBook {
return (
readStore(draft).commissionBases?.[acquisitionScope(draft)] ?? {
rows: [],
}
);
}
export function saveCommissionBaseBook(
draft: Record<string, unknown>,
book: CommissionBaseBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
commissionBases: {
...current.commissionBases,
[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 evaluateCommissionBase(
row: CommissionBaseRow,
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 = commissionBaseTreatments.find(
(item) => item.id === row.treatment,
);
if (!rule)
return stop(
'Escolha a transferência de bens em comissão de venda ou de compra; comissão desconhecida não determina uma 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 = commissionBaseConditions.filter(
([key]) => row.conditions[key] === 'no',
);
if (failed.length)
return stop(
'Não se aplica esta base de comissão: ' +
failed.map(([, label]) => label).join('; ') +
'. Não é isenção nem IVA zero.',
'not_applicable',
);
const missing = commissionBaseConditions.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.principal.trim() ||
!row.agent.trim() ||
!row.agreementRef.trim() ||
!row.priceRef.trim() ||
!row.commissionRef.trim() ||
!row.compositionRef.trim() ||
!row.documentRef.trim() ||
!row.evidence.trim() ||
!validDate(row.documentDate)
)
return stop(
'Identifique bens,comitente,comissário,contrato,documento/data e referências do preço,comissão e composição,ou pressupostos expressos;reutilize referências existentes,sem novo certificado.',
);
const price = decimal(row.agreedPrice),
commission = decimal(row.commissionAmount);
if (price === null || commission === null)
return stop(
'Indique preço acordado e comissão sem IVA;montante desconhecido ou inválido não é zero. Não se presume percentagem,base de percentagem ou valor da comissão.',
);
if (row.treatment === 'commission_sale_transfer' && commission > price)
return stop(
`Preço menos comissão produz diferença aritmética negativa de −${money(commission - price)}MZN. Valores conservados;este recorte não a apresenta como base fiscal determinada nem a substitui por zero ou valor absoluto.`,
);
const base = money(
row.treatment === 'commission_sale_transfer'
? price - commission
: price + commission,
);
return {
...result,
status: 'determined',
baseAmount: base,
reason: `Base15(2)(e),${rule.direction}: preço ${money(price)} ${rule.operator} comissão ${money(commission)} = ${base}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Valores sem IVA,ajuste efectuado uma única vez. 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: CommissionBaseBook): string {
return JSON.stringify({
version: commissionBaseVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function commissionBaseCurrent(
draft: Record<string, unknown>,
book: CommissionBaseBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordCommissionBases(
draft: Record<string, unknown>,
book: CommissionBaseBook,
): CommissionBaseBook {
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: CommissionBaseCalculation = {
version: commissionBaseVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: commissionBaseSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) =>
evaluateCommissionBase(row, draft.currency),
),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveCommissionBaseBook(draft, next);
return next;
}
export function commissionBaseReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = commissionBaseBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base P091 — ${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};${commissionBaseCurrent(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'};${commissionBaseTreatments.find((item) => item.id === row.treatment)?.label ?? row.treatment};comitente ${row.principal};comissário ${row.agent};contrato ${row.agreementRef};preço ${row.agreedPrice} ${row.currency},referência ${row.priceRef};comissão ${row.commissionAmount} ${row.currency},referência ${row.commissionRef};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${commissionBaseConditions.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 comissão',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}