Files
Investidor-V2/resources/js/components/simulation-fiscal-status.tsx
T

427 lines
20 KiB
TypeScript

import { useEffect, useState } from 'react';
import { fiscalComponentCodes } from '@/lib/simulation-contract';
import {
ivaIssueText,
type IvaDiagnostic,
type IvaIssue,
} from '@/lib/iva-diagnostics';
type Parameter = {
parameter_code: string;
status: string;
value: string | null;
unit?: string;
scope_key?: string;
rule_code?: string;
rule_version?: number;
validity?: { start: string; end: string | null };
source?: {
title: string;
diploma: string | null;
article: string | null;
url: string | null;
};
};
const labels: Record<string, string> = {
indeterminate_missing:
'Não existe parâmetro aprovado para este âmbito e período.',
indeterminate_context:
'As condições publicadas não correspondem às respostas indicadas.',
indeterminate_scope: 'O âmbito da regra não está suficientemente definido.',
indeterminate_overlap: 'Há regras sobrepostas; é necessária revisão.',
indeterminate_period_change:
'A vigência muda dentro do período seleccionado.',
indeterminate_invalid:
'O parâmetro publicado não passou a validação de formato.',
};
const apiComponents: Record<string, string> = {
irpc: 'IRPC',
iva: 'IVA',
ispc: 'ISPC',
irps: 'IRPS',
inss: 'INSS',
withholding: 'RETENCOES',
retenções: 'RETENCOES',
};
function getXsrfToken(): string {
return decodeURIComponent(
document.cookie
.split('; ')
.find((cookie) => cookie.startsWith('XSRF-TOKEN='))
?.slice(11) ?? '',
);
}
export function SimulationFiscalStatus({
salaryView,
scenarioMode,
component,
periodStart,
periodEnd,
context,
onDiagnostics,
onCorrect,
}: {
salaryView?: string;
scenarioMode?: string;
component?: string;
periodStart?: string;
periodEnd?: string;
context?: Record<string, string>;
onDiagnostics?: (issues: IvaIssue[]) => void;
onCorrect?: (field: string) => void;
}) {
const [parameters, setParameters] = useState<Parameter[]>([]);
const [diagnostics, setDiagnostics] = useState<IvaDiagnostic[]>([]);
const [requestState, setRequestState] = useState('idle');
const [retry, setRetry] = useState(0);
useEffect(() => {
const apiComponent = component
? apiComponents[component.toLowerCase()]
: undefined;
if (
scenarioMode !== 'approved_parameters' ||
!apiComponent ||
!periodStart ||
!periodEnd
) {
setParameters([]);
setDiagnostics([]);
onDiagnostics?.([]);
setRequestState('idle');
return;
}
const abort = new AbortController();
setDiagnostics([]);
onDiagnostics?.([]);
setRequestState('loading');
void fetch('/api/v1/tax-parameters/resolve', {
method: 'POST',
credentials: 'same-origin',
signal: abort.signal,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-XSRF-TOKEN': getXsrfToken(),
},
body: JSON.stringify({
component: apiComponent,
period_start: periodStart,
period_end: periodEnd,
context: context ?? {},
}),
})
.then(async (response) => {
if (!response.ok) throw new Error('lookup_failed');
const payload = (await response.json()) as {
parameters?: Parameter[];
diagnostics?: IvaDiagnostic[];
};
if (abort.signal.aborted) return;
setParameters(
Array.isArray(payload.parameters) ? payload.parameters : [],
);
const details = Array.isArray(payload.diagnostics)
? payload.diagnostics
: [];
setDiagnostics(details);
onDiagnostics?.(details.flatMap((detail) => detail.issues));
setRequestState('done');
})
.catch((error: unknown) => {
if (abort.signal.aborted) return;
if (
error instanceof DOMException &&
error.name === 'AbortError'
)
return;
setParameters([]);
setRequestState('error');
});
return () => abort.abort();
}, [
scenarioMode,
component,
periodStart,
periodEnd,
JSON.stringify(context ?? {}),
retry,
]);
const salaryViewMessages: Record<string, string> = {
monthly_net_estimate:
'Estimativa líquida mensal; retenção mensal e apuramento anual são resultados distintos.',
monthly_withholding:
'Retenção mensal não representa, por si só, o apuramento anual nem o cumprimento integral das obrigações fiscais.',
annual_assessment:
'Apuramento anual; retenções mensais não o substituem.',
};
return (
<section
className="space-y-3 rounded-lg border border-amber-400 bg-amber-50 p-4"
aria-labelledby="fiscal-status-title"
>
<h3 id="fiscal-status-title" className="font-semibold">
Componentes fiscais e contributivas
</h3>
<p className="text-sm">
A consulta identifica parâmetros publicados e as respectivas
fontes. Não executa fórmulas nem determina imposto devido ou
elegibilidade fiscal.
</p>
{scenarioMode === 'approved_parameters' && (
<p role="status" className="text-sm">
{requestState === 'loading'
? 'A consultar parâmetros aprovados para o período e âmbito indicados…'
: requestState === 'error'
? 'Falha na consulta do sistema: não foi possível obter os parâmetros. O problema não é atribuído às respostas; estas foram preservadas. Tente novamente.'
: !periodStart || !periodEnd
? 'Indique um período exacto. Para periodicidade mensal ou trimestral, seleccione o mês ou trimestre.'
: `Período consultado: ${periodStart} a ${periodEnd}.`}
</p>
)}
{requestState === 'error' &&
scenarioMode === 'approved_parameters' && (
<button
type="button"
onClick={() => setRetry((value) => value + 1)}
className="rounded border border-amber-900 px-4 py-2 text-amber-950 underline focus-visible:outline-2 focus-visible:outline-offset-2"
>
Tentar novamente
</button>
)}
{scenarioMode === 'custom_planning' && (
<p role="note" className="text-sm">
Modo de planeamento separado: pressupostos do utilizador,
não taxas legais confirmadas. Nenhum imposto ou
elegibilidade é calculado.
</p>
)}
{salaryView && salaryViewMessages[salaryView] && (
<p role="note" className="text-sm">
{salaryViewMessages[salaryView]}
</p>
)}
{diagnostics.length > 0 && (
<div
role="alert"
className="space-y-3 rounded border border-amber-700 p-3"
>
<h4 className="font-semibold">
IVA — taxa geral: condições não satisfeitas
</h4>
<p>
Não foi possível resolver este parâmetro. As respostas
foram preservadas; esta consulta não executa o cálculo
do imposto.
</p>
{diagnostics.map((detail, index) => (
<div key={index}>
<ul className="list-disc pl-5">
{detail.issues.map((issue, issueIndex) => (
<li key={issueIndex}>
{ivaIssueText(issue)}{' '}
{issue.field && (
<a
className="underline"
href={`#field-${issue.field}`}
onClick={(event) => {
if (onCorrect) {
event.preventDefault();
onCorrect(issue.field!);
}
}}
>
Rever{' '}
{issue.label.toLowerCase()}
</a>
)}
{issue.category ===
'system_configuration' &&
issue.field ===
'fiscal_operation' && (
<a
className="ml-2 underline"
href="#published-iva"
>
Abrir IVA — taxa geral
</a>
)}
</li>
))}
</ul>
<details className="mt-2">
<summary className="cursor-pointer underline">
Ver fundamento
</summary>
<p>
IVA — taxa geral · versão{' '}
{detail.rule_version}. Vigência:{' '}
{detail.validity.start} a{' '}
{detail.validity.end ??
'sem data final indicada'}
.
</p>
<p>
{detail.source.title} —{' '}
{detail.source.diploma},{' '}
{detail.source.article}
</p>
{detail.source.url?.startsWith('https://') && (
<a
className="underline"
href={detail.source.url}
target="_blank"
rel="noreferrer"
>
Consultar fonte
</a>
)}
</details>
</div>
))}
</div>
)}
<ul className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{fiscalComponentCodes.map((code) => {
const selected =
code ===
(component === 'withholding'
? 'RETENCOES'
: component?.toUpperCase());
return (
<li
key={code}
className="rounded border border-amber-200 bg-white p-3"
aria-label={`${code}: ${selected ? 'consulta seleccionada' : 'não consultado'}`}
>
<strong>
{code === 'RETENCOES' ? 'Retenções' : code}
</strong>
{!selected ||
scenarioMode !== 'approved_parameters' ? (
<p className="text-sm">Sem cálculo fiscal</p>
) : requestState !== 'done' ? (
<p className="text-sm">
Indeterminado — sem cálculo
</p>
) : parameters.length === 0 ? (
<p className="text-sm">
{code === 'IVA' ? (
'Indeterminado — não existe uma versão elegível visível nesta consulta. Consulte o diagnóstico do período acima, quando disponível. Sem esse diagnóstico, não é possível distinguir ausência de cobertura de indisponibilidade da regra ou da fonte; não se atribui o problema às suas respostas.'
) : (
<>
Indeterminado — nenhum parâmetro
publicado aplicável. Faltam
parâmetros aprovados para {code}, no
período {periodStart} a {periodEnd}{' '}
e no âmbito indicado. A consulta não
identifica um conjunto obrigatório
de parâmetros; não é possível
determinar o imposto nem substituir
as lacunas por zero.
</>
)}
</p>
) : (
<div className="space-y-2 text-sm">
{parameters.map((parameter) => (
<div
key={parameter.parameter_code}
className="border-t pt-2"
>
<strong>
{parameter.parameter_code ===
'IVA_GENERAL_RATE'
? 'IVA — taxa geral'
: parameter.parameter_code}
</strong>
{parameter.status === 'resolved' ? (
<>
<p>
Parâmetro:{' '}
{parameter.value}{' '}
{parameter.unit}
</p>
<p>
Regra{' '}
{parameter.parameter_code ===
'IVA_GENERAL_RATE'
? 'IVA — taxa geral'
: parameter.rule_code}{' '}
v
{parameter.rule_version}
; âmbito{' '}
{parameter.parameter_code ===
'IVA_GENERAL_RATE'
? 'operação interna geral sobre base líquida em MZN'
: parameter.scope_key}
</p>
<p>
Vigência:{' '}
{
parameter.validity
?.start
}{' '}
a{' '}
{parameter.validity
?.end ??
'sem data final'}
</p>
<p>
Fonte:{' '}
{
parameter.source
?.title
}
{parameter.source
?.diploma
? ` — ${parameter.source.diploma}`
: ''}
{parameter.source
?.article
? `, ${parameter.source.article}`
: ''}
</p>
{parameter.source?.url && (
<a
className="underline"
href={
parameter.source
.url
}
target="_blank"
rel="noreferrer"
>
Ver fundamento
</a>
)}
</>
) : (
<p>
{(parameter.parameter_code ===
'IVA_GENERAL_RATE' &&
diagnostics.length
? 'Consulte acima cada condição e o fundamento da versão.'
: labels[
parameter.status
]) ??
'Indeterminado — não foi possível validar o parâmetro.'}
</p>
)}
</div>
))}
</div>
)}
</li>
);
})}
</ul>
</section>
);
}