feat(irpc): reconcile CBF16 cessation and documented prior quotas in QA
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import {irpcSpecialVersion,irpcSpecialMonthlyVersion,irpcSpecialStartPeriodVersion,specialCalculationVersion,validateSpecial,calculateSpecial,specialReportRows,specialDependency,type SpecialInput,type SpecialModel} from './irpc-special.ts';
|
||||
import {irpcSpecialVersion,irpcSpecialMonthlyVersion,irpcSpecialStartPeriodVersion,irpcSpecialCessationPriorVersion,specialCalculationVersion,validateSpecial,calculateSpecial,specialReportRows,specialDependency,type SpecialInput,type SpecialModel} from './irpc-special.ts';
|
||||
import {normalFormPreparationVersion,validateNormalFormPreparation,prepareNormalForms,normalFormPreparationReport,type NormalFormPreparationInput,type NormalFormPreparationModel} from './irpc-normal-form-preparation.ts';
|
||||
import {normalPeriodFxVersion,validateNormalPeriodFx,mapNormalPeriodFx,normalPeriodFxDependency,normalPeriodFxReport,type NormalPeriodFxInput,type NormalPeriodFxModel} from './irpc-normal-period-fx.ts';
|
||||
import {normalDepreciationVersion,validateNormalDepreciation,calculateNormalDepreciation,normalDepreciationReport,type NormalDepreciationInput,type NormalDepreciationModel} from './irpc-normal-depreciation.ts';
|
||||
@@ -41,7 +41,7 @@ export function validateNormal(b:NormalBook){
|
||||
if(!b||b.version!==1||['year','mappingReference','scopeReference'].some(k=>typeof b[k as 'year']!=='string')||!['','resident_general','ep_general','other'].includes(b.scope)||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Preparação IRPC normal não recuperável.');
|
||||
for(const r of b.rows)if(!r||Object.keys(newNormalAdjustment('')).some(k=>typeof r[k as keyof NormalAdjustment]!=='string')||!['','noncontractual_fine','compensatory_interest','other'].includes(r.rule)||!['','yes','no'].includes(r.included))throw Error('Ajustamento IRPC não recuperável;original conservado.');
|
||||
if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identificador de ajustamento duplicado.');
|
||||
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcSpecialStartPeriodVersion,irpcSpecialMonthlyVersion,irpcSpecialVersion,irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion,normalSchedulesVersion,normalCostsVersion,normalDepreciationVersion,normalPeriodFxVersion,normalFormPreparationVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
|
||||
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcSpecialStartPeriodVersion,irpcSpecialCessationPriorVersion,irpcSpecialMonthlyVersion,irpcSpecialVersion,irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion,normalSchedulesVersion,normalCostsVersion,normalDepreciationVersion,normalPeriodFxVersion,normalFormPreparationVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
|
||||
}
|
||||
export function normalBook(d:Record<string,unknown>):NormalBook{
|
||||
const m=store(d).irpcNormal;if(m===undefined)return newNormalBook();if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa IRPC normal não recuperável.');const b=m[acquisitionScope(d)]??newNormalBook();validateNormal(b);return b;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {addFractions,subtractFractions,decimalFraction,fraction,fractionPresenta
|
||||
export const irpcSpecialVersion='IRPC-normal-adjustments/10';
|
||||
export const irpcSpecialMonthlyVersion='IRPC-normal-adjustments/11';
|
||||
export const irpcSpecialStartPeriodVersion='IRPC-normal-adjustments/12';
|
||||
export const irpcSpecialCessationPriorVersion='IRPC-normal-adjustments/13';
|
||||
export const specialCessationPriorSource='CBF3/9/10/16;CIRPC7(4);Decreto72/2013 art3(3),9(1–3) e10:residente,ano civil2026 ou período curto de cessação com meses completosL6. Activo novo a custo histórico,entrada2025/2026,método constante/taxa normal máxima e duodécimos desde a entrada. Taxa ×1,5,limites contabilizado elegível e saldo fiscal;quotas anteriores e parcela temporariamente tributada documentadas emL6,sem recalcular o passado nem consumir saldos oficiais. Saída do activo só quando declarada,excluindo o mês do evento;cessação não presume transmissão. Sem cumulação.';
|
||||
export const specialStartPeriodSource='CBF3/9/10/16;CIRPC7(4);Decreto72/2013 art3(3)/9(3):período curto por início de actividade de residente em2026. Taxa normal máxima ×1,5 ×meses efectivos/12 reutilizados de L6,sem anualização;activo novo emuso no período,método constante,sem saída/quotas anteriores/cumulação. Limites contabilizado elegível e saldo fiscal;apenas diferença adicional.';
|
||||
export const specialMonthlySource='CBF16 com Decreto72/2013 art9:taxa normal máxima ×1,5 ×duodécimos/12,meses reutilizados do mapaL6. Exercício anual civil2026,entrada emuso no ano,método constante,sem saída/quotas anteriores/cumulação. Dedução limitada ao contabilizado elegível e saldo fiscal;apenas diferença adicional,não nova amortização contabilística.';
|
||||
export const specialSources={expenses:'CIRPC,Lei34/2007,art61(4),PDF17/p768(71):35% sobre encargos não devidamente documentados e despesas confidenciais/ilícitas,sem prejuízo do36(1)g. Exercício2026;sem regra inferida da falta de anexo.',cooperative:'CIRPC,Lei34/2007,art12/14,PDF5/p768(59),e61(1):cooperativas agrárias,de artesanato e culturais;redução de50% da taxa geral32%=16%;rendimentos sujeitos a retenção excluídos deste benefício. Recorte2026,residente,parcela ordinária integralmente elegível.',acceleration:'CBF,Lei4/2009,art3/4/9/10/16;compilação oficial APIEX pp66–68/72:incremento50% das taxas normais nos imóveis novos do projecto e,nas mesmas condições,imóveis reabilitados/máquinas e equipamentos industriais ou agro-industriais. Decreto72/2013 art3(3):limite contabilizado e saldo fiscal. Recorte constante pela taxa normal máxima da tabela,primeiro exercício anual2026,sem cumulação presumida.'}as const;
|
||||
@@ -14,9 +16,10 @@ export type Qualification={state:''|'known'|'absent'|'not_applicable';eligible:'
|
||||
export const newQualification=():Qualification=>({state:'',eligible:'',from:'',to:'',reference:'',periodReference:''});
|
||||
export type SpecialExpense={id:string;scheduleId:string;nature:''|'undocumented'|'confidential'|'illicit'|'other';eligible:''|'yes'|'no';reference:string};
|
||||
export type AcceleratedAsset={id:string;assetId:string;kind:''|'new_project_building'|'rehabilitated_project_building'|'industrial_equipment'|'agroindustrial_equipment'|'other';eligible:''|'yes'|'no';reference:string};
|
||||
export type SpecialMonthlyInput={version:1;reference:string;startPeriod?:{version:1;reference:string}};
|
||||
export type SpecialContinuationInput={version:1;period:''|'annual'|'cessation';reference:string;historyReference:string;monthlyFromStart:''|'yes'|'no'};
|
||||
export type SpecialMonthlyInput={version:1;reference:string;startPeriod?:{version:1;reference:string};continuation?:SpecialContinuationInput};
|
||||
export type SpecialInput={version:1;expenses:{qualification:Qualification;rows:SpecialExpense[]};cooperative:{qualification:Qualification;kind:''|'agrarian'|'craft'|'cultural'|'other';allIncomeEligible:''|'yes'|'no';withheldIncome:''|'absent'|'known'};acceleration:{qualification:Qualification;cbfRequirements:''|'yes'|'no';recognition:''|'documented'|'not_required';recognitionReference:string;noOtherBenefits:''|'yes'|'no';rows:AcceleratedAsset[];monthly?:SpecialMonthlyInput}};
|
||||
export const specialCalculationVersion=(i:SpecialInput)=>i.acceleration.monthly?.startPeriod?irpcSpecialStartPeriodVersion:i.acceleration.monthly?irpcSpecialMonthlyVersion:irpcSpecialVersion;
|
||||
export const specialCalculationVersion=(i:SpecialInput)=>i.acceleration.monthly?.continuation?irpcSpecialCessationPriorVersion:i.acceleration.monthly?.startPeriod?irpcSpecialStartPeriodVersion:i.acceleration.monthly?irpcSpecialMonthlyVersion:irpcSpecialVersion;
|
||||
export const newSpecialInput=():SpecialInput=>({version:1,expenses:{qualification:newQualification(),rows:[]},cooperative:{qualification:newQualification(),kind:'',allIncomeEligible:'',withheldIncome:''},acceleration:{qualification:newQualification(),cbfRequirements:'',recognition:'',recognitionReference:'',noOtherBenefits:'',rows:[]}});
|
||||
export const newSpecialExpense=(id:string):SpecialExpense=>({id,scheduleId:'',nature:'',eligible:'',reference:''});
|
||||
export const newAcceleratedAsset=(id:string):AcceleratedAsset=>({id,assetId:'',kind:'',eligible:'',reference:''});
|
||||
@@ -25,6 +28,8 @@ export function validateSpecial(i:SpecialInput){
|
||||
if(!i||i.version!==1||!i.expenses||!i.cooperative||!i.acceleration)throw Error('Qualificação L7 não recuperável;originais conservados.');
|
||||
if(i.acceleration.monthly!==undefined&&(!i.acceleration.monthly||i.acceleration.monthly.version!==1||typeof i.acceleration.monthly.reference!=='string'))throw Error('Preparação CBF16 por duodécimos não recuperável;históricos conservados.');
|
||||
if(i.acceleration.monthly?.startPeriod!==undefined&&(!i.acceleration.monthly.startPeriod||i.acceleration.monthly.startPeriod.version!==1||typeof i.acceleration.monthly.startPeriod.reference!=='string'))throw Error('Preparação CBF16 para início de actividade não recuperável;históricos conservados.');
|
||||
const continuation=i.acceleration.monthly?.continuation;
|
||||
if(continuation!==undefined&&(!continuation||continuation.version!==1||!choices(continuation.period,['','annual','cessation'])||typeof continuation.reference!=='string'||typeof continuation.historyReference!=='string'||!choices(continuation.monthlyFromStart,['','yes','no'])||i.acceleration.monthly?.startPeriod!==undefined))throw Error('Preparação CBF16 de cessação/quotas anteriores não recuperável ou incompatível com início de actividade;históricos conservados.');
|
||||
for(const q of[i.expenses.qualification,i.cooperative.qualification,i.acceleration.qualification])if(!q||!choices(q.state,['','known','absent','not_applicable'])||!choices(q.eligible,['','yes','no'])||['from','to','reference','periodReference'].some(k=>typeof q[k as 'from']!=='string'))throw Error('Condição/período L7 não recuperável.');
|
||||
for(const [rows,keys]of [[i.expenses.rows,Object.keys(newSpecialExpense(''))],[i.acceleration.rows,Object.keys(newAcceleratedAsset(''))]]as const){if(!Array.isArray(rows)||new Set(rows.map(r=>r.id)).size!==rows.length||rows.some(r=>!r||keys.some(k=>typeof(r as unknown as Record<string,unknown>)[k]!=='string')||!choices(r.eligible,['','yes','no'])))throw Error('Parcela L7 não recuperável ou identificador repetido.');}
|
||||
if(i.expenses.rows.some(r=>!choices(r.nature,['','undocumented','confidential','illicit','other']))||i.acceleration.rows.some(r=>!choices(r.kind,['','new_project_building','rehabilitated_project_building','industrial_equipment','agroindustrial_equipment','other'])))throw Error('Natureza L7 desconhecida.');
|
||||
@@ -81,6 +86,9 @@ export function calculateSpecial(draft:Record<string,unknown>,b:NormalBook,m:Nor
|
||||
if(accDecision.status==='calculated'){
|
||||
if(i.acceleration.monthly&&!i.acceleration.monthly.reference.trim())fail(accDecision,'Falta suporte da reutilização dos meses e da opção por duodécimosCBF16 no mapaL6.');
|
||||
if(i.acceleration.monthly?.startPeriod&&!i.acceleration.monthly.startPeriod.reference.trim())fail(accDecision,'Falta suporte do período curto por início de actividadeCBF16;período e activosL6 conservados.');
|
||||
const continuation=i.acceleration.monthly?.continuation;
|
||||
if(continuation?.monthlyFromStart==='no')fail(accDecision,'Duodécimos desde a entrada emuso não aplicados:esta combinação de quotas/saída exige correspondência própria;L6 e os saldos declarados ficam preservados.');
|
||||
if(continuation&&(!continuation.period||!continuation.reference.trim()||!continuation.historyReference.trim()||!continuation.monthlyFromStart))fail(accDecision,'CBF16 cessação/quotas anteriores:identifique período,suporte da vigência/condições e das quotas anteriores ou ausência;duodécimos desde a entrada emuso por documentar. Sem recalcular ou presumir saldos anteriores.');
|
||||
if(i.acceleration.cbfRequirements!=='yes'||!i.acceleration.recognition||!i.acceleration.recognitionReference.trim())fail(accDecision,'Falta enquadramentoCBF3/9 e reconhecimento10 quando exigido;excepção deve ter norma/suporte,não localização.');
|
||||
if(i.acceleration.noOtherBenefits!=='yes')fail(accDecision,'CumulaçãoCBF4 por corresponder;este recorte não combina outros benefícios positivos.');
|
||||
if(coopDecision.status==='calculated')fail(accDecision,'Combinação de redução cooperativa comCBF16 ainda não correspondida;cada caso independente continua demonstrável.');
|
||||
@@ -99,7 +107,10 @@ export function calculateSpecial(draft:Record<string,unknown>,b:NormalBook,m:Nor
|
||||
if(base?.catalog&&cmp(decimalFraction(a.rate),decimalFraction(base.catalog.rate))!==0n)issues.push('RecorteCBF16 usa a taxa normal máxima da tabela;opção por quota inferior exige correspondência própria,sem a acelerar por aproximação.');
|
||||
if(i.acceleration.monthly){
|
||||
const p=specialPeriod(b,m),priorZero=[a.previousAccounting,a.previousFiscal,a.priorAvailable].every(v=>cmp(decimalFraction(v),z())===0n);
|
||||
if(i.acceleration.monthly.startPeriod){
|
||||
if(i.acceleration.monthly.continuation){
|
||||
const c=i.acceleration.monthly.continuation,annual=c.period==='annual'&&p.from==='2026-01-01'&&p.to==='2026-12-31'&&!m.periodsFx?.short,cessation=c.period==='cessation'&&b.periodsFx?.kind==='cessation'&&m.periodsFx?.short&&p.from==='2026-01-01'&&iso(p.to)&&p.to<'2026-12-01';
|
||||
if(b.scope!=='resident_general'||p.pending.length||!annual&&!cessation||a.method!=='constant'||a.timing!=='months'||!iso(a.start)||a.start<'2025-01-01'||a.start>p.to||a.start>='2026-01-01'&&!priorZero||a.exitState!=='absent'&&!(a.exitState==='known'&&iso(a.exitDate)&&a.exitDate>=p.from&&a.exitDate<=p.to)||base?.months===null||base?.months===undefined)issues.push('CBF16 cessação/quotas anteriores:residente,ano civil2026 ou cessaçãoL6 de meses completos desde1Janeiro,activo novo com entrada2025/2026 e saldosL6 reconciliados,método constante e duodécimos. Quotas anteriores positivas exigem entrada2025;saída só por evento documentado dentro do período. Outros anos/períodos/métodos mantêm correspondência própria.');
|
||||
}else if(i.acceleration.monthly.startPeriod){
|
||||
if(b.scope!=='resident_general'||b.periodsFx?.kind!=='start'||!m.periodsFx?.short||p.pending.length||!iso(p.from)||p.from<'2026-02-01'||p.from>'2026-12-01'||p.from.slice(-2)!=='01'||p.to!=='2026-12-31'||a.method!=='constant'||a.timing!=='months'||!iso(a.start)||a.start<p.from||a.start>p.to||a.exitState!=='absent'||!priorZero||base?.months===null||base?.months===undefined)issues.push('CBF16 início de actividade:residente,período curtoL6 de meses completos em2026 até31/12,activo novo emuso dentro do período,método constante,opção mensal e ausência documentada de saída/quotas anteriores;cessação/transição e demais situações permanecem separadas.');
|
||||
}else if(a.method!=='constant'||a.timing!=='months'||!iso(a.start)||a.start<'2026-01-01'||a.start>'2026-12-31'||a.exitState!=='absent'||!priorZero||p.from!=='2026-01-01'||p.to!=='2026-12-31'||base?.months===null||base?.months===undefined)issues.push('CBF16 por duodécimos:exercício anual civil2026,método constante,entrada no ano,opção mensalL6 e ausência documentada de saída/quotas anteriores;outros períodos/casos permanecem separados.');
|
||||
}else if(a.method!=='constant'||a.timing!=='annual'||a.start!=='2026-01-01'||a.exitState!=='absent'||a.previousAccounting!=='0'||a.previousFiscal!=='0'||a.priorAvailable!=='0'||m.fiscalPeriod?.from!=='2026-01-01'||m.fiscalPeriod?.to!=='2026-12-31')issues.push('CBF16 inicial:primeiro exercício anual civil2026,método constante,sem quotas anteriores ou saída;duodécimos e recuperação anterior continuam separados.');
|
||||
@@ -111,9 +122,9 @@ export function calculateSpecial(draft:Record<string,unknown>,b:NormalBook,m:Nor
|
||||
if(accDecision.status==='calculated'){
|
||||
for(const r of accelerations)if(r.status==='indeterminate')fail(accDecision,(r.description||r.id)+': '+r.reason);
|
||||
if(accDecision.status==='calculated'&&b.depreciation&&m.depreciation?.adjustment){
|
||||
const startPeriod=!!i.acceleration.monthly?.startPeriod;
|
||||
acceleratedDepreciation=calculateNormalDepreciation(b.depreciation,source,startPeriod?specialPeriod(b,m):{from:'2026-01-01',to:'2026-12-31'},[...b.rows,...(b.schedules?.rows??[]),...(b.costs?.rows??[])],startPeriod,new Set(accelerations.filter(r=>r.status==='calculated').map(r=>r.assetId)));
|
||||
if(startPeriod)acceleratedDepreciation={...acceleratedDepreciation,source:specialStartPeriodSource};
|
||||
const startPeriod=!!i.acceleration.monthly?.startPeriod,continuation=i.acceleration.monthly?.continuation;
|
||||
acceleratedDepreciation=calculateNormalDepreciation(b.depreciation,source,startPeriod||continuation?specialPeriod(b,m):{from:'2026-01-01',to:'2026-12-31'},[...b.rows,...(b.schedules?.rows??[]),...(b.costs?.rows??[])],startPeriod||continuation?.period==='cessation',new Set(accelerations.filter(r=>r.status==='calculated').map(r=>r.assetId)));
|
||||
if(startPeriod||continuation)acceleratedDepreciation={...acceleratedDepreciation,source:continuation?specialCessationPriorSource:specialStartPeriodSource};
|
||||
if(acceleratedDepreciation.adjustment===null)fail(accDecision,acceleratedDepreciation.pending.join(' '));
|
||||
else additionalDeduction=subtractFractions(m.depreciation.adjustment,acceleratedDepreciation.adjustment);
|
||||
}else if(accDecision.status==='calculated')fail(accDecision,'Reconciliação da amortização normal em falta.');
|
||||
@@ -125,12 +136,12 @@ export function calculateSpecial(draft:Record<string,unknown>,b:NormalBook,m:Nor
|
||||
const assessment=assessmentInput?calculateNormalAssessment(assessmentInput,base?.taxableBase??null,{year:b.year,scope:b.scope,currency:m.currency,extended:!!b.schedules||!!b.periodsFx,short:!!m.periodsFx?.short,period:specialPeriod(b,m)}):null;
|
||||
const normalColecta=assessment?.colecta??null,colecta=coopDecision.status==='indeterminate'?null:normalColecta?(coopDecision.status==='calculated'?mul(normalColecta,1n,2n):normalColecta):null;
|
||||
const combinedColecta=colecta&&autonomousTax?addFractions([colecta,autonomousTax]):null;
|
||||
const monthlySource=i.acceleration.monthly?.startPeriod?specialStartPeriodSource:specialMonthlySource;
|
||||
const monthlySource=i.acceleration.monthly?.continuation?specialCessationPriorSource:i.acceleration.monthly?.startPeriod?specialStartPeriodSource:specialMonthlySource;
|
||||
const monthlyModel=i.acceleration.monthly?{input:structuredClone(i.acceleration.monthly),period:specialPeriod(b,m),source:monthlySource,assets:accelerations.map(r=>{
|
||||
const input=b.depreciation?.assets.find(a=>a.id===r.assetId),normal=m.depreciation?.assets.find(a=>a.id===r.assetId),accelerated=acceleratedDepreciation?.assets.find(a=>a.id===r.assetId),applied=accDecision.status==='calculated'&&r.status==='calculated'&&accelerated?.status==='calculated',rate=normal?.status==='calculated'&&input?decimalFraction(input.rate):null;
|
||||
return{id:r.id,assetId:r.assetId,assetKey:r.assetKey,description:r.description,start:input?.start??'',months:normal?.months??null,taxBasis:normal?.taxBasis??null,normalRate:rate,acceleratedRate:applied&&rate?mul(rate,3n,2n):null,normalQuota:normal?.quota??null,acceleratedQuota:applied?accelerated.quota:null,normalDeduction:normal?.deduction??null,acceleratedDeduction:applied?accelerated.deduction:null,additionalDeduction:applied&&normal?.deduction&&accelerated.deduction?subtractFractions(accelerated.deduction,normal.deduction):r.status==='not_applicable'?z():null,applied,reason:r.reason};
|
||||
return{...(i.acceleration.monthly?.continuation?{prior:{previousAccounting:input?.previousAccounting??'',previousFiscal:input?.previousFiscal??'',available:input?.priorAvailable??'',reference:input?.previousReference??'',normalUsed:normal?.priorUsed??null,normalRemaining:normal?.priorRemaining??null,acceleratedUsed:applied?accelerated.priorUsed:null,acceleratedRemaining:applied?accelerated.priorRemaining:null,normalFiscalRemaining:normal?.remainingFiscal??null,acceleratedFiscalRemaining:applied?accelerated.remainingFiscal:null},exit:{state:input?.exitState??'',date:input?.exitDate??''}}:{}),id:r.id,assetId:r.assetId,assetKey:r.assetKey,description:r.description,start:input?.start??'',months:normal?.months??null,taxBasis:normal?.taxBasis??null,normalRate:rate,acceleratedRate:applied&&rate?mul(rate,3n,2n):null,normalQuota:normal?.quota??null,acceleratedQuota:applied?accelerated.quota:null,normalDeduction:normal?.deduction??null,acceleratedDeduction:applied?accelerated.deduction:null,additionalDeduction:applied&&normal?.deduction&&accelerated.deduction?subtractFractions(accelerated.deduction,normal.deduction):r.status==='not_applicable'?z():null,applied,reason:r.reason};
|
||||
})}:undefined;
|
||||
return{input:structuredClone(i),context:structuredClone(specialContext(draft)),subjectNuit:b.base?.subjectNuit??'',period:specialPeriod(b,m),expenses:{...expenseDecision,rows:expenses,knownAutonomous,autonomousTax,source:specialSources.expenses},cooperative:{...coopDecision,rate:coopDecision.status==='calculated'?fraction(16n):null,source:specialSources.cooperative},acceleration:{...accDecision,rows:accelerations,additionalDeduction,normal:m.depreciation??null,accelerated:acceleratedDepreciation,...(monthlyModel?{monthly:monthlyModel}:{}),source:i.acceleration.monthly?.startPeriod?specialStartPeriodSource:i.acceleration.monthly?specialSources.acceleration+' '+specialMonthlySource:specialSources.acceleration},reference:{taxableProfit:m.taxableProfit,taxableBase:m.taxableBase,colecta:m.colecta??null},taxableProfit,taxableBase:base?.taxableBase??null,colecta,combinedColecta,pending:[...expenseDecision.pending,...coopDecision.pending,...accDecision.pending,...(base?.pending??[]),...(assessment?.pending??[])],limitations:['L7 parcial:apenas61(4),cooperativa integralmente elegível12 eCBF16 no recorte documentado.','Localização/sector só contextualizam;não activam benefícios.','Parcelas mistas,cumulação,mais-valias e demais famílias têm correspondência própria.','M20/M22 permanecem preparaçãoL6 de referência;efeitosL7 sem campo oficial presumido.','Colecta combinada não é saldo final a pagar/recuperar;retenções,pagamentos e créditos anuais emL8.'],finalBalance:{scope:'L8',payable:null,recoverable:null}};
|
||||
return{input:structuredClone(i),context:structuredClone(specialContext(draft)),subjectNuit:b.base?.subjectNuit??'',period:specialPeriod(b,m),expenses:{...expenseDecision,rows:expenses,knownAutonomous,autonomousTax,source:specialSources.expenses},cooperative:{...coopDecision,rate:coopDecision.status==='calculated'?fraction(16n):null,source:specialSources.cooperative},acceleration:{...accDecision,rows:accelerations,additionalDeduction,normal:m.depreciation??null,accelerated:acceleratedDepreciation,...(monthlyModel?{monthly:monthlyModel}:{}),source:i.acceleration.monthly?.continuation?specialCessationPriorSource:i.acceleration.monthly?.startPeriod?specialStartPeriodSource:i.acceleration.monthly?specialSources.acceleration+' '+specialMonthlySource:specialSources.acceleration},reference:{taxableProfit:m.taxableProfit,taxableBase:m.taxableBase,colecta:m.colecta??null},taxableProfit,taxableBase:base?.taxableBase??null,colecta,combinedColecta,pending:[...expenseDecision.pending,...coopDecision.pending,...accDecision.pending,...(base?.pending??[]),...(assessment?.pending??[])],limitations:['L7 parcial:apenas61(4),cooperativa integralmente elegível12 eCBF16 no recorte documentado.','Localização/sector só contextualizam;não activam benefícios.','Parcelas mistas,cumulação,mais-valias e demais famílias têm correspondência própria.','M20/M22 permanecem preparaçãoL6 de referência;efeitosL7 sem campo oficial presumido.','Colecta combinada não é saldo final a pagar/recuperar;retenções,pagamentos e créditos anuais emL8.'],finalBalance:{scope:'L8',payable:null,recoverable:null}};
|
||||
}
|
||||
export type SpecialModel=ReturnType<typeof calculateSpecial>;
|
||||
const money=(v:FiscalFraction|null)=>{if(!v)return'Indeterminado';const p=fractionPresentation(v);return(p.approximate?'≈':'')+p.decimal+' MZN (exacto '+v.numerator+'/'+v.denominator+')';};
|
||||
@@ -148,5 +159,5 @@ export function specialReportRows(m:SpecialModel):Array<[string,string,string,st
|
||||
];}
|
||||
|
||||
function specialMonthlyReport(m:NonNullable<SpecialModel['acceleration']['monthly']>,deduction:FiscalFraction|null):Array<[string,string,string,string]>{
|
||||
return[['IRPC — L7','CBF16 — duodécimos reutilizados de L6','Período:'+m.period.from+'–'+m.period.to+';dedução adicional conciliada:'+money(deduction)+';sem nova dedução do activo de origem.',m.input.reference+(m.input.startPeriod?';início de actividade:'+m.input.startPeriod.reference:'')+';'+m.source],...m.assets.map(a=>['IRPC — L7','CBF16 por duodécimos — '+a.description,'Entrada emuso:'+(a.start||'Informação em falta')+';duodécimosL6:'+(a.months??'Indeterminados')+';base fiscal:'+money(a.taxBasis)+';taxa normal:'+(a.normalRate?fractionPresentation(a.normalRate).decimal+'%':'Indeterminada')+';taxa CBF16:'+(a.acceleratedRate?fractionPresentation(a.acceleratedRate).decimal+'%':'Não aplicada')+';quota normal:'+money(a.normalQuota)+';quota CBF16:'+money(a.acceleratedQuota)+';dedução normal:'+money(a.normalDeduction)+';dedução CBF16:'+money(a.acceleratedDeduction)+';diferença adicional:'+money(a.additionalDeduction)+';'+a.reason,m.source]as[string,string,string,string])];
|
||||
return[['IRPC — L7','CBF16 — duodécimos reutilizados de L6','Período:'+m.period.from+'–'+m.period.to+';dedução adicional conciliada:'+money(deduction)+';sem nova dedução do activo de origem.',m.input.reference+(m.input.startPeriod?';início de actividade:'+m.input.startPeriod.reference:'')+(m.input.continuation?';período:'+(m.input.continuation.period==='cessation'?'Cessação da actividade':m.input.continuation.period==='annual'?'Ano civil2026':'Informação em falta')+';suporte:'+m.input.continuation.reference+';histórico:'+m.input.continuation.historyReference:'')+';'+m.source],...m.assets.map(a=>['IRPC — L7','CBF16 por duodécimos — '+a.description,'Entrada emuso:'+(a.start||'Informação em falta')+';duodécimosL6:'+(a.months??'Indeterminados')+';base fiscal:'+money(a.taxBasis)+';taxa normal:'+(a.normalRate?fractionPresentation(a.normalRate).decimal+'%':'Indeterminada')+';taxa CBF16:'+(a.acceleratedRate?fractionPresentation(a.acceleratedRate).decimal+'%':'Não aplicada')+';quota normal:'+money(a.normalQuota)+';quota CBF16:'+money(a.acceleratedQuota)+';dedução normal:'+money(a.normalDeduction)+';dedução CBF16:'+money(a.acceleratedDeduction)+';diferença adicional:'+money(a.additionalDeduction)+(a.prior?';quotas contabilísticas anteriores:'+a.prior.previousAccounting+';quotas fiscais anteriores:'+a.prior.previousFiscal+';parcela anterior recuperável declarada:'+a.prior.available+';recuperada normal:'+money(a.prior.normalUsed)+';recuperada CBF16:'+money(a.prior.acceleratedUsed)+';saldo anterior aritmético normal:'+money(a.prior.normalRemaining)+';saldo anterior aritmético CBF16:'+money(a.prior.acceleratedRemaining)+';saldo fiscal normal:'+money(a.prior.normalFiscalRemaining)+';saldo fiscal CBF16:'+money(a.prior.acceleratedFiscalRemaining)+';saída:'+(a.exit?.state==='absent'?'Ausência declarada':a.exit?.state==='known'?'Evento documentado':'Informação em falta')+' '+(a.exit?.date??'')+';sem consumo de saldos oficiais':'')+';'+a.reason,m.source+(a.prior?';suporte das quotas anteriores:'+a.prior.reference:'')]as[string,string,string,string])];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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 IrpcSpecialCessationPriorDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_cbf16_cessation_and_prior_quotas_recover_without_consuming_declared_balances(): void
|
||||
{
|
||||
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
|
||||
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);
|
||||
$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));$id=null;$etag=null;
|
||||
foreach(['cessation','prior','annual','exit','missing','negative','absent']as$case){
|
||||
$d=json_decode(file_get_contents(base_path('outputs/irpc-l7-cessation-prior/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);
|
||||
$personal=['person_type'=>$d['person_type']];unset($d['person_type']);
|
||||
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
|
||||
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$personal,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
|
||||
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
|
||||
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
|
||||
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];$a=$m['special']['acceleration'];
|
||||
$this->assertSame('IRPC-normal-adjustments/13',$m['version']);$this->assertSame($b['special']['acceleration']['monthly'],$a['monthly']['input']);
|
||||
$this->assertContains('IRPC-normal-adjustments/12',array_column(array_column($b['history'],'model'),'version'));
|
||||
$this->assertContains('IRPC-normal-adjustments/11',array_column(array_column($b['history'],'model'),'version'));
|
||||
$this->assertSame($b['depreciation'],$a['normal']['input']);$this->assertSame(['scope'=>'L8','payable'=>null,'recoverable'=>null],$m['special']['finalBalance']);
|
||||
$this->assertNull($m['tax']);$this->assertArrayHasKey('irpcWithholdings',$s);
|
||||
if($case==='cessation'){
|
||||
$v=$a['monthly']['assets'][0];$this->assertSame(6,$v['months']);$this->assertSame('absent',$v['exit']['state']);$this->assertSame('10',$a['additionalDeduction']['numerator']);
|
||||
$this->assertSame('3120',$m['special']['taxableProfit']['numerator']);$this->assertSame(['numerator'=>'4992','denominator'=>'5'],$m['special']['colecta']);
|
||||
}elseif($case==='prior'||$case==='annual'){
|
||||
$v=$a['monthly']['assets'][0];$this->assertSame('150',$b['depreciation']['assets'][0]['previousAccounting']);$this->assertSame('100',$b['depreciation']['assets'][0]['previousFiscal']);$this->assertSame('50',$b['depreciation']['assets'][0]['priorAvailable']);
|
||||
$this->assertSame($case==='prior'?'15':'30',$v['prior']['acceleratedUsed']['numerator']);$this->assertSame($case==='prior'?'35':'20',$v['prior']['acceleratedRemaining']['numerator']);
|
||||
$this->assertSame($case==='prior'?'25':'50',$a['additionalDeduction']['numerator']);$this->assertSame($case==='prior'?'3105':'6210',$m['special']['taxableProfit']['numerator']);
|
||||
$this->assertSame(['numerator'=>$case==='prior'?'4968':'9936','denominator'=>'5'],$m['special']['colecta']);$this->assertSame($b['depreciation'],$a['accelerated']['input']);
|
||||
}elseif($case==='exit'){
|
||||
$v=$a['monthly']['assets'][0];$this->assertSame(5,$v['months']);$this->assertSame('2026-06-20',$v['exit']['date']);
|
||||
$this->assertSame(['numerator'=>'125','denominator'=>'3'],$v['normalQuota']);$this->assertSame(['numerator'=>'125','denominator'=>'6'],$a['additionalDeduction']);
|
||||
}elseif($case==='missing'){
|
||||
$this->assertSame('indeterminate',$a['status']);$this->assertNull($a['additionalDeduction']);$this->assertSame('3130',$m['taxableProfit']['numerator']);$this->assertNull($m['special']['colecta']);
|
||||
}elseif($case==='negative'){
|
||||
$this->assertSame('not_applicable',$a['rows'][0]['status']);$this->assertFalse($a['monthly']['assets'][0]['applied']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
|
||||
}else{
|
||||
$this->assertSame('absent',$a['status']);$this->assertSame([],$a['monthly']['assets']);$this->assertSame('0',$a['additionalDeduction']['numerator']);
|
||||
}
|
||||
file_put_contents(base_path('outputs/irpc-l7-cessation-prior/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml} from '../../resources/js/lib/irpc-normal.ts';
|
||||
import {validateSpecial} from '../../resources/js/lib/irpc-special.ts';
|
||||
import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const read=name=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+name,import.meta.url),'utf8'));
|
||||
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
|
||||
function accepted(){const d=read('irpc-l7-preserved.json').acceleration,f=read('irpc-l7-start-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);return{d,b:normalBook(d)};}
|
||||
function setup(period='cessation',prior=true){
|
||||
const{d,b}=accepted(),months=period==='annual'?12:6,p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);
|
||||
if(period==='annual')delete b.periodsFx;else Object.assign(b.periodsFx,{kind:'cessation',from:'2026-01-01',to:'2026-06-30',reference:'QA cessação documentada30Junho2026;meses completos sem transmissão presumida'});
|
||||
b.schedules.interest.amount=String(months*20);
|
||||
Object.assign(b.depreciation.assets[0],{start:prior?'2025-01-01':'2026-01-15',timing:'months',accounting:String(months*10),previousAccounting:prior?'150':'0',previousFiscal:prior?'100':'0',priorAvailable:prior?'50':'0',previousReference:prior?'QA quotas contabilísticas150,fiscais100,parcela50 temporariamente tributada;sem encargos permanentemente excluídos':'QA ausência declarada de quotas anteriores;activo entrou em2026',exitState:'absent',exitDate:''});
|
||||
for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])Object.assign(part.qualification,{from:'2026-01-01',to:period==='annual'?'2026-12-31':'2026-06-30',periodReference:period==='annual'?'QA ano civil2026':b.periodsFx.reference});
|
||||
delete b.special.acceleration.monthly.startPeriod;
|
||||
b.special.acceleration.monthly.continuation={version:1,period,reference:'QA CBF16 vigente no sujeito/projecto e período2026 declarado;sem cumulação',historyReference:'QA activo novo2025/2026,método constante e duodécimos desde entrada;quotas anteriores reconciliadas no mapaL6,não recalculadas',monthlyFromStart:'yes'};return{d,b};
|
||||
}
|
||||
|
||||
test('accepted12 result,key and fullhistory remain exact without continuation',()=>{const{d,b}=accepted();assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),b.result.model);assert.deepEqual(normalBook(d).history,b.history);});
|
||||
|
||||
test('cessation reuses6 months without assuming assetexit;50 normal,75 accelerated capped60,additional10',()=>{const{d,b}=setup('cessation',false),asset=JSON.stringify(b.depreciation),period=JSON.stringify(b.periodsFx),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(m.version,'IRPC-normal-adjustments/13');assert.equal(a.months,6);assert.equal(a.exit.state,'absent');assert.equal(val(a.normalQuota),50);assert.equal(val(a.acceleratedQuota),75);assert.equal(val(a.acceleratedDeduction),60);assert.equal(val(a.additionalDeduction),10);assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.colecta),1001.6);assert.equal(val(s.taxableProfit),3120);assert.equal(val(s.taxableBase),3120);assert.equal(val(s.colecta),998.4);assert.equal(JSON.stringify(b.depreciation),asset);assert.equal(JSON.stringify(b.periodsFx),period);assert.equal(s.finalBalance.payable,null);});
|
||||
|
||||
test('cessation with priorbook150,fiscal100,available50 recovers15 once and adds only25 to normaldeduction',()=>{const{d,b}=setup(),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,6);assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),75);assert.equal(val(a.prior.normalUsed),0);assert.equal(val(a.prior.acceleratedUsed),15);assert.equal(val(a.prior.normalRemaining),50);assert.equal(val(a.prior.acceleratedRemaining),35);assert.equal(val(a.prior.normalFiscalRemaining),850);assert.equal(val(a.prior.acceleratedFiscalRemaining),825);assert.equal(val(s.acceleration.additionalDeduction),25);assert.equal(val(s.taxableProfit),3105);assert.equal(val(s.taxableBase),3105);assert.equal(val(s.colecta),993.6);assert.equal(b.depreciation.assets[0].priorAvailable,'50');assert.equal(b.depreciation.assets[0].previousFiscal,'100');});
|
||||
|
||||
test('annual2026 continuation preserves2025 actualbalances and records30priorused within150 deduction,additional50',()=>{const{d,b}=setup('annual'),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,12);assert.equal(val(a.normalQuota),100);assert.equal(val(a.acceleratedQuota),150);assert.equal(val(a.prior.acceleratedUsed),30);assert.equal(val(a.prior.acceleratedRemaining),20);assert.equal(val(s.acceleration.additionalDeduction),50);assert.equal(val(m.taxableProfit),6260);assert.equal(val(s.taxableProfit),6210);assert.equal(val(s.colecta),1987.2);assert.deepEqual(s.acceleration.normal.input,s.acceleration.accelerated.input);});
|
||||
|
||||
test('documented assetexit excludes eventmonth and keeps exact fractional quotas;cessation date does not edit exit',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});const a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(a.months,5);assert.deepEqual(a.normalQuota,fraction(125n,3n));assert.deepEqual(a.acceleratedQuota,fraction(125n,2n));assert.deepEqual(a.additionalDeduction,fraction(125n,6n));assert.deepEqual(a.prior.acceleratedUsed,fraction(5n,2n));assert.deepEqual(a.prior.acceleratedRemaining,fraction(95n,2n));});
|
||||
|
||||
test('remaining fiscalbalance and accounted/priorcaps bind both calculations;available amount is never a second deduction',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{previousAccounting:'1000',previousFiscal:'980',priorAvailable:'20'});let a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),20);assert.equal(val(a.acceleratedDeduction),20);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedFiscalRemaining),0);const second=setup(),p=JSON.parse(second.d.business_plan_snapshot);p.plan.assetDepreciation='0';second.d.business_plan_snapshot=JSON.stringify(p);second.b.depreciation.assets[0].accounting='0';a=calculateNormal(second.d,second.b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),50);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedRemaining),0);});
|
||||
|
||||
test('missing support or missing priorbalance blocks only dependent CBF16;absence and negative remain explicit',()=>{for(const key of['reference','historyReference','monthlyFromStart']){const{d,b}=setup();b.special.acceleration.monthly.continuation[key]='';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}const missing=setup();missing.b.depreciation.assets[0].previousFiscal='';const pending=calculateNormal(missing.d,missing.b);assert.equal(pending.special.acceleration.additionalDeduction,null);assert.match(pending.depreciation.pending.join(' '),/em falta/);assert.equal(missing.b.depreciation.assets[0].previousFiscal,'');for(const state of['negative','absent']){const{d,b}=setup();if(state==='negative')b.special.acceleration.rows[0].eligible='no';else{b.special.acceleration.rows=[];Object.assign(b.special.acceleration.qualification,{state:'absent',eligible:''});}const s=calculateNormal(d,b).special;assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(s.acceleration.status,state==='absent'?'absent':'calculated');if(state==='negative')assert.equal(s.acceleration.rows[0].status,'not_applicable');}});
|
||||
|
||||
test('otherstartyears,nonmonthlyhistory,transition,cumulation and prioramounts for currentyearentry stay out of scope',()=>{for(const mutate of[b=>b.depreciation.assets[0].start='2024-01-01',b=>b.special.acceleration.monthly.continuation.monthlyFromStart='no',b=>b.periodsFx.kind='transition',b=>b.special.acceleration.noOtherBenefits='no',b=>b.depreciation.assets[0].start='2026-01-01']){const{d,b}=setup();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}});
|
||||
|
||||
test('save-recover13 preserves12result and allhistorical balances;recalc idempotent and reports reconciliation without officialconsumption',()=>{const{d,b}=setup(),history=structuredClone(b.history),old=structuredClone(b.result),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,others=JSON.parse(d.modelo_a_snapshot).irpcWithholdings;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history,[...history,old]);assert.equal(old.model.version,'IRPC-normal-adjustments/12');assert.equal(JSON.stringify(back.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,others);const html=normalHtml(d);assert.match(html,/2026-01-01–2026-06-30/);assert.match(html,/recuperada CBF16:15.00/);assert.match(html,/saldo anterior aritmético CBF16:35.00/);assert.match(html,/sem consumo de saldos oficiais/);assert.match(html,/colecta sobre a base:993.60/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/quotas contabilísticas anteriores/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.acceleration.additionalDeduction),25);assert.equal(back.depreciation.assets[0].priorAvailable,'50');back.depreciation.assets[0].previousFiscal='110';assert.equal(normalCurrent(d,back),false);});
|
||||
|
||||
test('invalid or overlapping optionalpreparation cannot replace preservedbooks',()=>{for(const c of[null,{version:2,period:'annual',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'},{version:1,period:'other',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'}]){const{b}=setup();b.special.acceleration.monthly.continuation=c;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}const{b}=setup();b.special.acceleration.monthly.startPeriod={version:1,reference:'QA'};assert.throws(()=>validateSpecial(b.special),/incompatível/);});
|
||||
|
||||
test('prepare only compatible new guardedAPI cases with missing,negative and absence variants',()=>{const cases={cessation:setup('cessation',false),prior:setup(),annual:setup('annual'),exit:setup(),missing:setup(),negative:setup(),absent:setup()};Object.assign(cases.exit.b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});cases.missing.b.special.acceleration.monthly.continuation.historyReference='';cases.negative.b.special.acceleration.rows[0].eligible='no';cases.absent.b.special.acceleration.rows=[];Object.assign(cases.absent.b.special.acceleration.qualification,{state:'absent',eligible:''});fs.mkdirSync('outputs/irpc-l7-cessation-prior',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T18:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.html',normalHtml(d));}});
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
$root=dirname(__DIR__);
|
||||
$operation=$argv[1]??'';
|
||||
if(!in_array($operation,['routes','test'],true)||basename($root)!=='portal-investidor-irpc-l7-common-qa-20261026')throw new RuntimeException('Explicit L7 QA workspace and routes/test operation required.');
|
||||
foreach(['storage/framework/views','storage/framework/cache','storage/framework/sessions','bootstrap/cache','storage/logs','outputs/irpc-l7-cessation-prior']as$dir)if(!is_dir($root.'/'.$dir))mkdir($root.'/'.$dir,0700,true);
|
||||
foreach(['APP_BASE_PATH'=>$root,'APP_ENV'=>'testing','APP_KEY'=>'base64:'.base64_encode(str_repeat('Q',32)),'DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55441','DB_DATABASE'=>'simulator_fiscal_delta_20261021_test','DB_USERNAME'=>'postgres','DB_PASSWORD'=>'','DB_URL'=>'','CACHE_STORE'=>'array','SESSION_DRIVER'=>'array','QUEUE_CONNECTION'=>'sync','MAIL_MAILER'=>'array'] as $k=>$v){putenv($k.'='.$v);$_ENV[$k]=$_SERVER[$k]=$v;}
|
||||
$loader=require $root.'/vendor/autoload.php';
|
||||
spl_autoload_register(function($class)use($root){foreach(['App\\'=>'app/','Tests\\'=>'tests/','Database\\Factories\\'=>'database/factories/']as$prefix=>$dir){if(str_starts_with($class,$prefix)){$file=$root.'/'.$dir.str_replace('\\','/',substr($class,strlen($prefix))).'.php';if(is_file($file)){require $file;return;}}}},true,true);
|
||||
$app=require $root.'/bootstrap/app.php';
|
||||
(new Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables)->bootstrap($app);
|
||||
(new Illuminate\Foundation\Bootstrap\LoadConfiguration)->bootstrap($app);
|
||||
$c=config('database.connections.pgsql');
|
||||
if($app->configurationIsCached()||$c['host']!=='127.0.0.1'||(int)$c['port']!==55441||$c['database']!=='simulator_fiscal_delta_20261021_test'||!empty($c['url']))throw new RuntimeException('QA guard');
|
||||
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$c['database'],$c['username'],$c['password']);
|
||||
if($pdo->query('select current_database()')->fetchColumn()!==$c['database'])throw new RuntimeException('QA identity');
|
||||
if($operation==='routes'){
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$status=Illuminate\Support\Facades\Artisan::call('wayfinder:generate',['--with-form'=>true]);
|
||||
echo Illuminate\Support\Facades\Artisan::output();exit($status);
|
||||
}
|
||||
$argv=['phpunit','-c','phpunit.investmoz.xml','tests/Feature/IrpcSpecialCessationPriorDraftTest.php'];$argc=count($argv);$_SERVER['argv']=$argv;$_SERVER['argc']=$argc;require $root.'/vendor/phpunit/phpunit/phpunit';
|
||||
Reference in New Issue
Block a user