422 lines
16 KiB
TypeScript
422 lines
16 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
|
export const propertyTransferVersion = 'property-transfers/1';
|
|
export const propertyTransferSource =
|
|
'CIVA Lei13/2016:3(3)a,PDF6/p1426(264),entrega material em locação com cláusula vinculante de transferência da propriedade;3(5),PDF7/p1426(265),cessão onerosa/gratuita de estabelecimento,totalidade ou parte de património que constitua ramo independente,adquirente2(1)a que pratique operações com direito à dedução;15(1),PDF13/p1426(271),contraprestação obtida/a obter.Lei22/2022PDF3 mantém15(1);Lei10/2025PDF12 mantém3(3)a/3(5),não altera15.P087não usa15(2)a,referente a consignação3(3)d.';
|
|
export const propertyTransferTreatments = [
|
|
{
|
|
id: 'binding_lease',
|
|
branch: 'P087',
|
|
label: 'Locação com transferência vinculativa da propriedade',
|
|
},
|
|
{
|
|
id: 'business_onerous',
|
|
branch: 'P127',
|
|
label: 'Cessão onerosa de estabelecimento/património autónomo',
|
|
},
|
|
{
|
|
id: 'business_free',
|
|
branch: 'P127',
|
|
label: 'Cessão gratuita de estabelecimento/património autónomo',
|
|
},
|
|
] as const;
|
|
export const propertyTransferConditions = {
|
|
binding_lease: [
|
|
[
|
|
'materialDelivery',
|
|
'Entrega material dos bens em execução do contrato de locação',
|
|
],
|
|
[
|
|
'bindingProperty',
|
|
'Contrato contém cláusula vinculante para ambas as partes de transferência da propriedade',
|
|
],
|
|
],
|
|
business_onerous: [
|
|
['onerous', 'Cessão a título oneroso identificada'],
|
|
[
|
|
'autonomousBusiness',
|
|
'Estabelecimento,totalidade ou parte de património susceptível de constituir ramo de actividade independente',
|
|
],
|
|
[
|
|
'acquirer2a',
|
|
'Adquirente é ou passa,pela aquisição,a sujeito passivo2(1)a',
|
|
],
|
|
[
|
|
'deductionActivities',
|
|
'Adquirente pratica operações que concedem direito à dedução',
|
|
],
|
|
],
|
|
business_free: [
|
|
['gratuitous', 'Cessão a título gratuito identificada'],
|
|
[
|
|
'autonomousBusiness',
|
|
'Estabelecimento,totalidade ou parte de património susceptível de constituir ramo de actividade independente',
|
|
],
|
|
[
|
|
'acquirer2a',
|
|
'Adquirente é ou passa,pela aquisição,a sujeito passivo2(1)a',
|
|
],
|
|
[
|
|
'deductionActivities',
|
|
'Adquirente pratica operações que concedem direito à dedução',
|
|
],
|
|
],
|
|
} as const;
|
|
export function transferConditions(
|
|
t: string,
|
|
): ReadonlyArray<readonly [string, string]> {
|
|
return (
|
|
propertyTransferConditions[
|
|
t as keyof typeof propertyTransferConditions
|
|
] ?? []
|
|
);
|
|
}
|
|
type Choice = '' | 'yes' | 'no';
|
|
export type PropertyTransferRow = {
|
|
id: string;
|
|
treatment: string;
|
|
description: string;
|
|
period: string;
|
|
origin: 'projected' | 'effective';
|
|
operationDate: string;
|
|
operationRef: string;
|
|
documentRef: string;
|
|
documentDate: string;
|
|
goodsRef: string;
|
|
scope: string;
|
|
evidence: string;
|
|
contractRef: string;
|
|
propertyClauseRef: string;
|
|
acquirerRef: string;
|
|
autonomousRef: string;
|
|
activitiesRef: string;
|
|
consideration: string;
|
|
considerationRef: string;
|
|
currency: string;
|
|
compositionConfirmed: Choice;
|
|
conditions: Partial<Record<string, Choice>>;
|
|
};
|
|
type LineResult = {
|
|
id: string;
|
|
status: 'determined' | 'not_applicable' | 'indeterminate';
|
|
classification:
|
|
| 'goods_transmission'
|
|
| 'tax_not_exigible_3_5'
|
|
| 'not_applicable'
|
|
| 'indeterminate';
|
|
baseAmount: string | null;
|
|
vat: null;
|
|
rate: null;
|
|
reason: string;
|
|
};
|
|
export type PropertyTransferCalculation = {
|
|
version: string;
|
|
revision: number;
|
|
calculatedAt: string;
|
|
opportunity: string;
|
|
location: string;
|
|
sourceKey: string;
|
|
source: string;
|
|
inputs: PropertyTransferRow[];
|
|
rows: LineResult[];
|
|
};
|
|
export type PropertyTransferBook = {
|
|
rows: PropertyTransferRow[];
|
|
result?: PropertyTransferCalculation;
|
|
history?: PropertyTransferCalculation[];
|
|
};
|
|
type Store = ReturnType<typeof readAcquisitionStore> & {
|
|
propertyTransfers?: Record<string, PropertyTransferBook>;
|
|
};
|
|
export function newPropertyTransfer(
|
|
id: string,
|
|
currency: string,
|
|
): PropertyTransferRow {
|
|
return {
|
|
id,
|
|
treatment: '',
|
|
description: '',
|
|
period: '',
|
|
origin: 'projected',
|
|
operationDate: '',
|
|
operationRef: '',
|
|
documentRef: '',
|
|
documentDate: '',
|
|
goodsRef: '',
|
|
scope: '',
|
|
evidence: '',
|
|
contractRef: '',
|
|
propertyClauseRef: '',
|
|
acquirerRef: '',
|
|
autonomousRef: '',
|
|
activitiesRef: '',
|
|
consideration: '',
|
|
considerationRef: '',
|
|
currency,
|
|
compositionConfirmed: '',
|
|
conditions: {},
|
|
};
|
|
}
|
|
export function switchPropertyTreatment(
|
|
row: PropertyTransferRow,
|
|
treatment: string,
|
|
): PropertyTransferRow {
|
|
return { ...row, treatment, conditions: {}, compositionConfirmed: '' };
|
|
}
|
|
function readStore(draft: Record<string, unknown>): Store {
|
|
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
|
if (
|
|
store.propertyTransfers !== undefined &&
|
|
(!store.propertyTransfers ||
|
|
typeof store.propertyTransfers !== 'object' ||
|
|
Array.isArray(store.propertyTransfers))
|
|
)
|
|
throw new Error(
|
|
'Transmissões de propriedade não recuperáveis;originais preservados.',
|
|
);
|
|
const fields = Object.keys(newPropertyTransfer('', '')).filter(
|
|
(k) => k !== 'conditions',
|
|
);
|
|
for (const book of Object.values(store.propertyTransfers ?? {}))
|
|
if (
|
|
!book ||
|
|
!Array.isArray(book.rows) ||
|
|
(book.history !== undefined && !Array.isArray(book.history)) ||
|
|
book.rows.some(
|
|
(row) =>
|
|
!row ||
|
|
fields.some(
|
|
(k) =>
|
|
typeof row[k as keyof PropertyTransferRow] !==
|
|
'string',
|
|
) ||
|
|
!row.conditions ||
|
|
typeof row.conditions !== 'object' ||
|
|
Array.isArray(row.conditions),
|
|
)
|
|
)
|
|
throw new Error(
|
|
'Linhas não recuperáveis;originais conservados,sem lista vazia por defeito.',
|
|
);
|
|
return store;
|
|
}
|
|
export function propertyTransferBook(
|
|
draft: Record<string, unknown>,
|
|
): PropertyTransferBook {
|
|
return (
|
|
readStore(draft).propertyTransfers?.[acquisitionScope(draft)] ?? {
|
|
rows: [],
|
|
}
|
|
);
|
|
}
|
|
export function savePropertyTransferBook(
|
|
draft: Record<string, unknown>,
|
|
book: PropertyTransferBook,
|
|
): string {
|
|
const store = readStore(draft),
|
|
encoded = JSON.stringify({
|
|
...store,
|
|
propertyTransfers: {
|
|
...store.propertyTransfers,
|
|
[acquisitionScope(draft)]: book,
|
|
},
|
|
});
|
|
readStore({ ...draft, modelo_a_snapshot: encoded });
|
|
return encoded;
|
|
}
|
|
function validDate(s: string): boolean {
|
|
const n = Date.parse(s + 'T00:00:00Z');
|
|
return (
|
|
/^\d{4}-\d{2}-\d{2}$/.test(s) &&
|
|
Number.isFinite(n) &&
|
|
new Date(n).toISOString().slice(0, 10) === s
|
|
);
|
|
}
|
|
function decimal(raw: string): bigint | null {
|
|
if (!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim())) return null;
|
|
const [w, f = ''] = raw.trim().split('.');
|
|
return BigInt(w) * 1000000n + BigInt(f.padEnd(6, '0'));
|
|
}
|
|
function money(n: bigint): string {
|
|
return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')}`;
|
|
}
|
|
export function evaluatePropertyTransfer(
|
|
row: PropertyTransferRow,
|
|
currency: unknown,
|
|
): LineResult {
|
|
const result: LineResult = {
|
|
id: row.id,
|
|
status: 'indeterminate',
|
|
classification: 'indeterminate',
|
|
baseAmount: null,
|
|
vat: null,
|
|
rate: null,
|
|
reason: '',
|
|
};
|
|
const stop = (
|
|
reason: string,
|
|
status: LineResult['status'] = 'indeterminate',
|
|
classification: LineResult['classification'] = status ===
|
|
'not_applicable'
|
|
? 'not_applicable'
|
|
: result.classification,
|
|
) => ({ ...result, status, classification, reason });
|
|
if (!propertyTransferTreatments.some((t) => t.id === row.treatment))
|
|
return stop(
|
|
'Identifique locação vinculativa ou cessão onerosa/gratuita de estabelecimento.',
|
|
);
|
|
if (
|
|
!validDate(row.operationDate) ||
|
|
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
|
row.period !== row.operationDate.slice(0, 7) ||
|
|
!['projected', 'effective'].includes(row.origin)
|
|
)
|
|
return stop(
|
|
'Identifique data/mês2026 e dados efectivos ou pressupostos expressos.',
|
|
);
|
|
if (
|
|
![
|
|
row.description,
|
|
row.operationRef,
|
|
row.documentRef,
|
|
row.goodsRef,
|
|
row.scope,
|
|
row.evidence,
|
|
row.contractRef,
|
|
].every((v) => !!v.trim()) ||
|
|
!validDate(row.documentDate)
|
|
)
|
|
return stop(
|
|
'Identifique objecto,operação,contrato/documento/data e âmbito factual ou pressupostos;reutilize referências,sem certificado/aprovador novo.',
|
|
);
|
|
const conditions = transferConditions(row.treatment);
|
|
if (conditions.some(([k]) => row.conditions[k] === 'no'))
|
|
return stop(
|
|
row.treatment === 'binding_lease'
|
|
? 'Não estão reunidos os factos de3(3)a;não converter automaticamente opção de compra em transferência vinculativa.'
|
|
: 'Não estão reunidos os requisitos de não exigibilidade3(5);classificar a transmissão pelos restantes percursos aplicáveis,sem isenção/IVA zero.',
|
|
'not_applicable',
|
|
);
|
|
if (conditions.some(([k]) => row.conditions[k] !== 'yes'))
|
|
return stop(
|
|
'Factos próprios por esclarecer;desconhecido não é não aplicável.',
|
|
);
|
|
if (row.treatment === 'binding_lease') {
|
|
if (!row.propertyClauseRef.trim())
|
|
return stop(
|
|
'Identifique cláusula vinculante para ambas as partes;mera opção de compra não a demonstra.',
|
|
);
|
|
result.classification = 'goods_transmission';
|
|
if (currency !== 'MZN' || row.currency !== currency)
|
|
return stop(
|
|
'Transmissão3(3)a identificada;base em moeda fora do recorteMZN ou divergente,sem conversão silenciosa.',
|
|
);
|
|
const value = decimal(row.consideration);
|
|
if (
|
|
value === null ||
|
|
!row.considerationRef.trim() ||
|
|
row.compositionConfirmed !== 'yes'
|
|
)
|
|
return stop(
|
|
'Transmissão3(3)a identificada;base15(1) por esclarecer. Identifique contraprestação obtida/a obter e composição correspondente,sem próprioIVA e sem voltar a subtrair componentes já excluídas;não somar rendas/valor residual automaticamente.',
|
|
);
|
|
return {
|
|
...result,
|
|
status: 'determined',
|
|
baseAmount: money(value),
|
|
reason: `P087:entrega material em locação com cláusula vinculativa3(3)a;base15(1)=${money(value)}MZN,contraprestação obtida/a obter segundo composição e âmbito identificados.Não é consignação3(3)d/15(2)a,nem mera opção de compra.Não calcula IVA,exigibilidade,nova dedução/campo11 ou totaisModeloA.`,
|
|
};
|
|
}
|
|
if (
|
|
![row.acquirerRef, row.autonomousRef, row.activitiesRef].every(
|
|
(v) => !!v.trim(),
|
|
)
|
|
)
|
|
return stop(
|
|
'Identifique adquirente2(1)a,conjunto susceptível de ramo independente e operações que conferem direito à dedução;simples venda de activos não basta.',
|
|
);
|
|
return stop(
|
|
'P127:cessão ' +
|
|
(row.treatment === 'business_onerous' ? 'onerosa' : 'gratuita') +
|
|
'3(5) identificada como transmissão de bens cujo imposto não é devido nem exigível,segundo conjunto/adquirente/operações e âmbito demonstrados.Não é isenção genérica,nem taxa/base zero.Não concede dedução ao adquirente;qualidade e operações são factos do enquadramento.Sem condição adicional de continuidade,certificado ou aprovador inventados.',
|
|
'determined',
|
|
'tax_not_exigible_3_5',
|
|
);
|
|
}
|
|
function key(
|
|
draft: Record<string, unknown>,
|
|
book: PropertyTransferBook,
|
|
): string {
|
|
return JSON.stringify({
|
|
version: propertyTransferVersion,
|
|
scope: acquisitionScope(draft),
|
|
location: draft.selection_labels ?? {},
|
|
currency: draft.currency,
|
|
rows: book.rows,
|
|
});
|
|
}
|
|
export function propertyTransferCurrent(
|
|
draft: Record<string, unknown>,
|
|
book: PropertyTransferBook,
|
|
): boolean {
|
|
return !!book.result && book.result.sourceKey === key(draft, book);
|
|
}
|
|
export function recordPropertyTransfers(
|
|
draft: Record<string, unknown>,
|
|
book: PropertyTransferBook,
|
|
): PropertyTransferBook {
|
|
if (
|
|
!book.rows.length ||
|
|
new Set(book.rows.map((r) => r.id)).size !== book.rows.length
|
|
)
|
|
throw new Error(
|
|
'Indique operações com identificadores distintos;originais conservados.',
|
|
);
|
|
const result: PropertyTransferCalculation = {
|
|
version: propertyTransferVersion,
|
|
revision: (book.result?.revision ?? 0) + 1,
|
|
calculatedAt: new Date().toISOString(),
|
|
opportunity: acquisitionScope(draft),
|
|
location: JSON.stringify(draft.selection_labels ?? {}),
|
|
sourceKey: key(draft, book),
|
|
source: propertyTransferSource,
|
|
inputs: structuredClone(book.rows),
|
|
rows: book.rows.map((row) =>
|
|
evaluatePropertyTransfer(row, draft.currency),
|
|
),
|
|
};
|
|
const next = {
|
|
...book,
|
|
result,
|
|
history: [
|
|
...(book.history ?? []),
|
|
...(book.result ? [book.result] : []),
|
|
],
|
|
};
|
|
savePropertyTransferBook(draft, next);
|
|
return next;
|
|
}
|
|
export function propertyTransferReportRows(
|
|
draft: Record<string, unknown>,
|
|
): Array<[string, string, string, string]> {
|
|
try {
|
|
const book = propertyTransferBook(draft),
|
|
r = book.result;
|
|
if (!r) return [];
|
|
return r.inputs.map((row, i) => [
|
|
'IVA mensal',
|
|
`${propertyTransferTreatments.find((t) => t.id === row.treatment)?.branch ?? 'Transmissão'} — ${row.period} — ${row.id}`,
|
|
`${row.description};${r.rows[i].reason};base:${r.rows[i].baseAmount ?? 'Indeterminada'} ${row.currency};IVA:Indeterminado`,
|
|
`${r.opportunity};${r.location};${propertyTransferCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};${row.origin};operação ${row.operationRef},${row.operationDate};documento ${row.documentRef},${row.documentDate};bens ${row.goodsRef};âmbito ${row.scope};contrato ${row.contractRef};${row.treatment === 'binding_lease' ? `cláusula ${row.propertyClauseRef};contraprestação ${row.consideration},${row.considerationRef};composição ${row.compositionConfirmed}` : `adquirente ${row.acquirerRef};conjunto autónomo ${row.autonomousRef};operações adquirente ${row.activitiesRef}`};condições${JSON.stringify(row.conditions)};fundamento ${row.evidence};sem nova dedução/campo11/IVA/margem automática.`,
|
|
]);
|
|
} catch {
|
|
return [
|
|
[
|
|
'IVA mensal',
|
|
'Transmissões de propriedade',
|
|
'Dados não recuperáveis',
|
|
'Originais conservados;sem classificação por defeito.',
|
|
],
|
|
];
|
|
}
|
|
}
|