Group ISPC occasional operations, excess acquisition treatment and 2026 formal preparation in QA
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?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 IspcResidualDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
public function test_new_residual_books_recover_without_rewriting_installed_habitual_or_L3_results():void
|
||||
{
|
||||
$t=DB::selectOne('select current_database() as name,inet_server_port() as port');$this->assertSame('simulator_fiscal_delta_20261021_test',$t->name);$this->assertSame(55441,(int)$t->port);config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
|
||||
$d=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-residual-preserved.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');$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');$this->assertSame($d['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
|
||||
$own=$s['ispcResiduals']['opportunity:376']['occasional']['result'];$this->assertSame('calculated',$own['status']);$this->assertSame(['numerator'=>'3000','denominator'=>'1'],$own['amounts']['tax']);$this->assertSame('singular',$own['input']['personType']);
|
||||
$a=$s['ispcResiduals']['opportunity:375']['acquisitions']['result'];$this->assertSame('calculated',$a['status']);$this->assertSame(['numerator'=>'5000','denominator'=>'1'],$a['amounts']['coveredTax']);$this->assertSame(['numerator'=>'20000','denominator'=>'1'],$a['amounts']['excessTax']);$this->assertSame(['numerator'=>'25000','denominator'=>'1'],$a['amounts']['total']);
|
||||
$this->assertSame('partial',$s['ispcResiduals']['opportunity:375']['formalities']['result']['status']);$this->assertStringContainsString('Indeterminado',$s['ispcResiduals']['opportunity:375']['formalities']['result']['details']['formPeriod']);
|
||||
$old=json_decode(file_get_contents(base_path('tests/frontend/fixtures/ispc-quarter-preserved.json')),true,flags:JSON_THROW_ON_ERROR);foreach(json_decode($old['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR)as$key=>$value){if($key==='ispcWithholdings')continue;if($key==='ispcEligibility'){foreach($value as$ctx=>$book)$this->assertSame($book,$s[$key][$ctx]);}else$this->assertSame($value,$s[$key]);}
|
||||
if(is_dir(base_path('outputs/ispc-residuals-20261025')))file_put_contents(base_path('outputs/ispc-residuals-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
|
||||
import {ispcEligibilityBook,saveIspcEligibilityBook}from '../../resources/js/lib/ispc-eligibility.ts';
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {ispcResidualBook,saveIspcResidualBook,recordIspcResidual,ispcResidualCurrent,ispcResidualReportRows,ispcFormalPreparationHtml}from '../../resources/js/lib/ispc-residuals.ts';
|
||||
import {ispcWithholdingBook,saveIspcWithholdingBook}from '../../resources/js/lib/ispc-withholding.ts';
|
||||
import {createProjectDossierReport}from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/ispc-quarter-preserved.json',import.meta.url),'utf8'));
|
||||
function own(p={}){const d=structuredClone(seed),b=ispcResidualBook(d,'occasional');b.input={...b.input,personType:'singular',currency:'MZN',origin:'projected',operationDate:'2026-06-15',quarter:'2',noHabitual:'yes',territory:'yes',sporadic:'yes',activity:'agriculture',reference:'SYNTHETIC QA colheita não reiterada',amount:'100000',allQuarterOperations:'yes',priorYearTurnover:'0',priorYearRef:'SYNTHETIC QA2025zeroexpresso',organizedRequired:'no',option:'preparation',optionRef:'',optionDate:'',settlement:'self',annual2026:'100000',annualOrigin:'forecast',annualRef:'SYNTHETIC QA2026',priorInYear:'0',...p};return{d,b};}
|
||||
function acq(p={},wpatch={}){const d=structuredClone(seed),w=ispcWithholdingBook(d);w.input={...w.input,applicable:'yes',quarter:'2',base:'200000',priorAnnualPurchases:'2400000',naturalOccasional:'yes',unregistered:'yes',related:'yes',activity:'agriculture',reference:'SYNTHETIC QA aquisição/ordem e acumulado',...wpatch};d.modelo_a_snapshot=saveIspcWithholdingBook(d,w);const b=ispcResidualBook(d,'acquisitions');b.input={...b.input,...p};return{d,b};}
|
||||
const calc=({d,b})=>recordIspcResidual(d,b,'2026-10-25T15:40:00Z');const number=x=>x?Number(x.numerator)/Number(x.denominator):null;
|
||||
test('positive ownoccasional agricultural isolated8h route;self3/4/5 andnoautomaticIRPS20',()=>{for(const[annual,n]of [['100000',3],['2000000',4],['3000000',5]]){const r=calc(own({annual2026:annual})).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(number(r.amounts.tax),1000*n);assert.match(r.source,/8\(3h,4\)/);assert.equal(r.details.IRPS.includes('Não se infere'),true);}});
|
||||
test('unknown notnonapplicable;known exclusions andactivityfamilyownscope',()=>{assert.equal(calc(own({personType:''})).result.status,'indeterminate');for(const p of [{personType:'collective'},{noHabitual:'no'},{sporadic:'no'},{territory:'no'},{activity:'other'}])assert.equal(calc(own(p)).result.status,'not_applicable');for(const p of [{territory:''},{currency:'USD'},{quarter:'3'},{reference:''},{priorYearTurnover:''},{organizedRequired:''}])assert.equal(calc(own(p)).result.status,'indeterminate');});
|
||||
test('ownminimum500withoutlosingdeclaration;zeroexplicit and4mexcessnotreused',()=>{let r=calc(own({amount:'1000'})).result;assert.equal(number(r.amounts.tax),30);assert.equal(number(r.amounts.payment),0);assert.match(r.details.declaration,/Mantida/);r=calc(own({amount:'0'})).result;assert.equal(number(r.amounts.tax),0);assert.equal(calc(own({amount:''})).result.status,'indeterminate');r=calc(own({amount:'200000',priorInYear:'3900000',annual2026:'4100000'})).result;assert.equal(number(r.amounts.ordinary),100000);assert.equal(number(r.amounts.excess),100000);assert.equal(number(r.amounts.tax),25000);});
|
||||
test('documentedliberatory5meansno secondselftax;smallwithholdingnotminimum500;divergenceunknown',()=>{const p={settlement:'withheld',origin:'effective',buyerIspc:'yes',unregistered:'yes',buyerAnnualPurchases:'200000',withheldActual:'5000',withheldRef:'SYNTHETIC QA recibo5'};let r=calc(own(p)).result;assert.equal(r.status,'settled_by_withholding',r.missing.join(';'));assert.equal(number(r.amounts.withheld),5000);assert.equal(r.amounts.payment,undefined);assert.match(r.reason,/não somar nova liquidação/);r=calc(own({...p,amount:'100',withheldActual:'5'})).result;assert.equal(r.status,'settled_by_withholding');assert.equal(number(r.amounts.tax),5);assert.equal(calc(own({...p,withheldActual:'4000'})).result.status,'indeterminate');assert.equal(calc(own({...p,buyerAnnualPurchases:'2500000.000001'})).result.status,'indeterminate');});
|
||||
test('selfeffective requiresapplicableoption;forecastcannot masqueradeasactual;nonoptionnoISPC',()=>{assert.equal(calc(own({origin:'effective'})).result.status,'indeterminate');assert.equal(calc(own({option:'not_opted'})).result.status,'not_applicable');const r=calc(own({origin:'effective',operationDate:'2026-12-31',quarter:'4',option:'opted',optionRef:'QA opção',optionDate:'2026-01-01',annualOrigin:'closed_year'})).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(calc(own({origin:'effective',option:'opted',optionRef:'QA',optionDate:'2026-07-01'})).result.status,'indeterminate');});
|
||||
test('above2.5m splits100kcovered/100kexcess;5kknownbuttotalnullwithunknownroute',()=>{const r=calc(acq()).result;assert.equal(r.status,'partial');assert.equal(number(r.amounts.covered),100000);assert.equal(number(r.amounts.coveredTax),5000);assert.equal(number(r.amounts.excess),100000);assert.equal(r.amounts.excessTax,null);assert.equal(r.amounts.total,null);});
|
||||
test('positiveIRPS57k20onidentifiedgrossact—separatefromISPCexcess20andownbuyerpayment',()=>{const p={route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'SYNTHETIC QA base ilíquida',reference:'SYNTHETIC QA8h/57k sem opção5'};const r=calc(acq(p)).result;assert.equal(r.status,'calculated',r.missing.join(';'));assert.equal(number(r.amounts.excessTax),20000);assert.equal(number(r.amounts.total),25000);assert.match(r.reason,/não são crédito/);for(const patch of [{grossExcess:''},{grossExcess:'80000'},{noIspcOnExcess:''},{isolatedAct:''},{incomeInMoz:'no'},{reference:''}]){const r=calc(acq({...p,...patch})).result;assert.equal(r.status,'partial');assert.equal(r.amounts.total,null);}});
|
||||
test('supplierISPCnotautomaticallyIRPS;crossingexactboundaryandprioroverlimit;noimplicit5onexcess',()=>{let r=calc(acq({route:'ispc_supplier',supplierOptionRef:'QA opção aplicável'})).result;assert.equal(r.status,'partial');assert.equal(r.amounts.excessTax,null);assert.match(r.reason,/art5impede/);r=calc(acq({}, {base:'100000'})).result;assert.equal(r.status,'not_applicable');assert.equal(number(r.amounts.excess),0);r=calc(acq({}, {priorAnnualPurchases:'2500001'})).result;assert.equal(number(r.amounts.covered),0);assert.equal(number(r.amounts.coveredTax),0);assert.equal(number(r.amounts.excess),200000);});
|
||||
test('newbooks preservehabitualandL3exactly;dependenciesstale withoutrewritingfrozenresults',()=>{let{d,b}=acq();const old=JSON.parse(d.modelo_a_snapshot);b=calc({d,b});d.modelo_a_snapshot=saveIspcResidualBook(d,b);for(const[k,v]of Object.entries(old))assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v);assert.equal(ispcResidualCurrent(d,b),true);const w=ispcWithholdingBook(d);w.input.base='300000';d.modelo_a_snapshot=saveIspcWithholdingBook(d,w);assert.equal(ispcResidualCurrent(d,b),false);assert.equal(number(b.result.amounts.excess),100000);const next=recordIspcResidual(d,b);assert.equal(next.history.length,1);assert.equal(number(next.result.amounts.excess),200000);assert.equal(number(next.history[0].amounts.excess),100000);});
|
||||
test('formalBGeneral mappingfromexistingcurrentquarterwithoutrecalculation;monthnotinferred;old30notused',()=>{const d=structuredClone(seed),b=ispcResidualBook(d,'formalities'),before=d.modelo_a_snapshot;const result=recordIspcResidual(d,b);assert.equal(result.result.status,'partial');assert.equal(number(result.result.amounts.tax),24000);assert.equal(number(result.result.amounts.payment),24000);assert.match(result.result.details.formPeriod,/Indeterminado/);assert.equal(result.result.details.paymentMonth,'Julho2026');assert.equal(result.input.formMonth,'');d.modelo_a_snapshot=saveIspcResidualBook(d,result);const html=ispcFormalPreparationHtml(d);assert.match(html,/24000.00/);assert.match(html,/Quadro8/);assert.match(html,/Quadro10/);assert.match(html,/não é formulário oficial/);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).ispcQuarters,JSON.parse(before).ispcQuarters);});
|
||||
test('formalinputsguardedandexternalactsdeclaredonly;signed/officialareasnotautofilled;HTMLescaped',()=>{let d=structuredClone(seed),b=ispcResidualBook(d,'formalities');b.input={...b.input,name:'QA <script>bad()</script>',nuit:'123456789',fiscalArea:'QAUGC',address:'QAendereço',formMonth:'06',formYear:'2026',formPeriodRef:'SYNTHETIC QA correspondência período',interestSituation:'none',interestRef:'SYNTHETIC QA sem juros'};b=recordIspcResidual(d,b);assert.equal(b.result.status,'calculated',b.result.missing.join(';'));d.modelo_a_snapshot=saveIspcResidualBook(d,b);const html=ispcFormalPreparationHtml(d);assert.ok(html.includes('<script>'));assert.ok(!html.includes('<script>bad'));assert.match(html,/não preencher.*assinatura/i);b.input.externalStatus='paid';assert.equal(recordIspcResidual(d,b).result.status,'partial');});
|
||||
test('documentedwithholdingownroutepreventsduplicatedformalselfpayment',()=>{let{d,b}=own({settlement:'withheld',buyerIspc:'yes',unregistered:'yes',buyerAnnualPurchases:'200000',withheldActual:'5000',withheldRef:'QA recibo5'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);const f=ispcResidualBook(d,'formalities');f.input.sourceKind='occasional';const r=recordIspcResidual(d,f).result;assert.equal(r.status,'not_applicable');assert.match(r.reason,/duplicada/);});
|
||||
test('invalidstoredmaps/enumspreserveerrors;noemptyfallback',()=>{const d=structuredClone(seed),s=JSON.parse(d.modelo_a_snapshot);s.ispcResiduals=[];assert.throws(()=>ispcResidualBook({...d,modelo_a_snapshot:JSON.stringify(s)},'occasional'));const b=ispcResidualBook(d,'occasional');b.input.activity='services12';assert.throws(()=>saveIspcResidualBook(d,b));});
|
||||
test('exports separate seller376/buyer375 and report;habitualbooksunchanged',()=>{let{d,b}=own();d.context_opportunity='opportunity:376';d.person_type='singular';d.project_title='SYNTHETIC QA — ISPC residuais';const e=ispcEligibilityBook(d);e.input.mode='occasional';e.input.personType='singular';d.modelo_a_snapshot=saveIspcEligibilityBook(d,e);b=recordIspcResidual(d,b,'2026-10-25T15:40:00Z');d.modelo_a_snapshot=saveIspcResidualBook(d,b);d.context_opportunity='opportunity:375';d.person_type='collective';const a=acq({route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'QA base ilíquida',reference:'QA acto57k'});const aw=ispcWithholdingBook(a.d);d.modelo_a_snapshot=saveIspcWithholdingBook(d,aw);d.modelo_a_snapshot=saveIspcResidualBook(d,recordIspcResidual(d,a.b,'2026-10-25T15:41:00Z'));const f=ispcResidualBook(d,'formalities');d.modelo_a_snapshot=saveIspcResidualBook(d,recordIspcResidual(d,f,'2026-10-25T15:42:00Z'));const report=createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA residuais',generatedAt:'2026-10-25T15:43:00Z'});assert.match(JSON.stringify(report),/25000.00/);assert.match(JSON.stringify(report),/24000.00/);assert.match(JSON.stringify(ispcResidualReportRows(d)),/IRPS57/);d.context_opportunity='opportunity:376';assert.equal(ispcResidualBook(d,'occasional').result.status,'calculated');assert.equal(number(ispcResidualBook(d,'occasional').result.amounts.tax),3000);d.context_opportunity='opportunity:375';if(process.env.ISPC_RESIDUAL_OUTPUT)fs.writeFileSync(process.env.ISPC_RESIDUAL_OUTPUT,JSON.stringify(d,null,2));});
|
||||
|
||||
test('new uniqueformsource preserves deliberatechoice;documentedoptionwithannualforecast staysscenario',()=>{let{d,b}=own({origin:'effective',option:'opted',optionRef:'QA opção',optionDate:'2026-01-01'});b=recordIspcResidual(d,b);assert.equal(b.result.status,'calculated');assert.equal(b.result.details.projected,'yes');assert.match(b.result.reason,/Cenário declarado/);const e=ispcEligibilityBook(d);e.input.mode='occasional';d.modelo_a_snapshot=saveIspcEligibilityBook(d,e);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');assert.equal(f.input.sourceKind,'occasional');f.input.sourceKind='quarter';d.modelo_a_snapshot=saveIspcResidualBook(d,f);assert.equal(ispcResidualBook(d,'formalities').input.sourceKind,'quarter');});
|
||||
|
||||
test('acquisitionformalpreparation distinguishesISPC5andIRPS20andkeepsspecificexternalmodelpending',()=>{let{d,b}=acq({route:'irps_isolated',noIspcOnExcess:'yes',isolatedAct:'yes',incomeInMoz:'yes',grossExcess:'100000',grossRef:'QA base ilíquida',reference:'QA57k'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');f.input.sourceKind='acquisitions';f=recordIspcResidual(d,f);assert.equal(number(f.result.amounts.tax),25000);assert.ok(f.result.missing.some(s=>s.includes('Modelo/código')));assert.equal(f.result.status,'partial');d.modelo_a_snapshot=saveIspcResidualBook(d,f);const html=ispcFormalPreparationHtml(d);assert.match(html,/ISPC—retenção12/);assert.match(html,/IRPS—acto isolado57/);assert.match(html,/5000.00/);assert.match(html,/20000.00/);assert.match(html,/Por corresponder/);});
|
||||
|
||||
test('knownownaccess exclusions are concrete notrequestsforrevalidation;unknownstillindeterminate',()=>{for(const patch of [{organizedRequired:'yes'},{priorYearTurnover:'4000000.000001'}]){const r=calc(own(patch)).result;assert.equal(r.status,'not_applicable');assert.equal(r.amounts.payment,undefined);assert.match(r.reason,/fora/i);}assert.equal(calc(own({organizedRequired:''})).result.status,'indeterminate');});
|
||||
|
||||
test('below500paymentdispensa iscalculatedbutformalrepresentationnotinventedaszerocalculatedtax',()=>{let{d,b}=own({amount:'1000'});b=recordIspcResidual(d,b);d.modelo_a_snapshot=saveIspcResidualBook(d,b);let f=ispcResidualBook(d,'formalities');f.input={...f.input,sourceKind:'occasional',name:'QA vendedor',nuit:'123456789',fiscalArea:'QAUGC',address:'QA endereço',formMonth:'06',formYear:'2026',formPeriodRef:'QA correspondência',interestSituation:'none',interestRef:'QA sem juros'};f=recordIspcResidual(d,f);assert.equal(number(f.result.amounts.tax),30);assert.equal(number(f.result.amounts.payment),0);assert.equal(f.result.status,'partial');assert.ok(f.result.missing.some(s=>s.includes('dispensa')));d.modelo_a_snapshot=saveIspcResidualBook(d,f);const html=ispcFormalPreparationHtml(d);assert.match(html,/Imposto calculado:30.00;prestação legal:0.00/);});
|
||||
Reference in New Issue
Block a user