Implement P087 binding lease and P127 business transfers in QA
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { SimulationPropertyTransfers } from './simulation-property-transfers';
|
||||
import { SimulationGoodsPresumptions } from './simulation-goods-presumptions';
|
||||
import { InvestmentContextAssessment } from './investment-context-assessment';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -122,6 +123,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationReducedBases draft={draft} onChange={onChange} />
|
||||
<SimulationOwnName draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
|
||||
<SimulationPropertyTransfers draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
|
||||
<SimulationSecondhand draft={draft} onChange={onChange} />
|
||||
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {useState} from 'react';
|
||||
import {propertyTransferBook,propertyTransferCurrent,propertyTransferReportRows,propertyTransferSource,propertyTransferTreatments,newPropertyTransfer,transferConditions,recordPropertyTransfers,savePropertyTransferBook,switchPropertyTreatment,type PropertyTransferBook,type PropertyTransferRow} from '@/lib/iva-property-transfers';
|
||||
export function SimulationPropertyTransfers({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:PropertyTransferBook;
|
||||
try{book=propertyTransferBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:PropertyTransferBook)=>{try{onChange('modelo_a_snapshot',savePropertyTransferBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
const change=(id:string,patch:Partial<PropertyTransferRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)});
|
||||
return <details className="rounded border p-3"><summary>Locação vinculativa e cessão de estabelecimento</summary>
|
||||
<p>Locação:entrega material e cláusula vinculante para ambas as partes;mera opção de compra não basta. Cessão de estabelecimento:identifique conjunto autónomo e enquadramento do adquirente. Não exigibilidade3(5) não é isenção genérica.</p>
|
||||
<p>Oportunidade: {String(draft.context_opportunity??'projecto')}. Reutilize dados/documentos disponíveis,identificando apenas o âmbito aplicável.</p>
|
||||
<p className="text-sm">{propertyTransferSource}</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" onClick={()=>save({...book,rows:[...book.rows,newPropertyTransfer(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar transmissão de propriedade</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Transmissão por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Percurso de transmissão<select value={row.treatment} onChange={e=>change(row.id,switchPropertyTreatment(row,e.target.value))}><option value="">Por indicar</option>{propertyTransferTreatments.map(t=><option key={t.id} value={t.id}>{t.label}</option>)}</select></label>
|
||||
<label>Natureza dos dados<select value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as PropertyTransferRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Transmissão identificada'],['operationRef','Referência da operação / pressuposto'],['operationDate','Data da operação / prevista'],['period','Mês da operação'],['documentRef','Documento / referência da previsão'],['documentDate','Data do documento / prevista'],['goodsRef','Bens e elementos identificados'],['scope','Âmbito:conjunto,operação e período'],['contractRef','Contrato/cessão e referência'],['evidence','Fundamento factual / pressupostos']] as const).map(([field,label])=><label key={field}>{label}<input className="block rounded border p-2" type={['operationDate','documentDate'].includes(field)?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
{row.treatment==='binding_lease'?<>
|
||||
{([['propertyClauseRef','Cláusula vinculante de transferência para ambas as partes'],['consideration','Contraprestação obtida/a obter,sem próprio IVA'],['considerationRef','Referência e composição da contraprestação'],['currency','Moeda da contraprestação']] as const).map(([field,label])=><label key={field}>{label}<input value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
<label>Composição15(1) corresponde ao âmbito e exclui próprioIVA,sem dupla subtracção<select value={row.compositionConfirmed} onChange={e=>change(row.id,{compositionConfirmed:e.target.value as PropertyTransferRow['compositionConfirmed']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
|
||||
<p>Não somar rendas,preço de aquisição e valor residual automaticamente. Base por esclarecer mantém classificação separada.</p>
|
||||
</>:row.treatment&&<>{([['acquirerRef','Adquirente e enquadramento2(1)a'],['autonomousRef','Conjunto susceptível de constituir ramo independente'],['activitiesRef','Operações do adquirente que conferem direito à dedução']] as const).map(([field,label])=><label key={field}>{label}<input value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}<p>Factos do enquadramento,não nova dedução concedida. Uma venda isolada de activos não demonstra o conjunto autónomo.</p></>}
|
||||
</div>
|
||||
<fieldset><legend>Condições próprias da transmissão</legend>{transferConditions(row.treatment).map(([key,label])=><label className="block" key={key}>{label}<select value={row.conditions[key]??''} onChange={e=>change(row.id,{conditions:{...row.conditions,[key]:e.target.value as ''|'yes'|'no'}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>
|
||||
<p>Trocar percurso conserva dados ocultos e referências;reveja as confirmações aplicáveis.</p>
|
||||
</details>)}
|
||||
{!!book.rows.length&&<button type="button" onClick={()=>{try{save(recordPropertyTransfers(draft,book));}catch(cause){setError(String(cause));}}}>Classificar transmissões e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{propertyTransferCurrent(draft,book)?'Resultados correspondentes às entradas guardadas.':'Entradas alteradas;resultado histórico conservado.Reveja e confira novamente.'} {book.history?.length??0} versões anteriores.Sem IVA,exigibilidade ou nova dedução calculados.</p><dl>{propertyTransferReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
|
||||
</details>;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||||
export const propertyTransferVersion='property-transfers/1';
|
||||
export const propertyTransferSource='CIVA Lei13/2016:3(3)a,PDF6/p1426(264),entrega material em locação com cláusula vinculante de transferência da propriedade;3(5),PDF7/p1426(265),cessão onerosa/gratuita de estabelecimento,totalidade ou parte de património que constitua ramo independente,adquirente2(1)a que pratique operações com direito à dedução;15(1),PDF13/p1426(271),contraprestação obtida/a obter.Lei22/2022PDF3 mantém15(1);Lei10/2025PDF12 mantém3(3)a/3(5),não altera15.P087não usa15(2)a,referente a consignação3(3)d.';
|
||||
export const propertyTransferTreatments=[{id:'binding_lease',branch:'P087',label:'Locação com transferência vinculativa da propriedade'},{id:'business_onerous',branch:'P127',label:'Cessão onerosa de estabelecimento/património autónomo'},{id:'business_free',branch:'P127',label:'Cessão gratuita de estabelecimento/património autónomo'}] as const;
|
||||
export const propertyTransferConditions={binding_lease:[['materialDelivery','Entrega material dos bens em execução do contrato de locação'],['bindingProperty','Contrato contém cláusula vinculante para ambas as partes de transferência da propriedade']],business_onerous:[['onerous','Cessão a título oneroso identificada'],['autonomousBusiness','Estabelecimento,totalidade ou parte de património susceptível de constituir ramo de actividade independente'],['acquirer2a','Adquirente é ou passa,pela aquisição,a sujeito passivo2(1)a'],['deductionActivities','Adquirente pratica operações que concedem direito à dedução']],business_free:[['gratuitous','Cessão a título gratuito identificada'],['autonomousBusiness','Estabelecimento,totalidade ou parte de património susceptível de constituir ramo de actividade independente'],['acquirer2a','Adquirente é ou passa,pela aquisição,a sujeito passivo2(1)a'],['deductionActivities','Adquirente pratica operações que concedem direito à dedução']]} as const;
|
||||
export function transferConditions(t:string):ReadonlyArray<readonly[string,string]>{return propertyTransferConditions[t as keyof typeof propertyTransferConditions]??[];}
|
||||
type Choice=''|'yes'|'no';
|
||||
export type PropertyTransferRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationDate:string;operationRef:string;documentRef:string;documentDate:string;goodsRef:string;scope:string;evidence:string;contractRef:string;propertyClauseRef:string;acquirerRef:string;autonomousRef:string;activitiesRef:string;consideration:string;considerationRef:string;currency:string;compositionConfirmed:Choice;conditions:Partial<Record<string,Choice>>};
|
||||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';classification:'goods_transmission'|'tax_not_exigible_3_5'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
|
||||
export type PropertyTransferCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:PropertyTransferRow[];rows:LineResult[]};
|
||||
export type PropertyTransferBook={rows:PropertyTransferRow[];result?:PropertyTransferCalculation;history?:PropertyTransferCalculation[]};
|
||||
type Store=ReturnType<typeof readAcquisitionStore>&{propertyTransfers?:Record<string,PropertyTransferBook>};
|
||||
export function newPropertyTransfer(id:string,currency:string):PropertyTransferRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsRef:'',scope:'',evidence:'',contractRef:'',propertyClauseRef:'',acquirerRef:'',autonomousRef:'',activitiesRef:'',consideration:'',considerationRef:'',currency,compositionConfirmed:'',conditions:{}};}
|
||||
export function switchPropertyTreatment(row:PropertyTransferRow,treatment:string):PropertyTransferRow{return{...row,treatment,conditions:{},compositionConfirmed:''};}
|
||||
function readStore(draft:Record<string,unknown>):Store{
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.propertyTransfers!==undefined&&(!store.propertyTransfers||typeof store.propertyTransfers!=='object'||Array.isArray(store.propertyTransfers)))throw new Error('Transmissões de propriedade não recuperáveis;originais preservados.');
|
||||
const fields=Object.keys(newPropertyTransfer('','')).filter(k=>k!=='conditions');
|
||||
for(const book of Object.values(store.propertyTransfers??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(k=>typeof row[k as keyof PropertyTransferRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas não recuperáveis;originais conservados,sem lista vazia por defeito.');
|
||||
return store;
|
||||
}
|
||||
export function propertyTransferBook(draft:Record<string,unknown>):PropertyTransferBook{return readStore(draft).propertyTransfers?.[acquisitionScope(draft)]??{rows:[]};}
|
||||
export function savePropertyTransferBook(draft:Record<string,unknown>,book:PropertyTransferBook):string{const store=readStore(draft),encoded=JSON.stringify({...store,propertyTransfers:{...store.propertyTransfers,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||||
function validDate(s:string):boolean{const n=Date.parse(s+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(s)&&Number.isFinite(n)&&new Date(n).toISOString().slice(0,10)===s;}
|
||||
function decimal(raw:string):bigint|null{if(!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[w,f='']=raw.trim().split('.');return BigInt(w)*1000000n+BigInt(f.padEnd(6,'0'));}
|
||||
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
|
||||
export function evaluatePropertyTransfer(row:PropertyTransferRow,currency:unknown):LineResult{
|
||||
const result:LineResult={id:row.id,status:'indeterminate',classification:'indeterminate',baseAmount:null,vat:null,rate:null,reason:''};
|
||||
const stop=(reason:string,status:LineResult['status']='indeterminate',classification:LineResult['classification']=status==='not_applicable'?'not_applicable':result.classification)=>({...result,status,classification,reason});
|
||||
if(!propertyTransferTreatments.some(t=>t.id===row.treatment))return stop('Identifique locação vinculativa ou cessão onerosa/gratuita de estabelecimento.');
|
||||
if(!validDate(row.operationDate)||!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||row.period!==row.operationDate.slice(0,7)||!['projected','effective'].includes(row.origin))return stop('Identifique data/mês2026 e dados efectivos ou pressupostos expressos.');
|
||||
if(![row.description,row.operationRef,row.documentRef,row.goodsRef,row.scope,row.evidence,row.contractRef].every(v=>!!v.trim())||!validDate(row.documentDate))return stop('Identifique objecto,operação,contrato/documento/data e âmbito factual ou pressupostos;reutilize referências,sem certificado/aprovador novo.');
|
||||
const conditions=transferConditions(row.treatment);
|
||||
if(conditions.some(([k])=>row.conditions[k]==='no'))return stop(row.treatment==='binding_lease'?'Não estão reunidos os factos de3(3)a;não converter automaticamente opção de compra em transferência vinculativa.':'Não estão reunidos os requisitos de não exigibilidade3(5);classificar a transmissão pelos restantes percursos aplicáveis,sem isenção/IVA zero.','not_applicable');
|
||||
if(conditions.some(([k])=>row.conditions[k]!=='yes'))return stop('Factos próprios por esclarecer;desconhecido não é não aplicável.');
|
||||
if(row.treatment==='binding_lease'){
|
||||
if(!row.propertyClauseRef.trim())return stop('Identifique cláusula vinculante para ambas as partes;mera opção de compra não a demonstra.');
|
||||
result.classification='goods_transmission';
|
||||
if(currency!=='MZN'||row.currency!==currency)return stop('Transmissão3(3)a identificada;base em moeda fora do recorteMZN ou divergente,sem conversão silenciosa.');
|
||||
const value=decimal(row.consideration);
|
||||
if(value===null||!row.considerationRef.trim()||row.compositionConfirmed!=='yes')return stop('Transmissão3(3)a identificada;base15(1) por esclarecer. Identifique contraprestação obtida/a obter e composição correspondente,sem próprioIVA e sem voltar a subtrair componentes já excluídas;não somar rendas/valor residual automaticamente.');
|
||||
return{...result,status:'determined',baseAmount:money(value),reason:`P087:entrega material em locação com cláusula vinculativa3(3)a;base15(1)=${money(value)}MZN,contraprestação obtida/a obter segundo composição e âmbito identificados.Não é consignação3(3)d/15(2)a,nem mera opção de compra.Não calcula IVA,exigibilidade,nova dedução/campo11 ou totaisModeloA.`};
|
||||
}
|
||||
if(![row.acquirerRef,row.autonomousRef,row.activitiesRef].every(v=>!!v.trim()))return stop('Identifique adquirente2(1)a,conjunto susceptível de ramo independente e operações que conferem direito à dedução;simples venda de activos não basta.');
|
||||
return stop('P127:cessão '+(row.treatment==='business_onerous'?'onerosa':'gratuita')+'3(5) identificada como transmissão de bens cujo imposto não é devido nem exigível,segundo conjunto/adquirente/operações e âmbito demonstrados.Não é isenção genérica,nem taxa/base zero.Não concede dedução ao adquirente;qualidade e operações são factos do enquadramento.Sem condição adicional de continuidade,certificado ou aprovador inventados.','determined','tax_not_exigible_3_5');
|
||||
}
|
||||
function key(draft:Record<string,unknown>,book:PropertyTransferBook):string{return JSON.stringify({version:propertyTransferVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||||
export function propertyTransferCurrent(draft:Record<string,unknown>,book:PropertyTransferBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||||
export function recordPropertyTransfers(draft:Record<string,unknown>,book:PropertyTransferBook):PropertyTransferBook{
|
||||
if(!book.rows.length||new Set(book.rows.map(r=>r.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
|
||||
const result:PropertyTransferCalculation={version:propertyTransferVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:propertyTransferSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluatePropertyTransfer(row,draft.currency))};
|
||||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};savePropertyTransferBook(draft,next);return next;
|
||||
}
|
||||
export function propertyTransferReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||||
try{const book=propertyTransferBook(draft),r=book.result;if(!r)return[];return r.inputs.map((row,i)=>['IVA mensal',`${propertyTransferTreatments.find(t=>t.id===row.treatment)?.branch??'Transmissão'} — ${row.period} — ${row.id}`,`${row.description};${r.rows[i].reason};base:${r.rows[i].baseAmount??'Indeterminada'} ${row.currency};IVA:Indeterminado`,`${r.opportunity};${r.location};${propertyTransferCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};${row.origin};operação ${row.operationRef},${row.operationDate};documento ${row.documentRef},${row.documentDate};bens ${row.goodsRef};âmbito ${row.scope};contrato ${row.contractRef};${row.treatment==='binding_lease'?`cláusula ${row.propertyClauseRef};contraprestação ${row.consideration},${row.considerationRef};composição ${row.compositionConfirmed}`:`adquirente ${row.acquirerRef};conjunto autónomo ${row.autonomousRef};operações adquirente ${row.activitiesRef}`};condições${JSON.stringify(row.conditions)};fundamento ${row.evidence};sem nova dedução/campo11/IVA/margem automática.`]);}
|
||||
catch{return[['IVA mensal','Transmissões de propriedade','Dados não recuperáveis','Originais conservados;sem classificação por defeito.']];}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { propertyTransferReportRows } from './iva-property-transfers.ts';
|
||||
import { goodsPresumptionReportRows } from './iva-goods-presumptions.ts';
|
||||
import { displayValue } from './display-value.ts';
|
||||
import { investmentFieldOrigin } from './context-inheritance.ts';
|
||||
@@ -654,6 +655,7 @@ export function buildProjectDossierRows(
|
||||
rows.push(...goodsAllocationReportRows(draft));
|
||||
rows.push(...secondhandReportRows(draft));
|
||||
rows.push(...goodsPresumptionReportRows(draft));
|
||||
rows.push(...propertyTransferReportRows(draft));
|
||||
rows.push(...offerPreparationReportRows(draft));
|
||||
const operationRows = salesReportRows(draft);
|
||||
rows.push(...operationRows);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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 IvaPropertyTransferDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_propertytransfers_recover_privately_preserving_all_delivered_books(): 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('tests/frontend/fixtures/iva-property-transfers.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], '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']);
|
||||
$this->assertSame('opportunity:356', $recovered['context_opportunity']);
|
||||
$this->assertSame('Manica', $recovered['selection_labels']['province_id']);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['propertyTransfers']['opportunity:356'];
|
||||
$this->assertCount(3, $book['rows']);
|
||||
$this->assertSame(['binding_lease','business_onerous','business_free'],array_column($book['rows'],'treatment'));
|
||||
$this->assertSame(['goods_transmission','tax_not_exigible_3_5','tax_not_exigible_3_5'],array_column($book['result']['rows'],'classification'));
|
||||
$this->assertSame(['1250.123456',null,null],array_column($book['result']['rows'],'baseAmount'));
|
||||
foreach ($book['result']['rows'] as $row) {
|
||||
$this->assertNull($row['vat']);$this->assertNull($row['rate']);
|
||||
}
|
||||
$this->assertSame($book['rows'], $book['result']['inputs']);
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertSame('1250.123456',$book['history'][0]['rows'][0]['baseAmount']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-property-transfers-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$this->assertSame($prior, array_diff_key($store, ['propertyTransfers' => true]));
|
||||
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
|
||||
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
|
||||
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
|
||||
file_put_contents(base_path('outputs/iva-l1-property-transfers-20261025/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {newPropertyTransfer,transferConditions,propertyTransferTreatments,evaluatePropertyTransfer,recordPropertyTransfers,savePropertyTransferBook,propertyTransferBook,propertyTransferCurrent,propertyTransferReportRows,switchPropertyTreatment} from '../../resources/js/lib/iva-property-transfers.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-property-transfers-preserved.json','utf8'));
|
||||
const draft={...frozen,currency:'MZN',context_opportunity:'opportunity:356',project_title:'SYNTHETIC QA — Transmissões de propriedade / Manica'};
|
||||
function row(t){return{...newPropertyTransfer(t,'MZN'),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',origin:'projected',operationDate:'2026-12-12',operationRef:'SYNTHETIC QA operação',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',goodsRef:'SYNTHETIC QA conjunto identificado',scope:'SYNTHETIC QA bens/operação/período',evidence:'SYNTHETIC QA factos/pressupostos expressos',contractRef:'SYNTHETIC QA contrato/cessão',propertyClauseRef:'SYNTHETIC QA cláusula vinculante ambas as partes',acquirerRef:'SYNTHETIC QA adquirente2(1)a',autonomousRef:'SYNTHETIC QA ramo independente',activitiesRef:'SYNTHETIC QA operações com direito à dedução',consideration:'1250.123456',considerationRef:'SYNTHETIC QA composição obtida/a obter,sem próprioIVA',compositionConfirmed:'yes',conditions:Object.fromEntries(transferConditions(t).map(([k])=>[k,'yes']))};}
|
||||
for(const rule of propertyTransferTreatments)test(rule.branch+' '+rule.id+' own facts and references',()=>{
|
||||
const r=row(rule.id),v=evaluatePropertyTransfer(r,'MZN');assert.equal(v.status,'determined');assert.equal(v.classification,rule.id==='binding_lease'?'goods_transmission':'tax_not_exigible_3_5');assert.equal(v.baseAmount,rule.id==='binding_lease'?'1250.123456':null);assert.equal(v.vat,null);assert.equal(v.rate,null);
|
||||
for(const[k]of transferConditions(rule.id)){const missing=structuredClone(r);delete missing.conditions[k];assert.equal(evaluatePropertyTransfer(missing,'MZN').status,'indeterminate');missing.conditions[k]='no';assert.equal(evaluatePropertyTransfer(missing,'MZN').status,'not_applicable');}
|
||||
for(const field of ['description','operationRef','documentRef','goodsRef','scope','evidence','contractRef',...(rule.id==='binding_lease'?['propertyClauseRef']:['acquirerRef','autonomousRef','activitiesRef'])])assert.equal(evaluatePropertyTransfer({...r,[field]:''},'MZN').status,'indeterminate',field);
|
||||
for(const patch of [{operationDate:'2026-02-30'},{documentDate:'2026-02-30'},{period:'2025-12'},{period:'2026-11'}])assert.equal(evaluatePropertyTransfer({...r,...patch},'MZN').status,'indeterminate');
|
||||
});
|
||||
test('P087 classification separatefrom missingbase,exact consideration and no double subtraction',()=>{
|
||||
const r=row('binding_lease');for(const patch of [{consideration:''},{consideration:'-1'},{consideration:'1e3'},{consideration:'1.1234567'},{compositionConfirmed:''},{compositionConfirmed:'no'},{considerationRef:''},{currency:'USD'}]){const v=evaluatePropertyTransfer({...r,...patch},'MZN');assert.equal(v.status,'indeterminate');assert.equal(v.classification,'goods_transmission');assert.equal(v.baseAmount,null);}
|
||||
assert.equal(evaluatePropertyTransfer({...r,consideration:'0'},'MZN').baseAmount,'0.00');assert.equal(evaluatePropertyTransfer({...r,consideration:'900719925474099.123456'},'MZN').baseAmount,'900719925474099.123456');assert.match(evaluatePropertyTransfer(r,'MZN').reason,/15\(1\)/);
|
||||
});
|
||||
test('P127 no exigibility is neitherzero nor genericexemption;switchretainsdata',()=>{
|
||||
for(const t of ['business_onerous','business_free']){const v=evaluatePropertyTransfer({...row(t),consideration:'',currency:'USD',compositionConfirmed:''},'MZN');assert.equal(v.classification,'tax_not_exigible_3_5');assert.equal(v.baseAmount,null);assert.match(v.reason,/não é isenção genérica/i);}
|
||||
const r=row('binding_lease'),sw=switchPropertyTreatment(r,'business_free');assert.equal(sw.consideration,r.consideration);assert.equal(sw.propertyClauseRef,r.propertyClauseRef);assert.deepEqual(sw.conditions,{});assert.equal(sw.compositionConfirmed,'');
|
||||
});
|
||||
test('draft/report/history and allpriorbooks preserved',()=>{
|
||||
const rows=propertyTransferTreatments.map(t=>row(t.id)),first=recordPropertyTransfers(draft,{rows});const next={...draft,modelo_a_snapshot:savePropertyTransferBook(draft,first)};const second=recordPropertyTransfers(next,first);next.modelo_a_snapshot=savePropertyTransferBook(next,second);
|
||||
assert.equal(second.history.length,1);assert.deepEqual(second.history[0].inputs,rows);assert.equal(propertyTransferCurrent(next,second),true);assert.equal(propertyTransferCurrent({...next,currency:'USD'},second),false);assert.equal(propertyTransferReportRows(next).length,3);assert.deepEqual(propertyTransferBook({...next,context_opportunity:'opportunity:357'}),{rows:[]});
|
||||
const store=JSON.parse(next.modelo_a_snapshot);delete store.propertyTransfers;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordPropertyTransfers(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>propertyTransferBook({...next,modelo_a_snapshot:JSON.stringify({...store,propertyTransfers:[]})}));
|
||||
if(process.env.IVA_PROPERTY_TRANSFERS_FIXTURE)fs.writeFileSync(process.env.IVA_PROPERTY_TRANSFERS_FIXTURE,JSON.stringify(next,null,2)+'\n');
|
||||
});
|
||||
Reference in New Issue
Block a user