feat: carry documented investment credits within original project window
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 +
|
||||
'; ' +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user