feat(irpc): prepare compatible L6 schedules and scoped form mappings in QA
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -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 IrpcNormalSchedulesDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
public function test_complementary_rules_recover_calculated_missing_and_current_loss_without_settlement(): 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(['calculated','missing','loss','ep'] as $case){
|
||||
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-sets/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
|
||||
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
|
||||
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'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'];
|
||||
$this->assertSame('IRPC-normal-adjustments/5',$m['version']);$this->assertNull($m['tax']);
|
||||
$this->assertSame('2026',$m['assessment']['period']['year']);$this->assertSame($case==='ep'?'2025-07-01':'2026-01-01',$m['assessment']['period']['from']);$this->assertSame($case==='ep'?'2026-06-30':'2026-12-31',$m['assessment']['period']['to']);
|
||||
$this->assertSame('L8',$m['assessment']['finalBalance']['scope']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['assessment']['finalBalance']['recoverable']);
|
||||
$this->assertContains('IRPC-normal-adjustments/4',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('schedules',$m);$this->assertArrayHasKey('irpcWithholdings',$s);
|
||||
if(in_array($case,['calculated','ep'],true)){$this->assertSame('6980',$m['taxableProfit']['numerator']);$this->assertSame('4680',$m['taxableBase']['numerator']);$this->assertSame(['numerator'=>'7488','denominator'=>'5'],$m['colecta']);$this->assertSame('120',$m['schedules']['interest']['numerator']);}
|
||||
elseif($case==='missing'){$this->assertNull($m['taxableProfit']);$this->assertNull($m['colecta']);$this->assertNull($m['schedules']['interest']);$this->assertSame('520',$m['schedules']['net']['numerator']);}
|
||||
else{$this->assertSame('3820',$m['currentLoss']['numerator']);$this->assertSame('0',$m['taxableBase']['numerator']);$this->assertSame('0',$m['colecta']['numerator']);$this->assertSame([],$b['base']['losses']);}
|
||||
file_put_contents(base_path('outputs/irpc-l6-sets/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,28 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalFinancialSource,normalReportRows} from '../../resources/js/lib/irpc-normal.ts';
|
||||
import {newNormalSchedules,newScheduleRow,newNormalAsset,calculateSchedules} from '../../resources/js/lib/irpc-normal-schedules.ts';
|
||||
import {normalFormFields} from '../../resources/js/lib/irpc-normal-forms.ts';import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-assessment-preserved.json',import.meta.url),'utf8'));
|
||||
function scenario(){const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyInterestExpense='20';p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);b.schedules=newNormalSchedules();const s=b.schedules;s.period={kind:'calendar',from:'2026-01-01',to:'2026-12-31',reference:'QA doze meses do plano correspondem ao ano civil completo'};
|
||||
s.interest={amount:'120',reference:'QA contrato bancário, exploração, sem relação especial, limites especiais ou capitalização; pagamentos separados',conditions:'yes'};
|
||||
for(const [id,rule,amount,sourceCode,fiscalYear]of[['thirdparty','tax_other','250','operating_expenses',''],['gain','patrimonial_gain','200','',''],['cost','patrimonial_cost','50','',''],['timing','future_cost','100','operating_expenses','2027']])s.rows.push({...newScheduleRow(id),rule,amount,sourceCode,fiscalYear,document:'QA-'+id,line:'1',reference:'QA factos documentados no recorte ordinário, não é operação especial',conditions:'yes'});
|
||||
for(const[k,amount]of Object.entries({expenseCorrections:'250',positivePatrimonial:'200',negativePatrimonial:'50',timing:'100'}))b.profit.topics[k]={state:'known',amount,reference:'QA reconciliação integral do tema com os documentos do mapa'};
|
||||
b.profit.lines.find(l=>l.code==='depreciation').state='known';s.assetsReference='QA único activo; total contabilístico120 reconciliado com o plano';s.assets=[{...newNormalAsset('server'),category:'server',description:'Servidor QA',cost:'500',residual:'0',rate:'20',accounting:'120',previousFiscal:'0',inUseFrom:'2026-01-01',tableReference:'Decreto72/2013 tabelaII grupo3 item9.1, sem taxa específica da tabelaI',reference:'QA factura500, activo próprio, vida útil fiscal, sem regras especiais',conditions:'yes'}];return{d,b};}
|
||||
const val=f=>f?Number(f.numerator)/Number(f.denominator):null;
|
||||
test('new rules reconcile financial interest, patrimonial variations, timing and server quota through profit/base/colecta',()=>{const{d,b}=scenario(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.additions),220);assert.equal(val(m.schedules.interest),120);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.schedules.net),520);assert.equal(val(m.taxableProfit),6980);assert.equal(val(m.taxableBase),4680);assert.equal(val(m.colecta),1497.6);assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.payable,null);assert.equal(m.schedules.assets[0].fiscal.numerator,'100');assert.equal(m.version,'IRPC-normal-adjustments/5');});
|
||||
test('ordinary interest is qualified independently from compensatory interest and cash payments',()=>{const{d,b}=scenario();b.schedules.interest.amount='119';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(val(m.schedules.net),520);assert.equal(m.schedules.interest,null);assert.match(m.profit.pending.join(' '),/Juros financeiros/);const c=scenario();c.b.schedules.rows[0].sourceCode='interest_expense';c.b.schedules.rows[0].amount='20';c.b.profit.topics.expenseCorrections.amount='20';assert.equal(calculateNormal(c.d,c.b).taxableProfit,null);c.b.schedules.interest.amount='100';assert.equal(val(calculateNormal(c.d,c.b).schedules.interest),100);assert.notEqual(calculateNormal(c.d,c.b).taxableProfit,null);});
|
||||
test('missing rule evidence blocks dependent totals while independent amounts remain calculated',()=>{const{d,b}=scenario();b.schedules.rows[0].reference='';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.colecta,null);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),200);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.accountingProfit),6240);});
|
||||
test('duplicate documents, over-allocation, wrong source and contradictory topic absence are rejected',()=>{for(const mutate of[b=>{b.schedules.rows[0].document=b.rows[0].document+' ';b.schedules.rows[0].line=b.rows[0].line;},b=>{b.schedules.rows[0].amount='1201';b.profit.topics.expenseCorrections.amount='1201';},b=>b.schedules.rows[0].sourceCode='revenue',b=>b.profit.topics.positivePatrimonial={state:'absent',amount:'',reference:'QA declared absent'}]){const{d,b}=scenario();mutate(b);assert.equal(calculateNormal(d,b).taxableProfit,null);}});
|
||||
test('capital contributions and distributions outside result are known amounts with zero tax effect, not ordinary gains/costs',()=>{const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='gain').rule='capital_in';b.schedules.rows.find(r=>r.id==='cost').rule='capital_out';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),6830);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),0);assert.equal(val(m.schedules.effects.find(e=>e.id==='cost').amount),50);});
|
||||
test('timing belongs to documented year; previous years and inconsistent mapping stay pending',()=>{for(const y of['','2025','2026']){const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='timing').fiscalYear=y;assert.equal(calculateNormal(d,b).taxableProfit,null);}});
|
||||
test('asset quota uses remaining fiscal value; unsupported rates, short-year facts and incomplete reconciliation do not default to accounting',()=>{const{d,b}=scenario();b.schedules.assets[0].previousFiscal='450';let m=calculateNormal(d,b);assert.equal(val(m.schedules.assets[0].fiscal),50);assert.equal(val(m.schedules.depreciation),70);for(const patch of[{rate:'25'},{category:'other'},{inUseFrom:'2026-02-01'},{inUseFrom:'2025-02-31'},{inUseFrom:'2014-01-01'},{accounting:'119'},{conditions:''}]){const{d,b}=scenario();Object.assign(b.schedules.assets[0],patch);m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.schedules.depreciation,null);}});
|
||||
test('known current loss gives zero base/colecta only with documented current-loss treatment and no self-deduction; no official carryforward created',()=>{const{d,b}=scenario(),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyOperatingExpenses='1000';d.business_plan_snapshot=JSON.stringify(p);b.base.losses=[];b.base.lossesState='absent';b.base.lossesReference='QA ausência expressa de perdas anteriores';let m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-3820);assert.equal(m.taxableBase,null);b.schedules.currentLossReference='QA perda fiscal corrente apurada, sem dedução de perdas anteriores ou reporte automático';m=calculateNormal(d,b);assert.equal(val(m.currentLoss),3820);assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);assert.deepEqual(b.base.losses,[]);assert.equal(normalFormFields(m).fields.find(f=>f.code==='269').value.numerator,'3820');assert.equal(normalFormFields(m).fields.find(f=>f.code==='270').value,null);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.mkdirSync('outputs/irpc-l6-sets',{recursive:true});fs.writeFileSync('outputs/irpc-l6-sets/loss.json',JSON.stringify(d));});
|
||||
test('EP ordinary annual MZN uses its attributable financial result; unsupported currency/year remains case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.scopeReference='QA único EP em Moçambique, imputação completa e exclusiva de custos/proveitos do EP, CIRPC15(1)c';assert.equal(val(calculateNormal(d,b).colecta),1497.6);b.year='2025';assert.equal(calculateNormal(d,b).colecta,null);});
|
||||
test('form preparation keeps exact fractions and unmapped fields without provisional codes or fabricated final settlement',()=>{const{d,b}=scenario(),m=calculateNormal(d,b),f=normalFormFields(m);assert.equal(val(f.fields.find(f=>f.code==='278').value),4680);assert.equal(f.fields.find(f=>f.code==='300').transcriptionReady,true);for(const code of['200','302','309–316'])assert.equal(f.fields.find(f=>f.code===code).value,null);assert.equal(f.m20.status,'partial');assert.equal(val(f.m20.fields.find(f=>f.account==='6.5').value),120);assert.match(f.m20.reason,/campo201.*campo200/);const partial=normalFormFields({...m,colecta:fraction(2n,625n)}).fields.find(f=>f.code==='300');assert.deepEqual(partial.value,{numerator:'2',denominator:'625'});assert.equal(partial.transcriptionReady,false);assert.match(partial.reason,/sem arredondamento/);});
|
||||
test('installed4 is current without recalculation; new inputs survive save/recovery/history/export/dossier and become stale on edit',()=>{const original=seed(),old=normalBook(original);assert.equal(normalCurrent(original,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/4');const{d,b}=scenario(),before=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-25T23:50:00Z'));const recovered=JSON.parse(JSON.stringify(d)),back=normalBook(recovered);assert.equal(normalCurrent(recovered,back),true);assert.deepEqual(back.history.at(-1),old.result);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books,before.books);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.match(normalHtml(d),/1497.6/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/M22 campo 300/);assert.match(JSON.stringify(normalReportRows(d)),/M20/);fs.writeFileSync('outputs/irpc-l6-sets/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-sets/preparation.html',normalHtml(d));back.schedules.rows[0].amount='251';recovered.modelo_a_snapshot=saveNormalBook(recovered,back);assert.equal(normalCurrent(recovered,normalBook(recovered)),false);assert.throws(()=>normalHtml(recovered));const missing=normalBook(d);missing.schedules.interest.conditions='';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,missing));fs.writeFileSync('outputs/irpc-l6-sets/missing.json',JSON.stringify(d));});
|
||||
test('unknown schema/rules fail without silently replacing saved originals',()=>{const{d,b}=scenario();b.schedules.version=2;assert.throws(()=>saveNormalBook(d,b));b.schedules.version=1;b.schedules.rows[0].rule='fiction';assert.throws(()=>saveNormalBook(d,b));});
|
||||
|
||||
test('documented noncalendar EP year preserves12 monthly periods; short periods and absent legal basis stay case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.schedules.period={kind:'ep_annual',from:'2025-07-01',to:'2026-06-30',reference:'QA comunicação expressa AT anterior, início e manutenção5exercícios, plano completo do EP'};b.schedules.assets[0].inUseFrom='2025-07-01';let m=calculateNormal(d,b);assert.equal(val(m.colecta),1497.6);assert.equal(m.assessment.period.from,'2025-07-01');assert.equal(m.assessment.period.to,'2026-06-30');d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.writeFileSync('outputs/irpc-l6-sets/ep.json',JSON.stringify(d));b.schedules.period.from='2026-01-01';assert.equal(calculateNormal(d,b).colecta,null);b.schedules.period.reference='';assert.equal(calculateNormal(d,b).taxableProfit,null);});
|
||||
|
||||
test('ordinary tax/undocumented addbacks and current income/cost timing apply their distinct signs without overwriting original accounting',()=>{for(const rule of['tax_own','undocumented']){const{d,b}=scenario();b.schedules.rows[0].rule=rule;assert.equal(val(calculateNormal(d,b).taxableProfit),6980);}for(const [rule,delta]of[['current_cost',-200],['current_income',0]]){const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule=rule;r.sourceCode='';r.fiscalYear='2026';assert.equal(val(calculateNormal(d,b).taxableProfit),6980+delta);}const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule='future_income';r.sourceCode='revenue';assert.equal(val(calculateNormal(d,b).taxableProfit),6780);assert.equal(normalFinancialSource(d).accountingProfitBeforeTax,'6240.00');});
|
||||
Reference in New Issue
Block a user