Files
Investidor-V2/resources/js/lib/iva-service-normal.ts
T

405 lines
15 KiB
TypeScript

import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
export const serviceNormalVersion = 'assimilated-service-normal/1';
export const serviceNormalSource =
'CIVA republicado Lei13/2016,15(2)(c),PDF13/p1426(271):operações4(2),base=valor normal do serviço definido15(4),PDF14/p1426(272).4(2)(a),PDF7/p1426(265):serviços gratuitos pela própria empresa para necessidades particulares do titular/pessoal ou fins alheios;4(2)(b):utilização de bens da empresa para uso próprio/titular/pessoal/fins alheios ou sectores isentos,quando houve dedução total/parcial nesses bens ou elementos constitutivos. A condição de dedução anterior é de4(2)(b),não de4(2)(a). Composição15(4)/(5)/(6),sem próprioIVA. Lei22/2022mantém15c/4por remissão,não altera4;Lei10/2025não altera4/15. Recorte2026/MZN;base apenas,sem calcularIVA/dedução/campo11/regularizações/totaisModeloA,nem confundir uso com transmissão permanenteP088.';
export const serviceNormalTreatments = [
{
id: 'free_company_service',
label: 'Serviço gratuito pela própria empresa para necessidades particulares ou fins alheios',
},
{
id: 'private_company_asset_use',
label: 'Utilização de bens da empresa para uso próprio,particular ou fins alheios',
},
{
id: 'exempt_sector_asset_use',
label: 'Utilização de bens da empresa em sector de actividade isento',
},
] as const;
export const serviceNormalConditions = [
[
'domesticOperation',
'A operação e o âmbito territorial aplicável estão identificados para esta base',
],
[
'normalValueBasis',
'O valor normal do serviço corresponde15(4):concorrência normal,fornecedor independente,estádio de comercialização,tempo/lugar da operação ou mais próximos,com fundamento e âmbito identificados',
],
[
'netComposition',
'O montante exclui o próprio IVA e reflecte15(4)/(5)/(6),sem omissões ou dupla contagem',
],
] as const;
export const serviceOwnConditions = {
free_company_service: [
[
'freeByCompany',
'A prestação é gratuita e efectuada pela própria empresa',
],
[
'nonBusinessPurpose',
'Destina-se a necessidades particulares do titular/pessoal ou,em geral,a fins alheios à empresa',
],
],
private_company_asset_use: [
[
'companyAssetUse',
'É utilização de bens da empresa,não afectação/transmissão permanente de bens',
],
[
'privatePurpose',
'Destina-se a uso próprio do titular/pessoal ou,em geral,a fins alheios à empresa',
],
[
'priorDeduction',
'Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem',
],
],
exempt_sector_asset_use: [
[
'companyAssetUse',
'É utilização de bens da empresa,não afectação/transmissão permanente de bens',
],
[
'exemptSector',
'A utilização corresponde a sector de actividade isento identificado',
],
[
'priorDeduction',
'Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem',
],
],
} as const;
export function ownServiceConditions(
treatment: string,
): ReadonlyArray<readonly [string, string]> {
return (
serviceOwnConditions[treatment as keyof typeof serviceOwnConditions] ??
[]
);
}
type Choice = '' | 'yes' | 'no';
export type ServiceNormalRow = {
id: string;
treatment: string;
description: string;
period: string;
origin: 'projected' | 'effective';
currency: string;
operationRef: string;
purposeRef: string;
assetRef: string;
deductionRef: string;
documentRef: string;
documentDate: string;
normalValue: string;
valuationRef: string;
valuationScope: string;
compositionRef: string;
evidence: string;
conditions: Partial<Record<string, Choice>>;
};
type LineResult = {
id: string;
status: 'determined' | 'not_applicable' | 'indeterminate';
baseAmount: string | null;
vat: null;
rate: null;
reason: string;
};
export type ServiceNormalCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: ServiceNormalRow[];
rows: LineResult[];
};
export type ServiceNormalBook = {
rows: ServiceNormalRow[];
result?: ServiceNormalCalculation;
history?: ServiceNormalCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
serviceNormals?: Record<string, ServiceNormalBook>;
};
export function newServiceNormal(
id: string,
currency: string,
): ServiceNormalRow {
return {
id,
treatment: '',
description: '',
period: '',
origin: 'projected',
currency,
operationRef: '',
purposeRef: '',
assetRef: '',
deductionRef: '',
documentRef: '',
documentDate: '',
normalValue: '',
valuationRef: '',
valuationScope: '',
compositionRef: '',
evidence: '',
conditions: {},
};
}
export function switchServiceTreatment(
row: ServiceNormalRow,
treatment: string,
): ServiceNormalRow {
return {
...row,
treatment,
conditions: Object.fromEntries(
serviceNormalConditions.map(([key]) => [
key,
row.conditions[key] ?? '',
]),
),
};
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.serviceNormals !== undefined &&
(!store.serviceNormals ||
typeof store.serviceNormals !== 'object' ||
Array.isArray(store.serviceNormals))
)
throw new Error(
'Bases de contraprestação não recuperáveis;originais preservados.',
);
const fields = Object.keys(newServiceNormal('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.serviceNormals ?? {}))
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 ServiceNormalRow] !==
'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 serviceNormalBook(
draft: Record<string, unknown>,
): ServiceNormalBook {
return (
readStore(draft).serviceNormals?.[acquisitionScope(draft)] ?? {
rows: [],
}
);
}
export function saveServiceNormalBook(
draft: Record<string, unknown>,
book: ServiceNormalBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
serviceNormals: {
...current.serviceNormals,
[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 evaluateServiceNormal(
row: ServiceNormalRow,
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 = serviceNormalTreatments.find((t) => t.id === row.treatment);
if (!rule)
return stop(
'Identifique serviço gratuito,utilização particular/alheia ou em sector isento;desconhecido não permite presumir aplicação.',
);
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 mês em2026 e dados efectivos declarados ou pressupostos expressos de projecção.',
);
const required = [
...serviceNormalConditions,
...ownServiceConditions(row.treatment),
];
const failed = required.filter(([key]) => row.conditions[key] === 'no');
if (failed.length)
return stop(
'Não se aplica esta base4(2)/15(2)(c): ' +
failed.map(([, label]) => label).join('; ') +
'. Não é isenção nem IVA zero.',
'not_applicable',
);
if (required.some(([key]) => row.conditions[key] !== 'yes'))
return stop(
'Confirme as condições próprias deste percurso;desconhecido não é zero,não aplicável ou dedução presumida.',
);
if (
!row.description.trim() ||
!row.operationRef.trim() ||
!row.purposeRef.trim() ||
!row.documentRef.trim() ||
!validDate(row.documentDate) ||
!row.valuationRef.trim() ||
!row.valuationScope.trim() ||
!row.compositionRef.trim() ||
!row.evidence.trim()
)
return stop(
'Identifique serviço/operação,finalidade,documento/data,fundamento e âmbito do valor normal e composição,ou pressupostos expressos;reutilize dados existentes,sem certificado/avaliador/aprovador novo.',
);
if (
row.treatment !== 'free_company_service' &&
(!row.assetRef.trim() || !row.deductionRef.trim())
)
return stop(
'Identifique os bens/elementos e a referência da dedução anterior total/parcial,ou pressupostos expressos;esta referência não calcula nem concede dedução/campo11.',
);
const normal = decimal(row.normalValue);
if (normal === null)
return stop(
'Indique o valor normal do serviço;desconhecido não é zero nem custo/preço de aquisição dos bens presumido.',
);
return {
...result,
status: 'determined',
baseAmount: money(normal),
reason: `Base15(2)(c),${rule.label}:valor normal do serviço ${money(normal)}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. ${row.treatment === 'free_company_service' ? '4(2)(a),sem impor a condição de dedução anterior da alínea b).' : '4(2)(b),bens/elementos e dedução anterior total/parcial referenciados;uso não é transmissão permanente.'} Valor normal com fundamento/âmbito,sem avaliação automática. Base não é imposto;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não determinados por este cálculo.`,
};
}
function key(draft: Record<string, unknown>, book: ServiceNormalBook): string {
return JSON.stringify({
version: serviceNormalVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function serviceNormalCurrent(
draft: Record<string, unknown>,
book: ServiceNormalBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordServiceNormals(
draft: Record<string, unknown>,
book: ServiceNormalBook,
): ServiceNormalBook {
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: ServiceNormalCalculation = {
version: serviceNormalVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: serviceNormalSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) =>
evaluateServiceNormal(row, draft.currency),
),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveServiceNormalBook(draft, next);
return next;
}
export function serviceNormalReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = serviceNormalBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base P089 — ${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};${serviceNormalCurrent(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'};${serviceNormalTreatments.find((t) => t.id === row.treatment)?.label ?? 'Percurso por indicar'};operação ${row.operationRef};finalidade ${row.purposeRef};${row.treatment === 'free_company_service' ? 'Dedução anterior não é condição de4(2)(a)' : `bens/elementos ${row.assetRef},dedução anterior ${row.deductionRef}`};valor normal ${row.normalValue} ${row.currency};fundamento ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${[...serviceNormalConditions, ...ownServiceConditions(row.treatment)].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',
'Valor normal de serviços assimilados',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}