Files
Investidor-V2/resources/js/components/simulation-irpc-annual.tsx
T
2026-11-15 18:56:39 +02:00

917 lines
41 KiB
TypeScript

import type { AdvanceFiscalPeriod } from '@/lib/irpc-ppc-obligation';
import { SimulationIrpcPecObligation } from './simulation-irpc-pec-obligation';
import { SimulationIrpcPpcObligation } from './simulation-irpc-ppc-obligation';
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
import { SimulationIrpcEconomic } from './simulation-irpc-economic';
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
import { SimulationIrpcCarry } from './simulation-irpc-carry';
import { SimulationIrpcInternational } from './simulation-irpc-international';
import { SimulationIrpcPec } from './simulation-irpc-pec';
import { useState } from 'react';
import {
advancePeriodBasis,
advanceCalendar,
newAdvancePeriodDeclaration,
advanceCashflowAmount,
annualAdvanceCashflow,
annualAdvancePeriod,
annualBook,
newAnnualBook,
saveAnnualBook,
reuseAnnualAssessment,
annualAssessment,
annualWithholdingOptions,
reuseAnnualWithholding,
newAnnualPpc,
recordAnnual,
annualReportRows,
annualHtml,
annualSource,
type AnnualBook,
type AnnualPpc,
} from '@/lib/irpc-annual-settlement';
type Props = {
draft: Record<string, unknown>;
onChange: (field: string, value: string) => void;
};
export function SimulationIrpcAnnual({ draft, onChange }: Props) {
const [error, setError] = useState('');
if (draft.fiscal_component !== 'irpc') return null;
if (
typeof draft.fiscal_period === 'string' &&
draft.fiscal_period &&
draft.fiscal_period !== '2026'
)
return (
<p role="alert">
Esta estimativa anual tem cobertura documentada para 2026. O
período escolhido, {draft.fiscal_period}, permanece inalterado;
os dados e resultados anteriores ficam preservados, sem aplicar
este cálculo a outro ano.
</p>
);
try {
const b = annualBook(draft),
save = (next: AnnualBook) => {
try {
onChange('modelo_a_snapshot', saveAnnualBook(draft, next));
setError('');
} catch (e) {
setError(String(e));
}
};
if (!b)
return (
<section
aria-label="IRPC — estimativa anual"
className="rounded border p-4"
>
<h4>Do imposto estimado ao saldo anual simulado</h4>
<p>
Reutiliza a colecta válida, retenções efectivamente
sofridas por conta e PPC comprovados do próprio
sujeito/exercício. PEC comprovado pode ser detalhado no
próprio exercício; créditos com regras próprias
permanecem separados.
</p>
<button
type="button"
onClick={() => {
try {
save(
reuseAnnualAssessment(
draft,
newAnnualBook(),
),
);
} catch (e) {
setError(String(e));
}
}}
>
Preparar estimativa anual
</button>
{error && <p role="alert">{error}</p>}
</section>
);
const patch = (p: Partial<AnnualBook>) => save({ ...b, ...p }),
a = annualAssessment(draft),
advancePeriod = annualAdvancePeriod(draft, b),
options = annualWithholdingOptions(draft);
const field = (
key: 'subjectNuit' | 'year' | 'reference' | 'assessmentReference',
label: string,
) => (
<label className="block">
{label}
<input
aria-label={label}
value={b[key]}
onChange={(e) => patch({ [key]: e.target.value })}
/>
</label>
);
const group = (key: 'withholdings' | 'ppc', label: string) => (
<>
<label className="block">
Estado — {label}
<select
aria-label={'Estado L8 — ' + label}
value={b[key].state}
onChange={(e) =>
patch({
[key]: {
...b[key],
state: e.target.value as
| ''
| 'known'
| 'absent',
},
})
}
>
<option value="">Informação em falta</option>
<option value="known">
Valores conhecidos — seleccionar comprovativos
</option>
<option value="absent">
Ausência declarada com suporte
</option>
</select>
</label>
<label className="block">
Suporte — {label}
<input
aria-label={'Suporte L8 — ' + label}
value={b[key].reference}
onChange={(e) =>
patch({
[key]: { ...b[key], reference: e.target.value },
})
}
/>
</label>
</>
);
return (
<section
aria-label="IRPC — estimativa anual"
className="space-y-3 rounded border p-4"
>
<h4>Imposto estimado, abatimentos e saldo anual simulado</h4>
<p>
A colecta acima é anterior aos abatimentos. Este saldo tem
cálculo e histórico próprios; retenção entregue pela empresa
por pagamento a terceiros não é retenção sofrida. O
resultado continua simulado, sem submissão, pagamento ou
reembolso automático.
</p>
<p>{annualSource(b)}</p>
{error && <p role="alert">{error}</p>}
{field('subjectNuit', 'NUIT do sujeito do fecho L8')}
{field('year', 'Exercício do fecho L8')}
{field(
'reference',
'Suporte do fecho da entidade e exercício L8',
)}
<p>
Colecta disponível:
{a.colecta
? a.colecta.numerator +
'/' +
a.colecta.denominator +
' MZN'
: 'Indeterminada'}
;origem:
{a.kind === 'special' ? 'L7 qualificado' : 'L6 normal'}
;NUIT:{a.subjectNuit || 'Por identificar'};exercício:
{a.year || 'Por identificar'}.
</p>
<button
type="button"
onClick={() => {
try {
save(reuseAnnualAssessment(draft, b));
} catch (e) {
setError(String(e));
}
}}
>
Reutilizar colecta corrente L6/L7
</button>
{field(
'assessmentReference',
'Suporte da colecta reutilizada L8',
)}
<fieldset className="space-y-2 rounded border p-3">
<legend>Retenções efectivamente sofridas por conta</legend>
{group('withholdings', 'Retenções sofridas')}
{options.length === 0 ? (
<p>
Não há afectações de comprovativosL5 neste contexto.
Reutilize e reconcilie a retenção recebida emL5 ou
declare ausência com suporte.
</p>
) : (
options.map((o) => (
<div
key={o.rowId + '|' + o.paymentId}
className="rounded border p-2"
>
<p>
{o.document || 'Prova por identificar'}
;emitente:{o.issuer};data:{o.date}
;parcelaL5:{o.amount}MZN;
{o.reason ||
'Retenção recebida/efectiva por conta reconciliada emL5.'}
</p>
<button
type="button"
disabled={
!o.ready ||
b.withholdings.rows.some(
(r) =>
r.rowId === o.rowId &&
r.paymentId === o.paymentId,
)
}
onClick={() => {
try {
save(
reuseAnnualWithholding(
draft,
b,
o.rowId,
o.paymentId,
crypto.randomUUID(),
),
);
} catch (e) {
setError(String(e));
}
}}
>
Seleccionar prova L5{' '}
{o.document || o.paymentId}
</button>
</div>
))
)}
{b.withholdings.rows.map((r, index) => {
const change = (p: Partial<typeof r>) =>
patch({
withholdings: {
...b.withholdings,
rows: b.withholdings.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>
Retenção seleccionada {index + 1}
</legend>
<p>
Rendimento:{r.rowId};prova:{r.paymentId}
;NUIT:{r.subjectNuit};exercício:{r.year}.
Fonte conservada. Se mudou, retire esta
selecção e seleccione a prova actual. O
histórico mantém-se.
</p>
<label className="block">
Parcela a abater
<input
aria-label={
'Parcela da retenção L8 ' +
(index + 1)
}
value={r.amount}
onChange={(e) =>
change({ amount: e.target.value })
}
/>
</label>
<label className="block">
O próprio sujeito é beneficiário e o
rendimento está incluído na base deste
exercício
<select
aria-label={
'Rendimento incluído L8 ' +
(index + 1)
}
value={r.incomeIncluded}
onChange={(e) =>
change({
incomeIncluded: e.target
.value as '' | 'yes' | 'no',
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — documentado
</option>
<option value="no">
Não — crédito não aplicável neste
fecho
</option>
</select>
</label>
<label className="block">
Suporte da titularidade e inclusão
<input
aria-label={
'Suporte da retenção L8 ' +
(index + 1)
}
value={r.reference}
onChange={(e) =>
change({
reference: e.target.value,
})
}
/>
</label>
<FiscalProofParcelFields
value={r}
change={change}
label={'Retenção L8 ' + (index + 1)}
/>
<button
type="button"
onClick={() =>
patch({
withholdings: {
...b.withholdings,
rows: b.withholdings.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar selecção L8 da retenção {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>Estimativas PPC e PEC para a viabilidade</legend>
<p>
Simulação fiscal para avaliar a viabilidade do negócio e
as necessidades de tesouraria. Os valores e meses
previstos são estimativas; o portal não executa
pagamentos nem submete declarações fiscais.
</p>
<SimulationIrpcAdvancePeriodDeclaration
draft={draft}
book={b}
onChange={(declaration) =>
patch({ advancePeriodDeclaration: declaration })
}
/>
<SimulationIrpcPecObligation
book={b}
period={advancePeriod}
onChange={(pecObligation) => patch({ pecObligation })}
/>
<SimulationIrpcPpcObligation
book={b}
period={advancePeriod}
onChange={(ppcObligation) => patch({ ppcObligation })}
/>
<SimulationIrpcAdvanceCashflow
book={b}
period={advancePeriod}
/>
</fieldset>
<fieldset className="space-y-2 rounded border p-3">
<legend>PPC documentados como pagos</legend>
{group('ppc', 'PPC efectuados')}
<p>
Regista comprovativos de pagamentos por conta; não
calcula automaticamente a obrigação PPC nem usa
pagamentos previstos no plano.
</p>
<button
type="button"
onClick={() =>
patch({
ppc: {
...b.ppc,
rows: [
...b.ppc.rows,
{
...newAnnualPpc(
crypto.randomUUID(),
),
subjectNuit: b.subjectNuit,
year: b.year,
},
],
},
})
}
>
Registar comprovativo PPC
</button>
{b.ppc.rows.map((r, index) => {
const change = (p: Partial<AnnualPpc>) =>
patch({
ppc: {
...b.ppc,
rows: b.ppc.rows.map((x) =>
x.id === r.id ? { ...x, ...p } : x,
),
},
});
return (
<fieldset key={r.id} className="rounded border p-2">
<legend>PPC {index + 1}</legend>
{(
[
['document', 'Comprovativo'],
['issuer', 'Emitente'],
['date', 'Data (AAAA-MM-DD)'],
['subjectNuit', 'NUIT beneficiário'],
['year', 'Exercício do comprovativo'],
['currency', 'Moeda do comprovativo'],
['proofAmount', 'Montante comprovado'],
['amount', 'Parcela a abater'],
[
'reference',
'Suporte do pagamento e afectação',
],
] as const
).map(([key, label]) => (
<label key={key} className="block">
{label}
<input
aria-label={
'PPC L8 ' +
(index + 1) +
' — ' +
label
}
value={r[key]}
onChange={(e) =>
change({
[key]: e.target.value,
})
}
/>
</label>
))}
<label className="block">
Pagamento efectivamente comprovado
<select
aria-label={
'PPC L8 ' +
(index + 1) +
' — Efectivo'
}
value={r.effective}
onChange={(e) =>
change({
effective: e.target.value as
| ''
| 'yes'
| 'no',
})
}
>
<option value="">
Informação em falta
</option>
<option value="yes">
Sim — comprovativo reconciliado
</option>
<option value="no">
Não — apenas previsão/outro estado
</option>
</select>
</label>
<FiscalProofParcelFields
value={r}
change={change}
label={'PPC L8 ' + (index + 1)}
/>
<button
type="button"
onClick={() =>
patch({
ppc: {
...b.ppc,
rows: b.ppc.rows.filter(
(x) => x.id !== r.id,
),
},
})
}
>
Retirar PPC {index + 1}
</button>
</fieldset>
);
})}
</fieldset>
<SimulationIrpcEconomic
book={b}
save={save}
residentConfirmed={
a.current &&
a.book.scope === 'resident_general' &&
b.subjectNuit === a.subjectNuit &&
b.year === a.year &&
b.assessmentKey === a.key
}
/>
<SimulationIrpcInternational book={b} save={save} />
<SimulationIrpcInvestment draft={draft} book={b} save={save} />
<SimulationIrpcPec book={b} save={save} />
<SimulationIrpcCarry book={b} save={save} />
{[
...(b.pecPayments ? [] : [['pec', 'PEC'] as const]),
...(b.internationalCredits
? []
: [
[
'credits',
'Créditos com regras próprias',
] as const,
]),
].map(([key, label]) => (
<fieldset key={key} className="rounded border p-3">
<legend>{label} — tratamento separado</legend>
<label className="block">
Estado — {label}
<select
aria-label={'Estado L8 — ' + label}
value={b[key].state}
onChange={(e) =>
patch({
[key]: {
...b[key],
state: e.target.value as
| ''
| 'known'
| 'absent',
},
})
}
>
<option value="">Informação em falta</option>
<option value="absent">
Ausência declarada com suporte
</option>
<option value="known">
Valor conhecido — positivo exige regra
própria
</option>
</select>
</label>
{(b[key].state === 'known' || b[key].amount) && (
<label className="block">
Montante declarado
<input
aria-label={'Montante L8 — ' + label}
value={b[key].amount}
onChange={(e) =>
patch({
[key]: {
...b[key],
amount: e.target.value,
},
})
}
/>
</label>
)}
<label className="block">
Suporte
<input
aria-label={'Suporte L8 — ' + label}
value={b[key].reference}
onChange={(e) =>
patch({
[key]: {
...b[key],
reference: e.target.value,
},
})
}
/>
</label>
</fieldset>
))}
<button
type="button"
onClick={() => {
try {
save(recordAnnual(draft, b));
} catch (e) {
setError(String(e));
}
}}
>
Calcular e guardar saldo anual L8
</button>
<button
type="button"
onClick={() => {
try {
const html = annualHtml(draft),
url = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
link = document.createElement('a');
link.href = url;
link.download = 'IRPC-L8-saldo-simulado.html';
link.click();
URL.revokeObjectURL(url);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar saldo anual L8
</button>
{annualReportRows(draft).map((r, n) => (
<p key={n}>
{r[1]} — {r[2]} — {r[3]}
</p>
))}
</section>
);
} catch (e) {
return (
<p role="alert">
PreparaçãoL8 não recuperável:{String(e)}. Originais e históricos
conservados.
</p>
);
}
}
export function SimulationIrpcAdvanceCashflow({
book,
period,
}: {
book: AnnualBook;
period?: AdvanceFiscalPeriod;
}) {
if (!book.ppcObligation && !book.pecObligation) return null;
const m = annualAdvanceCashflow(book, period);
const show = advanceCashflowAmount;
return (
<section
className="space-y-3 rounded border p-3"
aria-label="Projecção conjunta PPC e PEC"
>
<h5 className="font-semibold">Saídas previstas PPC/PEC por mês</h5>
{period && (
<p>
Período preparado em L6: {period.from || 'por confirmar'} —{' '}
{period.to || 'por confirmar'}.
</p>
)}
<p>
Reúne as estimativas PPC e a hipótese de tesouraria PEC. Os
comprovativos pagos e o fecho anual permanecem separados; esta
vista não acrescenta outra saída ao fluxo financeiro do negócio.
</p>
<p role="status">
{m.total
? 'Total da projecção: ' + show(m.total)
: 'Projecção parcial — total por determinar.'}
</p>
{!!m.pending.length && (
<ul className="list-disc pl-5">
{m.pending.map((p) => (
<li key={p}>{p}</li>
))}
</ul>
)}
{!!m.rows.length && (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<caption className="sr-only">
Projecção mensal das estimativas PPC e PEC
</caption>
<thead>
<tr>
<th className="p-2">Mês</th>
<th className="p-2">PPC</th>
<th className="p-2">PEC</th>
<th className="p-2">
{m.total
? 'Total previsto'
: 'Subtotal conhecido'}
</th>
</tr>
</thead>
<tbody>
{m.rows.map((r) => (
<tr key={r.month} className="border-t">
<th scope="row" className="p-2">
{r.month}
</th>
<td className="p-2">
{m.ppcKnown
? show(r.ppc)
: 'Por determinar'}
</td>
<td className="p-2">
{m.pecKnown
? show(r.pec)
: 'Por determinar'}
</td>
<td className="p-2">
{show(r.knownSubtotal)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{!m.rows.length && m.total && (
<p>Sem saída PPC/PEC estimada neste recorte.</p>
)}
<p>
Meses e montantes dependem das fontes e opções preparadas acima.
A opção PEC concentrada conserva o mês escolhido; esta
apresentação não certifica o prazo legal.
</p>
</section>
);
}
export function SimulationIrpcAdvancePeriodDeclaration({
draft,
book,
onChange,
}: {
draft: Record<string, unknown>;
book: AnnualBook;
onChange: (
value: NonNullable<AnnualBook['advancePeriodDeclaration']>,
) => void;
}) {
const period = annualAdvancePeriod(draft, book),
calendar = advanceCalendar(period, book.year, book.subjectNuit),
d = book.advancePeriodDeclaration;
if (!d && !calendar.candidate) return null;
if (!d)
return (
<section className="rounded border p-3">
<h5>Período anual não civil</h5>
<p>
Confronte o fundamento do período L6 e os doze meses do
período anterior. As datas sugeridas não substituem o
suporte documental.
</p>
<button
type="button"
className="rounded border p-2"
onClick={() =>
onChange(
newAdvancePeriodDeclaration(
book.subjectNuit,
book.year,
period,
),
)
}
>
Preparar correspondência dos períodos
</button>
</section>
);
const patch = (values: Partial<typeof d>) =>
onChange({ ...d, ...values, confirmed: '' });
const labels = {
subjectNuit: 'NUIT dos períodos',
year: 'Exercício de termo',
currentFrom: 'Início do período L6 declarado',
currentTo: 'Termo do período L6 declarado',
currentReference: 'Fundamento documentado do período anual não civil',
previousFrom: 'Início do período anterior',
previousTo: 'Termo do período anterior',
previousReference: 'Fonte e correspondência do período anterior',
};
return (
<section
className="space-y-2 rounded border p-3"
aria-label="Correspondência dos períodos anuais"
>
<h5>Correspondência dos períodos anuais</h5>
<p role="status">
{calendar.mode === 'annual_non_civil'
? 'Correspondência documental conferida;previsões nos meses do período L6.'
: calendar.mode === 'civil'
? 'L6 usa o ano civil;referência anterior conservada sem aplicação.'
: calendar.pending.join(' ')}
</p>
<p>
PPC: imposto anterior{' '}
{book.ppcObligation?.previousTax || 'por confirmar'}{' '}
MZN;retenções{' '}
{book.ppcObligation?.previousWithholdings || 'por confirmar'}{' '}
MZN. PEC: volume anterior{' '}
{book.pecObligation?.previousTurnover || 'por confirmar'}{' '}
MZN;PPC anteriores{' '}
{book.pecObligation?.previousPpcPaid || 'por confirmar'} MZN.
</p>
{d.confirmed === 'yes' &&
(d.ppcBasisKey !== advancePeriodBasis(book, 'ppc') ||
d.pecBasisKey !== advancePeriodBasis(book, 'pec')) && (
<button
type="button"
className="rounded border p-2"
onClick={() =>
onChange({
...d,
ppcBasisKey: advancePeriodBasis(book, 'ppc'),
pecBasisKey: advancePeriodBasis(book, 'pec'),
})
}
>
Reconferi os montantes e as fontes neste período
</button>
)}
<details
open={d.confirmed !== 'yes' || calendar.pending.length > 0}
>
<summary>Conferir ou alterar períodos</summary>
<label className="block">
Aplicação
<select
aria-label="Aplicação do período não civil"
value={d.mode}
onChange={(e) =>
patch({ mode: e.target.value as typeof d.mode })
}
>
<option value="annual_non_civil">
Período anual não civil
</option>
<option value="">Conservar sem aplicação</option>
</select>
</label>
{(Object.keys(labels) as Array<keyof typeof labels>).map(
(key) => (
<label className="block" key={key}>
{labels[key]}
<input
className="mt-1 block w-full rounded border px-3 py-2"
aria-label={labels[key]}
value={d[key]}
onChange={(e) =>
patch({ [key]: e.target.value })
}
/>
</label>
),
)}
<p>
No PPC, indique que o período anterior não é um ano civil
completo. Os doze meses e a continuidade são conferidos
pelas datas e fontes aqui indicadas.
</p>
<label className="block">
<input
type="checkbox"
checked={d.confirmed === 'yes'}
onChange={(e) =>
onChange({
...d,
confirmed: e.target.checked ? 'yes' : '',
ppcBasisKey: advancePeriodBasis(book, 'ppc'),
pecBasisKey: advancePeriodBasis(book, 'pec'),
})
}
/>
Conferi no suporte os dois períodos, o fundamento do
enquadramento e a correspondência dos montantes e fontes
PPC/PEC
</label>
</details>
<p>
Guardar conserva estas escolhas. Uma alteração nas datas L6
exige reconferência; o portal não autoriza uma mudança do
período fiscal.
</p>
</section>
);
}