372 lines
15 KiB
TypeScript
372 lines
15 KiB
TypeScript
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.',
|
||
],
|
||
];
|
||
}
|
||
}
|