161 lines
5.8 KiB
TypeScript
161 lines
5.8 KiB
TypeScript
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;
|
||
}
|