490 lines
19 KiB
TypeScript
490 lines
19 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
|
||
export const goodsAllocationVersion = 'goods-allocation-base/1';
|
||
export const goodsAllocationSource =
|
||
'CIVA3(3)e/f,Lei13/2016PDF6–7/p1426(264–265):afectação permanente de bens a uso próprio/particular/alheio,transmissão gratuita,afectação a sector isento ou ao activo imobilizado de bens20(1)a,com dedução total/parcial anterior nos bens/elementos;excepção de amostras/ofertas de pequeno valor na alíneae. Art15(2)b,PDF13/p1426(271):preço de aquisição ou,na sua falta,preço de custo,reportados ao momento da operação. Art20(1)a,PDF15/p1426(273). Lei22/2022PDF3mantém15(2)b e20(1)a;Lei10/2025mantém3(3)e/f e não altera15/20. Recorte2026/MZN,sem nova dedução/campo11,regularização,taxa,IVA ou totaisModeloA. Uso temporário de bens como serviçoP089 é distinto.';
|
||
export const goodsAllocationTreatments = [
|
||
{
|
||
id: 'permanent_private_goods',
|
||
label: 'Afectação permanente a uso próprio,particular ou fins alheios',
|
||
clause: 'e',
|
||
},
|
||
{
|
||
id: 'free_goods_transfer',
|
||
label: 'Transmissão gratuita de bens',
|
||
clause: 'e',
|
||
},
|
||
{
|
||
id: 'exempt_sector_goods',
|
||
label: 'Afectação de bens a sector de actividade isento',
|
||
clause: 'f',
|
||
},
|
||
{
|
||
id: 'fixed_asset_20a',
|
||
label: 'Afectação ao activo imobilizado de bens20(1)a',
|
||
clause: 'f',
|
||
},
|
||
] as const;
|
||
export const goodsAllocationConditions = [
|
||
[
|
||
'businessGoods',
|
||
'São bens da empresa identificados,não uma prestação de serviços ou venda onerosa normal',
|
||
],
|
||
[
|
||
'priorDeduction',
|
||
'Houve dedução total ou parcial anterior nos bens ou elementos que os constituem,com referência histórica',
|
||
],
|
||
[
|
||
'factsCorrespond',
|
||
'Objecto,destino,operação,data eâmbito correspondem ao percurso identificado',
|
||
],
|
||
] as const;
|
||
export const allocationOwnConditions = {
|
||
permanent_private_goods: [
|
||
[
|
||
'permanentAllocation',
|
||
'A afectação dos bens é permanente,não mero uso temporário',
|
||
],
|
||
[
|
||
'privatePurpose',
|
||
'Uso próprio do titular,pessoal ou fins alheios à empresa identificado',
|
||
],
|
||
[
|
||
'notSmallGift',
|
||
'Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e',
|
||
],
|
||
],
|
||
free_goods_transfer: [
|
||
[
|
||
'gratuitousTransfer',
|
||
'Está identificada uma transmissão gratuita dos bens',
|
||
],
|
||
[
|
||
'notSmallGift',
|
||
'Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e',
|
||
],
|
||
],
|
||
exempt_sector_goods: [
|
||
[
|
||
'exemptSector',
|
||
'Os bens são afectados ao sector de actividade isento identificado',
|
||
],
|
||
],
|
||
fixed_asset_20a: [
|
||
['fixedAsset', 'Está identificada a afectação ao activo imobilizado'],
|
||
[
|
||
'goods20a',
|
||
'Os bens correspondem aos referidos em20(1)a,com âmbito e referência identificados',
|
||
],
|
||
],
|
||
} as const;
|
||
export function allocationSpecificConditions(
|
||
treatment: string,
|
||
): ReadonlyArray<readonly [string, string]> {
|
||
return (
|
||
allocationOwnConditions[
|
||
treatment as keyof typeof allocationOwnConditions
|
||
] ?? []
|
||
);
|
||
}
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type GoodsAllocationRow = {
|
||
id: string;
|
||
treatment: string;
|
||
description: string;
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
currency: string;
|
||
operationDate: string;
|
||
operationRef: string;
|
||
documentRef: string;
|
||
documentDate: string;
|
||
goodsRef: string;
|
||
deductionRef: string;
|
||
destinationRef: string;
|
||
exceptionRef: string;
|
||
asset20aRef: string;
|
||
valuationMode: string;
|
||
acquisitionPrice: string;
|
||
acquisitionRef: string;
|
||
currentCost: string;
|
||
costRef: string;
|
||
valuationDate: string;
|
||
valuationScope: string;
|
||
evidence: string;
|
||
conditions: Partial<Record<string, Choice>>;
|
||
};
|
||
type LineResult = {
|
||
id: string;
|
||
status: 'determined' | 'not_applicable' | 'indeterminate';
|
||
classification: 'assimilated_goods' | 'not_applicable' | 'indeterminate';
|
||
baseAmount: string | null;
|
||
vat: null;
|
||
rate: null;
|
||
reason: string;
|
||
};
|
||
export type GoodsAllocationCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
location: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: GoodsAllocationRow[];
|
||
rows: LineResult[];
|
||
};
|
||
export type GoodsAllocationBook = {
|
||
rows: GoodsAllocationRow[];
|
||
result?: GoodsAllocationCalculation;
|
||
history?: GoodsAllocationCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
regulatedGoodsAllocations?: Record<string, GoodsAllocationBook>;
|
||
};
|
||
export function newGoodsAllocation(
|
||
id: string,
|
||
currency: string,
|
||
): GoodsAllocationRow {
|
||
return {
|
||
id,
|
||
treatment: '',
|
||
description: '',
|
||
period: '',
|
||
origin: 'projected',
|
||
currency,
|
||
operationDate: '',
|
||
operationRef: '',
|
||
documentRef: '',
|
||
documentDate: '',
|
||
goodsRef: '',
|
||
deductionRef: '',
|
||
destinationRef: '',
|
||
exceptionRef: '',
|
||
asset20aRef: '',
|
||
valuationMode: '',
|
||
acquisitionPrice: '',
|
||
acquisitionRef: '',
|
||
currentCost: '',
|
||
costRef: '',
|
||
valuationDate: '',
|
||
valuationScope: '',
|
||
evidence: '',
|
||
conditions: {},
|
||
};
|
||
}
|
||
export function switchAllocationTreatment(
|
||
row: GoodsAllocationRow,
|
||
treatment: string,
|
||
): GoodsAllocationRow {
|
||
return {
|
||
...row,
|
||
treatment,
|
||
conditions: Object.fromEntries(
|
||
goodsAllocationConditions.map(([key]) => [
|
||
key,
|
||
row.conditions[key] ?? '',
|
||
]),
|
||
),
|
||
};
|
||
}
|
||
export function switchAllocationValuation(
|
||
row: GoodsAllocationRow,
|
||
valuationMode: string,
|
||
): GoodsAllocationRow {
|
||
return {
|
||
...row,
|
||
valuationMode,
|
||
conditions: {
|
||
...row.conditions,
|
||
valuationConfirmed: '',
|
||
acquisitionUnavailable: '',
|
||
},
|
||
};
|
||
}
|
||
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
store.regulatedGoodsAllocations !== undefined &&
|
||
(!store.regulatedGoodsAllocations ||
|
||
typeof store.regulatedGoodsAllocations !== 'object' ||
|
||
Array.isArray(store.regulatedGoodsAllocations))
|
||
)
|
||
throw new Error(
|
||
'Afectações de bens não recuperáveis;originais preservados.',
|
||
);
|
||
const fields = Object.keys(newGoodsAllocation('', '')).filter(
|
||
(key) => key !== 'conditions',
|
||
);
|
||
for (const book of Object.values(store.regulatedGoodsAllocations ?? {}))
|
||
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 GoodsAllocationRow] !==
|
||
'string',
|
||
) ||
|
||
!row.conditions ||
|
||
typeof row.conditions !== 'object' ||
|
||
Array.isArray(row.conditions),
|
||
)
|
||
)
|
||
throw new Error(
|
||
'Linhas de afectações de bens não recuperáveis;originais preservados,sem substituição por lista vazia.',
|
||
);
|
||
return store;
|
||
}
|
||
export function goodsAllocationBook(
|
||
draft: Record<string, unknown>,
|
||
): GoodsAllocationBook {
|
||
return (
|
||
readStore(draft).regulatedGoodsAllocations?.[
|
||
acquisitionScope(draft)
|
||
] ?? { rows: [] }
|
||
);
|
||
}
|
||
export function saveGoodsAllocationBook(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsAllocationBook,
|
||
): string {
|
||
const current = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...current,
|
||
regulatedGoodsAllocations: {
|
||
...current.regulatedGoodsAllocations,
|
||
[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 evaluateGoodsAllocation(
|
||
row: GoodsAllocationRow,
|
||
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',
|
||
) => ({
|
||
...result,
|
||
status,
|
||
classification:
|
||
status === 'not_applicable'
|
||
? ('not_applicable' as const)
|
||
: result.classification,
|
||
reason,
|
||
});
|
||
const rule = goodsAllocationTreatments.find((t) => t.id === row.treatment);
|
||
if (!rule)
|
||
return stop(
|
||
'Identifique afectação permanente,transmissão gratuita,sector isento ou activo imobilizado20(1)a.',
|
||
);
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
||
!['projected', 'effective'].includes(row.origin) ||
|
||
!validDate(row.operationDate) ||
|
||
row.operationDate.slice(0, 7) !== row.period
|
||
)
|
||
return stop(
|
||
'Identifique mês2026,data correspondente e dados efectivos ou pressupostos expressos.',
|
||
);
|
||
const conditions = [
|
||
...goodsAllocationConditions,
|
||
...allocationSpecificConditions(row.treatment),
|
||
];
|
||
if (conditions.some(([key]) => row.conditions[key] === 'no'))
|
||
return stop(
|
||
'Este percurso de transmissão assimilada não se aplica aos factos declarados;conferir natureza,dedução histórica ou excepção,não presumir isenção/IVAzero.',
|
||
'not_applicable',
|
||
);
|
||
if (conditions.some(([key]) => row.conditions[key] !== 'yes'))
|
||
return stop(
|
||
'Factos próprios ou dedução anterior por esclarecer;desconhecido não é não aplicável ou zero. Dedução histórica não é nova dedução concedida.',
|
||
);
|
||
if (
|
||
!row.description.trim() ||
|
||
!row.operationRef.trim() ||
|
||
!row.documentRef.trim() ||
|
||
!validDate(row.documentDate) ||
|
||
!row.goodsRef.trim() ||
|
||
!row.deductionRef.trim() ||
|
||
!row.destinationRef.trim() ||
|
||
!row.evidence.trim()
|
||
)
|
||
return stop(
|
||
'Identifique bens,operação,documento/data,dedução histórica,destino eâmbito factual ou pressupostos expressos;reutilize referências,sem certificado/aprovador novo.',
|
||
);
|
||
if (rule.clause === 'e' && !row.exceptionRef.trim())
|
||
return stop(
|
||
'Identifique os factos/referência que distinguem a operação da excepção de amostras/ofertas de pequeno valor,ou pressuposto expresso;sem limiar inventado.',
|
||
);
|
||
if (row.treatment === 'fixed_asset_20a' && !row.asset20aRef.trim())
|
||
return stop(
|
||
'Identifique enquadramento dos bens20(1)a eafectação ao activo imobilizado,com referência/âmbito.',
|
||
);
|
||
result.classification = 'assimilated_goods';
|
||
if (currency !== 'MZN' || row.currency !== currency)
|
||
return stop(
|
||
'Operação3(3)' +
|
||
rule.clause +
|
||
' identificada;moeda fora do recorteMZN ou divergente,base indeterminada,sem conversão silenciosa.',
|
||
);
|
||
if (!['acquisition', 'cost'].includes(row.valuationMode))
|
||
return stop(
|
||
'Operação3(3)' +
|
||
rule.clause +
|
||
' identificada;base15(2)b por esclarecer:preço aquisição ou,na sua falta demonstrada,preço custo,reportados ao momento da operação.',
|
||
);
|
||
if (
|
||
row.conditions.valuationConfirmed !== 'yes' ||
|
||
!validDate(row.valuationDate) ||
|
||
row.valuationDate !== row.operationDate ||
|
||
!row.valuationScope.trim()
|
||
)
|
||
return stop(
|
||
'Operação assimilada identificada;base por esclarecer. Confirme preço de aquisição/custo reportado ao momento desta operação,data/âmbito ecomposição sem próprioIVA;não usar custo histórico automaticamente.',
|
||
);
|
||
if (
|
||
row.valuationMode === 'cost' &&
|
||
row.conditions.acquisitionUnavailable !== 'yes'
|
||
)
|
||
return stop(
|
||
'Operação assimilada identificada;não substituir aquisição desconhecida por custo. O custo15(2)b só é usado na falta demonstrada de preço aquisição;conferir esse facto.',
|
||
);
|
||
const raw =
|
||
row.valuationMode === 'acquisition'
|
||
? row.acquisitionPrice
|
||
: row.currentCost,
|
||
ref =
|
||
row.valuationMode === 'acquisition'
|
||
? row.acquisitionRef
|
||
: row.costRef;
|
||
const amount = decimal(raw);
|
||
if (amount === null || !ref.trim())
|
||
return stop(
|
||
'Operação assimilada identificada;indique o preço e referência correspondentes ao modo escolhido,reportados ao momento da operação;desconhecido não é zero.',
|
||
);
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
baseAmount: money(amount),
|
||
reason: `P124:transmissão assimilada3(3)${rule.clause};P088:base15(2)b=${money(amount)}MZN,${row.valuationMode === 'acquisition' ? 'preço aquisição' : 'preço custo,na falta demonstrada de preço aquisição'},reportado a${row.operationDate},${row.origin === 'projected' ? 'sob pressupostos expressos' : 'segundo dados efectivos declarados'}. Dedução anterior total/parcial é facto histórico dos bens/elementos,não nova dedução/campo11. Não confundir afectação/transmissão de bens comserviço por uso temporárioP089. Sem calcular IVA,taxa,regularização ou totaisModeloA.`,
|
||
};
|
||
}
|
||
|
||
function key(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsAllocationBook,
|
||
): string {
|
||
return JSON.stringify({
|
||
version: goodsAllocationVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
currency: draft.currency,
|
||
rows: book.rows,
|
||
});
|
||
}
|
||
export function goodsAllocationCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsAllocationBook,
|
||
): boolean {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
}
|
||
export function recordGoodsAllocations(
|
||
draft: Record<string, unknown>,
|
||
book: GoodsAllocationBook,
|
||
): GoodsAllocationBook {
|
||
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: GoodsAllocationCalculation = {
|
||
version: goodsAllocationVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
location: JSON.stringify(draft.selection_labels ?? {}),
|
||
sourceKey: key(draft, book),
|
||
source: goodsAllocationSource,
|
||
inputs: structuredClone(book.rows),
|
||
rows: book.rows.map((row) =>
|
||
evaluateGoodsAllocation(row, draft.currency),
|
||
),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
saveGoodsAllocationBook(draft, next);
|
||
return next;
|
||
}
|
||
export function goodsAllocationReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const book = goodsAllocationBook(draft),
|
||
result = book.result;
|
||
if (!result) return [];
|
||
return result.inputs.map((row, index) => {
|
||
const line = result.rows[index],
|
||
rule = goodsAllocationTreatments.find(
|
||
(t) => t.id === row.treatment,
|
||
);
|
||
return [
|
||
'IVA mensal',
|
||
`Afectação P124/baseP088 — ${row.period} — ${row.id}`,
|
||
`${row.description};classificação:${line.classification === 'assimilated_goods' ? 'Transmissão assimilada de bens' : line.classification === 'not_applicable' ? 'Percurso não aplicável' : 'Indeterminada'};base:${line.baseAmount ?? 'Indeterminada'} ${row.currency};IVA: Indeterminado;${line.reason}`,
|
||
`${result.opportunity};${result.location};${goodsAllocationCurrent(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'};${rule?.label ?? 'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};bens ${row.goodsRef};dedução histórica ${row.deductionRef};destino ${row.destinationRef};${rule?.clause === 'e' ? `excepção amostras/ofertas ${row.exceptionRef};` : ''}${row.treatment === 'fixed_asset_20a' ? `enquadramento20(1)a ${row.asset20aRef};` : ''}base ${row.valuationMode};${row.valuationMode === 'cost' ? `custo ${row.currentCost},${row.costRef};falta aquisição ${row.conditions.acquisitionUnavailable ?? 'por indicar'}` : `aquisição ${row.acquisitionPrice},${row.acquisitionRef}`};data/âmbito base ${row.valuationDate},${row.valuationScope};documento ${row.documentRef},${row.documentDate};condições ${[...goodsAllocationConditions, ...allocationSpecificConditions(row.treatment)].map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento ${row.evidence};sem nova dedução/campo11/IVA/regularização/totaisModeloA.`,
|
||
];
|
||
});
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'Afectação de bens',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados;sem base ou classificação por defeito.',
|
||
],
|
||
];
|
||
}
|
||
}
|