67 lines
4.9 KiB
TypeScript
67 lines
4.9 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;
|
||
}
|