Files
Investidor-V2/resources/js/components/simulation-irpc-mrfta.tsx
T

610 lines
26 KiB
TypeScript

import { inputText } from '../lib/input-text.ts';
import {
newIrpcInterestRule,
type IrpcInterestRule,
} from '@/lib/irpc-interest';
import { mrftaCases, mrftaCaseHtml } from '@/lib/irpc-mrfta-cases';
import { useState } from 'react';
import { irpcCurrent, type IrpcBook } from '@/lib/irpc-withholdings';
import {
newMrftaInput,
recordMrfta,
mrftaHtml,
mrftaReportRows,
mrftaCurrent,
type MrftaInput,
} from '@/lib/irpc-mrfta';
export function SimulationIrpcMrfta({
draft,
book,
save,
}: {
draft: Record<string, unknown>;
book: IrpcBook;
save: (book: IrpcBook) => void;
}) {
const [error, setError] = useState(''),
i = book.mrfta ?? newMrftaInput();
const change = (patch: Partial<MrftaInput>) =>
save({ ...book, mrfta: { ...i, ...patch } });
const interestMode =
i.interest.mode ||
(i.interest.calculation === 'automatic' ? 'automatic' : '');
const interest = (patch: Partial<MrftaInput['interest']>) =>
change({ interest: { ...i.interest, ...patch } });
const field = (
key:
| 'period'
| 'nuit'
| 'name'
| 'commercialName'
| 'taxArea'
| 'address'
| 'contact'
| 'completenessRef'
| 'otherRubricsRef',
label: string,
type = 'text',
) => (
<label className="block">
{label}
<input
className="block rounded border p-2"
type={type}
value={i[key]}
onChange={(e) => change({ [key]: e.target.value })}
/>
</label>
);
const select = (
key: 'declaration' | 'timing' | 'completeness' | 'otherRubrics',
label: string,
options: Array<[string, string]>,
) => (
<label className="block">
{label}
<select
value={i[key]}
onChange={(e) => change({ [key]: e.target.value })}
>
{options.map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
);
const periods = [
...new Set(
book.result?.rows.map((r) => r.period).filter(Boolean) ?? [],
),
];
const identities = Object.entries(
(JSON.parse(inputText(draft.modelo_a_snapshot || '{}'))
.irpcWithholdings ?? {}) as Record<string, IrpcBook>,
).filter(
([, b]) =>
b.mrfta?.nuit &&
b.mrfta?.name &&
(!i.nuit.trim() || i.nuit === b.mrfta.nuit) &&
(!i.name.trim() || i.name === b.mrfta.name) &&
(
[
'nuit',
'name',
'commercialName',
'taxArea',
'address',
'contact',
] as const
).some((key) => !i[key].trim() && b.mrfta![key].trim()),
);
return (
<fieldset
aria-label="Preparação MRFTA e juros"
className="space-y-3 rounded border p-4"
>
<legend>MRFTA — preenchimento e juros compensatórios</legend>
<p>
Reutiliza o cálculo guardado, documentos e entregas IRPC. Um
mês/ano por declaração. A retenção sofrida e as dispensas ficam
separadas. Quadros 8/9 reservados à assinatura e aos serviços;
não há submissão ou aceitação AT.
</p>
{identities.map(([scope, b]) => (
<button
key={scope}
type="button"
onClick={() => {
const p: Partial<MrftaInput> = {};
for (const key of [
'nuit',
'name',
'commercialName',
'taxArea',
'address',
'contact',
] as const)
if (!i[key].trim()) p[key] = b.mrfta![key];
change(p);
}}
>
Reutilizar identidade fiscal {b.mrfta!.name} ({scope}) nos
campos vazios
</button>
))}
<div className="grid gap-3 md:grid-cols-2">
{field('period', 'Quadro 2 — mês/ano', 'month')}
<label>
Período existente
<select
value=""
onChange={(e) => {
if (e.target.value)
change({ period: e.target.value });
}}
>
<option value="">Seleccione</option>
{periods.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</label>
{select('declaration', 'Quadro 1 — retenção na fonte', [
['', 'Por identificar'],
['initial', 'Inicial'],
['replacement', 'Substituição'],
])}
{select('timing', 'Quadro 2 — situação do prazo', [
['', 'Por esclarecer'],
['within', 'Dentro do prazo'],
['late', 'Fora do prazo'],
])}
{field('nuit', 'Quadro 3 — NUIT')}
{field('taxArea', 'Quadro 3 — área fiscal')}
{field('name', 'Quadro 4 — nome do sujeito passivo')}
{field('commercialName', 'Quadro 5 — nome comercial')}
{field('address', 'Quadro 6 — domicílio fiscal')}
{field('contact', 'Quadro 6 — contacto')}
{select(
'completeness',
'Retenções do sujeito passivo neste período conferidas, incluindo demais livros',
[
['', 'Por esclarecer'],
['yes', 'Sim'],
['no', 'Ainda não'],
],
)}
{field(
'completenessRef',
'Referência da reconciliação do período',
)}
{select(
'otherRubrics',
'IRPS, tributação autónoma e outras rubricas fora deste recorte',
[
['', 'Por esclarecer'],
[
'none',
'Ausência de outros valores devidos documentada',
],
['pending', 'Existem ou estão por apurar'],
],
)}
{field(
'otherRubricsRef',
'Documento da situação das outras rubricas',
)}
</div>
<h5>Quadro 7 — CER das rubricas correspondidas</h5>
<p>
Base, taxa e imposto vêm do cálculo guardado. O CER é transcrito
da referência aplicável; os números de campos da guia antiga não
são automaticamente códigos de receita. Códigos introduzidos não
são validados por catálogo oficial; códigos provisórios ou de
simulação não são correspondências oficiais.
</p>
{book.result?.inputs
.filter(
(r) =>
r.direction === 'paid' &&
book.result?.rows.some(
(v) =>
v.id === r.id &&
v.period === i.period &&
v.status === 'calculated' &&
v.treatment !== 'dispensed',
),
)
.map((r) => {
const code = i.codes.find((c) => c.rowId === r.id) ?? {
rowId: r.id,
cer: '',
reference: '',
};
const update = (p: Partial<typeof code>) =>
change({
codes: [
...i.codes.filter((c) => c.rowId !== r.id),
{ ...code, ...p },
],
});
return (
<div key={r.id} className="grid gap-2 md:grid-cols-3">
<span>
{r.documentRef} — {r.counterparty}
</span>
<label>
CER
<input
value={code.cer}
onChange={(e) =>
update({ cer: e.target.value })
}
/>
</label>
<label>
Documento / referência do CER
<input
value={code.reference}
onChange={(e) =>
update({ reference: e.target.value })
}
/>
</label>
</div>
);
})}
<h5>Juros compensatórios — separados do principal</h5>
<p>
Regulamento IRPC, artigos 25/35: a taxa refere MAIBOR de 12
meses + 2 pontos. O Banco de Moçambique documenta a
descontinuidade da MAIBOR. O portal não adopta MIMO ou Prime
como substituto nem calcula juros de mora.
</p>
<label>
Tratamento dos juros
<select
value={interestMode}
onChange={(e) =>
interest(
e.target.value === 'automatic'
? { mode: '', calculation: 'automatic' }
: {
mode: e.target
.value as MrftaInput['interest']['mode'],
calculation: undefined,
},
)
}
>
<option value="">
Informação insuficiente / cálculo automático por
resolver
</option>
<option value="assessment">
Transcrever liquidação documentada de juros
compensatórios
</option>
<option value="automatic">
Cálculo com taxa e regra documentadas — sem parâmetro
pré-definido
</option>
<option value="timely">
Conferir entrega integral dentro do prazo documentado
</option>
</select>
</label>
{interestMode && (
<>
<label>
Período documentado dos juros / entregas
<input
type="month"
value={i.interest.period}
onChange={(e) =>
interest({ period: e.target.value })
}
/>
</label>
<label>
Documento da liquidação / reconciliação
<input
value={i.interest.reference}
onChange={(e) =>
interest({ reference: e.target.value })
}
/>
</label>
</>
)}
{['assessment', 'automatic'].includes(interestMode) && (
<>
{i.interest.mode === 'assessment' && (
<label>
Montante dos juros em MZN
<input
value={i.interest.amount}
onChange={(e) =>
interest({ amount: e.target.value })
}
/>
</label>
)}
<label>
Data da liquidação
<input
type="date"
value={i.interest.date}
onChange={(e) => interest({ date: e.target.value })}
/>
</label>
</>
)}
{['timely', 'automatic'].includes(interestMode) && (
<>
<label>
Prazo aplicável documentado
<input
type="date"
value={i.interest.dueDate}
onChange={(e) =>
interest({ dueDate: e.target.value })
}
/>
</label>
<label>
Fonte do prazo, incluindo eventual ajuste de calendário
<input
value={i.interest.dueReference}
onChange={(e) =>
interest({ dueReference: e.target.value })
}
/>
</label>
</>
)}
{interestMode === 'automatic' && (
<fieldset className="space-y-2 rounded border p-2">
<legend>
Parâmetros documentados do cálculo — sem taxa fiscal
de2026 pré-definida
</legend>
<p>
Simulação pelos pressupostos introduzidos, sem
confirmação fiscal dos juros. Preencha apenas a regra
aplicável ao caso. Sem parâmetro completo, os juros
continuam indeterminados. A taxa bancária actual não é
adoptada por substituição. Imposto por conta não retido
e juros de mora mantêm regra própria.
</p>
{(
[
'annualRate',
'rateSource',
'fiscalBasis',
'validFrom',
'validTo',
'dayBasisRef',
'roundingRef',
'occurrenceRef',
] as const
).map((k) => (
<label key={k} className="block">
{
{
annualRate:
'Taxa anual final (%) documentada, incluindo acréscimos aplicáveis',
rateSource: 'Documento e citação da taxa',
fiscalBasis:
'Fundamento da aplicação fiscal ao caso',
validFrom: 'Parâmetro aplicável desde',
validTo: 'Parâmetro aplicável até',
dayBasisRef:
'Documento da base anual e contagem diária',
roundingRef:
'Documento do arredondamento do total',
occurrenceRef:
'Documento da retenção e ocorrência',
}[k]
}
<input
type={
k === 'validFrom' || k === 'validTo'
? 'date'
: 'text'
}
value={i.interest.automatic?.[k] ?? ''}
onChange={(e) =>
interest({
automatic: {
...(i.interest.automatic ??
newIrpcInterestRule()),
[k]: e.target.value,
},
})
}
/>
</label>
))}
{(['dayBasis', 'rounding', 'retention'] as const).map(
(k) => (
<label key={k} className="block">
{
{
dayBasis:
'Base anual conforme documento',
rounding:
'Arredondamento do total conforme documento',
retention: 'Retenção efectuada',
}[k]
}
<select
value={i.interest.automatic?.[k] ?? ''}
onChange={(e) =>
interest({
automatic: {
...(i.interest.automatic ??
newIrpcInterestRule()),
[k]: e.target.value,
} as IrpcInterestRule,
})
}
>
{(k === 'dayBasis'
? [
['', 'Por identificar'],
['365', '365 dias'],
['366', '366 dias'],
[
'actual_year',
'Dias efectivos de cada ano',
],
]
: k === 'rounding'
? [
['', 'Por identificar'],
[
'total_cent_half_up',
'Total aos cêntimos — metade por excesso',
],
[
'total_cent_down',
'Total aos cêntimos — por defeito',
],
]
: [
['', 'Por identificar'],
[
'withheld',
'Sim, documentada',
],
[
'not_withheld',
'Não efectuada, documentado',
],
]
).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</select>
</label>
),
)}
</fieldset>
)}
<button
type="button"
className="rounded border p-2"
onClick={() => {
try {
save(recordMrfta(book, irpcCurrent(draft, book)));
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Preparar e guardar MRFTA
</button>
<button
type="button"
className="rounded border p-2"
onClick={() => {
try {
const html = mrftaHtml(book, irpcCurrent(draft, book)),
u = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
a = document.createElement('a');
a.href = u;
a.download =
'MRFTA-' +
(i.period || 'periodo') +
'-preparacao.html';
a.click();
URL.revokeObjectURL(u);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar preenchimento MRFTA
</button>
{book.mrftaResult && (
<fieldset className="space-y-2 rounded border p-2">
<legend>
Casos de trabalho separados por período, rubrica, taxa e
CER
</legend>
<p>
Estas fichas conservam as pendências do período. Não são
declarações autónomas, anexos oficiais ou uma
autorização para fundir taxas.
</p>
{mrftaCases(book.mrftaResult.model).map((c) => (
<div key={c.key}>
<span>
{c.period}; {c.rubric}; {c.rate}%; CER:
{c.cer || 'por corresponder'}; documentos:
{c.documents.join('; ')}.
</span>
<button
type="button"
onClick={() => {
try {
if (
!irpcCurrent(draft, book) ||
!mrftaCurrent(book, true)
)
throw Error(
'Reprepare as entradas actuais antes de exportar o caso.',
);
const html = mrftaCaseHtml(
book.mrftaResult!.model,
c.key,
),
u = URL.createObjectURL(
new Blob([html], {
type: 'text/html;charset=utf-8',
}),
),
a = document.createElement('a');
a.href = u;
a.download =
'MRFTA-caso-' + c.key + '.html';
a.click();
URL.revokeObjectURL(u);
setError('');
} catch (e) {
setError(String(e));
}
}}
>
Exportar ficha de trabalho {c.rate}% /{' '}
{c.cer || 'CER pendente'}
</button>
</div>
))}
</fieldset>
)}
{error && <p role="alert">{error}</p>}
{mrftaReportRows(book, irpcCurrent(draft, book)).map((r, n) => (
<p key={n}>
<strong>{r[1]}</strong>: {r[2]}
<small>{r[3]}</small>
</p>
))}
</fieldset>
);
}