This commit is contained in:
@@ -1819,3 +1819,10 @@ Incluídos: consulta provincial nacional/corredores e detalhe territorial; pesqu
|
||||
Conferidos o commit servido, APIs filtradas e assets instalados. Reutilizadas as provas QA aceites; a bateria obrigatória do pipeline não reabre a sua aceitação. Mantém-se pendente a conferência visual autenticada, incluindo navegação, ida/volta a recursos e Gurué no mesmo dossier. A recusa relatada em Turismo ocorreu após um botão; a falha concreta do CTA de cadastro foi corrigida, mas não se declara reproduzida e resolvida a acção original sem a sessão real. A escolha do formulário fiscal concreto ainda precisa de ser esclarecida; os diplomas IVA mostrados não são formulários preenchíveis.
|
||||
|
||||
Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios QA anteriores conservam o estado histórico antes da instalação. As 435 observações de Gurué não são 435 empresas; as listas são internas, a cobertura nacional é parcial e a comparação sustenta hipóteses. L8 permanece parcial. Actualizados V2-01/V2-07/V2-10 na matriz e manual existentes.
|
||||
|
||||
|
||||
## 28/10/2026 — preparação de formulários fiscais, inventário antes das provas
|
||||
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
|
||||
@@ -7377,3 +7377,10 @@ Incluídos: consulta provincial nacional/corredores e detalhe territorial; pesqu
|
||||
Conferidos o commit servido, APIs filtradas e assets instalados. Reutilizadas as provas QA aceites; a bateria obrigatória do pipeline não reabre a sua aceitação. Mantém-se pendente a conferência visual autenticada, incluindo navegação, ida/volta a recursos e Gurué no mesmo dossier. A recusa relatada em Turismo ocorreu após um botão; a falha concreta do CTA de cadastro foi corrigida, mas não se declara reproduzida e resolvida a acção original sem a sessão real. A escolha do formulário fiscal concreto ainda precisa de ser esclarecida; os diplomas IVA mostrados não são formulários preenchíveis.
|
||||
|
||||
Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios QA anteriores conservam o estado histórico antes da instalação. As 435 observações de Gurué não são 435 empresas; as listas são internas, a cobertura nacional é parcial e a comparação sustenta hipóteses. L8 permanece parcial. Actualizados V2-01/V2-07/V2-10 na matriz e manual existentes.
|
||||
|
||||
|
||||
## 28/10/2026 — preparação de formulários fiscais, inventário antes das provas
|
||||
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PortalMenu } from '@/layouts/portal-navigation';
|
||||
import { fiscalForms, fiscalFormFields, fiscalPreparationIssues, newFiscalPreparation, readFiscalPreparation, type FiscalFormKind, type FiscalPreparation } from '@/lib/fiscal-form-preparation';
|
||||
|
||||
const input = 'mt-1 block w-full rounded border border-slate-400 bg-white p-3 text-slate-900';
|
||||
const button = 'rounded border border-emerald-800 px-4 py-3 font-semibold text-emerald-900 disabled:opacity-50';
|
||||
export default function FiscalForms({ initialTax = 'iva' }: { initialTax?: FiscalFormKind }) {
|
||||
const [kind, setKind] = useState<FiscalFormKind>(initialTax);
|
||||
const [drafts, setDrafts] = useState({ iva: newFiscalPreparation('iva'), irpc: newFiscalPreparation('irpc') });
|
||||
const [step, setStep] = useState(0);
|
||||
const [notice, setNotice] = useState('');
|
||||
const [pending, setPending] = useState<FiscalPreparation | null>(null);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const p = drafts[kind], source = fiscalForms[kind], fields = fiscalFormFields(kind);
|
||||
const issues = fiscalPreparationIssues(p);
|
||||
useEffect(() => {
|
||||
setKind(initialTax);
|
||||
}, [initialTax]);
|
||||
useEffect(() => {
|
||||
const warn = (e: BeforeUnloadEvent) => { if (dirty) { e.preventDefault(); e.returnValue = ''; } };
|
||||
window.addEventListener('beforeunload', warn);
|
||||
const unsubscribe = router.on('before', e => {
|
||||
if (dirty && !window.confirm('Os campos estão apenas nesta página. Guarde o ficheiro para continuar depois. Pretende sair?')) e.preventDefault();
|
||||
});
|
||||
return () => { window.removeEventListener('beforeunload', warn); unsubscribe(); };
|
||||
}, [dirty]);
|
||||
function patch(update: Partial<FiscalPreparation>) {
|
||||
setDrafts(d => ({ ...d, [kind]: { ...d[kind], ...update } }));
|
||||
setDirty(true); setNotice('');
|
||||
}
|
||||
function download() {
|
||||
const url = URL.createObjectURL(new Blob([JSON.stringify(p)], { type: 'application/json' }));
|
||||
const a = document.createElement('a'); a.href = url; a.download = `investmoz-preparacao-${kind}.json`; a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
setNotice('Ficheiro de preparação gerado. Guarde-o para recuperar os campos. Não foi enviado ao portal nem à AT.');
|
||||
// Other tax preparation may still contain unsaved edits.
|
||||
}
|
||||
return <div className="min-h-screen bg-slate-50 text-slate-900">
|
||||
<Head title="Preencher formulário fiscal" />
|
||||
<div className="print:hidden"><PortalMenu /></div>
|
||||
<main className="mx-auto max-w-5xl space-y-6 px-5 py-8">
|
||||
<div className="print:hidden"><Link href="/documentos" className="underline">Documentos e Formulários</Link></div>
|
||||
<h1 className="text-3xl font-bold">Preencher formulário fiscal</h1>
|
||||
<p>Prepare os valores de IVA ou IRPC, confira e guarde um ficheiro para continuar depois. Os campos vazios significam informação em falta.</p>
|
||||
<p className="rounded border border-amber-600 bg-amber-50 p-3">Preparação para conferência, sem submissão à Autoridade Tributária. Os valores são introduzidos por si; não há apuramento automático nem confirmação de elegibilidade de deduções.</p>
|
||||
<label className="block max-w-lg print:hidden">Formulário
|
||||
<select className={input} value={kind} onChange={e => { setKind(e.target.value as FiscalFormKind); setStep(0); setNotice(''); setPending(null); }}>
|
||||
<option value="iva">IVA — Modelo A</option><option value="irpc">IRPC — Modelo 22</option>
|
||||
</select>
|
||||
</label>
|
||||
<h2 className="text-xl font-bold">{source.title}</h2>
|
||||
<p>{source.coverage}</p>
|
||||
<nav aria-label="Etapas de preenchimento" className="flex flex-wrap gap-2 print:hidden">
|
||||
{['Identificação e período', 'Preencher valores', 'Conferir e guardar'].map((label, n) => <button type="button" key={label} className={button} aria-current={step === n ? 'step' : undefined} onClick={() => setStep(n)}>{n + 1}. {label}</button>)}
|
||||
</nav>
|
||||
<form onSubmit={e => e.preventDefault()} className="space-y-5">
|
||||
<fieldset className={`${step !== 0 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`}>
|
||||
<legend className="font-bold">Identificação e período</legend>
|
||||
<label className="block">Nome ou designação social<input className={input} autoComplete="organization" maxLength={5000} value={p.name} onChange={e => patch({ name: e.target.value })} /></label>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label>NUIT<input className={input} inputMode="numeric" maxLength={9} value={p.nuit} onChange={e => patch({ nuit: e.target.value })} /></label>
|
||||
<label>{kind === 'iva' ? 'Período — ano e mês' : 'Exercício — ano'}<input className={input} type={kind === 'iva' ? 'month' : 'text'} inputMode={kind === 'irpc' ? 'numeric' : undefined} placeholder={kind === 'iva' ? 'AAAA-MM' : 'AAAA'} maxLength={7} value={p.period} onChange={e => patch({ period: e.target.value })} /></label>
|
||||
</div>
|
||||
<p className="text-sm">Dados mantidos apenas nesta página até descarregar o ficheiro. Não há gravação automática. Guarde antes de sair ou actualizar.</p>
|
||||
</fieldset>
|
||||
<fieldset className={`${step !== 1 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`}>
|
||||
<legend className="font-bold">Valores em meticais (MZN)</legend>
|
||||
<p>Sem separador de milhares; vírgula ou ponto para os cêntimos. Não preencha zero quando desconhece o valor.</p>
|
||||
{kind === 'irpc' && <p>Crédito económico (303) e internacional (304) são distintos. Não utilize a mesma parcela nos dois campos. Esta preparação manual não verifica as parcelas; conserve o suporte para conferência.</p>}
|
||||
<div className="grid gap-5 sm:grid-cols-2">{fields.map(f => <div key={f.code} className="break-inside-avoid">
|
||||
<label className="block">{f.code} — {f.label}<input className={input} inputMode="decimal" maxLength={100} value={p.values[f.code] ?? ''} onChange={e => patch({ values: { ...p.values, [f.code]: e.target.value } })} /></label>
|
||||
<details className="mt-1 text-sm print:hidden"><summary className="cursor-pointer underline">Orientação do campo</summary><p>{f.guidance}</p></details>
|
||||
</div>)}</div>
|
||||
</fieldset>
|
||||
<section className={`${step !== 2 ? 'hidden print:block' : ''} space-y-4 rounded border bg-white p-5`} aria-label="Conferir e guardar">
|
||||
<h3 className="font-bold">Conferência da preparação</h3>
|
||||
<p>{fields.filter(f => (p.values[f.code] ?? '') !== '').length} de {fields.length} campos com valor. Campos vazios não foram convertidos em zero.</p>
|
||||
{issues.length > 0 ? <div><p>Por conferir — pode guardar a preparação incompleta:</p><ul className="list-disc pl-6">{issues.map(i => <li key={i}>{i}</li>)}</ul></div> : <p>Formato dos campos conferido. Os montantes e a aplicação fiscal ainda devem ser conferidos.</p>}
|
||||
<dl className="grid gap-3 sm:grid-cols-2 print:hidden">{fields.filter(f => (p.values[f.code] ?? '') !== '').map(f => <div key={f.code}><dt>{f.code} — {f.label}</dt><dd className="font-semibold">{p.values[f.code]} MZN</dd></div>)}</dl>
|
||||
<label className="block">Notas e referências dos valores<textarea className={input} rows={4} maxLength={5000} value={p.notes} onChange={e => patch({ notes: e.target.value })} /></label>
|
||||
<div className="flex flex-wrap gap-3 print:hidden"><button type="button" className={button} onClick={download}>Guardar ficheiro para continuar</button><button type="button" className={button} onClick={() => window.print()}>Imprimir preparação</button></div>
|
||||
<p className="text-sm">O ficheiro contém os dados introduzidos. Guarde-o num local privado. Não fica associado automaticamente ao dossier.</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-3 print:hidden"><button type="button" className={button} disabled={step === 0} onClick={() => setStep(step - 1)}>Anterior</button><button type="button" className={button} disabled={step === 2} onClick={() => setStep(step + 1)}>Continuar</button></div>
|
||||
</form>
|
||||
<section className="space-y-3 rounded border p-4 print:hidden" aria-label="Recuperar preparação">
|
||||
<label className="block">Recuperar ficheiro guardado<input className={input} type="file" accept=".json,application/json" onChange={async e => {
|
||||
const file = e.target.files?.[0]; e.target.value = ''; if (!file) return;
|
||||
try { if (file.size > 100_000) throw new Error('O ficheiro é demasiado grande.'); setPending(readFiscalPreparation(await file.text())); setNotice('Ficheiro lido. Confirme a recuperação abaixo.'); }
|
||||
catch (error) { setPending(null); setNotice(error instanceof Error ? error.message : 'Não foi possível ler o ficheiro. Os campos actuais foram preservados.'); }
|
||||
}} /></label>
|
||||
{pending && <div className="space-y-2"><p>Recuperar {fiscalForms[pending.kind].title}, período {pending.period || 'por indicar'}? Substitui apenas os campos desse formulário nesta página.</p><button type="button" className={button} onClick={() => { setDrafts(d => ({ ...d, [pending.kind]: pending })); setKind(pending.kind); setStep(2); setPending(null); setDirty(true); setNotice('Preparação recuperada.'); }}>Confirmar recuperação</button> <button type="button" className={button} onClick={() => setPending(null)}>Cancelar</button></div>}
|
||||
</section>
|
||||
{notice && <p role="status" className="rounded border p-3">{notice}</p>}
|
||||
<details className="rounded border p-4"><summary className="cursor-pointer font-semibold">Exemplar e cobertura</summary><p>{source.source}</p><p className="break-all text-sm">SHA256: {source.hash}</p><p>{source.coverage}</p><p>Os cálculos e correspondências já disponíveis no simulador permanecem separados desta introdução manual. Não se executam fórmulas nem se transportam valores entre edições automaticamente.</p></details>
|
||||
<div className="flex flex-wrap gap-4 print:hidden"><Link href="/simulador-fiscal" className="underline">Consultar cálculos e preparação M20/M22 no simulador</Link><Link href="/legislacao" className="underline">Consultar legislação</Link></div>
|
||||
</main>
|
||||
</div>;
|
||||
}
|
||||
@@ -538,6 +538,15 @@ export default function Services(p: Props) {
|
||||
opportunities={p.opportunities}
|
||||
/>
|
||||
)}
|
||||
{p.section === 'documents' && <section aria-label="Preencher formulários fiscais" className="space-y-3 rounded-xl border bg-white p-5">
|
||||
<h2 className="text-xl font-bold">Que formulário pretende preencher?</h2>
|
||||
<p>Abra os campos do formulário fiscal. A legislação e os guias de consulta ficam abaixo.</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link className={button} href="/formularios/fiscais?imposto=iva">Preencher IVA — Modelo A</Link>
|
||||
<Link className={button} href="/formularios/fiscais?imposto=irpc">Preparar IRPC — Modelo 22</Link>
|
||||
</div>
|
||||
<p className="text-sm">Preparação manual, com gravação em ficheiro. Cobertura parcial de IRPC; sem submissão à AT.</p>
|
||||
</section>}
|
||||
{['procedures', 'documents', 'legislation'].includes(
|
||||
p.section,
|
||||
) && <JourneyDocumentResources stages={p.stages} />}
|
||||
|
||||
@@ -50,6 +50,11 @@ use Laravel\Fortify\Features;
|
||||
// Route::inertia('/', 'welcome')->name('home');
|
||||
Route::get('/', [PortalServicesController::class, 'home'])->name('home');
|
||||
Route::inertia('/contextos-de-investimento', 'portal/investment-contexts')->name('portal.investment-contexts');
|
||||
Route::get('/formularios/fiscais', function (\Illuminate\Http\Request $request) {
|
||||
$input = $request->validate(['imposto' => ['nullable', 'in:iva,irpc']]);
|
||||
|
||||
return Inertia::render('portal/fiscal-forms', ['initialTax' => $input['imposto'] ?? 'iva']);
|
||||
})->name('portal.fiscal-forms');
|
||||
Route::inertia('/politica-de-privacidade', 'portal/privacy-policy')->name('legal.privacy');
|
||||
Route::inertia('/termos-de-uso', 'portal/terms-of-use')->name('legal.terms');
|
||||
foreach (['sectores' => 'sectors', 'como-investir' => 'procedures', 'legislacao' => 'legislation', 'documentos' => 'documents',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class FiscalFormEntryTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
\Tests\Support\ProactiveInvestmentFixture::guard();
|
||||
$this->withoutVite();
|
||||
}
|
||||
|
||||
public function test_both_fiscal_forms_are_public_and_do_not_redirect_to_legal_documents(): void
|
||||
{
|
||||
config(['portal.homologation_only' => false]);
|
||||
foreach (['iva', 'irpc'] as $tax) {
|
||||
$this->get('/formularios/fiscais?imposto='.$tax)
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('portal/fiscal-forms')->where('initialTax', $tax));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import React from 'react';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import ts from 'typescript';
|
||||
import * as forms from '../../resources/js/lib/fiscal-form-preparation.ts';
|
||||
const require=createRequire(import.meta.url);
|
||||
const source=fs.readFileSync(new URL('../../resources/js/pages/portal/fiscal-forms.tsx',import.meta.url),'utf8');
|
||||
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
|
||||
const module={exports:{}};
|
||||
new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({href,children,...props})=>React.createElement('a',{href,...props},children),router:{on:()=>()=>{}}}:name==='@/layouts/portal-navigation'?{PortalMenu:()=>null}:name==='@/lib/fiscal-form-preparation'?forms:require(name),module,module.exports);
|
||||
test('fiscal entry renders labelled editable fields and a guided preparation rather than legislation',()=>{
|
||||
const html=renderToStaticMarkup(React.createElement(module.exports.default));
|
||||
for(const label of ['Identificação e período','Nome ou designação social','NUIT','Período — ano e mês','Conferir e guardar','Guardar ficheiro para continuar','Recuperar ficheiro guardado','Confirmar recuperação']) {
|
||||
// Confirmation appears only after a compatible file is selected.
|
||||
if(label !== 'Confirmar recuperação') assert.ok(html.includes(label),label);
|
||||
}
|
||||
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,27);
|
||||
assert.match(html,/sem submissão à Autoridade Tributária/);
|
||||
assert.match(html,/Não há gravação automática/);
|
||||
assert.doesNotMatch(html,/value="0"/);
|
||||
});
|
||||
test('IRPC entry opens the requested form directly and keeps its partial coverage explicit',()=>{
|
||||
const html=renderToStaticMarkup(React.createElement(module.exports.default,{initialTax:'irpc'}));
|
||||
assert.match(html,/IRPC — Modelo 22/);
|
||||
assert.match(html,/L8 permanece parcial/);
|
||||
assert.match(html,/303 — Dupla tributação económica/);
|
||||
assert.match(html,/304 — Dupla tributação internacional/);
|
||||
assert.match(html,/Não utilize a mesma parcela/);
|
||||
assert.equal((html.match(/inputMode="decimal"/g)??[]).length,24);
|
||||
assert.doesNotMatch(html,/01 — Base das operações/);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {newFiscalPreparation, readFiscalPreparation, fiscalPreparationIssues, fiscalFormFields} from '../../resources/js/lib/fiscal-form-preparation.ts';
|
||||
|
||||
test('IVA and IRPC preparations round trip deliberate entries and unknown amounts without zero defaults', () => {
|
||||
for (const kind of ['iva', 'irpc']) {
|
||||
const p = newFiscalPreparation(kind);
|
||||
p.name = 'Investidor de teste'; p.nuit = '123456789'; p.period = kind === 'iva' ? '2026-10' : '2026';
|
||||
const [first, second] = fiscalFormFields(kind);
|
||||
p.values[first.code] = '123456789012345678901234,01';
|
||||
p.values[second.code] = '';
|
||||
const restored = readFiscalPreparation(JSON.stringify(p));
|
||||
assert.deepEqual(restored, p);
|
||||
assert.deepEqual(fiscalPreparationIssues(restored), []);
|
||||
assert.equal(restored.values[second.code], '');
|
||||
assert.equal(Object.values(restored.values).includes('0'), false);
|
||||
}
|
||||
});
|
||||
test('foreign editions, unknown fields and malformed or oversized files cannot replace preparation', () => {
|
||||
const p = newFiscalPreparation('iva');
|
||||
for (const v of [{...p, sourceHash: 'different'}, {...p, values: {'999': '1'}}, {...p, values: []}, {...p, nuit: 123}, {...p, version: 'later'}]) assert.throws(() => readFiscalPreparation(JSON.stringify(v)));
|
||||
assert.throws(() => readFiscalPreparation('{broken'));
|
||||
assert.throws(() => readFiscalPreparation('x'.repeat(100001)));
|
||||
assert.equal(p.kind, 'iva'); assert.deepEqual(p.values, {});
|
||||
});
|
||||
test('invalid format is identified without rounding or discarding an incomplete preparation', () => {
|
||||
const p = newFiscalPreparation('irpc');
|
||||
p.values['303'] = '1.234'; p.values['304'] = '25,00';
|
||||
const restored = readFiscalPreparation(JSON.stringify(p));
|
||||
assert.equal(restored.values['303'], '1.234');
|
||||
assert.ok(fiscalPreparationIssues(restored).some(i => i.startsWith('Campo 303:')));
|
||||
assert.equal(fiscalFormFields('irpc').find(f => f.code === '303').label, 'Dupla tributação económica');
|
||||
assert.equal(fiscalFormFields('irpc').find(f => f.code === '304').label, 'Dupla tributação internacional');
|
||||
});
|
||||
Reference in New Issue
Block a user