feat: carry documented investment credits within original project window
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 16:17:48 +02:00
parent d216830b06
commit 7476dc3430
14 changed files with 827 additions and 30 deletions
@@ -1,3 +1,19 @@
## 10/11/2026 — transporte documental CBF15 em QA
Homologação mantém `d216830b`; este delta ainda não está instalado. Tipos/lint/Pint/buildQA aprovados; pacote com aplicação/reversão exactas,sem migrations/dados/credenciais. Pendência encerrada no recorte normal residente2026/ano civil/MZN: crédito de investimento de origem anterior do mesmo contribuinte/projecto/local/regime, com activo/documento/parcela, exercício de origem, NUIT/projecto de origem, taxa original e qualificação históricas documentados. Exercício de origem anterior a2026,dentro da janela original e não anterior à aquisição; no projecto em funcionamento,a aquisição não precede o início aplicável. Não renova o prazo nem recalcula origem a outra taxa. Valor corrente/ausência e uso efectivo agregado exigem reconciliação explícita; histórico de simulações não equivale a utilização fiscal.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente com rollback. Exemplo sintético recuperado: nominal de origem4.000,uso efectivo anterior1.000,disponível3.000,quota documental do projecto200 → dedução200,não utilizado2.800,saldo500MZN. Início2022 → quinto exercício2026; mudando o início para2021 → sexto,parcela expirada3.000,disponível/dedução0,saldo700,com activo,uso anterior e origens preservados. Seis/sete resultados históricos conservados,incluindo o resultado ordinário aceite. Provas `outputs/irpc-cbf15-carry-20261110/{carry,expired}-verification.json` e relatórios respectivos; API um caso/28asserções novas,30casos frontend nas fronteiras alteradas incluindo formulário real compilado e prevenção de duplicação. SSR/formulário simulado não é navegador/visual. FonteCBF15(4/7),APIEX2025 PDF71–72 já aceite,reutilizada.
A preparação mantém os activos correntes e exige explicitar os dados em falta; activos de origem e correntes partilham a capacidade documental e a detecção de duplicação entre mapas. Origem com outro NUIT/projecto/taxa,exercício incoerente,uso superior ao nominal ou declaração de ausência incompatível não produz dedução. Sem pagamento,reembolso,consumo oficial ou criação automática de saldo transportado. Livros antigos permanecem nos formatos1/2; só o transporte preparado usa livro3/detalhe2,para o código anterior recusar em vez de ignorar o crédito novo. Snapshots e históricos aceites não são reescritos; reversão é de código e não deve sobrescrever dados posteriores.
Saldo preservado: outros projectos/localizações/regimes/cumulações/EP/L7,combinação positiva com transportes económicos/internacionais/PEC anteriores,repartição automática,benefícios/formulários oficiais e restantes resíduos L1–L7/restanteL8/L9–L16 mantêm os seus recortes. Os módulos não fiscais mantêm o saldo existente,sem nova auditoria geral. Próximo trabalho: primeira combinação residual executável respeitando a ordem de dedução e quota do projecto,ou percurso independente do saldo existente; visual continua pendente no mesmo registo. Não é conclusão integral do portal.
## 10/11/2026 — menus horizontais instalados
**Homologação: `d216830b068af8a7a40d43993795639e73aaed78`**, promovida às13:56:27UTC e conferida às13:56:54UTC (15:56 locais), em https://teste-investidor.tempo.co.mz. Delta10hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos; revisão d5cb41b1 preservada para reversão. Recibos `outputs/horizontal-menu-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`. Pacote anterior e dados/históricos preservados; sessãoSSH pronta para continuação autenticada, sem credencial em disco.
O recorte dos menus horizontais abaixo deixa de ser pendência de instalação: cabeçalho comum em páginas públicas/autenticação/definições/privadas, sem sidebar global concorrente; Descoberta sem repetição dos cinco atalhos mantidos na entrada principal. Prova estrutural dos146layouts e23casos dirigidos reutilizada; não é conferência visual. O conector continua indisponível,sem novos reinícios ou alterações ao navegador. Visual mantém-se pendente no mesmo registo. O restante saldo de funcionalidades por módulo e L1–L7/restanteL8/L9–L16 não é encerrado por esta instalação.
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
@@ -1,3 +1,19 @@
## 10/11/2026 — transporte documental CBF15 em QA
Homologação mantém `d216830b`; este delta ainda não está instalado. Tipos/lint/Pint/buildQA aprovados; pacote com aplicação/reversão exactas,sem migrations/dados/credenciais. Pendência encerrada no recorte normal residente2026/ano civil/MZN: crédito de investimento de origem anterior do mesmo contribuinte/projecto/local/regime, com activo/documento/parcela, exercício de origem, NUIT/projecto de origem, taxa original e qualificação históricas documentados. Exercício de origem anterior a2026,dentro da janela original e não anterior à aquisição; no projecto em funcionamento,a aquisição não precede o início aplicável. Não renova o prazo nem recalcula origem a outra taxa. Valor corrente/ausência e uso efectivo agregado exigem reconciliação explícita; histórico de simulações não equivale a utilização fiscal.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente com rollback. Exemplo sintético recuperado: nominal de origem4.000,uso efectivo anterior1.000,disponível3.000,quota documental do projecto200 → dedução200,não utilizado2.800,saldo500MZN. Início2022 → quinto exercício2026; mudando o início para2021 → sexto,parcela expirada3.000,disponível/dedução0,saldo700,com activo,uso anterior e origens preservados. Seis/sete resultados históricos conservados,incluindo o resultado ordinário aceite. Provas `outputs/irpc-cbf15-carry-20261110/{carry,expired}-verification.json` e relatórios respectivos; API um caso/28asserções novas,30casos frontend nas fronteiras alteradas incluindo formulário real compilado e prevenção de duplicação. SSR/formulário simulado não é navegador/visual. FonteCBF15(4/7),APIEX2025 PDF71–72 já aceite,reutilizada.
A preparação mantém os activos correntes e exige explicitar os dados em falta; activos de origem e correntes partilham a capacidade documental e a detecção de duplicação entre mapas. Origem com outro NUIT/projecto/taxa,exercício incoerente,uso superior ao nominal ou declaração de ausência incompatível não produz dedução. Sem pagamento,reembolso,consumo oficial ou criação automática de saldo transportado. Livros antigos permanecem nos formatos1/2; só o transporte preparado usa livro3/detalhe2,para o código anterior recusar em vez de ignorar o crédito novo. Snapshots e históricos aceites não são reescritos; reversão é de código e não deve sobrescrever dados posteriores.
Saldo preservado: outros projectos/localizações/regimes/cumulações/EP/L7,combinação positiva com transportes económicos/internacionais/PEC anteriores,repartição automática,benefícios/formulários oficiais e restantes resíduos L1–L7/restanteL8/L9–L16 mantêm os seus recortes. Os módulos não fiscais mantêm o saldo existente,sem nova auditoria geral. Próximo trabalho: primeira combinação residual executável respeitando a ordem de dedução e quota do projecto,ou percurso independente do saldo existente; visual continua pendente no mesmo registo. Não é conclusão integral do portal.
## 10/11/2026 — menus horizontais instalados
**Homologação: `d216830b068af8a7a40d43993795639e73aaed78`**, promovida às13:56:27UTC e conferida às13:56:54UTC (15:56 locais), em https://teste-investidor.tempo.co.mz. Delta10hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos; revisão d5cb41b1 preservada para reversão. Recibos `outputs/horizontal-menu-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`. Pacote anterior e dados/históricos preservados; sessãoSSH pronta para continuação autenticada, sem credencial em disco.
O recorte dos menus horizontais abaixo deixa de ser pendência de instalação: cabeçalho comum em páginas públicas/autenticação/definições/privadas, sem sidebar global concorrente; Descoberta sem repetição dos cinco atalhos mantidos na entrada principal. Prova estrutural dos146layouts e23casos dirigidos reutilizada; não é conferência visual. O conector continua indisponível,sem novos reinícios ou alterações ao navegador. Visual mantém-se pendente no mesmo registo. O restante saldo de funcionalidades por módulo e L1–L7/restanteL8/L9–L16 não é encerrado por esta instalação.
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,19 @@
## 10/11/2026 — transporte documental CBF15 em QA
Homologação mantém `d216830b`; este delta ainda não está instalado. Tipos/lint/Pint/buildQA aprovados; pacote com aplicação/reversão exactas,sem migrations/dados/credenciais. Pendência encerrada no recorte normal residente2026/ano civil/MZN: crédito de investimento de origem anterior do mesmo contribuinte/projecto/local/regime, com activo/documento/parcela, exercício de origem, NUIT/projecto de origem, taxa original e qualificação históricas documentados. Exercício de origem anterior a2026,dentro da janela original e não anterior à aquisição; no projecto em funcionamento,a aquisição não precede o início aplicável. Não renova o prazo nem recalcula origem a outra taxa. Valor corrente/ausência e uso efectivo agregado exigem reconciliação explícita; histórico de simulações não equivale a utilização fiscal.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente com rollback. Exemplo sintético recuperado: nominal de origem4.000,uso efectivo anterior1.000,disponível3.000,quota documental do projecto200 → dedução200,não utilizado2.800,saldo500MZN. Início2022 → quinto exercício2026; mudando o início para2021 → sexto,parcela expirada3.000,disponível/dedução0,saldo700,com activo,uso anterior e origens preservados. Seis/sete resultados históricos conservados,incluindo o resultado ordinário aceite. Provas `outputs/irpc-cbf15-carry-20261110/{carry,expired}-verification.json` e relatórios respectivos; API um caso/28asserções novas,30casos frontend nas fronteiras alteradas incluindo formulário real compilado e prevenção de duplicação. SSR/formulário simulado não é navegador/visual. FonteCBF15(4/7),APIEX2025 PDF71–72 já aceite,reutilizada.
A preparação mantém os activos correntes e exige explicitar os dados em falta; activos de origem e correntes partilham a capacidade documental e a detecção de duplicação entre mapas. Origem com outro NUIT/projecto/taxa,exercício incoerente,uso superior ao nominal ou declaração de ausência incompatível não produz dedução. Sem pagamento,reembolso,consumo oficial ou criação automática de saldo transportado. Livros antigos permanecem nos formatos1/2; só o transporte preparado usa livro3/detalhe2,para o código anterior recusar em vez de ignorar o crédito novo. Snapshots e históricos aceites não são reescritos; reversão é de código e não deve sobrescrever dados posteriores.
Saldo preservado: outros projectos/localizações/regimes/cumulações/EP/L7,combinação positiva com transportes económicos/internacionais/PEC anteriores,repartição automática,benefícios/formulários oficiais e restantes resíduos L1–L7/restanteL8/L9–L16 mantêm os seus recortes. Os módulos não fiscais mantêm o saldo existente,sem nova auditoria geral. Próximo trabalho: primeira combinação residual executável respeitando a ordem de dedução e quota do projecto,ou percurso independente do saldo existente; visual continua pendente no mesmo registo. Não é conclusão integral do portal.
## 10/11/2026 — menus horizontais instalados
**Homologação: `d216830b068af8a7a40d43993795639e73aaed78`**, promovida às13:56:27UTC e conferida às13:56:54UTC (15:56 locais), em https://teste-investidor.tempo.co.mz. Delta10hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração conferidos; revisão d5cb41b1 preservada para reversão. Recibos `outputs/horizontal-menu-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`. Pacote anterior e dados/históricos preservados; sessãoSSH pronta para continuação autenticada, sem credencial em disco.
O recorte dos menus horizontais abaixo deixa de ser pendência de instalação: cabeçalho comum em páginas públicas/autenticação/definições/privadas, sem sidebar global concorrente; Descoberta sem repetição dos cinco atalhos mantidos na entrada principal. Prova estrutural dos146layouts e23casos dirigidos reutilizada; não é conferência visual. O conector continua indisponível,sem novos reinícios ou alterações ao navegador. Visual mantém-se pendente no mesmo registo. O restante saldo de funcionalidades por módulo e L1–L7/restanteL8/L9–L16 não é encerrado por esta instalação.
## 10/11/2026 — janela CBF15 instalada; menus horizontais uniformizados em QA
**Homologação: `d5cb41b111cac95c431a3072b8c4cc0141481ec2`**, promovida às13:43:40UTC e conferida às13:44:18UTC (15:44 locais). Delta12hashes Git e revisão HTTPS iguais; root/up/login/revision HTTP200, configuração preservada, zero migrations/importações. Backup/restauro190tabelas e369ficheiros/configuração conferidos,89e6d72c preservada. Recibos `outputs/cbf15-window-delivery-20261110/`. Janela de cinco exercícios CBF15 deixa de ser pendência de instalação; transporte de créditos anteriores e restantes resíduos fiscais continuam separados.
@@ -2,6 +2,7 @@ import { QualificationFields } from './simulation-irpc-special';
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
import {
prepareInvestmentCredit,
prepareInvestmentCarry,
type AnnualBook,
} from '@/lib/irpc-annual-settlement';
import {
@@ -302,6 +303,127 @@ export function SimulationIrpcInvestment({
</button>
</>
)}
{!c.carryForward ? (
<button
type="button"
onClick={() => save(prepareInvestmentCarry(draft, book))}
>
Preparar transporte de crédito de anos anteriores
</button>
) : (
<fieldset className="space-y-2 rounded border p-3">
<legend>Transporte CBF15 do mesmo projecto</legend>
<p>
Declare activos de origem anterior, taxa e qualificação
históricas. Não copie saldos de simulações como uso
fiscal. O prazo original de cinco exercícios mantém-se;
investimentos correntes ficam separados.
</p>
<label className="block">
Correspondência histórica do crédito transportado
<input
aria-label="CBF15 transporte — Correspondência"
value={c.carryForward.reference}
onChange={(e) =>
patch({
carryForward: {
...c.carryForward!,
reference: e.target.value,
},
})
}
/>
</label>
<label className="block">
Investimento efectivo de 2026
<select
aria-label="CBF15 transporte — Investimento corrente"
value={c.carryForward.currentState}
onChange={(e) =>
patch({
carryForward: {
...c.carryForward!,
currentState: e.target.value as
| 'known'
| 'absent'
| '',
},
})
}
>
<option value="">Por documentar</option>
<option value="known">
Conhecido — activos correntes discriminados
</option>
<option value="absent">
Ausência documentada — apenas origens anteriores
</option>
</select>
</label>
<label className="block">
Suporte do investimento corrente/ausência
<input
aria-label="CBF15 transporte — Suporte corrente"
value={c.carryForward.currentReference}
onChange={(e) =>
patch({
carryForward: {
...c.carryForward!,
currentReference: e.target.value,
},
})
}
/>
</label>
<label className="block">
Uso efectivo de todo o crédito reconciliado
<select
aria-label="CBF15 transporte — Reconciliação do uso"
value={c.carryForward.useReconciled}
onChange={(e) =>
patch({
carryForward: {
...c.carryForward!,
useReconciled: e.target.value as
| ''
| 'yes'
| 'no',
},
})
}
>
<option value="">Por documentar</option>
<option value="yes">
Sim — inclui as origens anteriores e o crédito
corrente
</option>
<option value="no">Não reconciliado</option>
</select>
</label>
<button
type="button"
onClick={() =>
patch({
assets: [
...c.assets,
{
...newInvestmentAsset(
crypto.randomUUID(),
),
originYear: '',
originNuit: '',
originProject: '',
originReference: '',
originRate: '',
},
],
})
}
>
Acrescentar activo de origem anterior
</button>
</fieldset>
)}
{c.assets.map((r, index) => {
const change = (v: Partial<InvestmentAsset>) =>
patch({
@@ -315,6 +437,57 @@ export function SimulationIrpcInvestment({
className="space-y-2 rounded border p-3"
>
<legend>Investimento {index + 1}</legend>
{r.originYear !== undefined && (
<>
<p>
Origem anterior — mantém a data real de
aquisição e o prazo original do projecto.
</p>
{(
[
[
'originYear',
'Exercício que originou o crédito',
],
[
'originNuit',
'NUIT do contribuinte de origem',
],
[
'originProject',
'Referência do projecto de origem',
],
[
'originRate',
'Taxa original documentada — 5 ou 10',
],
[
'originReference',
'Suporte da qualificação e crédito de origem,mesmo projecto/local/regime',
],
] as const
).map(([key, label]) => (
<label key={key} className="block">
{label}
<input
aria-label={
'CBF15 origem ' +
(index + 1) +
' — ' +
label
}
value={r[key] ?? ''}
onChange={(e) =>
change({
[key]: e.target.value,
})
}
/>
</label>
))}
</>
)}
{(
[
['issuer', 'Fornecedor/emitente'],
@@ -444,7 +617,8 @@ export function SimulationIrpcInvestment({
<option value="absent">Ausência documentada</option>
</select>
<label className="block">
Montante anteriormente utilizado
Montante anteriormente utilizado em todo o crédito
discriminado
<input
aria-label="CBF15 — Montante de uso anterior"
value={c.previousUse.amount}
+74 -4
View File
@@ -106,7 +106,7 @@ type AnnualRecord = {
model: AnnualModel;
};
export type AnnualBook = {
version: 1 | 2;
version: 1 | 2 | 3;
subjectNuit: string;
year: string;
reference: string;
@@ -164,6 +164,27 @@ export function prepareInvestmentCredit(
c.qualification.to = a.period?.to ?? '';
return { ...next, version: 2, investmentCredit: c };
}
export function prepareInvestmentCarry(
d: Record<string, unknown>,
b: AnnualBook,
): AnnualBook {
const next = prepareInvestmentCredit(d, b);
if (next.investmentCredit!.carryForward) return next;
return {
...next,
version: 3,
investmentCredit: {
...next.investmentCredit!,
version: 2,
carryForward: {
reference: '',
useReconciled: '',
currentState: '',
currentReference: '',
},
},
};
}
export function prepareEconomicBook(b: AnnualBook): AnnualBook {
if (b.economicCredits) return b;
const next = prepareInternationalBook(b),
@@ -279,9 +300,10 @@ export function validateAnnual(b: AnnualBook) {
validateInternational(b.internationalCredits);
if (
!b ||
![1, 2].includes(b.version) ||
(b.version === 2 && b.investmentCredit === undefined) ||
(b.investmentCredit !== undefined && b.version !== 2) ||
![1, 2, 3].includes(b.version) ||
(b.version >= 2 && b.investmentCredit === undefined) ||
(b.investmentCredit !== undefined &&
b.version !== (b.investmentCredit.carryForward ? 3 : 2)) ||
[
'subjectNuit',
'year',
@@ -2132,6 +2154,28 @@ export function annualReportRows(
show(m.investmentCredit.nominal) +
'; uso anterior ' +
show(m.investmentCredit.priorUse) +
(m.investmentCredit.input.carryForward
? '; TransporteCBF15/1: nominal corrente ' +
show(
m.investmentCredit.currentNominal ??
null,
) +
'; nominal de origem anterior ' +
show(
m.investmentCredit.carryNominal ?? null,
) +
'; parcela expirada ' +
show(m.investmentCredit.expired ?? null) +
'; ' +
m.investmentCredit.input.carryForward
.reference +
'; investimento corrente ' +
m.investmentCredit.input.carryForward
.currentState +
': ' +
m.investmentCredit.input.carryForward
.currentReference
: '') +
'; disponível ' +
show(m.investmentCredit.available) +
'; dedução ' +
@@ -2189,6 +2233,32 @@ export function annualReportRows(
'; outros benefícios: ' +
m.investmentCredit.input
.noOtherBenefitsReference +
(m.investmentCredit.input.carryForward
? '; origens: ' +
m.investmentCredit.input.assets
.filter(
(r) => r.originYear !== undefined,
)
.map(
(r) =>
r.issuer +
'/' +
r.document +
'/' +
r.parcel +
': exercício ' +
r.originYear +
'; NUIT ' +
r.originNuit +
'; projecto ' +
r.originProject +
', taxa ' +
r.originRate +
'%; ' +
r.originReference,
)
.join(' | ')
: '') +
'; uso anterior: ' +
m.investmentCredit.input.previousUse.reference +
'; ' +
+131 -11
View File
@@ -21,11 +21,16 @@ import {
fraction,
type FiscalFraction,
} from './fiscal-rational.ts';
export const investmentCreditVersion = 'IRPC-CBF15/2';
export const investmentCreditVersion = 'IRPC-CBF15/3';
export const investmentCreditSource =
'CBF,Lei4/2009,arts3/4/9/10/12/13/15; compilação oficial APIEX2025,PDF66–72,art15PDF71–72. Liquidação2026,regime normal residente,ano civil/MZN,colecta integral ou quota residual documentada do projecto. Janela de cinco exercícios desde o início aplicável15(4/7),incluindo o primeiro; activos efectivos de2026,sem transporte de activos/créditos anteriores. 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.';
'CBF,Lei4/2009,arts3/4/9/10/12/13/15; compilação oficial APIEX2025,PDF66–72,art15PDF71–72. Liquidação2026,regime normal residente,ano civil/MZN,colecta integral ou quota residual documentada do projecto. Janela de cinco exercícios desde o início aplicável15(4/7),incluindo o primeiro; activos efectivos de2026,com transporte apenas no detalhe preparado das origens históricas. 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 criação automática de transporte,reembolso ou consumo de saldo oficial;outros exercícios,regimes e cumulações separados.';
type Choice = '' | 'yes' | 'no';
export type InvestmentAsset = {
originYear?: string;
originNuit?: string;
originProject?: string;
originReference?: string;
originRate?: string;
id: string;
issuer: string;
document: string;
@@ -61,7 +66,13 @@ export const newInvestmentAsset = (id: string): InvestmentAsset => ({
});
export type InvestmentCredit = {
projectQuota?: { amount: string; reference: string };
version: 1;
version: 1 | 2;
carryForward?: {
reference: string;
useReconciled: Choice;
currentState: '' | 'known' | 'absent';
currentReference: string;
};
qualification: Qualification;
subjectNuit: string;
year: string;
@@ -110,7 +121,7 @@ export function validateInvestmentCredit(c: InvestmentCredit) {
const template = newInvestmentCredit();
if (
!c ||
c.version !== 1 ||
![1, 2].includes(c.version) ||
Object.keys(template)
.filter(
(k) =>
@@ -139,6 +150,19 @@ export function validateInvestmentCredit(c: InvestmentCredit) {
typeof c.projectQuota.reference !== 'string')
)
throw Error('QuotaCBF15 não recuperável; original conservado.');
if ((c.version === 2) !== (c.carryForward !== undefined))
throw Error(
'Formato do transporteCBF15 não recuperável; original conservado.',
);
if (
c.carryForward &&
(!c.carryForward ||
typeof c.carryForward.reference !== 'string' ||
typeof c.carryForward.currentReference !== 'string' ||
!['', 'known', 'absent'].includes(c.carryForward.currentState) ||
!['', 'yes', 'no'].includes(c.carryForward.useReconciled))
)
throw Error('TransporteCBF15 não recuperável; original conservado.');
const q = c.qualification;
if (
!['', 'known', 'absent', 'not_applicable'].includes(q.state) ||
@@ -170,6 +194,20 @@ export function validateInvestmentCredit(c: InvestmentCredit) {
for (const r of c.assets) {
if (
!r ||
[
'originYear',
'originReference',
'originRate',
'originNuit',
'originProject',
].some(
(k) =>
(r as unknown as Record<string, unknown>)[k] !==
undefined &&
(c.version !== 2 ||
typeof (r as unknown as Record<string, unknown>)[k] !==
'string'),
) ||
Object.keys(newInvestmentAsset('')).some(
(k) =>
typeof (r as unknown as Record<string, unknown>)[k] !==
@@ -221,6 +259,9 @@ export function calculateInvestmentCredit(
priorUse: FiscalFraction | null = null,
available: FiscalFraction | null = null,
rate: number | null = null;
let currentNominal: FiscalFraction | undefined,
carryNominal: FiscalFraction | undefined,
expired: FiscalFraction | undefined;
let projectCeiling: FiscalFraction | undefined;
let window:
| {
@@ -307,6 +348,29 @@ export function calculateInvestmentCredit(
exercise: Number(b.year) - firstYear + 1,
status: Number(b.year) > firstYear + 4 ? 'expired' : 'active',
};
if (c.carryForward) {
const t = c.carryForward,
prior = c.assets.filter((r) => r.originYear !== undefined),
own = c.assets.filter((r) => r.originYear === undefined);
if (!t.reference.trim() || t.useReconciled !== 'yes')
throw Error(
'Transporte exige correspondência histórica e reconciliação do uso efectivo de todo o crédito, separado do histórico de simulações.',
);
if (
!t.currentReference.trim() ||
!t.currentState ||
(t.currentState === 'absent'
? own.length !== 0
: own.length === 0)
)
throw Error(
'Investimento corrente conhecido/ausente deve corresponder aos activos e ter suporte próprio; dados conservados.',
);
if (!prior.length)
throw Error(
'Transporte exige activo/documento e exercício de origem; não importar saldo agregado sem origem.',
);
}
if (!c.assets.length)
throw Error(
'Investimento conhecido exige activo/documento; zero não se presume.',
@@ -342,7 +406,8 @@ export function calculateInvestmentCredit(
!r.issuer.trim() ||
!r.document.trim() ||
!iso(r.date) ||
!r.date.startsWith(b.year + '-') ||
(r.originYear === undefined &&
!r.date.startsWith(b.year + '-')) ||
!r.reference.trim() ||
r.newTangible !== 'yes' ||
r.domesticUse !== 'yes' ||
@@ -352,6 +417,27 @@ export function calculateInvestmentCredit(
throw Error(
'Activo/documento2026,corpóreo novo,afectação nacional produtiva e exclusões15(6) por corresponder.',
);
if (r.originYear !== undefined) {
const origin = Number(r.originYear),
first = Number(c.startDate.slice(0, 4));
if (
!c.carryForward ||
!/^\d{4}$/.test(r.originYear) ||
origin < first ||
origin >= Number(b.year) ||
origin > first + 4 ||
Number(r.date.slice(0, 4)) > origin ||
r.originNuit !== c.subjectNuit ||
!r.originProject?.trim() ||
canonicalParcel(r.originProject) !==
canonicalParcel(c.projectReference) ||
!r.originReference?.trim() ||
r.originRate !== String(rate)
)
throw Error(
'OrigemCBF15 anterior à liquidação, dentro do prazo original, taxa e qualificação histórica do mesmo projecto por corresponder; não renovar/revalorizar o crédito.',
);
}
if (c.startKind === 'existing' && r.date < c.startDate)
throw Error(
'Investimento anterior ao início documentado deste recorte; correspondência própria necessária.',
@@ -384,12 +470,19 @@ export function calculateInvestmentCredit(
}
rows.push({
input: structuredClone(r),
value: value && window?.status === 'expired' ? zero() : value,
value:
value &&
window?.status === 'expired' &&
r.originYear === undefined
? zero()
: value,
reason: issues.length
? issues.join(' ')
: window?.status === 'expired'
? 'Prazo15(4) terminado no quinto exercício; investimento2026 conservado,sem crédito.'
: 'Activo elegível documentado.',
: r.originYear !== undefined
? 'Activo de origem histórica documentada; saldo depende do uso efectivo e prazo original.'
: window?.status === 'expired'
? 'Prazo15(4) terminado no quinto exercício; investimento2026 conservado,sem crédito.'
: 'Activo elegível documentado.',
pending: issues,
});
pending.push(...issues);
@@ -430,6 +523,25 @@ export function calculateInvestmentCredit(
BigInt(investment.numerator) * BigInt(rate!),
BigInt(investment.denominator) * 100n,
);
if (c.carryForward) {
const value = (prior: boolean) => {
const total = addFractions(
rows
.filter(
(r) =>
(r.input.originYear !== undefined) ===
prior,
)
.flatMap((r) => (r.value ? [r.value] : [])),
);
return fraction(
BigInt(total.numerator) * BigInt(rate!),
BigInt(total.denominator) * 100n,
);
};
currentNominal = value(false);
carryNominal = value(true);
}
try {
if (!c.previousUse.state || !c.previousUse.reference.trim())
throw Error(
@@ -443,6 +555,10 @@ export function calculateInvestmentCredit(
if (creditCompare(priorUse, nominal) > 0n)
throw Error('Uso anterior excede o crédito nominal.');
available = subtractFractions(nominal, priorUse);
if (c.carryForward) {
expired = window?.status === 'expired' ? available : zero();
if (window?.status === 'expired') available = zero();
}
} catch (e) {
pending.push(String(e));
}
@@ -463,17 +579,21 @@ export function calculateInvestmentCredit(
rate,
...(window ? { window } : {}),
nominal,
...(c.carryForward ? { currentNominal, carryNominal, expired } : {}),
priorUse,
available: pending.length ? null : available,
pending,
source:
c.soleProject === 'no'
(c.carryForward
? ' TransporteCBF15/1: activos de origem anterior identificados,qualificação/taxa histórica documentadas,uso efectivo agregado reconciliado,sem renovação do prazo15(4/7),consumo oficial ou reembolso automático. '
: '') +
(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,
: investmentCreditSource),
};
}
export function deductInvestmentCredit(
@@ -0,0 +1,53 @@
<?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 IrpcInvestmentCarryDraftTest 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 (['carry', 'expired'] as $name) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-cbf15-carry-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(3, $book['version']);
$this->assertCount(array_search($name, ['carry', 'expired'], true) + 6, $book['history']);
$model = $book['result']['model'];
$this->assertSame($name === 'carry' ? 'active' : 'expired', $model['investmentCredit']['window']['status']);
$this->assertSame(2, $book['investmentCredit']['version']);
$this->assertSame(['numerator' => $name === 'carry' ? '3000' : '0', 'denominator' => '1'], $model['investmentCredit']['available']);
$this->assertSame(['numerator' => $name === 'carry' ? '0' : '3000', 'denominator' => '1'], $model['investmentCredit']['expired']);
$this->assertSame('IRPC-CBF15/3', $model['investmentCredit']['version']);
$this->assertSame(['numerator' => $name === 'carry' ? '200' : '0', 'denominator' => '1'], $model['investmentCredit']['deducted']);
$this->assertSame(['numerator' => $name === 'carry' ? '500' : '700', 'denominator' => '1'], $model['arithmeticBalance']);
file_put_contents(base_path('outputs/irpc-cbf15-carry-20261110/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -0,0 +1,34 @@
import { investmentCase } from './irpc-investment-case.mjs';
import { prepareInvestmentCarry } from '../../../resources/js/lib/irpc-annual-settlement.ts';
export function investmentCarryCase() {
const c = investmentCase();
c.b = prepareInvestmentCarry(c.d, c.b);
const t = c.b.investmentCredit;
t.startDate = '2022-01-01';
t.carryForward = {
reference:
'SYNTHETIC historical investment/credit record,same project,qualification and rate',
useReconciled: 'yes',
currentState: 'absent',
currentReference: 'SYNTHETIC no realised2026 investment',
};
t.assets = [
{
...t.assets[0],
date: '2025-05-01',
originYear: '2025',
originNuit: t.subjectNuit,
originProject: t.projectReference,
originRate: '5',
originReference:
'SYNTHETIC 2025 CBF15 qualifying credit,same approved project and Maputo city; original value and tax origin',
},
];
t.previousUse = {
state: 'known',
amount: '100',
reference:
'SYNTHETIC actual tax credit use100,all origins reconciled; not simulation history',
};
return c;
}
@@ -0,0 +1,121 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { compileFunction } from 'node:vm';
import { createRequire } from 'node:module';
import React from 'react';
import ts from 'typescript';
import * as annual from '../../resources/js/lib/irpc-annual-settlement.ts';
import * as investment from '../../resources/js/lib/irpc-investment-credit.ts';
import { investmentCase } from './helpers/irpc-investment-case.mjs';
import { investmentCarryCase } from './helpers/irpc-investment-carry-case.mjs';
const require = createRequire(import.meta.url),
module = { exports: {} };
const source = fs.readFileSync(
new URL(
'../../resources/js/components/simulation-irpc-investment.tsx',
import.meta.url,
),
'utf8',
);
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
compileFunction(output, ['require', 'module', 'exports'])(
(name) =>
name === '@/lib/irpc-annual-settlement'
? annual
: name === '@/lib/irpc-investment-credit'
? investment
: name === './simulation-irpc-special'
? { QualificationFields: () => null }
: name === './fiscal-proof-parcel'
? { FiscalProofParcelFields: () => null }
: require(name),
module,
module.exports,
);
const Component = module.exports.SimulationIrpcInvestment;
const text = (node) =>
typeof node === 'string' || typeof node === 'number'
? String(node)
: React.isValidElement(node)
? React.Children.toArray(node.props.children).map(text).join('')
: '';
const all = (node) =>
React.isValidElement(node)
? [node, ...React.Children.toArray(node.props.children).flatMap(all)]
: [];
function harness(c) {
let b = c.b;
return {
book: () => b,
render: () =>
Component({
draft: c.d,
book: b,
save: (next) => {
b = next;
},
}),
click(label) {
const button = all(this.render()).find(
(n) => n.type === 'button' && text(n) === label,
);
assert.ok(button, label);
button.props.onClick();
},
change(label, value) {
const input = all(this.render()).find(
(n) => n.props['aria-label'] === label,
);
assert.ok(input, label);
input.props.onChange({ target: { value } });
},
};
}
void test('actual form prepares carry without inventing qualification or erasing current assets', () => {
const c = investmentCase(),
before = structuredClone(c.b.investmentCredit.assets),
h = harness(c);
h.click('Preparar transporte de crédito de anos anteriores');
assert.equal(h.book().version, 3);
assert.equal(h.book().investmentCredit.version, 2);
assert.deepEqual(h.book().investmentCredit.assets, before);
assert.equal(h.book().investmentCredit.carryForward.useReconciled, '');
h.click('Acrescentar activo de origem anterior');
assert.equal(h.book().investmentCredit.assets.length, 2);
assert.equal(h.book().investmentCredit.assets[1].originYear, '');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
});
void test('actual origin and use controls invalidate inconsistent credit and recover when reconciled', () => {
const c = investmentCarryCase(),
h = harness(c);
assert.deepEqual(annual.calculateAnnual(c.d, h.book()).pending, []);
h.change('CBF15 origem 1 — NUIT do contribuinte de origem', '999999999');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
h.change(
'CBF15 origem 1 — NUIT do contribuinte de origem',
c.b.subjectNuit,
);
h.change('CBF15 origem 1 — Referência do projecto de origem', 'OTHER');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
h.change(
'CBF15 origem 1 — Referência do projecto de origem',
c.b.investmentCredit.projectReference,
);
h.change('CBF15 origem 1 — Taxa original documentada — 5 ou 10', '10');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
h.change('CBF15 origem 1 — Taxa original documentada — 5 ou 10', '5');
h.change('CBF15 — Montante de uso anterior', '600');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
h.change('CBF15 — Montante de uso anterior', '100');
assert.deepEqual(annual.calculateAnnual(c.d, h.book()).pending, []);
h.change('CBF15 transporte — Reconciliação do uso', '');
assert.equal(annual.calculateAnnual(c.d, h.book()).arithmeticBalance, null);
assert.equal(h.book().investmentCredit.assets[0].originYear, '2025');
});
@@ -0,0 +1,160 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { investmentCase } from './helpers/irpc-investment-case.mjs';
import { investmentCarryCase } from './helpers/irpc-investment-carry-case.mjs';
import {
calculateAnnual,
recordAnnual,
saveAnnualBook,
annualBook,
annualCurrent,
annualHtml,
validateAnnual,
} from '../../resources/js/lib/irpc-annual-settlement.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
const f = (n) => ({ numerator: String(n), denominator: '1' });
void test('CBF15 carries only actual unused original credit:500 less100 used,without requiring a new investment', () => {
const c = investmentCarryCase(),
m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.deepEqual(m.investmentCredit.carryNominal, f(500));
assert.deepEqual(m.investmentCredit.currentNominal, f(0));
assert.deepEqual(m.investmentCredit.available, f(400));
assert.deepEqual(m.investmentCredit.deducted, f(400));
assert.deepEqual(m.arithmeticBalance, f(300));
assert.equal(m.investmentCredit.officialBalancesConsumed, false);
assert.equal(m.investmentCredit.carryCreated, false);
assert.deepEqual(
calculateAnnual(c.d, c.b).investmentCredit,
m.investmentCredit,
);
});
void test('CBF15 combines distinct current and original assets once,with project quota and actual prior use', () => {
const c = investmentCarryCase(),
t = c.b.investmentCredit;
t.carryForward.currentState = 'known';
t.assets.push({
...investmentCase().b.investmentCredit.assets[0],
id: 'current',
document: 'CURRENT2026',
});
let m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.deepEqual(m.investmentCredit.nominal, f(1000));
assert.deepEqual(m.investmentCredit.available, f(900));
assert.deepEqual(m.arithmeticBalance, f(-200));
t.soleProject = 'no';
t.projectQuota = {
amount: '200',
reference: 'SYNTHETIC residual project collecta200',
};
m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.deepEqual(m.investmentCredit.deducted, f(200));
assert.deepEqual(m.investmentCredit.unused, f(700));
});
void test('CBF15 carry expires with the original project window; prior use and source investment remain visible', () => {
const c = investmentCarryCase();
c.b.investmentCredit.startDate = '2021-01-01';
const m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.deepEqual(m.investmentCredit.carryNominal, f(500));
assert.deepEqual(m.investmentCredit.priorUse, f(100));
assert.deepEqual(m.investmentCredit.expired, f(400));
assert.deepEqual(m.investmentCredit.available, f(0));
assert.deepEqual(m.arithmeticBalance, f(700));
assert.equal(m.investmentCredit.input.assets[0].amount, '10000');
});
void test('CBF15 carry requires original exercise,rate,qualification and actual-use reconciliation', () => {
for (const patch of [
{ originNuit: '999999999' },
{ originProject: 'OTHER-PROJECT' },
{ originYear: '' },
{ originYear: '2026' },
{ originYear: '2021' },
{ originYear: '2024' },
{ originRate: '10' },
{ originReference: '' },
]) {
const c = investmentCarryCase();
Object.assign(c.b.investmentCredit.assets[0], patch);
assert.equal(
calculateAnnual(c.d, c.b).arithmeticBalance,
null,
JSON.stringify(patch),
);
}
for (const patch of [
{ useReconciled: '' },
{ reference: '' },
{ currentState: '' },
{ currentState: 'known' },
{ currentReference: '' },
]) {
const c = investmentCarryCase();
Object.assign(c.b.investmentCredit.carryForward, patch);
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
}
const c = investmentCarryCase();
c.b.investmentCredit.previousUse.amount = '501';
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
c.b.investmentCredit.previousUse.amount = '500';
assert.deepEqual(calculateAnnual(c.d, c.b).investmentCredit.deducted, f(0));
});
void test('CBF15 cannot duplicate historical investment through another id/date,another current row or another scope', () => {
const c = investmentCarryCase(),
t = c.b.investmentCredit;
t.assets.push({ ...t.assets[0], id: 'other', date: '2025-06-01' });
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
const d = investmentCarryCase(),
a = d.b.investmentCredit.assets[0];
d.b.investmentCredit.carryForward.currentState = 'known';
const current = { ...a, id: 'fake-current', date: '2026-01-01' };
delete current.originYear;
delete current.originNuit;
delete current.originProject;
delete current.originRate;
delete current.originReference;
d.b.investmentCredit.assets.push(current);
assert.equal(calculateAnnual(d.d, d.b).arithmeticBalance, null);
const e = investmentCarryCase(),
root = JSON.parse(e.d.modelo_a_snapshot);
root.irpcAnnual = { ...root.irpcAnnual, other: structuredClone(e.b) };
assert.equal(
calculateAnnual(
{ ...e.d, modelo_a_snapshot: JSON.stringify(root) },
e.b,
).arithmeticBalance,
null,
);
});
void test('version3 protects carry from older code; legacy books and prior result history are preserved', () => {
const c = investmentCarryCase();
assert.equal(c.b.version, 3);
assert.equal(c.b.investmentCredit.version, 2);
const broken = structuredClone(c.b);
broken.version = 2;
assert.throws(() => validateAnnual(broken));
const b = recordAnnual(c.d, c.b, '2026-11-10T16:00:00Z');
b.investmentCredit.previousUse.amount = '200';
const next = recordAnnual(c.d, b, '2026-11-10T16:01:00Z'),
d = { ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, next) },
back = annualBook(d);
assert.equal(annualCurrent(d, back), true);
assert.deepEqual(back.history[0].model.investmentCredit.deducted, f(400));
assert.deepEqual(
calculateAnnual(d, back).investmentCredit,
back.result.model.investmentCredit,
);
assert.match(annualHtml(d), /TransporteCBF15/);
assert.match(annualHtml(d), /2025/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-10T16:01:00Z',
}),
),
/TransporteCBF15/,
);
});
@@ -188,7 +188,7 @@ void test('CBF15 calculation-save-recovery-HTML and dossier reports retain sourc
numerator: '200',
denominator: '1',
});
assert.match(annualHtml(d), /IRPC-CBF15\/2/);
assert.match(annualHtml(d), /IRPC-CBF15\/3/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
@@ -196,7 +196,7 @@ void test('CBF15 calculation-save-recovery-HTML and dossier reports retain sourc
generatedAt: '2026-11-10T19:00:00+02:00',
}),
),
/IRPC-CBF15\/2/,
/IRPC-CBF15\/3/,
);
const old = internationalCase();
assert.equal(calculateAnnual(old.d, old.b).investmentCredit, undefined);
+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', 'investment' => 'IrpcInvestmentCreditDraftTest', 'investment-quota' => 'IrpcInvestmentQuotaDraftTest', 'investment-window' => 'IrpcInvestmentWindowDraftTest'][$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', 'investment-window' => 'IrpcInvestmentWindowDraftTest', 'investment-carry' => 'IrpcInvestmentCarryDraftTest'][$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;
+17
View File
@@ -0,0 +1,17 @@
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { investmentCase } from '../tests/frontend/helpers/irpc-investment-case.mjs';
import { prepareInvestmentCarry,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-carry-20261110/',import.meta.url);fs.mkdirSync(out,{recursive:true});
const original=JSON.parse(fs.readFileSync(new URL('../outputs/irpc-cbf15-window-20261110/recovered-fifth.json',import.meta.url),'utf8'));let current=prepareInvestmentCarry(original,annualBook(original));
for(const name of ['carry','expired']){
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.deepEqual(calculateAnnual(d,b).investmentCredit,m.investmentCredit);assert.equal(b.history.length,name==='carry'?6:7);assert.equal(b.history[0].model.investmentCredit?.version??'legacy','legacy');
const html=annualHtml(d),report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:new Date().toISOString()});assert.match(html,/TransporteCBF15/);assert.match(JSON.stringify(report),/TransporteCBF15/);
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,history:b.history.length,window:m.investmentCredit.window,nominal:m.investmentCredit.nominal,priorUse:m.investmentCredit.priorUse,available:m.investmentCredit.available,expired:m.investmentCredit.expired,deducted:m.investmentCredit.deducted,balance:m.arithmeticBalance,activeChanged:false},null,2));
}else{
current=structuredClone(current);const t=current.investmentCredit;t.startDate=name==='carry'?'2022-01-01':'2021-01-01';t.carryForward={reference:'SYNTHETIC documentary unused credit,original same project and Maputo Province10percent',useReconciled:'yes',currentState:'absent',currentReference:'SYNTHETIC no2026 realised investment'};t.assets=[{...t.assets[0],date:'2025-05-01',originYear:'2025',originNuit:t.subjectNuit,originProject:t.projectReference,originRate:'10',originReference:'SYNTHETIC qualified2025 credit,same project and province10percent; original records linked'}];t.previousUse={state:'known',amount:'1000',reference:'SYNTHETIC actual prior fiscal use1000 of original credit4000; not simulated usage'};current=recordAnnual(original,current,new Date().toISOString());fs.writeFileSync(new URL(name+'.json',out),JSON.stringify({...original,person_type:investmentCase().d.person_type,modelo_a_snapshot:saveAnnualBook(original,current)}));
}
}