feat: derive qualified Portugal treaty credit limits
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 13:38:56 +02:00
parent ac67f9c13a
commit 60b52fc8c7
13 changed files with 684 additions and 15 deletions
@@ -1,5 +1,18 @@
# Rastreabilidade, utilização e formação
## 10/11/2026 — económico instalado; CDT Portugal avançada em QA
**Homologação: `ac67f9c13a6561b66c5da788a48849beac90a905`**, promovida pelo executor Git existente às11:32:14UTC e conferida às11:33:16UTC (13:33 locais). Revisão HTTPS e27hashes Git iguais; portal/login/up/revision HTTP200; .env preservado, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos/conferidos. Release c359364d conservada. Recibos `outputs/economic-special-delivery-20261110/{backup,restore,files-restore,verification}.json`. Credencial não guardada; sessão SSH encerrada. Inclui quota económica, inclusão/reconciliação ordinária e especial L6, partilha64(3) e associações64(4). Entradas anteriores «apenas QA» são históricas nestes recortes; não reinstalar por consequência.
**Delta posterior, ainda QA:** correspondência CDT PT–MZ para royalties ordinárias não digitais, dividendos e juros ordinários de fonte portuguesa de residente convencional em Moçambique. País não activa a convenção. Exige beneficiário efectivo, definição/natureza, fonte/pagador, ausência de EP/instalação fixa ligada, período e suporte documentados; relações especiais verificadas nos juros/royalties, isenção governamental11(3) separada. Limite10% do bruto nos ramos suportados; dividendos usam alteração do Protocolo2009 art6, evitando taxa15% do texto1992. Não usa retenção L5 em sentido inverso nem tax sparing presumido. Mantém limite manual/históricos e parcela líquida nacional.
Prova sintética por ramo: bruto60 → limiteCDT6, imposto pago10/fracção nacional8 → crédito6 → saldo702 MZN. Cálculo/gravação/recuperação/HTML/dossier por API existente no QA55441; sem novo writer, schema, direito ou escrita activa. Provas `outputs/irpc-pt-mz-{credit,dividend,interest}-20261110/verification.json`. Fracções mantêm exactidão, sem arredondamento fiscal presumido. País/condição/período inválidos deixam apenas o caso dependente indeterminado. Este delta não integra ac67f9c1.
Restam CDT de outros Estados/naturezas, EP/instalação fixa, juros governamentais, excessos/relações especiais e casos fora do recorte, além dos demais resíduos L1–L7/L8/L9–L16. Conferência visual continua pendente no mesmo registo. Não declarar L8 ou o portal integralmente concluídos.
Conjunto CDT agrupado em `outputs/entrega-cdt-pt-20261110.zip`, base exacta ac67f9c1, aplicação/reversão conferidas; tipos/lint/Pint e build QA aprovados. Instalação deste delta ainda não executada.
## 10/11/2026 — crédito económico: partilha e associações em QA
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
@@ -1,3 +1,15 @@
## 10/11/2026 — económico instalado; CDT Portugal avançada em QA
**Homologação: `ac67f9c13a6561b66c5da788a48849beac90a905`**, promovida pelo executor Git existente às11:32:14UTC e conferida às11:33:16UTC (13:33 locais). Revisão HTTPS e27hashes Git iguais; portal/login/up/revision HTTP200; .env preservado, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos/conferidos. Release c359364d conservada. Recibos `outputs/economic-special-delivery-20261110/{backup,restore,files-restore,verification}.json`. Credencial não guardada; sessão SSH encerrada. Inclui quota económica, inclusão/reconciliação ordinária e especial L6, partilha64(3) e associações64(4). Entradas anteriores «apenas QA» são históricas nestes recortes; não reinstalar por consequência.
**Delta posterior, ainda QA:** correspondência CDT PT–MZ para royalties ordinárias não digitais, dividendos e juros ordinários de fonte portuguesa de residente convencional em Moçambique. País não activa a convenção. Exige beneficiário efectivo, definição/natureza, fonte/pagador, ausência de EP/instalação fixa ligada, período e suporte documentados; relações especiais verificadas nos juros/royalties, isenção governamental11(3) separada. Limite10% do bruto nos ramos suportados; dividendos usam alteração do Protocolo2009 art6, evitando taxa15% do texto1992. Não usa retenção L5 em sentido inverso nem tax sparing presumido. Mantém limite manual/históricos e parcela líquida nacional.
Prova sintética por ramo: bruto60 → limiteCDT6, imposto pago10/fracção nacional8 → crédito6 → saldo702 MZN. Cálculo/gravação/recuperação/HTML/dossier por API existente no QA55441; sem novo writer, schema, direito ou escrita activa. Provas `outputs/irpc-pt-mz-{credit,dividend,interest}-20261110/verification.json`. Fracções mantêm exactidão, sem arredondamento fiscal presumido. País/condição/período inválidos deixam apenas o caso dependente indeterminado. Este delta não integra ac67f9c1.
Restam CDT de outros Estados/naturezas, EP/instalação fixa, juros governamentais, excessos/relações especiais e casos fora do recorte, além dos demais resíduos L1–L7/L8/L9–L16. Conferência visual continua pendente no mesmo registo. Não declarar L8 ou o portal integralmente concluídos.
Conjunto CDT agrupado em `outputs/entrega-cdt-pt-20261110.zip`, base exacta ac67f9c1, aplicação/reversão conferidas; tipos/lint/Pint e build QA aprovados. Instalação deste delta ainda não executada.
## 10/11/2026 — crédito económico: partilha e associações em QA
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,15 @@
## 10/11/2026 — económico instalado; CDT Portugal avançada em QA
**Homologação: `ac67f9c13a6561b66c5da788a48849beac90a905`**, promovida pelo executor Git existente às11:32:14UTC e conferida às11:33:16UTC (13:33 locais). Revisão HTTPS e27hashes Git iguais; portal/login/up/revision HTTP200; .env preservado, zero migrations/importações. Backup/restauro190tabelas com contagens/checksums iguais e369ficheiros/configuração extraídos/conferidos. Release c359364d conservada. Recibos `outputs/economic-special-delivery-20261110/{backup,restore,files-restore,verification}.json`. Credencial não guardada; sessão SSH encerrada. Inclui quota económica, inclusão/reconciliação ordinária e especial L6, partilha64(3) e associações64(4). Entradas anteriores «apenas QA» são históricas nestes recortes; não reinstalar por consequência.
**Delta posterior, ainda QA:** correspondência CDT PT–MZ para royalties ordinárias não digitais, dividendos e juros ordinários de fonte portuguesa de residente convencional em Moçambique. País não activa a convenção. Exige beneficiário efectivo, definição/natureza, fonte/pagador, ausência de EP/instalação fixa ligada, período e suporte documentados; relações especiais verificadas nos juros/royalties, isenção governamental11(3) separada. Limite10% do bruto nos ramos suportados; dividendos usam alteração do Protocolo2009 art6, evitando taxa15% do texto1992. Não usa retenção L5 em sentido inverso nem tax sparing presumido. Mantém limite manual/históricos e parcela líquida nacional.
Prova sintética por ramo: bruto60 → limiteCDT6, imposto pago10/fracção nacional8 → crédito6 → saldo702 MZN. Cálculo/gravação/recuperação/HTML/dossier por API existente no QA55441; sem novo writer, schema, direito ou escrita activa. Provas `outputs/irpc-pt-mz-{credit,dividend,interest}-20261110/verification.json`. Fracções mantêm exactidão, sem arredondamento fiscal presumido. País/condição/período inválidos deixam apenas o caso dependente indeterminado. Este delta não integra ac67f9c1.
Restam CDT de outros Estados/naturezas, EP/instalação fixa, juros governamentais, excessos/relações especiais e casos fora do recorte, além dos demais resíduos L1–L7/L8/L9–L16. Conferência visual continua pendente no mesmo registo. Não declarar L8 ou o portal integralmente concluídos.
Conjunto CDT agrupado em `outputs/entrega-cdt-pt-20261110.zip`, base exacta ac67f9c1, aplicação/reversão conferidas; tipos/lint/Pint e build QA aprovados. Instalação deste delta ainda não executada.
## 10/11/2026 — crédito económico: partilha e associações em QA
Homologação mantém `c359364d`. Avanço posterior em QA, sem instalação: art64(3) distingue a parcela elegível do rendimento da partilha e o excesso com natureza de mais-valia; art64(4) distingue associação em participação e à quota, exigindo tributação efectiva documentada na participação. Reutiliza cálculo60%, prova do imposto subjacente, limites, prioridades, isolamento e writer privado existentes. Não calcula a mais-valia como crédito nem pressupõe inclusão L6 ou reconferência da colecta. Campos antigos e históricos são preservados; fontes incompletas ficam recuperáveis e indeterminadas.
@@ -1,3 +1,4 @@
import { SimulationIrpcPtCdt } from './simulation-irpc-pt-cdt';
import { FiscalProofParcelFields } from './fiscal-proof-parcel';
import CountrySelect, { type Country } from '@/components/country-select';
import { countryIsoCodes } from '@/lib/country-iso-codes';
@@ -387,23 +388,65 @@ export function SimulationIrpcInternational({
</option>
</select>
</label>
{(p.treaty === 'yes' || p.treatyLimit) && (
{p.treaty === 'yes' && (
<label className="block">
Imposto pago elegível nos termos da convenção —
limite MZN
<input
Determinar limite da convenção
<select
aria-label={
'Internacional ' +
(index + 1) +
' — Limite CDT'
' — Método CDT'
}
value={p.treatyLimit}
value={p.treatyMethod ?? 'documented'}
onChange={(e) =>
patch({ treatyLimit: e.target.value })
patch({
treatyMethod: e.target
.value as InternationalPosition['treatyMethod'],
})
}
/>
>
<option value="documented">
Limite documentado
</option>
<option value="ptMzRoyalty">
Portugal — royalties ordinárias,
art.12/23(3)a
</option>
<option value="ptMzDividend">
Portugal — dividendos, art.10/23(3)a
</option>
<option value="ptMzInterest">
Portugal — juros ordinários,
art.11/23(3)a
</option>
</select>
</label>
)}
<SimulationIrpcPtCdt
position={p}
patch={patch}
prefix={'Internacional ' + (index + 1) + ' — '}
/>
{(p.treatyMethod ?? 'documented') === 'documented' &&
(p.treaty === 'yes' || p.treatyLimit) && (
<label className="block">
Imposto pago elegível nos termos da
convenção — limite MZN
<input
aria-label={
'Internacional ' +
(index + 1) +
' — Limite CDT'
}
value={p.treatyLimit}
onChange={(e) =>
patch({
treatyLimit: e.target.value,
})
}
/>
</label>
)}
<label className="block">
Suporte da convenção ou da não aplicação
<input
@@ -0,0 +1,103 @@
import {
newPtMzRoyaltyPreparation,
type InternationalPosition,
type PtMzRoyaltyPreparation,
} from '@/lib/irpc-international-credit';
export function SimulationIrpcPtCdt({
position: p,
patch,
prefix,
}: {
position: InternationalPosition;
patch: (v: Partial<InternationalPosition>) => void;
prefix: string;
}) {
if (!p.treatyMethod || p.treatyMethod === 'documented') return null;
const change = (key: keyof PtMzRoyaltyPreparation, value: string) =>
patch({
treatyPreparation: {
...newPtMzRoyaltyPreparation(),
...p.treatyPreparation,
[key]: value,
},
});
const facts: [keyof PtMzRoyaltyPreparation, string][] = [
['residentMz', 'Beneficiário residente convencional em Moçambique'],
['beneficialOwner', 'Beneficiário efectivo do rendimento'],
[
'payerResidentPt',
'Devedor/sociedade distribuidora residente em Portugal',
],
[
'sourcePortugal',
'Fonte portuguesa, incluindo correspondência de quem suporta o encargo',
],
['noLinkedEstablishment', 'Sem EP/instalação fixa ligada em Portugal'],
];
if (p.treatyMethod !== 'ptMzDividend')
facts.push(['armLength', 'Sem excesso por relações especiais']);
if (p.treatyMethod === 'ptMzRoyalty')
facts.push(
[
'royaltyDefinition',
'Definição convencional de royalties documentada',
],
['nonDigital', 'Caso ordinário não digital'],
);
else
facts.push([
'capitalDefinition',
'Definição convencional de dividendos/juros documentada',
]);
if (p.treatyMethod === 'ptMzInterest')
facts.push([
'governmentInterestExemption',
'Juros abrangidos pela isenção governamental do art.11(3) (o cálculo ordinário exige Não)',
]);
return (
<fieldset className="space-y-2 rounded border p-3">
<legend>Portugal — correspondência CDT</legend>
<p>
Confirme rendimento bruto em MZN, residência convencional,
natureza e período coberto pelos documentos. A taxa de 10%
limita o imposto estrangeiro elegível; não altera a parcela
líquida nacional nem prova pagamento. O limite manual anterior
fica conservado.
</p>
{(
[
['gross', 'Rendimento bruto — MZN'],
['from', 'Início do período documentado — AAAA-MM-DD'],
['to', 'Fim do período documentado — AAAA-MM-DD'],
[
'reference',
'Suporte da residência, contrato e condições convencionais',
],
] as const
).map(([key, label]) => (
<label key={key} className="block">
{label}
<input
aria-label={prefix + label}
value={p.treatyPreparation?.[key] ?? ''}
onChange={(e) => change(key, e.target.value)}
/>
</label>
))}
{facts.map(([key, label]) => (
<label key={key} className="block">
{label}
<select
aria-label={prefix + label}
value={p.treatyPreparation?.[key] ?? ''}
onChange={(e) => change(key, e.target.value)}
>
<option value="">Informação em falta</option>
<option value="yes">Sim — documentado</option>
<option value="no">Não — documentado</option>
</select>
</label>
))}
</fieldset>
);
}
@@ -1740,6 +1740,22 @@ export function annualReportRows(
(p.input.treaty === 'no'
? 'Não aplicável documentado'
: show(p.treatyLimit)) +
(p.treatyDerivation
? '; ' +
p.treatyDerivation.version +
'; bruto ' +
show(p.treatyDerivation.gross) +
' × ' +
p.treatyDerivation.rate +
'%; período ' +
p.treatyDerivation.from +
' a ' +
p.treatyDerivation.to +
'; ' +
p.treatyDerivation.reference +
'; ' +
p.treatyDerivation.source
: '') +
'; elegível: ' +
show(p.eligible) +
'; utilização anterior: ' +
+191 -1
View File
@@ -26,6 +26,36 @@ export type InternationalTaxProof = AnnualPpc & {
taxReference: string;
exerciseReference: string;
};
export type PtMzRoyaltyPreparation = {
capitalDefinition?: '' | 'yes' | 'no';
governmentInterestExemption?: '' | 'yes' | 'no';
gross: string;
from: string;
to: string;
reference: string;
residentMz: '' | 'yes' | 'no';
beneficialOwner: '' | 'yes' | 'no';
sourcePortugal: '' | 'yes' | 'no';
payerResidentPt: '' | 'yes' | 'no';
noLinkedEstablishment: '' | 'yes' | 'no';
armLength: '' | 'yes' | 'no';
royaltyDefinition: '' | 'yes' | 'no';
nonDigital: '' | 'yes' | 'no';
};
export const newPtMzRoyaltyPreparation = (): PtMzRoyaltyPreparation => ({
gross: '',
from: '',
to: '',
reference: '',
residentMz: '',
beneficialOwner: '',
sourcePortugal: '',
payerResidentPt: '',
noLinkedEstablishment: '',
armLength: '',
royaltyDefinition: '',
nonDigital: '',
});
export type InternationalPosition = {
id: string;
country: string;
@@ -40,6 +70,12 @@ export type InternationalPosition = {
nationalLimitReference: string;
nationalMethod?: 'documented' | 'proportional';
nationalBasisReference?: string;
treatyMethod?:
| 'documented'
| 'ptMzRoyalty'
| 'ptMzDividend'
| 'ptMzInterest';
treatyPreparation?: PtMzRoyaltyPreparation;
treaty: '' | 'yes' | 'no';
treatyLimit: string;
treatyReference: string;
@@ -151,10 +187,63 @@ export function validateInternational(c: InternationalCredits) {
) ||
!yesNo.includes(p.incomeIncluded) ||
!yesNo.includes(p.treaty) ||
(p.treatyMethod !== undefined &&
![
'documented',
'ptMzRoyalty',
'ptMzDividend',
'ptMzInterest',
].includes(p.treatyMethod)) ||
(p.treatyPreparation !== undefined &&
(!p.treatyPreparation ||
Object.keys(newPtMzRoyaltyPreparation()).some(
(k) =>
typeof (
p.treatyPreparation as unknown as Record<
string,
unknown
>
)[k] !== 'string',
) ||
[
'residentMz',
'beneficialOwner',
'sourcePortugal',
'payerResidentPt',
'noLinkedEstablishment',
'armLength',
'royaltyDefinition',
'nonDigital',
].some(
(k) =>
!yesNo.includes(
(
p.treatyPreparation as unknown as Record<
string,
string
>
)[k],
),
))) ||
(p.nationalMethod !== undefined &&
!['documented', 'proportional'].includes(p.nationalMethod)) ||
(p.nationalBasisReference !== undefined &&
typeof p.nationalBasisReference !== 'string') ||
(p.treatyPreparation &&
['capitalDefinition', 'governmentInterestExemption'].some(
(k) => {
const v = (
p.treatyPreparation as unknown as Record<
string,
unknown
>
)[k];
return (
v !== undefined &&
(typeof v !== 'string' || !yesNo.includes(v))
);
},
)) ||
!evidenceValid(p.previousUse) ||
!Array.isArray(p.payments)
)
@@ -277,6 +366,102 @@ export function internationalGroupPending(
}
return pending;
}
const validDate = (s: string) =>
/^20\d{2}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s;
export function ptMzTreatyLimit(p: InternationalPosition) {
const q = p.treatyPreparation;
if (
p.treaty !== 'yes' ||
p.country !== 'PT' ||
p.qualificationCountry !== 'PT' ||
p.year !== '2026' ||
!q?.reference.trim()
)
throw Error(
'CDT PT–MZ12/23(3)a: royalties de fonte portuguesa de residente em Moçambique, exercício2026 e suporte por corresponder.',
);
for (const key of [
'residentMz',
'beneficialOwner',
'sourcePortugal',
'payerResidentPt',
'noLinkedEstablishment',
] as const)
if (q[key] !== 'yes')
throw Error(
'CDT PT–MZ royalties: condição por documentar — ' + key + '.',
);
if (p.treatyMethod !== 'ptMzDividend' && q.armLength !== 'yes')
throw Error(
'Excesso por relações especiais em juros/royalties por corresponder.',
);
if (
p.treatyMethod === 'ptMzRoyalty' &&
(q.royaltyDefinition !== 'yes' || q.nonDigital !== 'yes')
)
throw Error(
'Royalties ordinárias não digitais e definição convencional por documentar.',
);
if (p.treatyMethod !== 'ptMzRoyalty' && q.capitalDefinition !== 'yes')
throw Error(
'Definição convencional de dividendos/juros por documentar.',
);
if (
p.treatyMethod === 'ptMzInterest' &&
q.governmentInterestExemption !== 'no'
)
throw Error(
'Juros abrangidos ou por corresponder à isenção governamental11(3) exigem tratamento próprio; não presumir limite ordinário.',
);
if (
!validDate(q.from) ||
!validDate(q.to) ||
q.from > q.to ||
!p.payments.length ||
p.payments.some(
(r) => !validDate(r.date) || r.date < q.from || r.date > q.to,
)
)
throw Error(
'Comprovativos fora do período documentado da CDT/residência; não presumir cobertura.',
);
const gross = creditMoney(q.gross);
if (creditCompare(gross, creditMoney(p.incomeAmount)) < 0n)
throw Error(
'Rendimento bruto inferior à parcela líquida incluída; reconcilie custos/perdas e base.',
);
return {
version:
p.treatyMethod === 'ptMzRoyalty'
? 'IRPC-PT-MZ-royalty-credit/1'
: 'IRPC-PT-MZ-capital-credit/1',
...(p.treatyMethod !== 'ptMzRoyalty' ? { nature: p.treatyMethod } : {}),
gross,
rate: '10',
limit: fraction(
BigInt(gross.numerator),
BigInt(gross.denominator) * 10n,
),
from: q.from,
to: q.to,
reference: q.reference,
source:
p.treatyMethod === 'ptMzRoyalty'
? 'CDT PT–MZ, RAR36/1992 art12(2/4–6),PDF13–14 e23(3)a,PDF21; Protocolo RAR36/2009 arts1/7,PDF1–2; CIRPC65(2). Royalties ordinárias não digitais, sem EP/instalação fixa ligada nem excesso por relações especiais. Não confundir com retenção L5 paga por devedor moçambicano; sem isenção ou tax sparing presumidos.'
: 'CDT PT–MZ, RAR36/1992 art23(3)a,PDF21; CIRPC65(2). ' +
(p.treatyMethod === 'ptMzDividend'
? 'Dividendos10(2–4),PDF10; taxa10% actualizada pelo Protocolo RAR36/2009 art6,PDF2; IRPC/art2 actualizado pelo art1.'
: p.treatyMethod === 'ptMzInterest'
? 'Juros11(2–7),PDF11–12, excluída a isenção11(3); IRPC/art2 actualizado pelo Protocolo RAR36/2009 art1.'
: 'Royalties12(2/4–6),PDF13–14; definição actualizada pelo Protocolo RAR36/2009 art7,PDF2; IRPC/art2 actualizado pelo art1. Recorte ordinário não digital.') +
(p.treatyMethod === 'ptMzDividend'
? ' Sem EP/instalação fixa ligada.'
: ' Sem EP/instalação fixa ligada ou excesso convencional por relações especiais.') +
' Não confundir com retenção L5 de devedor moçambicano; sem isenção ou tax sparing presumidos.',
};
}
export function internationalPosition(
p: InternationalPosition,
paid: FiscalFraction,
@@ -297,6 +482,7 @@ export function internationalPosition(
eligible: FiscalFraction | null = null,
available: FiscalFraction | null = null,
aboveLimit: FiscalFraction | null = null;
let treatyDerivation: ReturnType<typeof ptMzTreatyLimit> | null = null;
let nationalDerivation: {
version: string;
income: FiscalFraction;
@@ -381,7 +567,10 @@ export function internationalPosition(
);
if (!p.treaty || !p.treatyReference.trim())
throw Error('Aplicabilidade da convenção e suporte em falta.');
if (p.treaty === 'yes') treatyLimit = creditLimit(p.treatyLimit);
if (p.treatyMethod && p.treatyMethod !== 'documented') {
treatyDerivation = ptMzTreatyLimit(p);
treatyLimit = treatyDerivation.limit;
} else if (p.treaty === 'yes') treatyLimit = creditLimit(p.treatyLimit);
else if (p.treatyLimit.trim())
throw Error('Ausência de convenção contradiz limite preenchido.');
if (!p.previousUse.state || !p.previousUse.reference.trim())
@@ -421,6 +610,7 @@ export function internationalPosition(
nationalLimit,
...(nationalDerivation ? { nationalDerivation } : {}),
treatyLimit,
...(treatyDerivation ? { treatyDerivation } : {}),
previousUse,
eligible,
available,
@@ -0,0 +1,46 @@
<?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 IrpcPtMzCapitalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_derived_pt_mz_capital_credit_uses_the_existing_private_draft_writer(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::findOrFail(1021));
$id = null;
$etag = null;
foreach (['dividend', 'interest'] as $kind) {
$d = json_decode(file_get_contents(base_path('outputs/irpc-pt-mz-'.$kind.'-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $d['person_type']];
unset($d['person_type']);
$payload = ['personal_snapshot' => $personal, 'economic_snapshot' => $d];
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($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
$this->assertSame($kind === 'dividend' ? 'ptMzDividend' : 'ptMzInterest', $b['internationalCredits']['positions'][0]['treatyMethod']);
$this->assertSame(['numerator' => '6', 'denominator' => '1'], $b['result']['model']['internationalCredit']['positions'][0]['treatyLimit']);
$this->assertSame(['numerator' => '702', 'denominator' => '1'], $b['result']['model']['arithmeticBalance']);
file_put_contents(base_path('outputs/irpc-pt-mz-'.$kind.'-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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 IrpcPtMzCreditDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_derived_pt_mz_credit_uses_the_existing_private_draft_writer(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('proactive_investment_20261027_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::findOrFail(1021));
$d = json_decode(file_get_contents(base_path('outputs/irpc-pt-mz-credit-20261110/fixture.json')), true, flags: JSON_THROW_ON_ERROR);
$personal = ['person_type' => $d['person_type']];
unset($d['person_type']);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => $personal, 'economic_snapshot' => $d], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$back = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'], $back['modelo_a_snapshot']);
$this->assertSame($d['business_plan_snapshot'], $back['business_plan_snapshot']);
$b = json_decode($back['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['irpcAnnual'][$d['context_opportunity']];
$this->assertSame('ptMzRoyalty', $b['internationalCredits']['positions'][0]['treatyMethod']);
$this->assertSame(['numerator' => '6', 'denominator' => '1'], $b['result']['model']['internationalCredit']['positions'][0]['treatyLimit']);
$this->assertSame(['numerator' => '702', 'denominator' => '1'], $b['result']['model']['arithmeticBalance']);
file_put_contents(base_path('outputs/irpc-pt-mz-credit-20261110/recovered.json'), json_encode($back, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
+175
View File
@@ -0,0 +1,175 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { internationalCase } from './helpers/irpc-international-case.mjs';
import { newPtMzRoyaltyPreparation } from '../../resources/js/lib/irpc-international-credit.ts';
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';
function treatyCase() {
const c = internationalCase();
Object.assign(c.b.internationalCredits.positions[0], {
country: 'PT',
qualificationCountry: 'PT',
incomeAmount: '40',
treaty: 'yes',
treatyMethod: 'ptMzRoyalty',
treatyReference:
'SYNTHETIC QA PT-MZ12/23(3)a actual royalty qualification, no real treaty decision',
treatyPreparation: {
...newPtMzRoyaltyPreparation(),
gross: '60',
from: '2026-01-01',
to: '2026-12-31',
reference:
'SYNTHETIC QA residence certificate, royalty contract and all conditions documented',
residentMz: 'yes',
beneficialOwner: 'yes',
sourcePortugal: 'yes',
payerResidentPt: 'yes',
noLinkedEstablishment: 'yes',
armLength: 'yes',
royaltyDefinition: 'yes',
nonDigital: 'yes',
},
});
return c;
}
void test('PT royalty credit derives a6 treaty ceiling from gross60 and leaves national fraction,paid tax and L6 separate', () => {
const c = treatyCase(),
before = c.d.modelo_a_snapshot,
m = calculateAnnual(c.d, c.b),
p = m.internationalCredit.positions[0];
assert.deepEqual(m.pending, []);
assert.deepEqual(p.treatyLimit, { numerator: '6', denominator: '1' });
assert.deepEqual(p.eligible, { numerator: '6', denominator: '1' });
assert.deepEqual(m.arithmeticBalance, {
numerator: '702',
denominator: '1',
});
assert.equal(c.d.modelo_a_snapshot, before);
});
void test('PT royalty matching rejects wrong country,unknown facts,expired coverage and contradictory gross without silently using the manual limit', () => {
for (const patch of [
{ country: 'ZA' },
{ treaty: 'no' },
{ treatyPreparation: { beneficialOwner: '' } },
{ treatyPreparation: { sourcePortugal: 'no' } },
{ treatyPreparation: { to: '2026-09-01' } },
{ treatyPreparation: { gross: '30' } },
{ treatyPreparation: { from: '2026-02-30' } },
]) {
const c = treatyCase(),
p = c.b.internationalCredits.positions[0];
const q = patch.treatyPreparation;
if (q) Object.assign(p.treatyPreparation, q);
else Object.assign(p, patch);
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
}
});
void test('PT royalty ceiling is exact and can be lower than paid tax without creating official use or refund', () => {
const c = treatyCase();
c.b.internationalCredits.positions[0].treatyPreparation.gross = '60.01';
const m = calculateAnnual(c.d, c.b);
assert.deepEqual(m.internationalCredit.positions[0].treatyLimit, {
numerator: '6001',
denominator: '1000',
});
assert.equal(m.internationalCredit.officialBalancesConsumed, false);
});
void test('PT royalty limit survives save,recovery and both report consumers; previous documented mode stays unchanged', () => {
const c = treatyCase(),
b = recordAnnual(c.d, c.b, '2026-11-10T17:00:00+02:00'),
d = JSON.parse(
JSON.stringify({
...c.d,
modelo_a_snapshot: saveAnnualBook(c.d, b),
}),
),
back = annualBook(d);
assert.equal(annualCurrent(d, back), true);
assert.deepEqual(
calculateAnnual(d, back).internationalCredit,
back.result.model.internationalCredit,
);
assert.match(annualHtml(d), /IRPC-PT-MZ-royalty-credit\/1/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-10T17:00:00+02:00',
}),
),
/IRPC-PT-MZ-royalty-credit\/1/,
);
const old = internationalCase();
assert.equal(
calculateAnnual(old.d, old.b).internationalCredit.positions[0]
.treatyDerivation,
undefined,
);
});
void test('PT dividends use the2009 treaty10percent ceiling and interest uses ordinary11 conditions, preserving royalty evidence', () => {
for (const method of ['ptMzDividend', 'ptMzInterest']) {
const c = treatyCase(),
p = c.b.internationalCredits.positions[0];
p.treatyMethod = method;
Object.assign(p.treatyPreparation, {
capitalDefinition: 'yes',
governmentInterestExemption: 'no',
royaltyDefinition: '',
nonDigital: '',
...(method === 'ptMzDividend' ? { armLength: '' } : {}),
});
const m = calculateAnnual(c.d, c.b),
r = m.internationalCredit.positions[0];
assert.deepEqual(m.pending, []);
assert.deepEqual(r.treatyLimit, { numerator: '6', denominator: '1' });
assert.equal(r.treatyDerivation.nature, method);
assert.match(
r.treatyDerivation.source,
method === 'ptMzDividend' ? /art6/ : /11/,
);
const d = {
...c.d,
modelo_a_snapshot: saveAnnualBook(
c.d,
recordAnnual(c.d, c.b, '2026-11-10T17:10:00+02:00'),
),
};
assert.match(annualHtml(d), /IRPC-PT-MZ-capital-credit\/1/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
generatedAt: '2026-11-10T17:10:00+02:00',
}),
),
/IRPC-PT-MZ-capital-credit\/1/,
);
}
});
void test('PT capital matching does not presume article11 governmental exemption or classification', () => {
for (const patch of [
{ capitalDefinition: '' },
{ governmentInterestExemption: '' },
{ governmentInterestExemption: 'yes' },
{ armLength: '' },
]) {
const c = treatyCase(),
p = c.b.internationalCredits.positions[0];
p.treatyMethod = 'ptMzInterest';
Object.assign(
p.treatyPreparation,
{ capitalDefinition: 'yes', governmentInterestExemption: 'no' },
patch,
);
assert.equal(calculateAnnual(c.d, c.b).arithmeticBalance, null);
}
});
+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'][$argv[2] ?? ''] ?? throw new RuntimeException('Choose quota, inclusion, liquidation or association explicitly.');
$case = ['quota' => 'IrpcEconomicQuotaDraftTest', 'inclusion' => 'IrpcEconomicInclusionDraftTest', 'liquidation' => 'IrpcEconomicLiquidationDraftTest', 'association' => 'IrpcEconomicAssociationDraftTest', 'special-inclusion' => 'IrpcEconomicSpecialInclusionDraftTest', 'pt-royalty' => 'IrpcPtMzCreditDraftTest', 'pt-capital' => 'IrpcPtMzCapitalDraftTest'][$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;
+24
View File
@@ -0,0 +1,24 @@
import { createProjectDossierReport } from '../resources/js/lib/project-dossier-report.ts';
import fs from 'node:fs';
import { internationalCase } from '../tests/frontend/helpers/irpc-international-case.mjs';
import { recordAnnual, saveAnnualBook, annualBook, calculateAnnual, annualHtml } from '../resources/js/lib/irpc-annual-settlement.ts';
import assert from 'node:assert/strict';
const capital = ['dividend','interest'].find(k=>process.argv.includes(k));
const out = new URL(capital ? '../outputs/irpc-pt-mz-'+capital+'-20261110/' : '../outputs/irpc-pt-mz-credit-20261110/', import.meta.url);
fs.mkdirSync(out, { recursive: true });
if (process.argv[2] === 'recovered') {
const d = JSON.parse(fs.readFileSync(new URL('recovered.json', out), 'utf8'));
const b = annualBook(d), m = b.result.model;
assert.deepEqual(calculateAnnual(d, b).internationalCredit, m.internationalCredit);
const html = annualHtml(d);assert.match(html, /IRPC-PT-MZ-(royalty|capital)-credit\/1/);
const report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),generatedAt:'2026-11-10T17:10:00+02:00'});
assert.match(JSON.stringify(report),/IRPC-PT-MZ-(royalty|capital)-credit\/1/);
fs.writeFileSync(new URL('report.html', out), html);
fs.writeFileSync(new URL('verification.json', out), JSON.stringify({ synthetic: true, apiRecovered: true, dossierReport: true, currentRecalculation: true, html: true, quota: m.internationalCredit.positions[0].treatyLimit, balance: m.arithmeticBalance, activeChanged: false }, null, 2));
} else {
const c = internationalCase();
Object.assign(c.b.internationalCredits.positions[0],{country:'PT',qualificationCountry:'PT',incomeAmount:'40',treaty:'yes',treatyMethod:'ptMzRoyalty',treatyReference:'SYNTHETIC QA qualified PT-MZ12/23(3)a ordinary royalties',treatyPreparation:{gross:'60',from:'2026-01-01',to:'2026-12-31',reference:'SYNTHETIC QA residence certificate and royalty agreement covering conditions',residentMz:'yes',beneficialOwner:'yes',sourcePortugal:'yes',payerResidentPt:'yes',noLinkedEstablishment:'yes',armLength:'yes',royaltyDefinition:'yes',nonDigital:'yes'}});
if(capital) {const p=c.b.internationalCredits.positions[0];p.treatyMethod=capital==='dividend'?'ptMzDividend':'ptMzInterest';p.treatyReference='SYNTHETIC QA explicit PT-MZ article'+(capital==='dividend'?'10 updated2009':'11 ordinary non-governmental')+'/23(3)a qualification';p.treatyPreparation.reference='SYNTHETIC QA residence certificate and '+capital+' contract, source and conditions documented';Object.assign(p.treatyPreparation,{capitalDefinition:'yes',governmentInterestExemption:'no'});}
const b = recordAnnual(c.d, c.b, '2026-11-10T14:00:00+02:00');
fs.writeFileSync(new URL('fixture.json', out), JSON.stringify({ ...c.d, modelo_a_snapshot: saveAnnualBook(c.d, b) }));
}