Prepare scoped MRFTA and documented compensatory interest with QA recovery
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?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 IrpcMrftaDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_mrfta_and_documented_interest_recover_from_private_api_without_losing_history(): 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()]));
|
||||
$draft = json_decode(file_get_contents(base_path('outputs/irpc-mrfta/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$personal = ['person_type'=>$draft['person_type']];
|
||||
unset($draft['person_type']);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot'=>$personal, 'economic_snapshot'=>$draft], ['Idempotency-Key'=>(string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['irpcWithholdings'][$draft['context_opportunity']];
|
||||
$model = $book['mrftaResult']['model'];
|
||||
$this->assertSame('2026-10', $model['period']);
|
||||
$this->assertSame('20000', $model['principal']['numerator']);
|
||||
$this->assertSame('3000', $model['paid']['numerator']);
|
||||
$this->assertSame('500', $model['interest']['numerator']);
|
||||
$this->assertSame('17500', $model['total']['numerator']);
|
||||
$this->assertSame([], $model['pending']);
|
||||
$this->assertSame('999', $model['lines'][0]['cer']);
|
||||
$this->assertStringContainsString('fictício', $model['input']['codes'][0]['reference']);
|
||||
$this->assertNotEmpty($book['history']);
|
||||
file_put_contents(base_path('outputs/irpc-mrfta/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {newIrpcRow,emptyIrpcOrdinaryFacts,recordIrpc,saveIrpcBook,irpcBook,irpcCurrent,irpcReportRows} from '../../resources/js/lib/irpc-withholdings.ts';
|
||||
import {newMrftaInput,prepareMrfta,recordMrfta,mrftaCurrent,mrftaHtml} from '../../resources/js/lib/irpc-mrfta.ts';
|
||||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const draft=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8'));
|
||||
const row=(id='principal')=>({...newIrpcRow(id),counterparty:'QA contraparte',documentRef:'QA-'+id,base:'100000',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'QA contrato e residência',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'QA custo'}],settlement:'complete',settlementRef:'QA reconciliação',allocations:[{paymentId:'p',amount:'3000'}],ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'QA royalties não digitais'}});
|
||||
const input=()=>({...newMrftaInput(),period:'2026-10',declaration:'initial',timing:'late',nuit:'999999999',name:'QA sujeito passivo',commercialName:'QA',taxArea:'QA área fiscal',address:'QA domicílio',contact:'qa@example.invalid',completeness:'yes',completenessRef:'QA conferência do período e livros',otherRubrics:'none',otherRubricsRef:'QA ausência documentada de IRPS/tributação autónoma/outros valores',codes:[{rowId:'principal',cer:'999',reference:'QA CER fictício, apenas teste'}],interest:{...newMrftaInput().interest,mode:'assessment',period:'2026-10',amount:'500',date:'2026-11-25',reference:'QA liquidação de juros compensatórios'}});
|
||||
function book(rows=[row()]){return {...recordIrpc(draft(),{version:1,rows,payments:[{id:'p',kind:'remittance',reference:'QA entrega IRPC',date:'2026-11-15',amount:'3000',currency:'MZN'}]}),mrfta:input()};}
|
||||
|
||||
test('MRFTA fills one period with principal, documented interest, payment and balance separately',()=>{
|
||||
const b=book(),m=prepareMrfta(b,true);assert.deepEqual(m.pending,[]);assert.equal(m.principal.numerator,'20000');assert.equal(m.paid.numerator,'3000');assert.equal(m.interest.numerator,'500');assert.equal(m.total.numerator,'17500');assert.equal(m.lines[0].rate,20);assert.equal(m.lines[0].cer,'999');assert.match(m.interestReason,/transcritos/);
|
||||
});
|
||||
test('unknown MAIBOR2026 never becomes zero or MIMO; independent principal remains calculated',()=>{
|
||||
const b=book();b.mrfta.interest.mode='';const m=prepareMrfta(b,true);assert.equal(m.interest,null);assert.equal(m.total,null);assert.equal(m.principal.numerator,'20000');assert.equal(b.result.rows[0].status,'calculated');assert.match(m.pending.join(' '),/MAIBOR2026/);
|
||||
});
|
||||
test('insufficient CER or contradictory rate per rubric blocks total without losing rows',()=>{
|
||||
const b=book();b.mrfta.codes=[];let m=prepareMrfta(b,true);assert.equal(m.total,null);assert.equal(m.lines.length,1);assert.match(m.pending.join(' '),/CER/);
|
||||
const other=structuredClone(b.result.rows[0]);other.id='other';other.rate=10;other.tax={numerator:'10000',denominator:'1'};b.result.rows.push(other);b.result.inputs.push({...row('other'),allocations:[]});b.mrfta.codes=[{rowId:'principal',cer:'999',reference:'QA ref'},{rowId:'other',cer:'999',reference:'QA ref'}];m=prepareMrfta(b,true);assert.equal(m.lines.length,2);assert.match(m.pending.join(' '),/taxas ou CER distintos/);assert.equal(m.total,null);
|
||||
});
|
||||
test('other months,suffered withholdings and documented dispensas stay outside payable rows',()=>{
|
||||
const b=book();for(const[id,patch,original]of [['other-month',{period:'2026-11'},{}],['suffered',{}, {direction:'received'}],['dispensed',{treatment:'dispensed',tax:{numerator:'0',denominator:'1'}},{}]]){b.result.rows.push({...b.result.rows[0],id,...patch});b.result.inputs.push({...row(id),...original});}
|
||||
const m=prepareMrfta(b,true);assert.equal(m.lines.length,1);assert.equal(m.excluded.length,3);assert.equal(m.principal.numerator,'20000');assert.equal(m.total.numerator,'17500');
|
||||
});
|
||||
test('unsupported digital and indeterminate cases retain explicit gaps without guessed rubric',()=>{
|
||||
const b=book();b.result.inputs[0].kind='digital_services';let m=prepareMrfta(b,true);assert.equal(m.lines.length,0);assert.equal(m.total,null);assert.match(m.pending.join(' '),/digital/);b.result.inputs[0].kind='royalty';b.result.rows[0].status='indeterminate';b.result.rows[0].tax=null;m=prepareMrfta(b,true);assert.equal(m.total,null);assert.match(m.pending.join(' '),/indeterminado/);
|
||||
});
|
||||
test('timely remittance only yields zero interest with complete proof and explicit applicable deadline',()=>{
|
||||
const r=row();r.base='15000';const b=book([r]);b.mrfta.interest={...b.mrfta.interest,mode:'timely',period:'2026-10',dueDate:'2026-11-20',dueReference:'QA prazo documentado e calendário'};assert.equal(prepareMrfta(b,true).interest.numerator,'0');assert.equal(prepareMrfta(b,true).total.numerator,'0');
|
||||
b.mrfta.interest.dueDate='2026-11-10';assert.equal(prepareMrfta(b,true).interest,null);b.mrfta.interest.dueDate='2026-11-20';b.result.rows[0].settled=null;assert.equal(prepareMrfta(b,true).interest,null);
|
||||
});
|
||||
test('assessment cannot leak into another period or accept negative amount and missing reference',()=>{
|
||||
const b=book();for(const patch of [{period:'2026-11'},{amount:'-10'},{reference:''},{date:'2026-02-30'}]){const copy=structuredClone(b);Object.assign(copy.mrfta.interest,patch);assert.equal(prepareMrfta(copy,true).interest,null);assert.equal(prepareMrfta(copy,true).total,null);}
|
||||
});
|
||||
test('MRFTA save,recover,history and dossier preserve existing tax maps and freeze evidence',()=>{
|
||||
const d=draft(),prior=irpcBook(d),original=JSON.parse(d.modelo_a_snapshot);let b=book();b.history=[prior.result];b=recordMrfta(b,true);d.modelo_a_snapshot=saveIrpcBook(d,b);const recovered=JSON.parse(JSON.stringify(d)),saved=irpcBook(recovered);assert.deepEqual(saved,b);assert.equal(irpcCurrent(recovered,saved),true);assert.equal(mrftaCurrent(saved,true),true);
|
||||
for(const[k,v]of Object.entries(original))if(k!=='irpcWithholdings')assert.deepEqual(JSON.parse(recovered.modelo_a_snapshot)[k],v,k);
|
||||
const report=irpcReportRows(recovered);assert.match(JSON.stringify(report),/17500/);assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/MRFTA/);
|
||||
assert.match(mrftaHtml(saved,true),/Quadro7/);assert.match(mrftaHtml(saved,true),/QA CER fictício/);const before=structuredClone(saved.mrftaResult);saved.mrfta.interest.amount='600';assert.equal(mrftaCurrent(saved,true),false);assert.throws(()=>mrftaHtml(saved,true));const next=recordMrfta(saved,true);assert.deepEqual(next.mrftaHistory.at(-1),before);assert.equal(next.mrftaResult.model.total.numerator,'17600');
|
||||
fs.mkdirSync('outputs/irpc-mrfta',{recursive:true});fs.writeFileSync('outputs/irpc-mrfta/calculated.json',JSON.stringify(recovered));fs.writeFileSync('outputs/irpc-mrfta/preparation.html',mrftaHtml(b,true));
|
||||
});
|
||||
test('malformed persisted MRFTA rejects recovery without replacing originals',()=>{
|
||||
const b=book();b.mrfta.codes.push({...b.mrfta.codes[0]});assert.throws(()=>saveIrpcBook(draft(),b));b.mrfta.codes=[];b.mrfta.interest.amount=5;assert.throws(()=>saveIrpcBook(draft(),b));
|
||||
});
|
||||
test('export escapes identities,documents and assessment references',()=>{
|
||||
const b=book();b.mrfta.name='<script>QA</script>';b.mrfta.interest.reference='<img src=x onerror=QA>';const html=mrftaHtml(recordMrfta(b,true),true);assert.ok(!html.includes('<script>QA'));assert.ok(html.includes('<script>'));assert.ok(html.includes('<img'));
|
||||
});
|
||||
test('other tax rubrics cannot silently become zero and partial rows remain available',()=>{
|
||||
const b=book();b.mrfta.otherRubrics='pending';const m=prepareMrfta(b,true);assert.equal(m.total,null);assert.equal(m.principal.numerator,'20000');assert.match(m.pending.join(' '),/IRPS,tributação autónoma/);assert.throws(()=>mrftaHtml(recordMrfta(b,false),false));
|
||||
});
|
||||
test('same rubric rate and documented CER aggregate without losing document references',()=>{
|
||||
const other={...row('other'),allocations:[],settlement:'none'},b=book([row(),other]);b.mrfta.codes.push({rowId:'other',cer:'999',reference:'QA segundo documento de CER fictício'});const m=prepareMrfta(b,true);assert.equal(m.lines.length,1);assert.equal(m.lines[0].base.numerator,'200000');assert.equal(m.principal.numerator,'40000');assert.equal(m.total.numerator,'37500');assert.equal(m.lines[0].references.length,2);assert.equal(m.lines[0].documents.length,2);
|
||||
});
|
||||
Reference in New Issue
Block a user