feat: apply investment credit five-exercise window
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 15:35:24 +02:00
parent 89e6d72c1e
commit d5cb41b111
12 changed files with 274 additions and 20 deletions
@@ -1,3 +1,17 @@
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente, com rollback, cinco/seis resultados históricos e todos os anteriores preservados. Provas `outputs/irpc-cbf15-window-20261110/{fifth,expired}-verification.json` e relatórios respectivos. Modelo `IRPC-CBF15/2`, livro existente versão2 sem migration; snapshots aceites não são reescritos. Fonte já aceite APIEX2025 PDF71–72,CBF15(4/7), reutilizada sem nova validação de autenticidade. Provas anteriores reutilizadas; apenas fronteiras alteradas conferidas.
Saldo: transporte de créditos de investimento de anos anteriores, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários oficiais continuam por implementar nos respectivos recortes. A janela corrente não declara o transporte entregue. Resíduos L1–L7, restanteL8 e L9–L16 e saldo dos módulos não fiscais preservados. Conferência visual pendente no mesmo registo; portal não integral. Próximo trabalho executável: transporte documental CBF15 dentro da mesma janela, com consumo anterior reconciliado e sem duplicar o investimento corrente.
## 10/11/2026 — CBF15 instalado na homologação
**Revisão instalada: `89e6d72c1e86df8eb86e7be74583659975585036`**, promovida às13:27:59UTC e conferida às13:28:21UTC (15:28 locais), em https://teste-investidor.tempo.co.mz. Os17hashes do delta coincidem com Git; revisão/root/up/login HTTP200, configuração preservada, zero migrations/importações. Backup fresco/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos e conferidos;7c8d4726 preservada para reversão. Recibos `outputs/cbf15-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`; pacote imutável `outputs/entrega-cbf15-20261110.zip`, SHA25613bdecef2697edcc821d6fa490586d0c2f4b171b4308c655aa60ba5632664c83.
O recorte CBF15 abaixo deixa de ser pendência de instalação: primeiro exercício2026, residente geral/MZN, activos e condições documentados, colecta integral do projecto ou quota residual documentada, cálculo/gravação/recuperação/relatórios com histórico. As notas QA anteriores ficam como histórico das provas aceites. Outros exercícios/transporte/caducidade, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários e restantes resíduos L1–L7/L8/L9–L16 permanecem no plano existente. Próximo trabalho: residual executável de CBF15/L8, preservando o recorte instalado e sem repetir as provas aceites. Conferência visual continua pendente no mesmo registo; portal não declarado integral.
# Rastreabilidade, utilização e formação
## 10/11/2026 — acordo PT–MZ instalado; crédito CBF15 demonstrado em QA
@@ -1,3 +1,17 @@
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente, com rollback, cinco/seis resultados históricos e todos os anteriores preservados. Provas `outputs/irpc-cbf15-window-20261110/{fifth,expired}-verification.json` e relatórios respectivos. Modelo `IRPC-CBF15/2`, livro existente versão2 sem migration; snapshots aceites não são reescritos. Fonte já aceite APIEX2025 PDF71–72,CBF15(4/7), reutilizada sem nova validação de autenticidade. Provas anteriores reutilizadas; apenas fronteiras alteradas conferidas.
Saldo: transporte de créditos de investimento de anos anteriores, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários oficiais continuam por implementar nos respectivos recortes. A janela corrente não declara o transporte entregue. Resíduos L1–L7, restanteL8 e L9–L16 e saldo dos módulos não fiscais preservados. Conferência visual pendente no mesmo registo; portal não integral. Próximo trabalho executável: transporte documental CBF15 dentro da mesma janela, com consumo anterior reconciliado e sem duplicar o investimento corrente.
## 10/11/2026 — CBF15 instalado na homologação
**Revisão instalada: `89e6d72c1e86df8eb86e7be74583659975585036`**, promovida às13:27:59UTC e conferida às13:28:21UTC (15:28 locais), em https://teste-investidor.tempo.co.mz. Os17hashes do delta coincidem com Git; revisão/root/up/login HTTP200, configuração preservada, zero migrations/importações. Backup fresco/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos e conferidos;7c8d4726 preservada para reversão. Recibos `outputs/cbf15-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`; pacote imutável `outputs/entrega-cbf15-20261110.zip`, SHA25613bdecef2697edcc821d6fa490586d0c2f4b171b4308c655aa60ba5632664c83.
O recorte CBF15 abaixo deixa de ser pendência de instalação: primeiro exercício2026, residente geral/MZN, activos e condições documentados, colecta integral do projecto ou quota residual documentada, cálculo/gravação/recuperação/relatórios com histórico. As notas QA anteriores ficam como histórico das provas aceites. Outros exercícios/transporte/caducidade, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários e restantes resíduos L1–L7/L8/L9–L16 permanecem no plano existente. Próximo trabalho: residual executável de CBF15/L8, preservando o recorte instalado e sem repetir as provas aceites. Conferência visual continua pendente no mesmo registo; portal não declarado integral.
## 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.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,17 @@
## 10/11/2026 — janela de cinco exercícios CBF15 em QA
Homologação mantém `89e6d72c`; o delta seguinte ainda não está instalado. Pendência encerrada em QA: contagem inclusiva dos cinco exercícios civis do CBF15(4/7) na liquidação2026, para investimento efectivo do próprio2026. Início de exploração para projecto novo; início dos procedimentos após aprovação para projecto em funcionamento. Não usa cinco anos móveis nem reinicia o prazo pela data de aquisição. Início2022, mesmo em31/12 → quinto exercício2026, dedução200 pela quota residual, saldo500MZN. Início2021 → sexto exercício2026, prazo expirado, dedução0 e saldo700; activo e documento preservados. Datas inválidas/futuras e activos antigos apresentados como investimento corrente permanecem indeterminados/recusados, não zero por omissão.
Cálculo → POST/PUT do mesmo rascunho privado → GET → recálculo → HTML/dossier demonstrados no QA55441 existente, com rollback, cinco/seis resultados históricos e todos os anteriores preservados. Provas `outputs/irpc-cbf15-window-20261110/{fifth,expired}-verification.json` e relatórios respectivos. Modelo `IRPC-CBF15/2`, livro existente versão2 sem migration; snapshots aceites não são reescritos. Fonte já aceite APIEX2025 PDF71–72,CBF15(4/7), reutilizada sem nova validação de autenticidade. Provas anteriores reutilizadas; apenas fronteiras alteradas conferidas.
Saldo: transporte de créditos de investimento de anos anteriores, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários oficiais continuam por implementar nos respectivos recortes. A janela corrente não declara o transporte entregue. Resíduos L1–L7, restanteL8 e L9–L16 e saldo dos módulos não fiscais preservados. Conferência visual pendente no mesmo registo; portal não integral. Próximo trabalho executável: transporte documental CBF15 dentro da mesma janela, com consumo anterior reconciliado e sem duplicar o investimento corrente.
## 10/11/2026 — CBF15 instalado na homologação
**Revisão instalada: `89e6d72c1e86df8eb86e7be74583659975585036`**, promovida às13:27:59UTC e conferida às13:28:21UTC (15:28 locais), em https://teste-investidor.tempo.co.mz. Os17hashes do delta coincidem com Git; revisão/root/up/login HTTP200, configuração preservada, zero migrations/importações. Backup fresco/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos e conferidos;7c8d4726 preservada para reversão. Recibos `outputs/cbf15-delivery-20261110/{backup,restore,files-restore,verification,delivery}.json`; pacote imutável `outputs/entrega-cbf15-20261110.zip`, SHA25613bdecef2697edcc821d6fa490586d0c2f4b171b4308c655aa60ba5632664c83.
O recorte CBF15 abaixo deixa de ser pendência de instalação: primeiro exercício2026, residente geral/MZN, activos e condições documentados, colecta integral do projecto ou quota residual documentada, cálculo/gravação/recuperação/relatórios com histórico. As notas QA anteriores ficam como histórico das provas aceites. Outros exercícios/transporte/caducidade, repartição automática/múltiplos projectos, EP/L7/cumulações/outros benefícios/formulários e restantes resíduos L1–L7/L8/L9–L16 permanecem no plano existente. Próximo trabalho: residual executável de CBF15/L8, preservando o recorte instalado e sem repetir as provas aceites. Conferência visual continua pendente no mesmo registo; portal não declarado integral.
## 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.
@@ -27,10 +27,11 @@ export function SimulationIrpcInvestment({
<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.
benefícios, que fica conservado. O recorte cobre
investimento efectivo de2026 dentro dos cinco exercícios do
prazo,com colecta integral ou quota residual documental do
projecto, sem outros benefícios próprios ou transportes
anteriores.
</p>
<button
type="button"
@@ -275,7 +276,7 @@ export function SimulationIrpcInvestment({
{text('startDate', 'Data do início aplicável — AAAA-MM-DD')}
{text(
'startReference',
'Suporte do início e primeiro exercício2026',
'Suporte do início e contagem dos cinco exercícios',
)}
<p>
Indique investimento efectivamente realizado,
@@ -2147,6 +2147,19 @@ export function annualReportRows(
m.investmentCredit.version +
'; ' +
m.investmentCredit.source +
(m.investmentCredit.window
? '; JanelaCBF15/1: exercícios ' +
m.investmentCredit.window.firstYear +
'–' +
m.investmentCredit.window.lastYear +
'; exercício ' +
m.investmentCredit.window.exercise +
'/5; ' +
(m.investmentCredit.window.status ===
'expired'
? 'prazo expirado'
: 'dentro do prazo')
: '') +
'; projecto ' +
m.investmentCredit.input.projectReference +
'; ' +
+31 -6
View File
@@ -21,9 +21,9 @@ import {
fraction,
type FiscalFraction,
} from './fiscal-rational.ts';
export const investmentCreditVersion = 'IRPC-CBF15/1';
export const investmentCreditVersion = 'IRPC-CBF15/2';
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.';
'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.';
type Choice = '' | 'yes' | 'no';
export type InvestmentAsset = {
id: string;
@@ -197,7 +197,7 @@ 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) &&
/^\d{4}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s;
export const investmentAssetIdentity = (r: InvestmentAsset) =>
@@ -222,6 +222,15 @@ export function calculateInvestmentCredit(
available: FiscalFraction | null = null,
rate: number | null = null;
let projectCeiling: FiscalFraction | undefined;
let window:
| {
firstYear: number;
lastYear: number;
currentYear: number;
exercise: number;
status: 'active' | 'expired';
}
| undefined;
const rows: {
input: InvestmentAsset;
value: FiscalFraction | null;
@@ -284,8 +293,20 @@ export function calculateInvestmentCredit(
!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.',
'Local efectivo do investimento e início aplicável por documentar: início de exploração para projecto novo,procedimentos após aprovação para projecto em funcionamento.',
);
const firstYear = Number(c.startDate.slice(0, 4));
if (firstYear < 1 || firstYear > Number(b.year))
throw Error(
'Ano inicial inválido ou posterior à liquidação2026; não antecipar o crédito.',
);
window = {
firstYear,
lastYear: firstYear + 4,
currentYear: Number(b.year),
exercise: Number(b.year) - firstYear + 1,
status: Number(b.year) > firstYear + 4 ? 'expired' : 'active',
};
if (!c.assets.length)
throw Error(
'Investimento conhecido exige activo/documento; zero não se presume.',
@@ -321,6 +342,7 @@ export function calculateInvestmentCredit(
!r.issuer.trim() ||
!r.document.trim() ||
!iso(r.date) ||
!r.date.startsWith(b.year + '-') ||
!r.reference.trim() ||
r.newTangible !== 'yes' ||
r.domesticUse !== 'yes' ||
@@ -362,10 +384,12 @@ export function calculateInvestmentCredit(
}
rows.push({
input: structuredClone(r),
value,
value: value && window?.status === 'expired' ? zero() : value,
reason: issues.length
? issues.join(' ')
: 'Activo elegível documentado.',
: window?.status === 'expired'
? 'Prazo15(4) terminado no quinto exercício; investimento2026 conservado,sem crédito.'
: 'Activo elegível documentado.',
pending: issues,
});
pending.push(...issues);
@@ -437,6 +461,7 @@ export function calculateInvestmentCredit(
investment,
...(projectCeiling === undefined ? {} : { projectCeiling }),
rate,
...(window ? { window } : {}),
nominal,
priorUse,
available: pending.length ? null : available,
@@ -0,0 +1,50 @@
<?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 IrpcInvestmentWindowDraftTest 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 (['fifth', 'expired'] as $name) {
$snapshot = json_decode(file_get_contents(base_path('outputs/irpc-cbf15-window-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, ['fifth', 'expired'], true) + 5, $book['history']);
$model = $book['result']['model'];
$this->assertSame($name === 'fifth' ? 'active' : 'expired', $model['investmentCredit']['window']['status']);
$this->assertSame('IRPC-CBF15/2', $model['investmentCredit']['version']);
$this->assertSame(['numerator' => $name === 'fifth' ? '200' : '0', 'denominator' => '1'], $model['investmentCredit']['deducted']);
$this->assertSame(['numerator' => $name === 'fifth' ? '500' : '700', 'denominator' => '1'], $model['arithmeticBalance']);
file_put_contents(base_path('outputs/irpc-cbf15-window-20261110/recovered-'.$name.'.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
@@ -84,7 +84,7 @@ void test('CBF15 requires actual project,source and first-exercise conditions wi
{ soleProject: '' },
{ noOtherBenefits: 'no' },
{ assessmentKey: 'stale' },
{ startDate: '2025-01-01' },
{ startDate: '2027-01-01' },
]) {
const c = investmentCase();
Object.assign(c.b.investmentCredit, patch);
@@ -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\/1/);
assert.match(annualHtml(d), /IRPC-CBF15\/2/);
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\/1/,
/IRPC-CBF15\/2/,
);
const old = internationalCase();
assert.equal(calculateAnnual(old.d, old.b).investmentCredit, undefined);
@@ -0,0 +1,106 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { investmentCase } from './helpers/irpc-investment-case.mjs';
import {
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 includes first through fifth civil fiscal exercises, including a late-year start', () => {
for (const [start, exercise] of [
['2026-06-01', 1],
['2025-12-31', 2],
['2024-12-31', 3],
['2023-12-31', 4],
['2022-12-31', 5],
]) {
const c = investmentCase();
c.b.investmentCredit.startDate = start;
const m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.equal(m.investmentCredit.window.exercise, exercise);
assert.equal(m.investmentCredit.window.status, 'active');
assert.equal(
m.investmentCredit.window.lastYear,
Number(start.slice(0, 4)) + 4,
);
assert.deepEqual(m.investmentCredit.deducted, {
numerator: '500',
denominator: '1',
});
}
});
void test('CBF15 sixth exercise expires without erasing actual investment or creating a carry/refund', () => {
const c = investmentCase();
c.b.investmentCredit.startDate = '2021-12-31';
const m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.pending, []);
assert.equal(m.investmentCredit.window.exercise, 6);
assert.equal(m.investmentCredit.window.status, 'expired');
assert.deepEqual(m.investmentCredit.deducted, {
numerator: '0',
denominator: '1',
});
assert.deepEqual(m.arithmeticBalance, {
numerator: '700',
denominator: '1',
});
assert.equal(m.investmentCredit.rows[0].input.amount, '10000');
assert.match(m.investmentCredit.rows[0].reason, /quinto/);
assert.equal(m.investmentCredit.carryCreated, false);
});
void test('CBF15 does not admit future/invalid start dates or import older assets as current-year investments', () => {
for (const start of ['2027-01-01', '2025-02-29', '0000-01-01', '']) {
const c = investmentCase();
c.b.investmentCredit.startDate = start;
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
}
const c = investmentCase();
c.b.investmentCredit.startDate = '2022-01-01';
c.b.investmentCredit.assets[0].date = '2025-12-31';
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
});
void test('CBF15 existing project uses procedural start; new project can invest before operations within the same window', () => {
const c = investmentCase();
c.b.investmentCredit.startKind = 'existing';
c.b.investmentCredit.startDate = '2022-12-31';
assert.deepEqual(calculateAnnual(c.d, c.b).pending, []);
c.b.investmentCredit.startDate = '2026-06-01';
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
c.b.investmentCredit.startKind = 'new';
assert.deepEqual(calculateAnnual(c.d, c.b).pending, []);
});
void test('CBF15 fifth-year then expiry recovers and reports window while preserving earlier result', () => {
const c = investmentCase();
c.b.investmentCredit.startDate = '2022-12-31';
let b = recordAnnual(c.d, c.b, '2026-11-10T14:00:00Z');
b.investmentCredit.startDate = '2021-12-31';
b = recordAnnual(c.d, b, '2026-11-10T14:01:00Z');
const d = { ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, b) },
back = annualBook(d);
assert.equal(annualCurrent(d, back), true);
assert.equal(back.history.length, 1);
assert.deepEqual(back.history[0].model.investmentCredit.deducted, {
numerator: '500',
denominator: '1',
});
assert.deepEqual(
calculateAnnual(d, back).investmentCredit,
back.result.model.investmentCredit,
);
assert.match(annualHtml(d), /JanelaCBF15\/1/);
assert.match(annualHtml(d), /prazo expirado/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-10T14:01:00Z',
}),
),
/JanelaCBF15/,
);
});
+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'][$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'][$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 { 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-window-20261110/',import.meta.url);fs.mkdirSync(out,{recursive:true});
const original=JSON.parse(fs.readFileSync(new URL('../outputs/irpc-cbf15-20261110/recovered-partial.json',import.meta.url),'utf8'));let current=annualBook(original);
for(const name of ['fifth','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==='fifth'?5:6);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,/JanelaCBF15/);assert.match(JSON.stringify(report),/JanelaCBF15/);
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,deducted:m.investmentCredit.deducted,balance:m.arithmeticBalance,activeChanged:false},null,2));
}else{
current=structuredClone(current);current.investmentCredit.startDate=name==='fifth'?'2022-12-31':'2021-12-31';current.investmentCredit.startReference='SYNTHETIC documented original start, civil exercises inclusive; not a new administrative decision';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)}));
}
}