Files
Investidor-V2/resources/js/lib/fiscal-form-preparation.ts
T

161 lines
5.8 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 { modeloAFields, modeloAReference } from './iva-modelo-a.ts';
export type FiscalFormKind = 'iva' | 'irpc';
export const fiscalFormVersion = 'investmoz-fiscal-preparation/1';
export const fiscalForms = {
iva: {
title: 'IVA — Modelo A',
source: modeloAReference.file,
hash: modeloAReference.sha256,
coverage:
'Campos 01–27 do exemplar, para preenchimento manual. A edição e as divergências já documentadas continuam identificadas.',
},
irpc: {
title: 'IRPC — Modelo 22',
source: 'IRPC M-22- DECLARACAO DE RENDIMENTOS.pdf',
hash: '4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88',
coverage:
'Preparação parcial: resultado, lucro, matéria colectável e liquidação nos campos abaixo. Os restantes ajustamentos e anexos do exemplar não são preenchidos aqui. L8 permanece parcial.',
},
} as const;
const irpcFields = [
['200', 'Resultado do exercício'],
['259', 'Prejuízo fiscal'],
['260', 'Lucro tributável'],
['269', 'Prejuízo fiscal — regime geral'],
['270', 'Lucro tributável — regime geral'],
['276', 'Prejuízos fiscais deduzidos'],
['277', 'Benefícios fiscais a deduzir'],
['278', 'Matéria colectável — regime geral'],
['300', 'Imposto à taxa normal'],
['301', 'Imposto à taxa reduzida'],
['302', 'Colecta total'],
['303', 'Dupla tributação económica'],
['304', 'Dupla tributação internacional'],
['305', 'Benefícios fiscais'],
['306', 'Pagamento especial por conta'],
['308', 'Total das deduções'],
['309', 'IRPC liquidado'],
['310', 'Retenções na fonte'],
['311', 'Pagamentos por conta'],
['312', 'IRPC a pagar antes de juros'],
['313', 'IRPC a recuperar antes de juros'],
['314', 'Juros compensatórios'],
['315', 'IRPC a pagar'],
['316', 'IRPC a recuperar'],
] as const;
export function fiscalFormFields(kind: FiscalFormKind) {
return kind === 'iva'
? modeloAFields.map((f) => ({
code: f.field,
label: f.purpose,
guidance: f.basis,
}))
: irpcFields.map(([code, label]) => ({
code,
label,
guidance:
Number(code) < 269
? 'Quadro 10 do exemplar.'
: Number(code) < 300
? 'Quadro 11, regime geral. Outros regimes não incluídos nesta preparação.'
: 'Quadro 12 do exemplar. Preenchimento manual, sem cálculo ou validação automática da liquidação.',
}));
}
export type FiscalPreparation = {
version: typeof fiscalFormVersion;
kind: FiscalFormKind;
sourceHash: string;
name: string;
nuit: string;
period: string;
notes: string;
values: Record<string, string>;
};
export function newFiscalPreparation(kind: FiscalFormKind): FiscalPreparation {
return {
version: fiscalFormVersion,
kind,
sourceHash: fiscalForms[kind].hash,
name: '',
nuit: '',
period: '',
notes: '',
values: {},
};
}
export function readFiscalPreparation(raw: string): FiscalPreparation {
if (raw.length > 100_000)
throw new Error('O ficheiro excede o tamanho de uma preparação.');
let p: unknown;
try {
p = JSON.parse(raw);
} catch {
throw new Error(
'Não foi possível ler este ficheiro de preparação. Os campos actuais foram preservados.',
);
}
if (!p || typeof p !== 'object' || Array.isArray(p))
throw new Error('Ficheiro de preparação inválido.');
const v = p as Record<string, unknown>;
if (
v.version !== fiscalFormVersion ||
(v.kind !== 'iva' && v.kind !== 'irpc') ||
v.sourceHash !== fiscalForms[v.kind].hash
)
throw new Error(
'Versão ou exemplar diferente. A preparação actual foi preservada.',
);
for (const key of ['name', 'nuit', 'period', 'notes']) {
if (typeof v[key] !== 'string' || v[key].length > 5000)
throw new Error('Identificação ou notas inválidas.');
}
if (!v.values || typeof v.values !== 'object' || Array.isArray(v.values))
throw new Error('Campos inválidos.');
const allowed = new Set(fiscalFormFields(v.kind).map((f) => f.code));
const values: Record<string, string> = {};
for (const [code, value] of Object.entries(v.values)) {
if (
!allowed.has(code) ||
typeof value !== 'string' ||
value.length > 100
)
throw new Error(
'Campo não reconhecido. A preparação actual foi preservada.',
);
values[code] = value;
}
return {
...newFiscalPreparation(v.kind),
name: v.name as string,
nuit: v.nuit as string,
period: v.period as string,
notes: v.notes as string,
values,
};
}
export function fiscalPreparationIssues(p: FiscalPreparation): string[] {
const issues: string[] = [];
if (!p.name.trim()) issues.push('Indique o nome ou designação social.');
if (!/^\d{9}$/.test(p.nuit))
issues.push('Indique o NUIT com 9 algarismos.');
if (
!(p.kind === 'iva' ? /^\d{4}-(0[1-9]|1[0-2])$/ : /^\d{4}$/).test(
p.period,
)
)
issues.push(
p.kind === 'iva'
? 'Indique ano e mês do período.'
: 'Indique o ano do exercício.',
);
for (const f of fiscalFormFields(p.kind)) {
const value = p.values[f.code] ?? '';
if (value !== '' && !/^-?\d{1,24}(?:[.,]\d{1,2})?$/.test(value))
issues.push(
`Campo ${f.code}: use um montante sem separador de milhares, com até duas casas decimais.`,
);
}
return issues;
}