feat: calculate qualified investment credits in annual settlement
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 15:09:31 +02:00
parent 7c8d4726e9
commit 89e6d72c1e
17 changed files with 1888 additions and 19 deletions
@@ -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,
+290 -11
View File
@@ -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())
+496
View File
@@ -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;
}
}
+2
View File
@@ -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);
});
+1 -1
View File
@@ -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;
+35
View File
@@ -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)}));
}
}