feat: calculate qualified investment credits in annual settlement
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,20 @@
|
||||
# Rastreabilidade, utilização e formação
|
||||
|
||||
## 10/11/2026 — acordo PT–MZ instalado; crédito CBF15 demonstrado em QA
|
||||
|
||||
**Homologação: `7c8d4726e9f93325480e2ee6e5e790e9adbcaf19`**, promovida às12:36:07UTC e conferida às12:37:11UTC (14:37 locais). Revisão HTTPS/10hashes iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos, b10b35f3 preservada. Recibos `outputs/cdt-agreement-delivery-20261110/`. Falha inicial de recepção Git delimitada ao directório objects/7c pertencente aroot: apenas proprietário/grupo ajustados a conta do serviço,modo0755 inalterado; pedido original validado recolocado sem alterar corpo/SHA ou controlos. Recibos git-permission-repair/request-retry; não foi falha do portal nem alteração de segurança do navegador. Sessão SSH preservada para continuação autenticada, sem credencial em disco.
|
||||
|
||||
**Pendência efectivamente encerrada em QA, ainda sem instalação:** CBF15, crédito fiscal por investimento efectivo no primeiro exercício2026, residente geral/ano civil/MZN, colecta inteiramente da actividade do projecto ou quota residual documentada após créditos anteriores e sem outros benefícios próprios/cumulações ou transportes positivos combinados. Reutiliza qualificação CBF de L7 e fonte APIEX já aceite (recibo irpc-l7-common-qa-20261026,compilação oficial2025,PDF66–72,art15PDF71–72), não importação como activação. Localização do investimento,aprovação/condiçõesCBF3/9/10/regime genérico13,início aplicável e activos/documentos exigem correspondência própria. Activo novo corpóreo,nacional,produtivo e fora15(6); condições negativas/exclusões documentadas produzem crédito zero nessa linha,preservando os dados. Informação em falta não é zero.
|
||||
|
||||
Percurso completo: preparar detalhe → calcular após económico/internacional e antesPEC/RF/PPC → gravar/actualizar pelo writer privado existente → recuperar → recalcular → HTML/dossier com fornecedor/documento/parcela/valores/qualificação/período/histórico. Maputo cidade5%: investimento10.000 → crédito500 → saldo200 MZN. Restantes províncias10%: crédito1.000 → saldo−300, recuperável simulado devido aoPPC. Cenário de limite: nominal4.000,dedução1.400,crédito não usado2.600,PEC não utilizado200,PPC500; nenhum reembolso de crédito/PEC ou saldo oficial criado/consumido. Provas `outputs/irpc-cbf15-20261110/{city,province,capped,partial}-verification.json` e relatórios respectivos. Quatro versões do mesmo rascunho API em transacção revertida QA55441,com histórico anterior e versões seguintes preservados; zero alterações activas.
|
||||
|
||||
Quota residual documentada: nominal4.000, limite do projecto200, dedução200, não utilizado3.800 e saldo500 MZN; recuperada pela API com quatro resultados históricos. Quota superior à colecta global remanescente, negativa ou sem fundamento é recusada. Não há repartição automática de rendimentos/custos/perdas. Tipos, lint/Pint e compilação QA aprovados; provas anteriores reutilizadas.
|
||||
|
||||
Livros anteriores L8 permanecem versão1,com chaves/resultados inalterados quando sem CBF15. Só o livro com este novo detalhe usa versão2: cliente anterior recusa o formato,conservando o snapshot,em vez de recalcular ignorando o novo benefício. Não é migration de base. O resumo antigo dos benefícios fica conservado e não reaplicado; ausência dos restantes benefícios exige declaração/reconciliação explícita,mesmo quando CBF15 é ausente. Fonte documental tem capacidade por factura/parcela e entre mapas da entidade; mudar id/data não duplica investimento. Repetir simulação não é uso fiscal anterior.
|
||||
|
||||
**Saldo preservado:** CBF15 de outros exercícios/transporte/caducidade,repartição automática/múltiplos projectos,EP/L7/cumulações,outros benefícios e obrigações/formulários oficiais continuam separados. Ausência/zeroCBF15 não bloqueia os saldos anteriores aceites. Restantes L1–L7/L8/L9–L16 e módulos não fiscais mantêm o saldo existente; o portal não está integral. Visual permanece pendente. Conjunto CBF15 preparado para entrega sobre7c8d4726,sem instalação nesta entrada.
|
||||
|
||||
|
||||
## 10/11/2026 — b10b35f3 instalado; acordo institucional PT–MZ em QA
|
||||
|
||||
**Homologação: `b10b35f30830922dae00afde50ac2bdb0e8cef6f`**, promovida às12:15:13UTC e conferida às12:16:31UTC (14:16 locais). Revisão HTTPS e16hashes Git iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos;60b52fc8 disponível para reversão. Recibos `outputs/government-fraction-delivery-20261110/{backup,restore,files-restore,verification}.json`. Sessão SSH mantida aberta para continuação autenticada, sem credencial em disco. Isenção por devedor público11(3)a e fracções económicas L6 deixam de ser pendências de instalação; as anotações anteriores QA são históricas nesses recortes.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
## 10/11/2026 — acordo PT–MZ instalado; crédito CBF15 demonstrado em QA
|
||||
|
||||
**Homologação: `7c8d4726e9f93325480e2ee6e5e790e9adbcaf19`**, promovida às12:36:07UTC e conferida às12:37:11UTC (14:37 locais). Revisão HTTPS/10hashes iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos, b10b35f3 preservada. Recibos `outputs/cdt-agreement-delivery-20261110/`. Falha inicial de recepção Git delimitada ao directório objects/7c pertencente aroot: apenas proprietário/grupo ajustados a conta do serviço,modo0755 inalterado; pedido original validado recolocado sem alterar corpo/SHA ou controlos. Recibos git-permission-repair/request-retry; não foi falha do portal nem alteração de segurança do navegador. Sessão SSH preservada para continuação autenticada, sem credencial em disco.
|
||||
|
||||
**Pendência efectivamente encerrada em QA, ainda sem instalação:** CBF15, crédito fiscal por investimento efectivo no primeiro exercício2026, residente geral/ano civil/MZN, colecta inteiramente da actividade do projecto ou quota residual documentada após créditos anteriores e sem outros benefícios próprios/cumulações ou transportes positivos combinados. Reutiliza qualificação CBF de L7 e fonte APIEX já aceite (recibo irpc-l7-common-qa-20261026,compilação oficial2025,PDF66–72,art15PDF71–72), não importação como activação. Localização do investimento,aprovação/condiçõesCBF3/9/10/regime genérico13,início aplicável e activos/documentos exigem correspondência própria. Activo novo corpóreo,nacional,produtivo e fora15(6); condições negativas/exclusões documentadas produzem crédito zero nessa linha,preservando os dados. Informação em falta não é zero.
|
||||
|
||||
Percurso completo: preparar detalhe → calcular após económico/internacional e antesPEC/RF/PPC → gravar/actualizar pelo writer privado existente → recuperar → recalcular → HTML/dossier com fornecedor/documento/parcela/valores/qualificação/período/histórico. Maputo cidade5%: investimento10.000 → crédito500 → saldo200 MZN. Restantes províncias10%: crédito1.000 → saldo−300, recuperável simulado devido aoPPC. Cenário de limite: nominal4.000,dedução1.400,crédito não usado2.600,PEC não utilizado200,PPC500; nenhum reembolso de crédito/PEC ou saldo oficial criado/consumido. Provas `outputs/irpc-cbf15-20261110/{city,province,capped,partial}-verification.json` e relatórios respectivos. Quatro versões do mesmo rascunho API em transacção revertida QA55441,com histórico anterior e versões seguintes preservados; zero alterações activas.
|
||||
|
||||
Quota residual documentada: nominal4.000, limite do projecto200, dedução200, não utilizado3.800 e saldo500 MZN; recuperada pela API com quatro resultados históricos. Quota superior à colecta global remanescente, negativa ou sem fundamento é recusada. Não há repartição automática de rendimentos/custos/perdas. Tipos, lint/Pint e compilação QA aprovados; provas anteriores reutilizadas.
|
||||
|
||||
Livros anteriores L8 permanecem versão1,com chaves/resultados inalterados quando sem CBF15. Só o livro com este novo detalhe usa versão2: cliente anterior recusa o formato,conservando o snapshot,em vez de recalcular ignorando o novo benefício. Não é migration de base. O resumo antigo dos benefícios fica conservado e não reaplicado; ausência dos restantes benefícios exige declaração/reconciliação explícita,mesmo quando CBF15 é ausente. Fonte documental tem capacidade por factura/parcela e entre mapas da entidade; mudar id/data não duplica investimento. Repetir simulação não é uso fiscal anterior.
|
||||
|
||||
**Saldo preservado:** CBF15 de outros exercícios/transporte/caducidade,repartição automática/múltiplos projectos,EP/L7/cumulações,outros benefícios e obrigações/formulários oficiais continuam separados. Ausência/zeroCBF15 não bloqueia os saldos anteriores aceites. Restantes L1–L7/L8/L9–L16 e módulos não fiscais mantêm o saldo existente; o portal não está integral. Visual permanece pendente. Conjunto CBF15 preparado para entrega sobre7c8d4726,sem instalação nesta entrada.
|
||||
|
||||
## 10/11/2026 — b10b35f3 instalado; acordo institucional PT–MZ em QA
|
||||
|
||||
**Homologação: `b10b35f30830922dae00afde50ac2bdb0e8cef6f`**, promovida às12:15:13UTC e conferida às12:16:31UTC (14:16 locais). Revisão HTTPS e16hashes Git iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos;60b52fc8 disponível para reversão. Recibos `outputs/government-fraction-delivery-20261110/{backup,restore,files-restore,verification}.json`. Sessão SSH mantida aberta para continuação autenticada, sem credencial em disco. Isenção por devedor público11(3)a e fracções económicas L6 deixam de ser pendências de instalação; as anotações anteriores QA são históricas nesses recortes.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,17 @@
|
||||
## 10/11/2026 — acordo PT–MZ instalado; crédito CBF15 demonstrado em QA
|
||||
|
||||
**Homologação: `7c8d4726e9f93325480e2ee6e5e790e9adbcaf19`**, promovida às12:36:07UTC e conferida às12:37:11UTC (14:37 locais). Revisão HTTPS/10hashes iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos, b10b35f3 preservada. Recibos `outputs/cdt-agreement-delivery-20261110/`. Falha inicial de recepção Git delimitada ao directório objects/7c pertencente aroot: apenas proprietário/grupo ajustados a conta do serviço,modo0755 inalterado; pedido original validado recolocado sem alterar corpo/SHA ou controlos. Recibos git-permission-repair/request-retry; não foi falha do portal nem alteração de segurança do navegador. Sessão SSH preservada para continuação autenticada, sem credencial em disco.
|
||||
|
||||
**Pendência efectivamente encerrada em QA, ainda sem instalação:** CBF15, crédito fiscal por investimento efectivo no primeiro exercício2026, residente geral/ano civil/MZN, colecta inteiramente da actividade do projecto ou quota residual documentada após créditos anteriores e sem outros benefícios próprios/cumulações ou transportes positivos combinados. Reutiliza qualificação CBF de L7 e fonte APIEX já aceite (recibo irpc-l7-common-qa-20261026,compilação oficial2025,PDF66–72,art15PDF71–72), não importação como activação. Localização do investimento,aprovação/condiçõesCBF3/9/10/regime genérico13,início aplicável e activos/documentos exigem correspondência própria. Activo novo corpóreo,nacional,produtivo e fora15(6); condições negativas/exclusões documentadas produzem crédito zero nessa linha,preservando os dados. Informação em falta não é zero.
|
||||
|
||||
Percurso completo: preparar detalhe → calcular após económico/internacional e antesPEC/RF/PPC → gravar/actualizar pelo writer privado existente → recuperar → recalcular → HTML/dossier com fornecedor/documento/parcela/valores/qualificação/período/histórico. Maputo cidade5%: investimento10.000 → crédito500 → saldo200 MZN. Restantes províncias10%: crédito1.000 → saldo−300, recuperável simulado devido aoPPC. Cenário de limite: nominal4.000,dedução1.400,crédito não usado2.600,PEC não utilizado200,PPC500; nenhum reembolso de crédito/PEC ou saldo oficial criado/consumido. Provas `outputs/irpc-cbf15-20261110/{city,province,capped,partial}-verification.json` e relatórios respectivos. Quatro versões do mesmo rascunho API em transacção revertida QA55441,com histórico anterior e versões seguintes preservados; zero alterações activas.
|
||||
|
||||
Quota residual documentada: nominal4.000, limite do projecto200, dedução200, não utilizado3.800 e saldo500 MZN; recuperada pela API com quatro resultados históricos. Quota superior à colecta global remanescente, negativa ou sem fundamento é recusada. Não há repartição automática de rendimentos/custos/perdas. Tipos, lint/Pint e compilação QA aprovados; provas anteriores reutilizadas.
|
||||
|
||||
Livros anteriores L8 permanecem versão1,com chaves/resultados inalterados quando sem CBF15. Só o livro com este novo detalhe usa versão2: cliente anterior recusa o formato,conservando o snapshot,em vez de recalcular ignorando o novo benefício. Não é migration de base. O resumo antigo dos benefícios fica conservado e não reaplicado; ausência dos restantes benefícios exige declaração/reconciliação explícita,mesmo quando CBF15 é ausente. Fonte documental tem capacidade por factura/parcela e entre mapas da entidade; mudar id/data não duplica investimento. Repetir simulação não é uso fiscal anterior.
|
||||
|
||||
**Saldo preservado:** CBF15 de outros exercícios/transporte/caducidade,repartição automática/múltiplos projectos,EP/L7/cumulações,outros benefícios e obrigações/formulários oficiais continuam separados. Ausência/zeroCBF15 não bloqueia os saldos anteriores aceites. Restantes L1–L7/L8/L9–L16 e módulos não fiscais mantêm o saldo existente; o portal não está integral. Visual permanece pendente. Conjunto CBF15 preparado para entrega sobre7c8d4726,sem instalação nesta entrada.
|
||||
|
||||
## 10/11/2026 — b10b35f3 instalado; acordo institucional PT–MZ em QA
|
||||
|
||||
**Homologação: `b10b35f30830922dae00afde50ac2bdb0e8cef6f`**, promovida às12:15:13UTC e conferida às12:16:31UTC (14:16 locais). Revisão HTTPS e16hashes Git iguais, quatro entradasHTTP200; configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos;60b52fc8 disponível para reversão. Recibos `outputs/government-fraction-delivery-20261110/{backup,restore,files-restore,verification}.json`. Sessão SSH mantida aberta para continuação autenticada, sem credencial em disco. Isenção por devedor público11(3)a e fracções económicas L6 deixam de ser pendências de instalação; as anotações anteriores QA são históricas nesses recortes.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SimulationIrpcInvestment } from './simulation-irpc-investment';
|
||||
import { SimulationIrpcEconomic } from './simulation-irpc-economic';
|
||||
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
|
||||
import { SimulationIrpcCarry } from './simulation-irpc-carry';
|
||||
@@ -479,6 +480,7 @@ export function SimulationIrpcAnnual({ draft, onChange }: Props) {
|
||||
}
|
||||
/>
|
||||
<SimulationIrpcInternational book={b} save={save} />
|
||||
<SimulationIrpcInvestment draft={draft} book={b} save={save} />
|
||||
<SimulationIrpcPec book={b} save={save} />
|
||||
<SimulationIrpcCarry book={b} save={save} />
|
||||
{[
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
import { QualificationFields } from './simulation-irpc-special';
|
||||
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
|
||||
import {
|
||||
prepareInvestmentCredit,
|
||||
type AnnualBook,
|
||||
} from '@/lib/irpc-annual-settlement';
|
||||
import {
|
||||
investmentCreditSource,
|
||||
newInvestmentAsset,
|
||||
type InvestmentCredit,
|
||||
type InvestmentAsset,
|
||||
} from '@/lib/irpc-investment-credit';
|
||||
export function SimulationIrpcInvestment({
|
||||
draft,
|
||||
book,
|
||||
save,
|
||||
}: {
|
||||
draft: Record<string, unknown>;
|
||||
book: AnnualBook;
|
||||
save: (b: AnnualBook) => void;
|
||||
}) {
|
||||
const c = book.investmentCredit;
|
||||
if (!c)
|
||||
return (
|
||||
<fieldset className="rounded border p-3">
|
||||
<legend>Crédito fiscal por investimento — CBF15</legend>
|
||||
<p>
|
||||
Prepare o detalhe do investimento efectivo e da qualificação
|
||||
do projecto. Substitui neste cálculo o resumo anterior dos
|
||||
benefícios, que fica conservado. O primeiro recorte cobre o
|
||||
exercício inicial2026,com colecta integral ou quota residual
|
||||
documental do projecto, sem outros benefícios próprios ou
|
||||
transportes anteriores.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => save(prepareInvestmentCredit(draft, book))}
|
||||
>
|
||||
Preparar crédito por investimento
|
||||
</button>
|
||||
</fieldset>
|
||||
);
|
||||
const patch = (v: Partial<InvestmentCredit>) =>
|
||||
save({ ...book, investmentCredit: { ...c, ...v } });
|
||||
const text = (
|
||||
key:
|
||||
| 'projectReference'
|
||||
| 'locationReference'
|
||||
| 'startDate'
|
||||
| 'startReference'
|
||||
| 'recognitionReference'
|
||||
| 'noOtherBenefitsReference'
|
||||
| 'soleProjectReference',
|
||||
label: string,
|
||||
) => (
|
||||
<label className="block">
|
||||
{label}
|
||||
<input
|
||||
aria-label={'CBF15 — ' + label}
|
||||
value={c[key]}
|
||||
onChange={(e) => patch({ [key]: e.target.value })}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
const choice = (
|
||||
key:
|
||||
| 'cbfRequirements'
|
||||
| 'genericOnly'
|
||||
| 'noOtherBenefits'
|
||||
| 'soleProject',
|
||||
label: string,
|
||||
) => (
|
||||
<label className="block">
|
||||
{label}
|
||||
<select
|
||||
aria-label={'CBF15 — ' + label}
|
||||
value={c[key]}
|
||||
onChange={(e) => patch({ [key]: e.target.value })}
|
||||
>
|
||||
<option value="">Informação em falta</option>
|
||||
<option value="yes">Sim — documentado</option>
|
||||
<option value="no">Não — documentado</option>
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
return (
|
||||
<fieldset className="space-y-3 rounded border p-3">
|
||||
<legend>Crédito fiscal por investimento — CBF15</legend>
|
||||
<p>{investmentCreditSource}</p>
|
||||
<p>
|
||||
Beneficiário {c.subjectNuit}, exercício {c.year}. Resumo
|
||||
anterior conservado:{' '}
|
||||
{book.internationalCredits?.benefits.state ||
|
||||
'informação em falta'}
|
||||
; montante{' '}
|
||||
{book.internationalCredits?.benefits.amount || 'não preenchido'}
|
||||
. O detalhe aplica apenas o benefício aqui qualificado; não
|
||||
volta a deduzir o resumo.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
patch({
|
||||
subjectNuit: book.subjectNuit,
|
||||
year: book.year,
|
||||
assessmentKey: book.assessmentKey,
|
||||
})
|
||||
}
|
||||
>
|
||||
Vincular CBF15 à colecta seleccionada
|
||||
</button>
|
||||
<QualificationFields
|
||||
label="CBF15"
|
||||
value={c.qualification}
|
||||
onChange={(qualification) => patch({ qualification })}
|
||||
/>
|
||||
{choice(
|
||||
'noOtherBenefits',
|
||||
'Sem outros benefícios próprios/cumulações neste recorte',
|
||||
)}
|
||||
{text(
|
||||
'noOtherBenefitsReference',
|
||||
'Reconciliação dos outros benefícios e tratamentos L6/L7',
|
||||
)}
|
||||
{c.qualification.state === 'known' && (
|
||||
<>
|
||||
{text(
|
||||
'projectReference',
|
||||
'Despacho/termos ou instrumento legal do projecto',
|
||||
)}
|
||||
{choice(
|
||||
'cbfRequirements',
|
||||
'Condições CBF3/9 e âmbito do projecto documentados',
|
||||
)}
|
||||
{choice(
|
||||
'genericOnly',
|
||||
'Investimento no regime genérico, fora dos benefícios específicos — art.13',
|
||||
)}
|
||||
<label className="block">
|
||||
Reconhecimento — art.10
|
||||
<select
|
||||
aria-label="CBF15 — Reconhecimento"
|
||||
value={c.recognition}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
recognition: e.target
|
||||
.value as InvestmentCredit['recognition'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Por corresponder</option>
|
||||
<option value="documented">
|
||||
Reconhecimento documentado
|
||||
</option>
|
||||
<option value="not_required">
|
||||
Excepção com norma e suporte
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{text(
|
||||
'recognitionReference',
|
||||
'Suporte do reconhecimento ou norma de excepção',
|
||||
)}
|
||||
{choice(
|
||||
'soleProject',
|
||||
'Toda a colecta corrente respeita à actividade do projecto',
|
||||
)}
|
||||
{c.soleProject === 'yes'
|
||||
? text(
|
||||
'soleProjectReference',
|
||||
'Suporte da correspondência integral da colecta ao projecto',
|
||||
)
|
||||
: c.soleProject === 'no' && (
|
||||
<fieldset className="rounded border p-3">
|
||||
<legend>
|
||||
Parte da colecta respeitante ao projecto
|
||||
</legend>
|
||||
<p>
|
||||
Indique a colecta residual do projecto
|
||||
depois dos créditos
|
||||
económicos/internacionais que lhe
|
||||
respeitam. Documente a atribuição entre
|
||||
actividades; a quota não pode exceder a
|
||||
colecta global remanescente. O portal não
|
||||
reparte rendimentos, custos ou perdas por
|
||||
presunção.
|
||||
</p>
|
||||
<label className="block">
|
||||
Quota residual do projecto — MZN ou n/d
|
||||
<input
|
||||
aria-label="CBF15 — Quota residual do projecto"
|
||||
value={c.projectQuota?.amount ?? ''}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
projectQuota: {
|
||||
amount: e.target.value,
|
||||
reference:
|
||||
c.projectQuota
|
||||
?.reference ?? '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
Suporte da quota e atribuição dos créditos
|
||||
anteriores
|
||||
<input
|
||||
aria-label="CBF15 — Suporte da quota do projecto"
|
||||
value={
|
||||
c.projectQuota?.reference ?? ''
|
||||
}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
projectQuota: {
|
||||
amount:
|
||||
c.projectQuota
|
||||
?.amount ?? '',
|
||||
reference: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
<label className="block">
|
||||
Local efectivo do investimento
|
||||
<select
|
||||
aria-label="CBF15 — Local do investimento"
|
||||
value={c.location}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
location: e.target
|
||||
.value as InvestmentCredit['location'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Por documentar</option>
|
||||
<option value="maputo_city">
|
||||
Cidade de Maputo —5%
|
||||
</option>
|
||||
<option value="other">
|
||||
Restantes províncias —10%, incluindo Província
|
||||
de Maputo
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{text(
|
||||
'locationReference',
|
||||
'Suporte da localização do investimento',
|
||||
)}
|
||||
<label className="block">
|
||||
Início do prazo do crédito
|
||||
<select
|
||||
aria-label="CBF15 — Início"
|
||||
value={c.startKind}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
startKind: e.target
|
||||
.value as InvestmentCredit['startKind'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Por documentar</option>
|
||||
<option value="new">
|
||||
Projecto novo — início de exploração
|
||||
</option>
|
||||
<option value="existing">
|
||||
Projecto em funcionamento — início dos
|
||||
procedimentos após aprovação
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
{text('startDate', 'Data do início aplicável — AAAA-MM-DD')}
|
||||
{text(
|
||||
'startReference',
|
||||
'Suporte do início e primeiro exercício2026',
|
||||
)}
|
||||
<p>
|
||||
Indique investimento efectivamente realizado,
|
||||
documentado em MZN, adquirido novo e afecto à exploração
|
||||
nacional. Art15(6) exclui edifícios, viaturas ligeiras,
|
||||
mobiliário/conforto/decoração, equipamentos sociais,
|
||||
tecnologia de ponta e outros bens não directa e
|
||||
imprescindivelmente ligados à actividade produtiva. Não
|
||||
copiar investimento projectado como realização.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
patch({
|
||||
assets: [
|
||||
...c.assets,
|
||||
newInvestmentAsset(crypto.randomUUID()),
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Acrescentar activo/documento
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{c.assets.map((r, index) => {
|
||||
const change = (v: Partial<InvestmentAsset>) =>
|
||||
patch({
|
||||
assets: c.assets.map((x) =>
|
||||
x.id === r.id ? { ...r, ...v } : x,
|
||||
),
|
||||
});
|
||||
return (
|
||||
<fieldset
|
||||
key={r.id}
|
||||
className="space-y-2 rounded border p-3"
|
||||
>
|
||||
<legend>Investimento {index + 1}</legend>
|
||||
{(
|
||||
[
|
||||
['issuer', 'Fornecedor/emitente'],
|
||||
['document', 'Factura/documento'],
|
||||
[
|
||||
'date',
|
||||
'Data do investimento efectivo — AAAA-MM-DD',
|
||||
],
|
||||
[
|
||||
'proofAmount',
|
||||
'Montante total do documento — MZN',
|
||||
],
|
||||
[
|
||||
'amount',
|
||||
'Investimento atribuído a este activo/projecto — MZN',
|
||||
],
|
||||
['currency', 'Moeda documental (recorte MZN)'],
|
||||
[
|
||||
'reference',
|
||||
'Suporte do activo, valor e realização',
|
||||
],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<label key={key} className="block">
|
||||
{label}
|
||||
<input
|
||||
aria-label={
|
||||
'CBF15 activo ' +
|
||||
(index + 1) +
|
||||
' — ' +
|
||||
label
|
||||
}
|
||||
value={r[key]}
|
||||
onChange={(e) =>
|
||||
change({ [key]: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<FiscalProofParcelFields
|
||||
label={'InvestimentoCBF15 ' + (index + 1)}
|
||||
value={r}
|
||||
change={change}
|
||||
/>
|
||||
{(
|
||||
[
|
||||
[
|
||||
'realised',
|
||||
'Investimento efectivamente realizado',
|
||||
],
|
||||
[
|
||||
'newTangible',
|
||||
'Activo imobilizado corpóreo adquirido em estado novo',
|
||||
],
|
||||
[
|
||||
'domesticUse',
|
||||
'Afecto à exploração do projecto em território nacional',
|
||||
],
|
||||
[
|
||||
'directProductive',
|
||||
'Directa e imprescindivelmente ligado à actividade produtiva',
|
||||
],
|
||||
[
|
||||
'excluded15_6',
|
||||
'Abrangido por alguma exclusão15(6) (para deduzir exige Não)',
|
||||
],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<label key={key} className="block">
|
||||
{label}
|
||||
<select
|
||||
aria-label={
|
||||
'CBF15 activo ' +
|
||||
(index + 1) +
|
||||
' — ' +
|
||||
label
|
||||
}
|
||||
value={r[key]}
|
||||
onChange={(e) =>
|
||||
change({ [key]: e.target.value })
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
Informação em falta
|
||||
</option>
|
||||
<option value="yes">
|
||||
Sim — documentado
|
||||
</option>
|
||||
<option value="no">
|
||||
Não — documentado
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
patch({
|
||||
assets: c.assets.filter(
|
||||
(x) => x.id !== r.id,
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Retirar activo {index + 1}
|
||||
</button>
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
<fieldset className="space-y-2 rounded border p-2">
|
||||
<legend>Uso anterior efectivo do crédito</legend>
|
||||
<select
|
||||
aria-label="CBF15 — Uso anterior"
|
||||
value={c.previousUse.state}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
previousUse: {
|
||||
...c.previousUse,
|
||||
state: e.target
|
||||
.value as InvestmentCredit['previousUse']['state'],
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Informação em falta</option>
|
||||
<option value="known">Montante conhecido</option>
|
||||
<option value="absent">Ausência documentada</option>
|
||||
</select>
|
||||
<label className="block">
|
||||
Montante anteriormente utilizado
|
||||
<input
|
||||
aria-label="CBF15 — Montante de uso anterior"
|
||||
value={c.previousUse.amount}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
previousUse: {
|
||||
...c.previousUse,
|
||||
amount: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
Suporte do uso/ausência
|
||||
<input
|
||||
aria-label="CBF15 — Suporte do uso anterior"
|
||||
value={c.previousUse.reference}
|
||||
onChange={(e) =>
|
||||
patch({
|
||||
previousUse: {
|
||||
...c.previousUse,
|
||||
reference: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<p>
|
||||
Calcule e guarde pelo percurso L8 existente. O crédito não
|
||||
utilizado fica discriminado; transporte para outros exercícios e
|
||||
formalidades oficiais conservam percurso próprio.
|
||||
</p>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const states = [
|
||||
['absent', 'Ausência declarada com suporte'],
|
||||
['not_applicable', 'Tratamento não aplicável — justificar'],
|
||||
] as const;
|
||||
function QualificationFields({
|
||||
export function QualificationFields({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
newInvestmentCredit,
|
||||
validateInvestmentCredit,
|
||||
calculateInvestmentCredit,
|
||||
deductInvestmentCredit,
|
||||
investmentCreditSource,
|
||||
investmentAssetIdentity,
|
||||
investmentAssetClaimed,
|
||||
type InvestmentCredit,
|
||||
} from './irpc-investment-credit.ts';
|
||||
import { inputText } from './input-text.ts';
|
||||
import {
|
||||
carryPlan,
|
||||
@@ -23,6 +33,7 @@ import {
|
||||
requireProofParcel,
|
||||
parcelProjection,
|
||||
overlappingProofParcels,
|
||||
canonicalParcel,
|
||||
type FiscalProofParcel,
|
||||
} from './fiscal-proof-parcel.ts';
|
||||
import {
|
||||
@@ -32,6 +43,8 @@ import {
|
||||
internationalPosition,
|
||||
internationalDeduction,
|
||||
internationalCreditSource,
|
||||
creditMoney,
|
||||
creditCompare,
|
||||
newInternationalCredits,
|
||||
type InternationalCredits,
|
||||
type InternationalTaxProof,
|
||||
@@ -93,7 +106,7 @@ type AnnualRecord = {
|
||||
model: AnnualModel;
|
||||
};
|
||||
export type AnnualBook = {
|
||||
version: 1;
|
||||
version: 1 | 2;
|
||||
subjectNuit: string;
|
||||
year: string;
|
||||
reference: string;
|
||||
@@ -108,6 +121,7 @@ export type AnnualBook = {
|
||||
internationalCredits?: InternationalCredits;
|
||||
creditCarry?: CreditCarry;
|
||||
economicCredits?: EconomicCredits;
|
||||
investmentCredit?: InvestmentCredit;
|
||||
result?: AnnualRecord;
|
||||
history: AnnualRecord[];
|
||||
};
|
||||
@@ -131,9 +145,25 @@ export const annualSource = (b: AnnualBook) => {
|
||||
(b.pecPayments ? ' ' + pecSource : '') +
|
||||
(b.internationalCredits ? ' ' + internationalCreditSource : '') +
|
||||
(b.creditCarry ? ' ' + carrySource : '') +
|
||||
(b.economicCredits ? ' ' + economicSource : '')
|
||||
(b.economicCredits ? ' ' + economicSource : '') +
|
||||
(b.investmentCredit ? ' ' + investmentCreditSource : '')
|
||||
);
|
||||
};
|
||||
export function prepareInvestmentCredit(
|
||||
d: Record<string, unknown>,
|
||||
b: AnnualBook,
|
||||
): AnnualBook {
|
||||
if (b.investmentCredit) return b;
|
||||
const next = prepareInternationalBook(b),
|
||||
c = newInvestmentCredit(),
|
||||
a = annualAssessment(d);
|
||||
c.subjectNuit = b.subjectNuit;
|
||||
c.year = b.year;
|
||||
c.assessmentKey = b.assessmentKey;
|
||||
c.qualification.from = a.period?.from ?? '';
|
||||
c.qualification.to = a.period?.to ?? '';
|
||||
return { ...next, version: 2, investmentCredit: c };
|
||||
}
|
||||
export function prepareEconomicBook(b: AnnualBook): AnnualBook {
|
||||
if (b.economicCredits) return b;
|
||||
const next = prepareInternationalBook(b),
|
||||
@@ -240,6 +270,8 @@ function books(d: Record<string, unknown>): Record<string, AnnualBook> {
|
||||
return m;
|
||||
}
|
||||
export function validateAnnual(b: AnnualBook) {
|
||||
if (b?.investmentCredit !== undefined)
|
||||
validateInvestmentCredit(b.investmentCredit);
|
||||
if (b?.economicCredits !== undefined) validateEconomic(b.economicCredits);
|
||||
if (b?.creditCarry !== undefined) validateCreditCarry(b.creditCarry);
|
||||
if (b?.pecPayments !== undefined) validatePec(b.pecPayments);
|
||||
@@ -247,7 +279,9 @@ export function validateAnnual(b: AnnualBook) {
|
||||
validateInternational(b.internationalCredits);
|
||||
if (
|
||||
!b ||
|
||||
b.version !== 1 ||
|
||||
![1, 2].includes(b.version) ||
|
||||
(b.version === 2 && b.investmentCredit === undefined) ||
|
||||
(b.investmentCredit !== undefined && b.version !== 2) ||
|
||||
[
|
||||
'subjectNuit',
|
||||
'year',
|
||||
@@ -467,7 +501,7 @@ export function reuseAnnualWithholding(
|
||||
},
|
||||
};
|
||||
}
|
||||
function dependencies(d: Record<string, unknown>) {
|
||||
function dependencies(d: Record<string, unknown>, includeInvestment = false) {
|
||||
let normal: unknown, L5: unknown;
|
||||
try {
|
||||
const a = annualAssessment(d);
|
||||
@@ -496,6 +530,9 @@ function dependencies(d: Record<string, unknown>) {
|
||||
...(b.internationalCredits ? [b.internationalCredits] : []),
|
||||
...(b.creditCarry ? [b.creditCarry] : []),
|
||||
...(b.economicCredits ? [b.economicCredits] : []),
|
||||
...(includeInvestment && b.investmentCredit
|
||||
? [b.investmentCredit]
|
||||
: []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -505,7 +542,7 @@ function key(d: Record<string, unknown>, b: AnnualBook) {
|
||||
annualSettlementVersion,
|
||||
acquisitionScope(d),
|
||||
input,
|
||||
dependencies(d),
|
||||
dependencies(d, !!b.investmentCredit),
|
||||
]);
|
||||
}
|
||||
export function annualCurrent(d: Record<string, unknown>, b: AnnualBook) {
|
||||
@@ -809,6 +846,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
...internationalGroupPending(
|
||||
b.internationalCredits,
|
||||
!!b.economicCredits,
|
||||
!!b.investmentCredit,
|
||||
),
|
||||
);
|
||||
const countries = b.internationalCredits.positions.map(
|
||||
@@ -1230,6 +1268,122 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
'Crédito económico delimitado à colecta normal corrente;efeitos L7 exigem correspondência própria.',
|
||||
);
|
||||
}
|
||||
const investmentPreview =
|
||||
b.investmentCredit && assessment?.record
|
||||
? calculateInvestmentCredit(
|
||||
b.investmentCredit,
|
||||
assessment.book,
|
||||
assessment.record.model,
|
||||
assessment.key,
|
||||
assessment.current,
|
||||
)
|
||||
: null;
|
||||
if (b.investmentCredit && !investmentPreview)
|
||||
pending.push('CréditoCBF15 exige colecta L6 recuperável.');
|
||||
if (investmentPreview) pending.push(...investmentPreview.pending);
|
||||
if (
|
||||
investmentPreview?.available &&
|
||||
cmp(investmentPreview.available, zero()) > 0n &&
|
||||
(!investmentPreview.projectCeiling ||
|
||||
cmp(investmentPreview.projectCeiling, zero()) > 0n) &&
|
||||
b.creditCarry?.lots.length
|
||||
)
|
||||
pending.push(
|
||||
'CombinaçãoCBF15 com saldos anteriores exige correspondência própria; transportes anteriores preservados.',
|
||||
);
|
||||
if (
|
||||
b.investmentCredit?.qualification.state === 'known' &&
|
||||
b.investmentCredit.qualification.eligible === 'yes' &&
|
||||
investmentPreview?.nominal &&
|
||||
cmp(investmentPreview.nominal, zero()) > 0n
|
||||
) {
|
||||
for (const [scope, other] of Object.entries(books(d))) {
|
||||
if (
|
||||
scope === acquisitionScope(d) ||
|
||||
other.subjectNuit !== b.subjectNuit ||
|
||||
other.year !== b.year ||
|
||||
!other.investmentCredit
|
||||
)
|
||||
continue;
|
||||
try {
|
||||
validateInvestmentCredit(other.investmentCredit);
|
||||
const c = other.investmentCredit;
|
||||
if (
|
||||
c.qualification.state !== 'known' ||
|
||||
c.qualification.eligible !== 'yes'
|
||||
)
|
||||
continue;
|
||||
if (!c.assets.some(investmentAssetClaimed)) continue;
|
||||
for (const r of b.investmentCredit.assets.filter(
|
||||
investmentAssetClaimed,
|
||||
)) {
|
||||
const id = investmentAssetIdentity(r),
|
||||
others = c.assets.filter(
|
||||
(x) =>
|
||||
investmentAssetClaimed(x) &&
|
||||
investmentAssetIdentity(x) === id,
|
||||
);
|
||||
if (!others.length) continue;
|
||||
const proof = creditMoney(r.proofAmount);
|
||||
if (
|
||||
others.some(
|
||||
(x) =>
|
||||
creditCompare(
|
||||
creditMoney(x.proofAmount),
|
||||
proof,
|
||||
) !== 0n,
|
||||
)
|
||||
)
|
||||
pending.push(
|
||||
'Montante documentalCBF15 contraditório entre mapas; reconcilie a prova.',
|
||||
);
|
||||
const own = b.investmentCredit.assets.filter(
|
||||
(x) =>
|
||||
investmentAssetClaimed(x) &&
|
||||
investmentAssetIdentity(x) === id,
|
||||
);
|
||||
if (
|
||||
creditCompare(
|
||||
addFractions(
|
||||
[...own, ...others].map((x) =>
|
||||
creditMoney(x.amount),
|
||||
),
|
||||
),
|
||||
proof,
|
||||
) > 0n
|
||||
)
|
||||
pending.push(
|
||||
'InvestimentoCBF15 atribuído entre mapas ultrapassa o documento efectivo; não duplicar capacidade.',
|
||||
);
|
||||
}
|
||||
const sameProject =
|
||||
canonicalParcel(c.projectReference) ===
|
||||
canonicalParcel(b.investmentCredit.projectReference);
|
||||
const sharedAsset = c.assets
|
||||
.filter(investmentAssetClaimed)
|
||||
.some((r) =>
|
||||
b.investmentCredit!.assets.some((p) =>
|
||||
overlappingProofParcels(
|
||||
{ ...r, identity: investmentAssetIdentity(r) },
|
||||
{ ...p, identity: investmentAssetIdentity(p) },
|
||||
),
|
||||
),
|
||||
);
|
||||
if (sameProject || sharedAsset)
|
||||
pending.push(
|
||||
'CBF15 já preparado noutro mapa da entidade; consolide projecto/documentos sem duplicar: ' +
|
||||
scope,
|
||||
);
|
||||
} catch (e) {
|
||||
pending.push(
|
||||
'Outro mapaCBF15 não recuperável para conferir duplicação: ' +
|
||||
scope +
|
||||
'; ' +
|
||||
String(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const economicPreview = b.economicCredits
|
||||
? economicDeduction(economicRows, colecta, true)
|
||||
: null;
|
||||
@@ -1286,6 +1440,27 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
)
|
||||
: null;
|
||||
if (carry) pending.push(...carry.pending);
|
||||
if (investmentPreview?.projectCeiling && !pending.length) {
|
||||
const beforeInternational = economicPreview?.afterCredit ?? colecta;
|
||||
const preview = b.internationalCredits
|
||||
? internationalDeduction(
|
||||
positions,
|
||||
beforeInternational,
|
||||
true,
|
||||
carry?.international ?? undefined,
|
||||
)
|
||||
: null;
|
||||
const remaining = carry
|
||||
? subtractFractions(
|
||||
preview?.afterCredit ?? beforeInternational!,
|
||||
carry.priorInternational,
|
||||
)
|
||||
: (preview?.afterCredit ?? beforeInternational);
|
||||
if (!remaining || cmp(investmentPreview.projectCeiling, remaining) > 0n)
|
||||
pending.push(
|
||||
'Quota residualCBF15 do projecto excede a colecta após créditos económicos/internacionais; reconcilie a atribuição antes de deduzir.',
|
||||
);
|
||||
}
|
||||
const ready = pending.length === 0 && colecta !== null,
|
||||
economic = b.economicCredits
|
||||
? economicDeduction(economicRows, colecta, ready)
|
||||
@@ -1306,10 +1481,14 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
carry.priorInternational,
|
||||
)
|
||||
: (international?.afterCredit ?? afterEconomic);
|
||||
const investment = investmentPreview
|
||||
? deductInvestmentCredit(investmentPreview, creditCollecta, ready)
|
||||
: null;
|
||||
const afterInvestment = investment?.afterCredit ?? creditCollecta;
|
||||
const pec = b.pecPayments
|
||||
? pecDeduction(
|
||||
pecAvailable,
|
||||
creditCollecta,
|
||||
afterInvestment,
|
||||
ready,
|
||||
carry?.pec ?? undefined,
|
||||
)
|
||||
@@ -1317,10 +1496,10 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
const afterPec =
|
||||
ready && carry
|
||||
? subtractFractions(
|
||||
pec?.afterPec ?? creditCollecta!,
|
||||
pec?.afterPec ?? afterInvestment!,
|
||||
carry.priorPec,
|
||||
)
|
||||
: (pec?.afterPec ?? creditCollecta);
|
||||
: (pec?.afterPec ?? afterInvestment);
|
||||
const arithmeticBalance = ready
|
||||
? subtractFractions(
|
||||
subtractFractions(afterPec!, knownWithholdings),
|
||||
@@ -1387,6 +1566,7 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
claims,
|
||||
...(economic ? { economicCredit: economic } : {}),
|
||||
internationalCredit: international,
|
||||
...(investment ? { investmentCredit: investment } : {}),
|
||||
pecDeduction: pec,
|
||||
...(carry
|
||||
? {
|
||||
@@ -1427,8 +1607,12 @@ export function calculateAnnual(d: Record<string, unknown>, b: AnnualBook) {
|
||||
? 'Crédito internacional do próprio exercício:fracção nacional derivada no caso L6 ordinário com parcela líquida correspondida;CDT e montantes MZN documentados;sem repartição presumida de custos/perdas,transporte ou consumo oficial. '
|
||||
: 'Crédito internacional do próprio exercício:fracção nacional,convenção e montantes MZN são documentados;sem cálculo presumido,transporte ou consumo oficial. ') +
|
||||
(b.economicCredits
|
||||
? 'Benefícios positivos ficam separados;crédito económico no detalhe próprio.'
|
||||
: 'Créditos económicos/benefícios positivos ficam separados.'),
|
||||
? b.investmentCredit
|
||||
? 'CBF15 do primeiro exercício no detalhe próprio;restantes benefícios positivos separados;crédito económico no detalhe próprio.'
|
||||
: 'Benefícios positivos ficam separados;crédito económico no detalhe próprio.'
|
||||
: b.investmentCredit
|
||||
? 'CBF15 do primeiro exercício no detalhe próprio;restantes créditos económicos/benefícios positivos separados.'
|
||||
: 'Créditos económicos/benefícios positivos ficam separados.'),
|
||||
]
|
||||
: []),
|
||||
b.economicCredits
|
||||
@@ -1815,7 +1999,9 @@ export function annualReportRows(
|
||||
m.input.internationalCredits?.benefits
|
||||
.state ?? '',
|
||||
) +
|
||||
'.',
|
||||
(m.investmentCredit
|
||||
? '. Resumo anterior dos benefícios conservado, não reaplicado; CBF15 no detalhe próprio.'
|
||||
: '.'),
|
||||
m.internationalCredit.source +
|
||||
'; ' +
|
||||
(m.input.internationalCredits?.reference ?? '') +
|
||||
@@ -1904,6 +2090,99 @@ export function annualReportRows(
|
||||
] as [string, string, string, string],
|
||||
]
|
||||
: []),
|
||||
...(m.investmentCredit
|
||||
? [
|
||||
...m.investmentCredit.rows.map(
|
||||
(r) =>
|
||||
[
|
||||
'IRPC — L8',
|
||||
'CBF15 — ' +
|
||||
r.input.issuer +
|
||||
' / ' +
|
||||
r.input.document +
|
||||
' / ' +
|
||||
r.input.parcel +
|
||||
'; data ' +
|
||||
r.input.date,
|
||||
'Valor declarado: ' +
|
||||
r.input.amount +
|
||||
' ' +
|
||||
r.input.currency +
|
||||
'; investimento elegível: ' +
|
||||
show(r.value),
|
||||
r.input.reference +
|
||||
'; ' +
|
||||
r.reason +
|
||||
'; parcela: ' +
|
||||
r.input.parcelReference +
|
||||
'; ' +
|
||||
r.pending.join(' | '),
|
||||
] as [string, string, string, string],
|
||||
),
|
||||
[
|
||||
'IRPC — L8',
|
||||
'CBF15 — crédito fiscal por investimento',
|
||||
'Investimento: ' +
|
||||
show(m.investmentCredit.investment) +
|
||||
'; taxa ' +
|
||||
(m.investmentCredit.rate === null
|
||||
? 'não aplicável'
|
||||
: m.investmentCredit.rate + '%') +
|
||||
'; nominal ' +
|
||||
show(m.investmentCredit.nominal) +
|
||||
'; uso anterior ' +
|
||||
show(m.investmentCredit.priorUse) +
|
||||
'; disponível ' +
|
||||
show(m.investmentCredit.available) +
|
||||
'; dedução ' +
|
||||
show(m.investmentCredit.deducted) +
|
||||
'; sem colecta neste exercício ' +
|
||||
show(m.investmentCredit.unused) +
|
||||
(m.investmentCredit.projectCeiling
|
||||
? '; quota documental do projecto ' +
|
||||
show(m.investmentCredit.projectCeiling)
|
||||
: '') +
|
||||
'; colecta após benefício ' +
|
||||
show(m.investmentCredit.afterCredit),
|
||||
m.investmentCredit.version +
|
||||
'; ' +
|
||||
m.investmentCredit.source +
|
||||
'; projecto ' +
|
||||
m.investmentCredit.input.projectReference +
|
||||
'; ' +
|
||||
m.investmentCredit.input.qualification.reference +
|
||||
'; período ' +
|
||||
m.investmentCredit.input.qualification.from +
|
||||
' a ' +
|
||||
m.investmentCredit.input.qualification.to +
|
||||
'; ' +
|
||||
m.investmentCredit.input.qualification
|
||||
.periodReference +
|
||||
'; ' +
|
||||
m.investmentCredit.input.locationReference +
|
||||
'; ' +
|
||||
'início ' +
|
||||
m.investmentCredit.input.startDate +
|
||||
'; ' +
|
||||
m.investmentCredit.input.startReference +
|
||||
'; reconhecimento ' +
|
||||
m.investmentCredit.input.recognitionReference +
|
||||
'; exclusividade ' +
|
||||
(m.investmentCredit.input.soleProject === 'no'
|
||||
? m.investmentCredit.input.projectQuota
|
||||
?.reference
|
||||
: m.investmentCredit.input
|
||||
.soleProjectReference) +
|
||||
'; outros benefícios: ' +
|
||||
m.investmentCredit.input
|
||||
.noOtherBenefitsReference +
|
||||
'; uso anterior: ' +
|
||||
m.investmentCredit.input.previousUse.reference +
|
||||
'; ' +
|
||||
m.investmentCredit.pending.join(' | '),
|
||||
] as [string, string, string, string],
|
||||
]
|
||||
: []),
|
||||
...(m.pecDeduction
|
||||
? [
|
||||
[
|
||||
|
||||
@@ -383,6 +383,7 @@ export function internationalProofValue(
|
||||
export function internationalGroupPending(
|
||||
c: InternationalCredits,
|
||||
economicDetailed = false,
|
||||
benefitsDetailed = false,
|
||||
) {
|
||||
const pending: string[] = [];
|
||||
if (!c.state || !c.reference.trim())
|
||||
@@ -401,7 +402,9 @@ export function internationalGroupPending(
|
||||
...(economicDetailed
|
||||
? []
|
||||
: [['Dupla tributação económica', c.economic] as const]),
|
||||
['Benefícios/créditos próprios', c.benefits] as const,
|
||||
...(benefitsDetailed
|
||||
? []
|
||||
: [['Benefícios/créditos próprios', c.benefits] as const]),
|
||||
]) {
|
||||
try {
|
||||
if (!e.state || !e.reference.trim())
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
import {
|
||||
newQualification,
|
||||
qualifySpecialTreatment,
|
||||
type Qualification,
|
||||
} from './irpc-special.ts';
|
||||
import type { NormalBook, NormalModel } from './irpc-normal.ts';
|
||||
import {
|
||||
creditMoney,
|
||||
creditCompare,
|
||||
creditLimit,
|
||||
type CreditEvidence,
|
||||
} from './irpc-international-credit.ts';
|
||||
import {
|
||||
canonicalParcel,
|
||||
requireProofParcel,
|
||||
overlappingProofParcels,
|
||||
} from './fiscal-proof-parcel.ts';
|
||||
import {
|
||||
addFractions,
|
||||
subtractFractions,
|
||||
fraction,
|
||||
type FiscalFraction,
|
||||
} from './fiscal-rational.ts';
|
||||
export const investmentCreditVersion = 'IRPC-CBF15/1';
|
||||
export const investmentCreditSource =
|
||||
'CBF,Lei4/2009,arts3/4/9/10/12/13/15; compilação oficial APIEX2025,PDF66–72,art15PDF71–72. Primeiro exercício2026,regime normal residente,ano civil/MZN,colecta inteiramente do projecto documentada. Activo corpóreo novo afecto à exploração nacional,sem exclusões15(6). Cidade de Maputo5%,restantes províncias10%. Menor entre crédito disponível e colecta remanescente depois dos créditos económicos/internacionais,antesPEC/RF/PPC (RIRPC21). Sem transporte/reembolso ou consumo de saldo oficial presumidos;outros exercícios,regimes e cumulações separados.';
|
||||
type Choice = '' | 'yes' | 'no';
|
||||
export type InvestmentAsset = {
|
||||
id: string;
|
||||
issuer: string;
|
||||
document: string;
|
||||
parcel: string;
|
||||
parcelReference: string;
|
||||
date: string;
|
||||
proofAmount: string;
|
||||
amount: string;
|
||||
currency: string;
|
||||
realised: Choice;
|
||||
newTangible: Choice;
|
||||
domesticUse: Choice;
|
||||
directProductive: Choice;
|
||||
excluded15_6: Choice;
|
||||
reference: string;
|
||||
};
|
||||
export const newInvestmentAsset = (id: string): InvestmentAsset => ({
|
||||
id,
|
||||
issuer: '',
|
||||
document: '',
|
||||
parcel: '',
|
||||
parcelReference: '',
|
||||
date: '',
|
||||
proofAmount: '',
|
||||
amount: '',
|
||||
currency: 'MZN',
|
||||
realised: '',
|
||||
newTangible: '',
|
||||
domesticUse: '',
|
||||
directProductive: '',
|
||||
excluded15_6: '',
|
||||
reference: '',
|
||||
});
|
||||
export type InvestmentCredit = {
|
||||
projectQuota?: { amount: string; reference: string };
|
||||
version: 1;
|
||||
qualification: Qualification;
|
||||
subjectNuit: string;
|
||||
year: string;
|
||||
assessmentKey: string;
|
||||
projectReference: string;
|
||||
location: '' | 'maputo_city' | 'other';
|
||||
locationReference: string;
|
||||
startKind: '' | 'new' | 'existing';
|
||||
startDate: string;
|
||||
startReference: string;
|
||||
cbfRequirements: Choice;
|
||||
recognition: '' | 'documented' | 'not_required';
|
||||
recognitionReference: string;
|
||||
genericOnly: Choice;
|
||||
noOtherBenefits: Choice;
|
||||
noOtherBenefitsReference: string;
|
||||
soleProject: Choice;
|
||||
soleProjectReference: string;
|
||||
assets: InvestmentAsset[];
|
||||
previousUse: CreditEvidence;
|
||||
};
|
||||
export const newInvestmentCredit = (): InvestmentCredit => ({
|
||||
version: 1,
|
||||
qualification: newQualification(),
|
||||
subjectNuit: '',
|
||||
year: '',
|
||||
assessmentKey: '',
|
||||
projectReference: '',
|
||||
location: '',
|
||||
locationReference: '',
|
||||
startKind: '',
|
||||
startDate: '',
|
||||
startReference: '',
|
||||
cbfRequirements: '',
|
||||
recognition: '',
|
||||
recognitionReference: '',
|
||||
genericOnly: '',
|
||||
noOtherBenefits: '',
|
||||
noOtherBenefitsReference: '',
|
||||
soleProject: '',
|
||||
soleProjectReference: '',
|
||||
assets: [],
|
||||
previousUse: { state: '', amount: '', reference: '' },
|
||||
});
|
||||
export function validateInvestmentCredit(c: InvestmentCredit) {
|
||||
const template = newInvestmentCredit();
|
||||
if (
|
||||
!c ||
|
||||
c.version !== 1 ||
|
||||
Object.keys(template)
|
||||
.filter(
|
||||
(k) =>
|
||||
![
|
||||
'version',
|
||||
'qualification',
|
||||
'assets',
|
||||
'previousUse',
|
||||
].includes(k),
|
||||
)
|
||||
.some(
|
||||
(k) =>
|
||||
typeof (c as unknown as Record<string, unknown>)[k] !==
|
||||
'string',
|
||||
) ||
|
||||
!c.qualification ||
|
||||
!c.previousUse ||
|
||||
!Array.isArray(c.assets) ||
|
||||
new Set(c.assets.map((r) => r?.id)).size !== c.assets.length
|
||||
)
|
||||
throw Error('CréditoCBF15 não recuperável; original conservado.');
|
||||
if (
|
||||
c.projectQuota !== undefined &&
|
||||
(!c.projectQuota ||
|
||||
typeof c.projectQuota.amount !== 'string' ||
|
||||
typeof c.projectQuota.reference !== 'string')
|
||||
)
|
||||
throw Error('QuotaCBF15 não recuperável; original conservado.');
|
||||
const q = c.qualification;
|
||||
if (
|
||||
!['', 'known', 'absent', 'not_applicable'].includes(q.state) ||
|
||||
!['', 'yes', 'no'].includes(q.eligible) ||
|
||||
['from', 'to', 'reference', 'periodReference'].some(
|
||||
(k) =>
|
||||
typeof (q as unknown as Record<string, unknown>)[k] !==
|
||||
'string',
|
||||
) ||
|
||||
!['', 'maputo_city', 'other'].includes(c.location) ||
|
||||
!['', 'new', 'existing'].includes(c.startKind) ||
|
||||
!['', 'documented', 'not_required'].includes(c.recognition) ||
|
||||
[
|
||||
'cbfRequirements',
|
||||
'genericOnly',
|
||||
'noOtherBenefits',
|
||||
'soleProject',
|
||||
].some(
|
||||
(k) =>
|
||||
!['', 'yes', 'no'].includes(
|
||||
(c as unknown as Record<string, string>)[k],
|
||||
),
|
||||
) ||
|
||||
!['', 'known', 'absent'].includes(c.previousUse.state) ||
|
||||
typeof c.previousUse.amount !== 'string' ||
|
||||
typeof c.previousUse.reference !== 'string'
|
||||
)
|
||||
throw Error('QualificaçãoCBF15 não recuperável; original conservado.');
|
||||
for (const r of c.assets) {
|
||||
if (
|
||||
!r ||
|
||||
Object.keys(newInvestmentAsset('')).some(
|
||||
(k) =>
|
||||
typeof (r as unknown as Record<string, unknown>)[k] !==
|
||||
'string',
|
||||
) ||
|
||||
[
|
||||
'realised',
|
||||
'newTangible',
|
||||
'domesticUse',
|
||||
'directProductive',
|
||||
'excluded15_6',
|
||||
].some(
|
||||
(k) =>
|
||||
!['', 'yes', 'no'].includes(
|
||||
(r as unknown as Record<string, string>)[k],
|
||||
),
|
||||
)
|
||||
)
|
||||
throw Error(
|
||||
'InvestimentoCBF15 não recuperável; original conservado.',
|
||||
);
|
||||
}
|
||||
}
|
||||
const zero = () => fraction(0n);
|
||||
const minimum = (a: FiscalFraction, b: FiscalFraction) =>
|
||||
creditCompare(a, b) < 0n ? a : b;
|
||||
const iso = (s: string) =>
|
||||
/^2026-\d{2}-\d{2}$/.test(s) &&
|
||||
Number.isFinite(Date.parse(s)) &&
|
||||
new Date(s).toISOString().slice(0, 10) === s;
|
||||
export const investmentAssetIdentity = (r: InvestmentAsset) =>
|
||||
JSON.stringify([canonicalParcel(r.issuer), canonicalParcel(r.document)]);
|
||||
export function calculateInvestmentCredit(
|
||||
c: InvestmentCredit,
|
||||
b: NormalBook,
|
||||
m: NormalModel,
|
||||
currentKey: string,
|
||||
current: boolean,
|
||||
) {
|
||||
validateInvestmentCredit(c);
|
||||
const decision = qualifySpecialTreatment(c.qualification, b, m),
|
||||
pending = [...decision.pending];
|
||||
if (c.noOtherBenefits !== 'yes' || !c.noOtherBenefitsReference.trim())
|
||||
pending.push(
|
||||
'Restantes benefícios próprios por reconciliar; o detalheCBF15 não os deduz nem ignora por presunção.',
|
||||
);
|
||||
let investment: FiscalFraction | null = null,
|
||||
nominal: FiscalFraction | null = null,
|
||||
priorUse: FiscalFraction | null = null,
|
||||
available: FiscalFraction | null = null,
|
||||
rate: number | null = null;
|
||||
let projectCeiling: FiscalFraction | undefined;
|
||||
const rows: {
|
||||
input: InvestmentAsset;
|
||||
value: FiscalFraction | null;
|
||||
reason: string;
|
||||
pending: string[];
|
||||
}[] = [];
|
||||
if (decision.status === 'calculated') {
|
||||
try {
|
||||
if (
|
||||
!current ||
|
||||
b.special ||
|
||||
b.periodsFx ||
|
||||
b.scope !== 'resident_general' ||
|
||||
b.year !== '2026' ||
|
||||
c.year !== b.year ||
|
||||
c.subjectNuit !== b.base?.subjectNuit ||
|
||||
!c.assessmentKey ||
|
||||
c.assessmentKey !== currentKey ||
|
||||
!m.colecta
|
||||
)
|
||||
throw Error(
|
||||
'CBF15 exige colecta L6 normal corrente,residente/ano civil2026/MZN; outros âmbitos separados.',
|
||||
);
|
||||
if (
|
||||
!c.projectReference.trim() ||
|
||||
c.cbfRequirements !== 'yes' ||
|
||||
!c.recognition ||
|
||||
!c.recognitionReference.trim() ||
|
||||
c.genericOnly !== 'yes' ||
|
||||
c.noOtherBenefits !== 'yes' ||
|
||||
!c.noOtherBenefitsReference.trim()
|
||||
)
|
||||
throw Error(
|
||||
'CondiçõesCBF3/9/10,regime genérico13,reconhecimento/excepção documentada e ausência de outros benefícios por corresponder.',
|
||||
);
|
||||
if (c.soleProject === 'yes') {
|
||||
if (!c.soleProjectReference.trim())
|
||||
throw Error(
|
||||
'Correspondência da colecta integral do projecto por documentar.',
|
||||
);
|
||||
} else if (c.soleProject === 'no') {
|
||||
if (!c.projectQuota?.reference.trim())
|
||||
throw Error(
|
||||
'Quota residual da colecta do projecto após créditos económicos/internacionais exige suporte próprio.',
|
||||
);
|
||||
projectCeiling = creditLimit(c.projectQuota.amount);
|
||||
if (creditCompare(projectCeiling, m.colecta) > 0n)
|
||||
throw Error(
|
||||
'Quota do projecto excede a colecta L6; reconcilie.',
|
||||
);
|
||||
} else
|
||||
throw Error(
|
||||
'Correspondência da colecta ao projecto por documentar.',
|
||||
);
|
||||
if (
|
||||
!c.location ||
|
||||
!c.locationReference.trim() ||
|
||||
!c.startKind ||
|
||||
!iso(c.startDate) ||
|
||||
!c.startReference.trim()
|
||||
)
|
||||
throw Error(
|
||||
'Local efectivo do investimento e primeiro exercício2026 por documentar: início de exploração para projecto novo,procedimentos após aprovação para projecto em funcionamento.',
|
||||
);
|
||||
if (!c.assets.length)
|
||||
throw Error(
|
||||
'Investimento conhecido exige activo/documento; zero não se presume.',
|
||||
);
|
||||
rate = c.location === 'maputo_city' ? 5 : 10;
|
||||
} catch (e) {
|
||||
pending.push(String(e));
|
||||
}
|
||||
for (const r of c.assets) {
|
||||
const issues: string[] = [];
|
||||
let value: FiscalFraction | null = null;
|
||||
try {
|
||||
const excluded =
|
||||
r.realised === 'no' ||
|
||||
r.newTangible === 'no' ||
|
||||
r.domesticUse === 'no' ||
|
||||
r.directProductive === 'no' ||
|
||||
r.excluded15_6 === 'yes';
|
||||
if (excluded && r.reference.trim()) {
|
||||
rows.push({
|
||||
input: structuredClone(r),
|
||||
value: zero(),
|
||||
reason: 'Condição negativa/exclusão15(5–6) documentada; valor conservado,crédito zero para este activo.',
|
||||
pending: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
requireProofParcel(r);
|
||||
if (
|
||||
r.currency !== 'MZN' ||
|
||||
r.realised !== 'yes' ||
|
||||
!r.id.trim() ||
|
||||
!r.issuer.trim() ||
|
||||
!r.document.trim() ||
|
||||
!iso(r.date) ||
|
||||
!r.reference.trim() ||
|
||||
r.newTangible !== 'yes' ||
|
||||
r.domesticUse !== 'yes' ||
|
||||
r.directProductive !== 'yes' ||
|
||||
r.excluded15_6 !== 'no'
|
||||
)
|
||||
throw Error(
|
||||
'Activo/documento2026,corpóreo novo,afectação nacional produtiva e exclusões15(6) por corresponder.',
|
||||
);
|
||||
if (c.startKind === 'existing' && r.date < c.startDate)
|
||||
throw Error(
|
||||
'Investimento anterior ao início documentado deste recorte; correspondência própria necessária.',
|
||||
);
|
||||
value = creditMoney(r.amount);
|
||||
if (creditCompare(value, creditMoney(r.proofAmount)) > 0n)
|
||||
throw Error(
|
||||
'Investimento atribuído excede o documento efectivo.',
|
||||
);
|
||||
if (
|
||||
c.assets.some(
|
||||
(other) =>
|
||||
other !== r &&
|
||||
investmentAssetClaimed(other) &&
|
||||
overlappingProofParcels(
|
||||
{ ...r, identity: investmentAssetIdentity(r) },
|
||||
{
|
||||
...other,
|
||||
identity: investmentAssetIdentity(other),
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
throw Error(
|
||||
'Documento/parcela de investimento repetido; não duplicar activos por mudar id/data.',
|
||||
);
|
||||
} catch (e) {
|
||||
issues.push(String(e));
|
||||
value = null;
|
||||
}
|
||||
rows.push({
|
||||
input: structuredClone(r),
|
||||
value,
|
||||
reason: issues.length
|
||||
? issues.join(' ')
|
||||
: 'Activo elegível documentado.',
|
||||
pending: issues,
|
||||
});
|
||||
pending.push(...issues);
|
||||
}
|
||||
const pools = new Map<
|
||||
string,
|
||||
{ amount: FiscalFraction; proof: FiscalFraction }
|
||||
>();
|
||||
for (const row of rows) {
|
||||
if (
|
||||
!row.value ||
|
||||
creditCompare(row.value, zero()) === 0n ||
|
||||
row.pending.length
|
||||
)
|
||||
continue;
|
||||
const id = investmentAssetIdentity(row.input),
|
||||
proof = creditMoney(row.input.proofAmount),
|
||||
existing = pools.get(id);
|
||||
if (existing && creditCompare(existing.proof, proof) !== 0n)
|
||||
pending.push(
|
||||
'Montante total do mesmo documento difere entre parcelas; reconcilie a prova.',
|
||||
);
|
||||
const amount = addFractions([
|
||||
existing?.amount ?? zero(),
|
||||
row.value,
|
||||
]);
|
||||
pools.set(id, { amount, proof });
|
||||
if (creditCompare(amount, proof) > 0n)
|
||||
pending.push(
|
||||
'Soma das parcelas elegíveis ultrapassa o documento efectivo; não duplicar investimento.',
|
||||
);
|
||||
}
|
||||
if (!pending.length) {
|
||||
investment = addFractions(
|
||||
rows.flatMap((r) => (r.value ? [r.value] : [])),
|
||||
);
|
||||
nominal = fraction(
|
||||
BigInt(investment.numerator) * BigInt(rate!),
|
||||
BigInt(investment.denominator) * 100n,
|
||||
);
|
||||
try {
|
||||
if (!c.previousUse.state || !c.previousUse.reference.trim())
|
||||
throw Error(
|
||||
'Uso anterior efectivo do crédito por documentar; histórico de simulações não é uso fiscal.',
|
||||
);
|
||||
if (c.previousUse.state === 'absent') {
|
||||
if (c.previousUse.amount.trim())
|
||||
throw Error('Ausência de uso contradiz montante');
|
||||
priorUse = zero();
|
||||
} else priorUse = creditLimit(c.previousUse.amount);
|
||||
if (creditCompare(priorUse, nominal) > 0n)
|
||||
throw Error('Uso anterior excede o crédito nominal.');
|
||||
available = subtractFractions(nominal, priorUse);
|
||||
} catch (e) {
|
||||
pending.push(String(e));
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
['absent', 'not_applicable'].includes(decision.status) &&
|
||||
!pending.length
|
||||
) {
|
||||
investment = nominal = priorUse = available = zero();
|
||||
}
|
||||
return {
|
||||
version: investmentCreditVersion,
|
||||
input: structuredClone(c),
|
||||
decision,
|
||||
rows,
|
||||
investment,
|
||||
...(projectCeiling === undefined ? {} : { projectCeiling }),
|
||||
rate,
|
||||
nominal,
|
||||
priorUse,
|
||||
available: pending.length ? null : available,
|
||||
pending,
|
||||
source:
|
||||
c.soleProject === 'no'
|
||||
? investmentCreditSource.replace(
|
||||
'colecta inteiramente do projecto documentada',
|
||||
'quota residual documental da colecta do projecto após créditos económicos/internacionais',
|
||||
) +
|
||||
' QuotaCBF15-project/1: limite documental não superior ao saldo global corrente, com atribuição dos créditos anteriores ao projecto expressamente reconciliada; não é repartição automática de custos/perdas ou rendimentos.'
|
||||
: investmentCreditSource,
|
||||
};
|
||||
}
|
||||
export function deductInvestmentCredit(
|
||||
model: ReturnType<typeof calculateInvestmentCredit>,
|
||||
collecta: FiscalFraction | null,
|
||||
ready: boolean,
|
||||
) {
|
||||
const deducted =
|
||||
ready && collecta && model.available
|
||||
? minimum(
|
||||
model.available,
|
||||
model.projectCeiling === undefined
|
||||
? collecta
|
||||
: minimum(collecta, model.projectCeiling),
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
...model,
|
||||
deducted,
|
||||
afterCredit:
|
||||
deducted && collecta ? subtractFractions(collecta, deducted) : null,
|
||||
unused:
|
||||
deducted && model.available
|
||||
? subtractFractions(model.available, deducted)
|
||||
: null,
|
||||
officialBalancesConsumed: false,
|
||||
carryCreated: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function investmentAssetClaimed(r: InvestmentAsset) {
|
||||
try {
|
||||
return (
|
||||
r.realised === 'yes' &&
|
||||
r.newTangible === 'yes' &&
|
||||
r.domesticUse === 'yes' &&
|
||||
r.directProductive === 'yes' &&
|
||||
r.excluded15_6 === 'no' &&
|
||||
r.currency === 'MZN' &&
|
||||
creditCompare(creditMoney(r.amount), zero()) > 0n
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1400,3 +1400,5 @@ function specialCombinationsReport(
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
export { qualify as qualifySpecialTreatment };
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IrpcInvestmentCreditDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_derived_national_fraction_round_trips_through_the_existing_private_draft_writer(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||
$this->actingAs(User::findOrFail(1021));
|
||||
$id = null;
|
||||
$etag = null;
|
||||
foreach (['city', 'province', 'capped'] as $name) {
|
||||
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-cbf15-20261110/'.$name.'.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$personal = ['person_type' => $snapshot['person_type']];
|
||||
unset($snapshot['person_type']);
|
||||
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $snapshot];
|
||||
if ($id === null) {
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
} else {
|
||||
$this->putJson('/api/v1/simulation-drafts/'.$id, $payload, ['If-Match' => $etag])->assertOk();
|
||||
}
|
||||
$response = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
|
||||
$etag = $response->headers->get('ETag');
|
||||
$back = $response->json('data.economic_snapshot');
|
||||
$this->assertSame($snapshot['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||
$this->assertSame($snapshot['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||
$book = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$snapshot['context_opportunity']];
|
||||
$this->assertSame(2, $book['version']);
|
||||
$this->assertCount(array_search($name, ['city', 'province', 'capped'], true) + 1, $book['history']);
|
||||
$model = $book['result']['model'];
|
||||
$this->assertSame('IRPC-CBF15/1', $model['investmentCredit']['version']);
|
||||
$this->assertSame(['numerator' => $name === 'city' ? '500' : ($name === 'province' ? '1000' : '1400'), 'denominator' => '1'], $model['investmentCredit']['deducted']);
|
||||
$this->assertSame(['numerator' => $name === 'city' ? '200' : ($name === 'province' ? '-300' : '-500'), 'denominator' => '1'], $model['arithmeticBalance']);
|
||||
file_put_contents(base_path('outputs/irpc-cbf15-20261110/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IrpcInvestmentQuotaDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_documented_project_quota_uses_the_existing_private_draft_writer(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('proactive_investment_20261027_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||
$this->actingAs(User::findOrFail(1021));
|
||||
$d = json_decode(file_get_contents(base_path('outputs/irpc-cbf15-20261110/partial.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$personal = ['person_type' => $d['person_type']];
|
||||
unset($d['person_type']);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
|
||||
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
|
||||
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
|
||||
$this->assertSame(2, $b['version']);
|
||||
$this->assertSame(['numerator' => '200', 'denominator' => '1'], $b['result']['model']['investmentCredit']['projectCeiling']);
|
||||
$this->assertSame(['numerator' => '200', 'denominator' => '1'], $b['result']['model']['investmentCredit']['deducted']);
|
||||
$this->assertSame(['numerator' => '500', 'denominator' => '1'], $b['result']['model']['arithmeticBalance']);
|
||||
$this->assertCount(4, $b['history']);
|
||||
file_put_contents(base_path('outputs/irpc-cbf15-20261110/recovered-partial.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { internationalCase } from './irpc-international-case.mjs';
|
||||
import { prepareInvestmentCredit } from '../../../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import { newInvestmentAsset } from '../../../resources/js/lib/irpc-investment-credit.ts';
|
||||
export function investmentCase() {
|
||||
const c = internationalCase();
|
||||
c.b = prepareInvestmentCredit(c.d, c.b);
|
||||
Object.assign(c.b.investmentCredit, {
|
||||
qualification: {
|
||||
state: 'known',
|
||||
eligible: 'yes',
|
||||
from: '2026-01-01',
|
||||
to: '2026-12-31',
|
||||
reference: 'SYNTHETIC CBF3/9/13 qualification and eligible project',
|
||||
periodReference: 'SYNTHETIC current civil2026 fiscal period',
|
||||
},
|
||||
projectReference: 'SYNTHETIC-APPROVED-PROJECT-2026',
|
||||
location: 'maputo_city',
|
||||
locationReference:
|
||||
'SYNTHETIC actual investment in Maputo city, not company address',
|
||||
startKind: 'new',
|
||||
startDate: '2026-06-01',
|
||||
startReference:
|
||||
'SYNTHETIC start of taxable operations in first exercise2026',
|
||||
cbfRequirements: 'yes',
|
||||
recognition: 'documented',
|
||||
recognitionReference:
|
||||
'SYNTHETIC competent investment authorisation and CBF10 recognition presented; not a real administrative act',
|
||||
genericOnly: 'yes',
|
||||
noOtherBenefits: 'yes',
|
||||
noOtherBenefitsReference:
|
||||
'SYNTHETIC no specific or other CBF/IRPC benefits in this case; statutory international/PEC remain separate',
|
||||
soleProject: 'yes',
|
||||
soleProjectReference:
|
||||
'SYNTHETIC whole current collecta relates to project activity',
|
||||
assets: [
|
||||
{
|
||||
...newInvestmentAsset('machine-1'),
|
||||
issuer: 'SYNTHETIC-SUPPLIER',
|
||||
document: 'SYNTHETIC-INVESTMENT-INVOICE-2026',
|
||||
parcel: 'machine-1',
|
||||
parcelReference:
|
||||
'SYNTHETIC supported asset line, not another use of same investment',
|
||||
date: '2026-05-01',
|
||||
proofAmount: '10000',
|
||||
amount: '10000',
|
||||
realised: 'yes',
|
||||
newTangible: 'yes',
|
||||
domesticUse: 'yes',
|
||||
directProductive: 'yes',
|
||||
excluded15_6: 'no',
|
||||
reference:
|
||||
'SYNTHETIC realised ordinary productive new machine, not advanced technology or other15(6) exclusions',
|
||||
},
|
||||
],
|
||||
previousUse: {
|
||||
state: 'known',
|
||||
amount: '0',
|
||||
reference: 'SYNTHETIC actual prior credit use known zero',
|
||||
},
|
||||
});
|
||||
return c;
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
import {
|
||||
normalBook,
|
||||
recordNormal,
|
||||
saveNormalBook,
|
||||
} from '../../resources/js/lib/irpc-normal.ts';
|
||||
import { carryCase } from './helpers/irpc-carry-case.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { investmentCase } from './helpers/irpc-investment-case.mjs';
|
||||
import { internationalCase } from './helpers/irpc-international-case.mjs';
|
||||
import {
|
||||
reuseAnnualAssessment,
|
||||
prepareInvestmentCredit,
|
||||
calculateAnnual,
|
||||
recordAnnual,
|
||||
saveAnnualBook,
|
||||
annualBook,
|
||||
annualCurrent,
|
||||
annualHtml,
|
||||
} from '../../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
void test('CBF15 city5percent and other provinces10percent deduct once after international and before PEC/PPC', () => {
|
||||
const c = investmentCase(),
|
||||
before = c.d.modelo_a_snapshot,
|
||||
m = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(m.pending, []);
|
||||
assert.deepEqual(m.investmentCredit.nominal, {
|
||||
numerator: '500',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.investmentCredit.afterCredit, {
|
||||
numerator: '900',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.arithmeticBalance, {
|
||||
numerator: '200',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.equal(c.d.modelo_a_snapshot, before);
|
||||
c.b.investmentCredit.location = 'other';
|
||||
const other = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(other.investmentCredit.deducted, {
|
||||
numerator: '1000',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(other.arithmeticBalance, {
|
||||
numerator: '-300',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.equal(other.investmentCredit.officialBalancesConsumed, false);
|
||||
});
|
||||
void test('CBF15 caps at remaining collection and separates unused credit from unrecovered PEC and PPC', () => {
|
||||
const c = investmentCase();
|
||||
c.b.investmentCredit.location = 'other';
|
||||
Object.assign(c.b.investmentCredit.assets[0], {
|
||||
amount: '40000',
|
||||
proofAmount: '40000',
|
||||
});
|
||||
const m = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(m.pending, []);
|
||||
assert.deepEqual(m.investmentCredit.deducted, {
|
||||
numerator: '1400',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.investmentCredit.unused, {
|
||||
numerator: '2600',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.pecDeduction.deducted, {
|
||||
numerator: '0',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.recoverable, { numerator: '500', denominator: '1' });
|
||||
assert.equal(m.investmentCredit.carryCreated, false);
|
||||
assert.equal(m.refundReceived, false);
|
||||
});
|
||||
void test('CBF15 requires actual project,source and first-exercise conditions without treating projected investment as realised', () => {
|
||||
for (const patch of [
|
||||
{ location: '' },
|
||||
{ locationReference: '' },
|
||||
{ cbfRequirements: '' },
|
||||
{ recognitionReference: '' },
|
||||
{ genericOnly: '' },
|
||||
{ soleProject: '' },
|
||||
{ noOtherBenefits: 'no' },
|
||||
{ assessmentKey: 'stale' },
|
||||
{ startDate: '2025-01-01' },
|
||||
]) {
|
||||
const c = investmentCase();
|
||||
Object.assign(c.b.investmentCredit, patch);
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
}
|
||||
const c = investmentCase();
|
||||
c.b.investmentCredit.assets[0].realised = '';
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
});
|
||||
void test('documented excluded,used,foreign or unrealised assets keep their inputs but receive zero credit', () => {
|
||||
for (const patch of [
|
||||
{ excluded15_6: 'yes' },
|
||||
{ newTangible: 'no' },
|
||||
{ domesticUse: 'no' },
|
||||
{ realised: 'no' },
|
||||
]) {
|
||||
const c = investmentCase();
|
||||
Object.assign(c.b.investmentCredit.assets[0], patch);
|
||||
const m = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(m.pending, []);
|
||||
assert.deepEqual(m.investmentCredit.nominal, {
|
||||
numerator: '0',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.arithmeticBalance, {
|
||||
numerator: '700',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.equal(m.investmentCredit.rows[0].input.amount, '10000');
|
||||
}
|
||||
});
|
||||
void test('source identity cannot be duplicated by a new id/date; supported parcels share one invoice capacity', () => {
|
||||
const c = investmentCase(),
|
||||
r = c.b.investmentCredit.assets[0];
|
||||
c.b.investmentCredit.assets.push({ ...r, id: 'copy', date: '2026-05-02' });
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
const p = investmentCase(),
|
||||
a = p.b.investmentCredit.assets[0];
|
||||
a.amount = '6000';
|
||||
p.b.investmentCredit.assets.push({ ...a, id: 'line2', parcel: 'machine2' });
|
||||
assert.equal(calculateAnnual(p.d, p.b).arithmeticBalance, null);
|
||||
a.amount = '4000';
|
||||
assert.deepEqual(calculateAnnual(p.d, p.b).pending, []);
|
||||
});
|
||||
void test('CBF15 detects another map using the same project or actual investment,including split invoice capacity', () => {
|
||||
const c = investmentCase(),
|
||||
other = structuredClone(c.b);
|
||||
other.investmentCredit.projectReference = 'OTHER-SYNTHETIC-PROJECT';
|
||||
other.investmentCredit.assets[0].parcel = 'OTHER-LINE';
|
||||
other.investmentCredit.assets[0].amount = '6000';
|
||||
c.b.investmentCredit.assets[0].amount = '6000';
|
||||
const root = JSON.parse(c.d.modelo_a_snapshot);
|
||||
root.irpcAnnual = { ...root.irpcAnnual, other };
|
||||
const d = { ...c.d, modelo_a_snapshot: JSON.stringify(root) };
|
||||
assert.ok(
|
||||
calculateAnnual(d, c.b).pending.some((e) =>
|
||||
e.includes('ultrapassa o documento efectivo'),
|
||||
),
|
||||
);
|
||||
});
|
||||
void test('actual previous credit use is distinct from calculating or saving the same scenario again', () => {
|
||||
const c = investmentCase();
|
||||
c.b.investmentCredit.previousUse.amount = '100';
|
||||
const m = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(m.investmentCredit.deducted, {
|
||||
numerator: '400',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(
|
||||
calculateAnnual(c.d, c.b).investmentCredit,
|
||||
m.investmentCredit,
|
||||
);
|
||||
assert.deepEqual(m.arithmeticBalance, {
|
||||
numerator: '300',
|
||||
denominator: '1',
|
||||
});
|
||||
});
|
||||
void test('CBF15 calculation-save-recovery-HTML and dossier reports retain sources,history and legacy benefits summary', () => {
|
||||
const c = investmentCase();
|
||||
c.b.internationalCredits.benefits = {
|
||||
state: 'known',
|
||||
amount: '777',
|
||||
reference:
|
||||
'SYNTHETIC earlier deliberate summary retained,not reapplied',
|
||||
};
|
||||
const b = recordAnnual(c.d, c.b, '2026-11-10T19:00:00+02:00'),
|
||||
d = JSON.parse(
|
||||
JSON.stringify({
|
||||
...c.d,
|
||||
modelo_a_snapshot: saveAnnualBook(c.d, b),
|
||||
}),
|
||||
),
|
||||
back = annualBook(d);
|
||||
assert.equal(annualCurrent(d, back), true);
|
||||
assert.deepEqual(
|
||||
calculateAnnual(d, back).investmentCredit,
|
||||
back.result.model.investmentCredit,
|
||||
);
|
||||
assert.equal(back.internationalCredits.benefits.amount, '777');
|
||||
assert.deepEqual(back.result.model.arithmeticBalance, {
|
||||
numerator: '200',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.match(annualHtml(d), /IRPC-CBF15\/1/);
|
||||
assert.match(
|
||||
JSON.stringify(
|
||||
createProjectDossierReport(d, {
|
||||
selectedSections: new Set(['finance']),
|
||||
generatedAt: '2026-11-10T19:00:00+02:00',
|
||||
}),
|
||||
),
|
||||
/IRPC-CBF15\/1/,
|
||||
);
|
||||
const old = internationalCase();
|
||||
assert.equal(calculateAnnual(old.d, old.b).investmentCredit, undefined);
|
||||
});
|
||||
|
||||
void test('absence of CBF15 cannot silently discard other positive benefits or unknown benefit inventory', () => {
|
||||
const c = investmentCase();
|
||||
c.b.investmentCredit.qualification.state = 'absent';
|
||||
c.b.investmentCredit.qualification.eligible = '';
|
||||
c.b.internationalCredits.benefits = {
|
||||
state: 'known',
|
||||
amount: '777',
|
||||
reference: 'SYNTHETIC old unspecified positive benefit',
|
||||
};
|
||||
c.b.investmentCredit.noOtherBenefits = '';
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
c.b.investmentCredit.noOtherBenefits = 'yes';
|
||||
assert.deepEqual(calculateAnnual(c.d, c.b).arithmeticBalance, {
|
||||
numerator: '700',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.equal(c.b.internationalCredits.benefits.amount, '777');
|
||||
});
|
||||
|
||||
void test('CBF15 uses a versioned L8 book while legacy books remain version1 and cannot silently carry the new feature', () => {
|
||||
const c = investmentCase();
|
||||
assert.equal(c.b.version, 2);
|
||||
const old = internationalCase();
|
||||
assert.equal(old.b.version, 1);
|
||||
const invalid = structuredClone(c.b);
|
||||
invalid.version = 1;
|
||||
assert.throws(() => calculateAnnual(c.d, invalid));
|
||||
});
|
||||
|
||||
void test('adding CBF15 retains the accepted legacy settlement result in history', () => {
|
||||
const c = investmentCase(),
|
||||
legacy = recordAnnual(
|
||||
c.d,
|
||||
internationalCase().b,
|
||||
'2026-11-10T18:00:00+02:00',
|
||||
),
|
||||
before = structuredClone(legacy.result);
|
||||
const book = {
|
||||
...legacy,
|
||||
version: 2,
|
||||
investmentCredit: structuredClone(c.b.investmentCredit),
|
||||
};
|
||||
const next = recordAnnual(c.d, book, '2026-11-10T19:00:00+02:00'),
|
||||
d = { ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, next) };
|
||||
assert.deepEqual(annualBook(d).history.at(-1), before);
|
||||
assert.deepEqual(annualBook(d).result.model.arithmeticBalance, {
|
||||
numerator: '200',
|
||||
denominator: '1',
|
||||
});
|
||||
});
|
||||
void test('first-year ongoing project uses documented investment-procedure start,not new-project exploitation start', () => {
|
||||
const c = investmentCase();
|
||||
Object.assign(c.b.investmentCredit, {
|
||||
startKind: 'existing',
|
||||
startDate: '2026-04-01',
|
||||
startReference:
|
||||
'SYNTHETIC procedures after project approval startedApril2026',
|
||||
});
|
||||
assert.deepEqual(calculateAnnual(c.d, c.b).pending, []);
|
||||
c.b.investmentCredit.startDate = '2026-06-01';
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
});
|
||||
|
||||
void test('declared absence of CBF15 preserves the accepted positive prior-credit carry journey', () => {
|
||||
const c = carryCase(),
|
||||
before = calculateAnnual(c.d, c.b),
|
||||
b = prepareInvestmentCredit(c.d, c.b);
|
||||
Object.assign(b.investmentCredit, {
|
||||
qualification: {
|
||||
state: 'absent',
|
||||
eligible: '',
|
||||
from: '2026-01-01',
|
||||
to: '2026-12-31',
|
||||
reference: 'SYNTHETIC no CBF15 benefit',
|
||||
periodReference: 'SYNTHETIC current fiscal period',
|
||||
},
|
||||
noOtherBenefits: 'yes',
|
||||
noOtherBenefitsReference: 'SYNTHETIC no other own fiscal benefits',
|
||||
});
|
||||
const after = calculateAnnual(c.d, b);
|
||||
assert.deepEqual(after.pending, []);
|
||||
assert.deepEqual(after.arithmeticBalance, before.arithmeticBalance);
|
||||
assert.deepEqual(after.creditCarry, before.creditCarry);
|
||||
});
|
||||
|
||||
void test('documented residual project quota limits CBF15 without allocating income/costs by presumption', () => {
|
||||
const c = investmentCase();
|
||||
Object.assign(c.b.investmentCredit, {
|
||||
soleProject: 'no',
|
||||
projectQuota: {
|
||||
amount: '200',
|
||||
reference:
|
||||
'SYNTHETIC project residual collecta200 after allocated economic/international credits',
|
||||
},
|
||||
});
|
||||
const m = calculateAnnual(c.d, c.b);
|
||||
assert.deepEqual(m.pending, []);
|
||||
assert.deepEqual(m.investmentCredit.deducted, {
|
||||
numerator: '200',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.arithmeticBalance, {
|
||||
numerator: '500',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.deepEqual(m.investmentCredit.projectCeiling, {
|
||||
numerator: '200',
|
||||
denominator: '1',
|
||||
});
|
||||
assert.match(m.investmentCredit.source, /QuotaCBF15-project/);
|
||||
c.b.investmentCredit.projectQuota.amount = '0';
|
||||
assert.deepEqual(calculateAnnual(c.d, c.b).arithmeticBalance, {
|
||||
numerator: '700',
|
||||
denominator: '1',
|
||||
});
|
||||
});
|
||||
void test('project quota requires support and cannot exceed actual collection remaining after credits', () => {
|
||||
for (const quota of [
|
||||
{ amount: '200', reference: '' },
|
||||
{
|
||||
amount: '1405',
|
||||
reference: 'SYNTHETIC invalid residual quota above1400',
|
||||
},
|
||||
{ amount: '-1', reference: 'SYNTHETIC invalid' },
|
||||
]) {
|
||||
const c = investmentCase();
|
||||
Object.assign(c.b.investmentCredit, {
|
||||
soleProject: 'no',
|
||||
projectQuota: quota,
|
||||
});
|
||||
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
|
||||
}
|
||||
});
|
||||
void test('zero project residual quota preserves the accepted prior-credit journey without applying a competing benefit', () => {
|
||||
const c = carryCase(),
|
||||
before = calculateAnnual(c.d, c.b),
|
||||
b = prepareInvestmentCredit(c.d, c.b),
|
||||
template = investmentCase().b.investmentCredit;
|
||||
b.investmentCredit = {
|
||||
...template,
|
||||
soleProject: 'no',
|
||||
projectQuota: {
|
||||
amount: '0',
|
||||
reference:
|
||||
'SYNTHETIC project residual collecta zero after attribution',
|
||||
},
|
||||
};
|
||||
const after = calculateAnnual(c.d, b);
|
||||
assert.deepEqual(after.pending, []);
|
||||
assert.deepEqual(after.arithmeticBalance, before.arithmeticBalance);
|
||||
});
|
||||
|
||||
void test('CBF15 does not assume compatibility with an existing positive deduction/benefit in the L6 base', () => {
|
||||
const c = investmentCase(),
|
||||
normal = normalBook(c.d);
|
||||
normal.base.benefits = {
|
||||
state: 'known',
|
||||
amount: '100',
|
||||
reference:
|
||||
'SYNTHETIC positive base benefit requiring separate cumulation correspondence',
|
||||
};
|
||||
const d = {
|
||||
...c.d,
|
||||
modelo_a_snapshot: saveNormalBook(
|
||||
c.d,
|
||||
recordNormal(c.d, normal, '2026-11-10T19:10:00+02:00'),
|
||||
),
|
||||
},
|
||||
b = reuseAnnualAssessment(d, c.b);
|
||||
b.investmentCredit.assessmentKey = b.assessmentKey;
|
||||
const m = calculateAnnual(d, b);
|
||||
assert.equal(m.arithmeticBalance, null);
|
||||
assert.ok(m.investmentCredit.pending.length > 0);
|
||||
});
|
||||
@@ -18,7 +18,7 @@ $identity = $pdo->query('select current_database() as name,inet_server_port() as
|
||||
if ($identity['name'] !== 'proactive_investment_20261027_test' || (int) $identity['port'] !== 55441) {
|
||||
throw new RuntimeException('Isolated QA identity required.');
|
||||
}
|
||||
$case = ['quota' => 'IrpcEconomicQuotaDraftTest', 'inclusion' => 'IrpcEconomicInclusionDraftTest', 'liquidation' => 'IrpcEconomicLiquidationDraftTest', 'association' => 'IrpcEconomicAssociationDraftTest', 'special-inclusion' => 'IrpcEconomicSpecialInclusionDraftTest', 'pt-royalty' => 'IrpcPtMzCreditDraftTest', 'pt-capital' => 'IrpcPtMzCapitalDraftTest', 'pt-government' => 'IrpcPtMzGovernmentDraftTest', 'economic-fraction' => 'IrpcEconomicFractionDraftTest', 'pt-agreement' => 'IrpcPtMzAgreementDraftTest'][$argv[2] ?? ''] ?? throw new RuntimeException('Choose an explicit supported QA case.');
|
||||
$case = ['quota' => 'IrpcEconomicQuotaDraftTest', 'inclusion' => 'IrpcEconomicInclusionDraftTest', 'liquidation' => 'IrpcEconomicLiquidationDraftTest', 'association' => 'IrpcEconomicAssociationDraftTest', 'special-inclusion' => 'IrpcEconomicSpecialInclusionDraftTest', 'pt-royalty' => 'IrpcPtMzCreditDraftTest', 'pt-capital' => 'IrpcPtMzCapitalDraftTest', 'pt-government' => 'IrpcPtMzGovernmentDraftTest', 'economic-fraction' => 'IrpcEconomicFractionDraftTest', 'pt-agreement' => 'IrpcPtMzAgreementDraftTest', 'investment' => 'IrpcInvestmentCreditDraftTest', 'investment-quota' => 'IrpcInvestmentQuotaDraftTest'][$argv[2] ?? ''] ?? throw new RuntimeException('Choose an explicit supported QA case.');
|
||||
$argv = ['phpunit', '-c', 'phpunit.investmoz.xml', '--do-not-cache-result', 'tests/Feature/'.$case.'.php'];
|
||||
$argc = count($argv);
|
||||
$_SERVER['argv'] = $argv;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { internationalCase } from '../tests/frontend/helpers/irpc-international-case.mjs';
|
||||
import { investmentCase } from '../tests/frontend/helpers/irpc-investment-case.mjs';
|
||||
import { prepareInvestmentCredit,recordAnnual,saveAnnualBook,annualBook,annualCurrent,calculateAnnual,annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
|
||||
import { createProjectDossierReport } from '../resources/js/lib/project-dossier-report.ts';
|
||||
const out=new URL('../outputs/irpc-cbf15-20261110/',import.meta.url);fs.mkdirSync(out,{recursive:true});
|
||||
if(process.argv[2]==='partial'){
|
||||
const d=JSON.parse(fs.readFileSync(new URL('recovered-capped.json',out),'utf8')),b=annualBook(d);
|
||||
b.investmentCredit.soleProject='no';b.investmentCredit.projectQuota={amount:'200',reference:'SYNTHETIC documented project residual collecta200 after attribution of previous credits; other activities retained'};
|
||||
const next=recordAnnual(d,b,new Date().toISOString());fs.writeFileSync(new URL('partial.json',out),JSON.stringify({...d,person_type:investmentCase().d.person_type,modelo_a_snapshot:saveAnnualBook(d,next)}));
|
||||
process.exit(0);
|
||||
}
|
||||
if(process.argv[2]==='partial-recovered'){
|
||||
const d=JSON.parse(fs.readFileSync(new URL('recovered-partial.json',out),'utf8')),b=annualBook(d),m=b.result.model;
|
||||
assert.equal(annualCurrent(d,b),true);assert.deepEqual(calculateAnnual(d,b).investmentCredit,m.investmentCredit);assert.equal(b.history.length,4);
|
||||
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:new Date().toISOString()});assert.match(html,/QuotaCBF15-project/);assert.match(JSON.stringify(report),/QuotaCBF15-project/);
|
||||
fs.writeFileSync(new URL('partial-report.html',out),html);fs.writeFileSync(new URL('partial-verification.json',out),JSON.stringify({synthetic:true,apiRecovered:true,currentRecalculation:true,html:true,dossierReport:true,historyPreserved:true,projectQuota:m.investmentCredit.projectCeiling,deducted:m.investmentCredit.deducted,unused:m.investmentCredit.unused,balance:m.arithmeticBalance,activeChanged:false},null,2));process.exit(0);
|
||||
}
|
||||
const cases=['city','province','capped'];
|
||||
const prepared=investmentCase();let current=recordAnnual(prepared.d,internationalCase().b,new Date().toISOString());current=prepareInvestmentCredit(prepared.d,current);current.investmentCredit=structuredClone(prepared.b.investmentCredit);
|
||||
for(const name of cases){
|
||||
if(process.argv[2]==='recovered'){
|
||||
const d=JSON.parse(fs.readFileSync(new URL('recovered-'+name+'.json',out),'utf8')),b=annualBook(d),m=b.result.model;
|
||||
assert.equal(annualCurrent(d,b),true);assert.equal(b.history.length,cases.indexOf(name)+1);assert.deepEqual(calculateAnnual(d,b).investmentCredit,m.investmentCredit);
|
||||
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:new Date().toISOString()});assert.match(html,/IRPC-CBF15\/1/);assert.match(JSON.stringify(report),/IRPC-CBF15\/1/);
|
||||
fs.writeFileSync(new URL(name+'-report.html',out),html);fs.writeFileSync(new URL(name+'-verification.json',out),JSON.stringify({synthetic:true,apiRecovered:true,currentRecalculation:true,html:true,dossierReport:true,historyPreserved:true,nominal:m.investmentCredit.nominal,deducted:m.investmentCredit.deducted,unused:m.investmentCredit.unused,balance:m.arithmeticBalance,officialBalancesConsumed:false,carryCreated:false,activeChanged:false},null,2));
|
||||
}else{
|
||||
current=structuredClone(current);
|
||||
if(name!=='city'){current.investmentCredit.location='other';current.investmentCredit.locationReference='SYNTHETIC actual investment in Maputo Province outside Maputo city';}
|
||||
if(name==='capped')Object.assign(current.investmentCredit.assets[0],{amount:'40000',proofAmount:'40000'});
|
||||
current=recordAnnual(prepared.d,current,new Date().toISOString());
|
||||
fs.writeFileSync(new URL(name+'.json',out),JSON.stringify({...prepared.d,modelo_a_snapshot:saveAnnualBook(prepared.d,current)}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user