454 lines
20 KiB
TypeScript
454 lines
20 KiB
TypeScript
import { inputText } from '../lib/input-text.ts';
|
|
import { SimulationRemissionImports } from './simulation-remission-imports';
|
|
import { SimulationObjectiveImports } from './simulation-objective-imports';
|
|
import { SimulationMiners } from './simulation-miners';
|
|
import { SimulationReturnees } from './simulation-returnees';
|
|
import { SimulationDigital } from './simulation-digital';
|
|
import { SimulationTravel } from './simulation-travel';
|
|
import { SimulationTravelEvents } from './simulation-travel-events';
|
|
import { SimulationForeignOperations } from './simulation-foreign-operations';
|
|
import { SimulationCurrencyEvents } from './simulation-currency-events';
|
|
import { SimulationPropertyTransfers } from './simulation-property-transfers';
|
|
import { SimulationGoodsPresumptions } from './simulation-goods-presumptions';
|
|
import { InvestmentContextAssessment } from './investment-context-assessment';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
monthlyIvaInput,
|
|
monthlyIvaKey,
|
|
monthlySalesFromDraft,
|
|
readMonthlyIvaRecord,
|
|
readMonthlyIvaSettings,
|
|
recoverMonthlyIvaRecord,
|
|
monthlyIvaCashTreatment,
|
|
type MonthlyIvaInput,
|
|
type MonthlyIvaSettings,
|
|
} from '@/lib/monthly-iva';
|
|
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
|
|
import { type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
|
|
import { SimulationModeloA } from './simulation-modelo-a';
|
|
import { SimulationAcquisitions } from './simulation-acquisitions';
|
|
import { SimulationImportExemptions } from './simulation-import-exemptions';
|
|
import { SimulationPublicAuthorityBase } from './simulation-public-authority-base';
|
|
import { SimulationCommissionBase } from './simulation-commission-base';
|
|
import { SimulationActSaleBase } from './simulation-act-sale-base';
|
|
import { SimulationFuelBase } from './simulation-fuel-base';
|
|
import { SimulationNoncashBase } from './simulation-noncash-base';
|
|
import { SimulationCompositionBase } from './simulation-composition-base';
|
|
import { SimulationExclusionBase } from './simulation-exclusion-base';
|
|
import { SimulationValueReview } from './simulation-value-review';
|
|
import { SimulationServiceNormal } from './simulation-service-normal';
|
|
import { SimulationGoodsLocation } from './simulation-goods-location';
|
|
import { SimulationServiceLocation } from './simulation-service-location';
|
|
import { SimulationListedServices } from './simulation-listed-services';
|
|
import { SimulationReducedBases } from './simulation-reduced-bases';
|
|
import { SimulationOwnName } from './simulation-own-name';
|
|
import { SimulationGoodsAllocation } from './simulation-goods-allocation';
|
|
import { SimulationSecondhand } from './simulation-secondhand';
|
|
import { SimulationOfferPreparations } from './simulation-offer-preparations';
|
|
import { SimulationOperations } from './simulation-operations';
|
|
|
|
export function SimulationMonthlyIva({
|
|
draft,
|
|
busy,
|
|
onCalculate,
|
|
onChange,
|
|
}: {
|
|
draft: Record<string, unknown>;
|
|
busy: boolean;
|
|
onCalculate: (
|
|
input: MonthlyIvaInput,
|
|
) => Promise<ApiSimulationExecutionV1 | null | undefined>;
|
|
onChange: (field: string, value: string) => void;
|
|
}) {
|
|
const settings = readMonthlyIvaSettings(
|
|
inputText(draft.monthly_iva_settings ?? ''),
|
|
);
|
|
const record = readMonthlyIvaRecord(
|
|
inputText(draft.monthly_iva_snapshot ?? ''),
|
|
);
|
|
const [retry, setRetry] = useState(0);
|
|
const [error, setError] = useState('');
|
|
const [available, setAvailable] = useState<boolean | null>(null);
|
|
const attempt = useRef('');
|
|
const mounted = useRef(true);
|
|
const latestSnapshot = useRef(draft.monthly_iva_snapshot);
|
|
latestSnapshot.current = draft.monthly_iva_snapshot;
|
|
useEffect(() => {
|
|
mounted.current = true;
|
|
return () => {
|
|
mounted.current = false;
|
|
};
|
|
}, []);
|
|
const callbacks = useRef({ onCalculate, onChange });
|
|
callbacks.current = { onCalculate, onChange };
|
|
let input: MonthlyIvaInput | null = null;
|
|
let inputError = '';
|
|
let source = '';
|
|
try {
|
|
source = monthlySalesFromDraft(draft).source;
|
|
input = monthlyIvaInput(draft, settings);
|
|
} catch (cause) {
|
|
inputError =
|
|
cause instanceof Error
|
|
? cause.message
|
|
: 'Reveja a projecção de vendas.';
|
|
}
|
|
const key = input ? monthlyIvaKey(input) : '';
|
|
const current = !!(input && record && monthlyIvaKey(record.input) === key);
|
|
const completed = current && record?.execution.status === 'completed';
|
|
const pendingRecord =
|
|
record && ['queued', 'running'].includes(record.execution.status)
|
|
? record
|
|
: null;
|
|
const actionKey = pendingRecord
|
|
? `recover:${pendingRecord.execution.id}`
|
|
: key;
|
|
const saveSettings = (patch: Partial<MonthlyIvaSettings>) =>
|
|
onChange(
|
|
'monthly_iva_settings',
|
|
JSON.stringify({ ...settings, ...patch }),
|
|
);
|
|
useEffect(() => {
|
|
let active = true;
|
|
fetch('/api/v1/fiscal-consumer', {
|
|
credentials: 'same-origin',
|
|
headers: { Accept: 'application/json' },
|
|
})
|
|
.then(async (response) => {
|
|
if (!response.ok) throw new Error();
|
|
const payload = await response.json();
|
|
if (active) setAvailable(payload?.data?.enabled === true);
|
|
})
|
|
.catch(() => {
|
|
if (active)
|
|
setError(
|
|
'Não foi possível consultar a disponibilidade da regra. As vendas foram preservadas.',
|
|
);
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [retry]);
|
|
useEffect(() => {
|
|
if (!settings.auto || busy || attempt.current === actionKey) return;
|
|
if (
|
|
!pendingRecord &&
|
|
(!input || input.currency !== 'MZN' || !available || completed)
|
|
)
|
|
return;
|
|
const request = pendingRecord?.input ?? input!;
|
|
const savedSnapshot = draft.monthly_iva_snapshot;
|
|
const timer = window.setTimeout(() => {
|
|
attempt.current = actionKey;
|
|
setError('');
|
|
void (async () => {
|
|
try {
|
|
let execution;
|
|
if (pendingRecord) {
|
|
execution = (
|
|
await recoverMonthlyIvaRecord(pendingRecord)
|
|
).execution;
|
|
} else
|
|
execution =
|
|
await callbacks.current.onCalculate(request);
|
|
if (!execution)
|
|
throw new Error(
|
|
'O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.',
|
|
);
|
|
if (
|
|
!mounted.current ||
|
|
(pendingRecord &&
|
|
latestSnapshot.current !== savedSnapshot)
|
|
)
|
|
return;
|
|
callbacks.current.onChange(
|
|
'monthly_iva_snapshot',
|
|
JSON.stringify({ input: request, execution }),
|
|
);
|
|
} catch (cause) {
|
|
if (mounted.current)
|
|
setError(
|
|
cause instanceof Error
|
|
? cause.message
|
|
: 'Não foi possível calcular o IVA mensal.',
|
|
);
|
|
}
|
|
})();
|
|
}, 700);
|
|
return () => window.clearTimeout(timer);
|
|
}, [actionKey, key, settings.auto, busy, available, completed, retry]);
|
|
|
|
const snapshot = record?.execution.result_snapshot;
|
|
const projection = snapshot?.monthly_projection as
|
|
| {
|
|
rows?: Array<{
|
|
date: string;
|
|
sales_declared: string | null;
|
|
net_sales: string | null;
|
|
rate: string | null;
|
|
vat: string | null;
|
|
gross_sales: string | null;
|
|
status: string;
|
|
}>;
|
|
}
|
|
| undefined;
|
|
return (
|
|
<section
|
|
className="space-y-3 rounded border border-emerald-700 p-4"
|
|
aria-label="IVA mensal das vendas projectadas"
|
|
>
|
|
<InvestmentContextAssessment
|
|
raw={draft.context_applicability_snapshot}
|
|
/>
|
|
<h3 className="font-semibold">IVA mensal das vendas projectadas</h3>
|
|
<p>{source}</p>
|
|
<p>
|
|
Declare se os preços projectados são sem IVA ou com IVA
|
|
incluído. Nenhuma taxa é fixada neste formulário; o servidor
|
|
selecciona a versão publicada.
|
|
</p>
|
|
<label className="block">
|
|
Primeiro mês da projecção
|
|
<input
|
|
type="month"
|
|
value={settings.start}
|
|
min="2026-01"
|
|
max="2026-12"
|
|
onChange={(event) =>
|
|
saveSettings({ start: event.target.value })
|
|
}
|
|
className="block rounded border p-2"
|
|
/>
|
|
</label>
|
|
<label className="block">
|
|
Tratamento do IVA nos preços projectados
|
|
<select
|
|
value={settings.basis}
|
|
onChange={(event) =>
|
|
saveSettings({
|
|
basis: event.target
|
|
.value as MonthlyIvaSettings['basis'],
|
|
})
|
|
}
|
|
className="block rounded border p-2"
|
|
>
|
|
<option value="unknown">Por indicar</option>
|
|
<option value="net">Vendas sem IVA (base líquida)</option>
|
|
<option value="included">Vendas com IVA incluído</option>
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
<input
|
|
type="checkbox"
|
|
checked={settings.declared}
|
|
onChange={(event) =>
|
|
saveSettings({ declared: event.target.checked })
|
|
}
|
|
/>{' '}
|
|
Declaro que as vendas projectadas são tributáveis à taxa geral
|
|
interna e não incluem as operações excluídas deste lote.
|
|
</label>
|
|
<p className="text-sm">
|
|
Sem importações, taxas reduzidas, isenções, regimes especiais,
|
|
deduções, reembolsos ou apuramento final. Valores com IVA
|
|
incluído permanecem indeterminados neste lote; não se aplica a
|
|
taxa ao preço bruto.
|
|
</p>
|
|
<p className="text-sm">{monthlyIvaCashTreatment}</p>
|
|
{!!draft.currency && draft.currency !== 'MZN' && (
|
|
<p role="alert">
|
|
Moeda do projecto: {inputText(draft.currency)}. A regra
|
|
disponível aceita apenas MZN. As vendas e a moeda são
|
|
preservadas, sem conversão; o IVA mensal não pode ser
|
|
determinado por esta versão.
|
|
</p>
|
|
)}
|
|
{inputError && <p role="alert">{inputError}</p>}
|
|
{available === null && !error && (
|
|
<p role="status">
|
|
A consultar a disponibilidade da versão publicada…
|
|
</p>
|
|
)}
|
|
{available === false && (
|
|
<p role="status">
|
|
A versão publicada de IVA está indisponível neste ambiente
|
|
para novas execuções. Os pressupostos e históricos
|
|
mantêm-se.
|
|
</p>
|
|
)}
|
|
{record && !current && (
|
|
<p role="alert">
|
|
As vendas ou os pressupostos mudaram. Os resultados abaixo
|
|
são históricos e estão desactualizados; não são usados como
|
|
resultados da projecção actual.
|
|
</p>
|
|
)}
|
|
{settings.auto && !completed && current && (
|
|
<p role="status">
|
|
Execução mensal guardada:{' '}
|
|
{record?.execution.status === 'queued'
|
|
? 'em fila'
|
|
: record?.execution.status === 'running'
|
|
? 'em processamento'
|
|
: 'não concluída'}
|
|
. Pode voltar a consultar sem duplicar.
|
|
</p>
|
|
)}
|
|
{busy && (
|
|
<p role="status">
|
|
A actualizar os resultados dependentes das vendas…
|
|
</p>
|
|
)}
|
|
{error && <p role="alert">{error}</p>}
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
busy ||
|
|
(!pendingRecord &&
|
|
(!available || !input || input.currency !== 'MZN'))
|
|
}
|
|
onClick={() => {
|
|
attempt.current = '';
|
|
setRetry((value) => value + 1);
|
|
saveSettings({ auto: true });
|
|
}}
|
|
className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50"
|
|
>
|
|
{pendingRecord
|
|
? 'Recuperar execução mensal admitida pelo mesmo ID'
|
|
: 'Calcular / actualizar IVA mensal e guardar versão'}
|
|
</button>
|
|
{pendingRecord && (
|
|
<p>
|
|
Consulta da execução {pendingRecord.execution.id}. A
|
|
suspensão de novos cálculos não impede esta recuperação; não
|
|
é criado outro pedido.
|
|
</p>
|
|
)}
|
|
{settings.auto && (
|
|
<button
|
|
type="button"
|
|
className="ml-3 underline"
|
|
onClick={() => saveSettings({ auto: false })}
|
|
>
|
|
Pausar actualização automática
|
|
</button>
|
|
)}
|
|
{input && (
|
|
<p className="text-sm">
|
|
{input.monthly_sales.length} meses; moeda{' '}
|
|
{input.currency || 'por indicar'}. Campo vazio significa
|
|
informação em falta; zero declarado permanece zero. Depois
|
|
da primeira execução, alterações das vendas actualizam o
|
|
cálculo enquanto a actualização automática estiver ligada.
|
|
</p>
|
|
)}
|
|
{projection?.rows && (
|
|
<div className="overflow-x-auto" tabIndex={0}>
|
|
<table className="min-w-full text-sm">
|
|
<caption>
|
|
IVA mensal —{' '}
|
|
{current
|
|
? 'versão correspondente às entradas'
|
|
: 'versão histórica'}
|
|
</caption>
|
|
<thead>
|
|
<tr>
|
|
{[
|
|
'Mês',
|
|
'Vendas declaradas',
|
|
'Vendas tributáveis sem IVA',
|
|
'Taxa aplicada (%)',
|
|
'IVA calculado',
|
|
'Total com IVA',
|
|
'Estado',
|
|
].map((label) => (
|
|
<th key={label} className="p-2 text-left">
|
|
{label}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{projection.rows.map((row) => (
|
|
<tr key={row.date}>
|
|
<th scope="row" className="p-2">
|
|
{row.date.slice(0, 7)}
|
|
</th>
|
|
<td>
|
|
{row.sales_declared ?? 'Sem informação'}
|
|
</td>
|
|
<td>{row.net_sales ?? 'Indeterminado'}</td>
|
|
<td>{row.rate ?? 'Indeterminada'}</td>
|
|
<td>{row.vat ?? 'Indeterminado'}</td>
|
|
<td>
|
|
{row.gross_sales ?? 'Indeterminado'}
|
|
</td>
|
|
<td>
|
|
{{
|
|
calculated: 'Calculado',
|
|
missing_sales:
|
|
'Sem informação de vendas',
|
|
unsupported_basis:
|
|
'Base líquida não determinada',
|
|
indeterminate:
|
|
'Enquadramento ou regra indeterminados',
|
|
}[row.status] ?? 'Indeterminado'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
{snapshot && (
|
|
<details>
|
|
<summary>
|
|
Ver fundamento e relatório da versão guardada
|
|
</summary>
|
|
<dl>
|
|
{fiscalQaRows(snapshot).map(([label, value], index) => (
|
|
<div key={index}>
|
|
<dt className="font-semibold">{label}</dt>
|
|
<dd>{value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</details>
|
|
)}
|
|
<SimulationOperations draft={draft} onChange={onChange} />
|
|
<SimulationAcquisitions draft={draft} onChange={onChange} />
|
|
<SimulationImportExemptions draft={draft} onChange={onChange} />
|
|
<SimulationObjectiveImports draft={draft} onChange={onChange} />
|
|
<SimulationMiners draft={draft} onChange={onChange} />
|
|
<SimulationRemissionImports draft={draft} onChange={onChange} />
|
|
<SimulationReturnees draft={draft} onChange={onChange} />
|
|
<SimulationPublicAuthorityBase draft={draft} onChange={onChange} />
|
|
<SimulationCommissionBase draft={draft} onChange={onChange} />
|
|
<SimulationActSaleBase draft={draft} onChange={onChange} />
|
|
<SimulationFuelBase draft={draft} onChange={onChange} />
|
|
<SimulationNoncashBase draft={draft} onChange={onChange} />
|
|
<SimulationCompositionBase draft={draft} onChange={onChange} />
|
|
<SimulationExclusionBase draft={draft} onChange={onChange} />
|
|
<SimulationValueReview draft={draft} onChange={onChange} />
|
|
<SimulationServiceNormal draft={draft} onChange={onChange} />
|
|
<SimulationGoodsLocation draft={draft} onChange={onChange} />
|
|
<SimulationServiceLocation draft={draft} onChange={onChange} />
|
|
<SimulationListedServices draft={draft} onChange={onChange} />
|
|
<SimulationReducedBases draft={draft} onChange={onChange} />
|
|
<SimulationOwnName draft={draft} onChange={onChange} />
|
|
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
|
|
<SimulationForeignOperations draft={draft} onChange={onChange} />
|
|
<SimulationDigital draft={draft} onChange={onChange} />
|
|
<SimulationTravel draft={draft} onChange={onChange} />
|
|
<SimulationTravelEvents draft={draft} onChange={onChange} />
|
|
<SimulationCurrencyEvents draft={draft} onChange={onChange} />
|
|
<SimulationPropertyTransfers draft={draft} onChange={onChange} />
|
|
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
|
|
<SimulationSecondhand draft={draft} onChange={onChange} />
|
|
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
|
<SimulationModeloA draft={draft} />
|
|
</section>
|
|
);
|
|
}
|