feat(iva): prepare own-name goods and service chains together in QA
This commit is contained in:
@@ -19,6 +19,7 @@ import { SimulationGoodsLocation } from './simulation-goods-location';
|
||||
import { SimulationServiceLocation } from './simulation-service-location';
|
||||
import { SimulationListedServices } from './simulation-listed-services';
|
||||
import { SimulationReducedBases } from './simulation-reduced-bases';
|
||||
import { SimulationOwnName } from './simulation-own-name';
|
||||
import { SimulationOfferPreparations } from './simulation-offer-preparations';
|
||||
import { SimulationOperations } from './simulation-operations';
|
||||
|
||||
@@ -116,6 +117,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationServiceLocation draft={draft} onChange={onChange} />
|
||||
<SimulationListedServices draft={draft} onChange={onChange} />
|
||||
<SimulationReducedBases draft={draft} onChange={onChange} />
|
||||
<SimulationOwnName draft={draft} onChange={onChange} />
|
||||
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
||||
<SimulationModeloA draft={draft} />
|
||||
</section>;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useState} from 'react';
|
||||
import {newOwnName,switchOwnNameTreatment,ownNameBook,ownNameConditions,ownNameSpecificConditions,ownNameCurrent,ownNameReportRows,ownNameSource,ownNameTreatments,recordOwnNames,saveOwnNameBook,type OwnNameBook,type OwnNameRow} from '@/lib/iva-own-name';
|
||||
|
||||
export function SimulationOwnName({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:OwnNameBook;
|
||||
try{book=ownNameBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:OwnNameBook)=>{try{onChange('modelo_a_snapshot',saveOwnNameBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
const change=(id:string,patch:Partial<OwnNameRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
|
||||
return<details className="space-y-3 rounded border p-3">
|
||||
<summary>Comissão e mandato — circuitos emnome próprio</summary>
|
||||
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Venda/prestação:mandante ou comitente → agente → terceiro. Aquisição:terceiro → agente → mandante ou comitente. Nos serviços,o mandatário emnome próprio é adquirente eprestador sucessivamente;nos bens,os papéis dependem da comissão de compra ou venda.</p>
|
||||
<p>Conserve as contraprestações originais das duas relações separadas. Este percurso não as soma,não calcula imposto nem dedução e não cria uma comissão autónoma. A base de comissões de bensP091já entregue mantém o seu cálculo e histórico próprios.</p>
|
||||
<p>{ownNameSource}</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newOwnName(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar circuito emnome próprio</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Circuito por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Natureza e sentido do circuito<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,switchOwnNameTreatment(row,e.target.value))}><option value="">Por indicar</option>{ownNameTreatments.map(t=><option key={t.id} value={t.id}>{t.label}</option>)}</select></label>
|
||||
<label>Natureza dos dados<select className="block rounded border p-2" value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as OwnNameRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Circuito identificado'],['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'],['principalRef','Mandante ou comitente e referência'],['agentRef','Comissário ou mandatário e referência'],['thirdPartyRef','Terceiro e referência'],['contractRef','Contrato de comissão ou mandato'],['subjectRef','Bens ou serviço e âmbito do objecto'],['currency','Moeda original das contraprestações'],['firstAmount','Contraprestação original da relação1'],['firstLegRef','Referência da relação1'],['secondAmount','Contraprestação original da relação2'],['secondLegRef','Referência da relação2'],['scope','Âmbito:partes,objecto,operação eperíodo'],['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>)}
|
||||
{ownNameTreatments.find(t=>t.id===row.treatment)?.kind==='goods'&&<label>Referência de baseP091já existente,se aplicável<input className="block rounded border p-2" value={row.existingBaseRef} onChange={e=>change(row.id,{existingBaseRef:e.target.value})}/></label>}
|
||||
</div>
|
||||
<p>As relações seguem a ordem indicada no sentido escolhido;contraprestação desconhecida não é zero. A referênciaP091é reutilizável e não constitui nova exigência para identificar o circuito. Mudar sentido/natureza conserva partes,contrato,valores e referências,mas exige rever os factos próprios.</p>
|
||||
<fieldset><legend>Factos próprios do circuito</legend>{[...ownNameConditions,...ownNameSpecificConditions(row.treatment)].map(([key,label])=><label className="block" key={key}>{label}<select className="block rounded border p-2" 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>
|
||||
</details>)}
|
||||
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordOwnNames(draft,book));}catch(cause){setError(String(cause));}}}>Identificar circuitos e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{ownNameCurrent(draft,book)?'Circuitos correspondentes às entradas guardadas.':'Entradas alteradas;circuitos históricos conservados. Reveja e identifique novamente.'} {book.history?.length??0} versões anteriores. Duas relações separadas,sem soma de bases,IVA ou dedução/campo11.</p><dl>{ownNameReportRows(draft).map(([,label,value,origin],index)=><div key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
|
||||
</details>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||||
|
||||
export const ownNameVersion='own-name-chains/1';
|
||||
export const ownNameSource='CIVA3(3)c,Lei13/2016PDF6/p1426(264):transferências em execução de contrato de comissão,incluindo consignação;comissário considerado comprador na comissão de venda,comitente na de compra. Art4(3),PDF7/p1426(265):mandatário emnome próprio é adquirente eprestador do serviço sucessivamente. Lei10/2025PDF12mantém3(3)c e não altera4. Base de bens15(2)e/P091já entregue,não recalculada ou somada neste percurso. Recorte2026:circuito/contraprestações originais separados,sem localização,isenção,taxa,IVA,dedução/campo11 ou totaisModeloA presumidos.';
|
||||
export const ownNameTreatments=[{id:'goods_sale_commission',branch:'P122',label:'P122 — Comissão de venda de bens',kind:'goods',direction:'sale'},{id:'goods_purchase_commission',branch:'P122',label:'P122 — Comissão de compra de bens',kind:'goods',direction:'purchase'},{id:'service_sale_mandate',branch:'P128',label:'P128 — Mandato de venda/prestação de serviços',kind:'services',direction:'sale'},{id:'service_purchase_mandate',branch:'P128',label:'P128 — Mandato de aquisição de serviços',kind:'services',direction:'purchase'}] as const;
|
||||
export const ownNameConditions=[['ownName','O comissário/mandatário actua emnome próprio,não apenas emnome do mandante'],['forAnother','Actua por conta de outrem identificado ecom relação documentada ou pressuposto expresso'],['factsCorrespond','Objecto,partes,referências eperíodo correspondem à operação identificada']] as const;
|
||||
const goodsConditions=[['goodsNature','São transferências de bens,não serviço autónomo de intermediação'],['commissionContract','As transferências executam contrato de comissão identificado,conforme3(3)c,incluindo consignação quando aplicável'],['directionConfirmed','Papéis e duas relações correspondem ao sentido de venda ou compra escolhido']] as const;
|
||||
const serviceConditions=[['serviceNature','Está identificada uma prestação de serviços,não transferência de bens'],['mandateContract','A prestação é efectuada por intervenção de mandatário emnome próprio,com mandato identificado4(3)'],['successiveService','Mandatário é adquirente eprestador do serviço sucessivamente;referências distinguem as duas prestações'],['directionConfirmed','Papéis e duas relações correspondem ao sentido de venda/prestação ou aquisição escolhido']] as const;
|
||||
export function ownNameSpecificConditions(treatment:string):ReadonlyArray<readonly[string,string]>{const rule=ownNameTreatments.find(t=>t.id===treatment);return rule?.kind==='goods'?goodsConditions:rule?.kind==='services'?serviceConditions:[];}
|
||||
type Choice=''|'yes'|'no';type Condition=typeof ownNameConditions[number][0];
|
||||
export type OwnNameRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationDate:string;operationRef:string;documentRef:string;documentDate:string;principalRef:string;agentRef:string;thirdPartyRef:string;contractRef:string;subjectRef:string;currency:string;firstAmount:string;secondAmount:string;firstLegRef:string;secondLegRef:string;existingBaseRef:string;scope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
|
||||
type Leg={supplier:string;acquirer:string;consideration:string;currency:string;reference:string};
|
||||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';legs:Leg[];baseAmount:null;vat:null;rate:null;reason:string};
|
||||
export type OwnNameCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:OwnNameRow[];rows:LineResult[]};
|
||||
export type OwnNameBook={rows:OwnNameRow[];result?:OwnNameCalculation;history?:OwnNameCalculation[]};
|
||||
type Store=ReturnType<typeof readAcquisitionStore>&{ownNames?:Record<string,OwnNameBook>};
|
||||
export function newOwnName(id:string,currency=''):OwnNameRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',principalRef:'',agentRef:'',thirdPartyRef:'',contractRef:'',subjectRef:'',currency,firstAmount:'',secondAmount:'',firstLegRef:'',secondLegRef:'',existingBaseRef:'',scope:'',evidence:'',conditions:{}};}
|
||||
export function switchOwnNameTreatment(row:OwnNameRow,treatment:string):OwnNameRow{return{...row,treatment,conditions:Object.fromEntries(ownNameConditions.map(([key])=>[key,row.conditions[key]??'']))};}
|
||||
|
||||
function readStore(draft:Record<string,unknown>):Store{
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.ownNames!==undefined&&(!store.ownNames||typeof store.ownNames!=='object'||Array.isArray(store.ownNames)))throw new Error('Circuitos emnome próprio não recuperáveis;originais preservados.');
|
||||
const fields=Object.keys(newOwnName('')).filter(key=>key!=='conditions');
|
||||
for(const book of Object.values(store.ownNames??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof OwnNameRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de circuitos emnome próprio não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
||||
return store;
|
||||
}
|
||||
export function ownNameBook(draft:Record<string,unknown>):OwnNameBook{return readStore(draft).ownNames?.[acquisitionScope(draft)]??{rows:[]};}
|
||||
export function saveOwnNameBook(draft:Record<string,unknown>,book:OwnNameBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,ownNames:{...current.ownNames,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||||
function validDate(value:string):boolean{const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
|
||||
export function evaluateOwnName(row:OwnNameRow,currency:unknown):LineResult{
|
||||
const result:LineResult={id:row.id,status:'indeterminate',legs:[],baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||||
const rule=ownNameTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique bens/serviços e o sentido da comissão/mandato;sem inferir pelo pagamento de uma comissão.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique mês2026,data correspondente e dados efectivos ou pressupostos expressos.');
|
||||
const required=[...ownNameConditions,...ownNameSpecificConditions(row.treatment)];if(required.some(([key])=>row.conditions[key]==='no'))return stop('Este circuito emnome próprio não se aplica aos factos declarados;rever mandato/comissão/natureza/papéis,sem classificar mera intermediação como duas operações ou presumir isenção/IVAzero.','not_applicable');
|
||||
if(required.some(([key])=>row.conditions[key]!=='yes'))return stop('Natureza,actuação emnome próprio ou papéis por esclarecer;desconhecido não é não aplicável ou zero.');
|
||||
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.scope.trim()||!row.evidence.trim()||!row.subjectRef.trim()||!row.contractRef.trim()||![row.principalRef,row.agentRef,row.thirdPartyRef,row.firstLegRef,row.secondLegRef].every(v=>!!v.trim()))return stop('Identifique objecto,mandante/comitente,comissário/mandatário,terceiro,contrato,duas relações,operação/documento/data eâmbito;reutilize referências ou pressupostos expressos,sem certificado/aprovador novo.');
|
||||
if(typeof currency!=='string'||!/^[A-Z]{3}$/.test(currency)||row.currency!==currency)return stop('Identifique moeda original coerente com o contexto;sem converter ou reinterpretarUSDcomoMZN.');
|
||||
const amount=(v:string):string|null=>/^\d{1,15}(\.\d{1,6})?$/.test(v.trim())?v.trim():null;const first=amount(row.firstAmount),second=amount(row.secondAmount);
|
||||
if(first===null||second===null)return stop('Identifique separadamente as contraprestações originais das duas relações;desconhecido não é zero. Não inferir uma delas da comissão ou somá-las como uma única base.');
|
||||
const before=rule.direction==='sale'?row.principalRef:row.thirdPartyRef,after=rule.direction==='sale'?row.thirdPartyRef:row.principalRef;
|
||||
const legs=[{supplier:before,acquirer:row.agentRef,consideration:first,currency:row.currency,reference:row.firstLegRef},{supplier:row.agentRef,acquirer:after,consideration:second,currency:row.currency,reference:row.secondLegRef}];
|
||||
return{...result,status:'determined',legs,reason:`${rule.branch}:${rule.kind==='goods'?`art3(3)c,comissão de${rule.direction==='sale'?'venda:comissário considerado comprador':'compra:comitente considerado comprador'}`:'art4(3),mandatário emnome próprio considerado adquirente eprestador do serviço sucessivamente'}. ${row.origin==='projected'?'Circuito sob pressupostos expressos.':'Circuito segundo dados efectivos declarados.'} Duas relações com objecto/partes/contraprestações originais separadas,sem soma ou compensação automática. ${rule.kind==='goods'?'BaseP091/15(2)e já entregue permanece separada e não é recalculada.':'Não transformar este circuito emcomissão autónoma ou aplicar15(2)e de bens aos serviços.'} Sem determinar localização,isenção,base,taxa,IVA,dedução/campo11 ou totaisModeloA.`};
|
||||
}
|
||||
|
||||
function key(draft:Record<string,unknown>,book:OwnNameBook):string{return JSON.stringify({version:ownNameVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||||
export function ownNameCurrent(draft:Record<string,unknown>,book:OwnNameBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||||
export function recordOwnNames(draft:Record<string,unknown>,book:OwnNameBook):OwnNameBook{
|
||||
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
|
||||
const result:OwnNameCalculation={version:ownNameVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:ownNameSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateOwnName(row,draft.currency))};
|
||||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveOwnNameBook(draft,next);return next;
|
||||
}
|
||||
export function ownNameReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||||
try{const book=ownNameBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>{const line=result.rows[index],rule=ownNameTreatments.find(t=>t.id===row.treatment);return['IVA mensal',`Circuito ${rule?.branch??'emnome próprio'} — ${row.period} — ${row.id}`,`${row.description};${line.status==='determined'?'Circuito identificado':line.status==='not_applicable'?'Circuito não aplicável':'Circuito indeterminado'};IVA: Indeterminado;${line.reason};${line.legs.map((leg,i)=>`Relação${i+1}:${leg.supplier} → ${leg.acquirer},contraprestação original ${leg.consideration} ${leg.currency},referência ${leg.reference}`).join('; ')};sem soma das duas relações`,`${result.opportunity};${result.location};${ownNameCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};${rule?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};objecto ${row.subjectRef};contrato ${row.contractRef};mandante/comitente ${row.principalRef};agente ${row.agentRef};terceiro ${row.thirdPartyRef};relações ${row.firstLegRef}/${row.secondLegRef};${rule?.kind==='goods'?`referênciaP091existente ${row.existingBaseRef||'não indicada;sem nova base calculada'};`:''}âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...ownNameConditions,...ownNameSpecificConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};sem IVA/dedução/campo11/totaisModeloA.`];});}
|
||||
catch{return[['IVA mensal','Circuito emnome próprio','Dados não recuperáveis','Originais preservados;sem circuito ou valor por defeito.']];}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { goodsLocationReportRows } from './iva-goods-location.ts';
|
||||
import { serviceLocationReportRows } from './iva-service-location.ts';
|
||||
import { listedServiceReportRows } from './iva-listed-services.ts';
|
||||
import { reducedBaseReportRows } from './iva-reduced-bases.ts';
|
||||
import { ownNameReportRows } from './iva-own-name.ts';
|
||||
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
|
||||
import { salesReportRows } from './iva-operations.ts';
|
||||
import { fiscalQaRows } from './fiscal-qa-preview.ts';
|
||||
@@ -646,6 +647,7 @@ export function buildProjectDossierRows(
|
||||
rows.push(...serviceLocationReportRows(draft));
|
||||
rows.push(...listedServiceReportRows(draft));
|
||||
rows.push(...reducedBaseReportRows(draft));
|
||||
rows.push(...ownNameReportRows(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 IvaOwnNameDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_own_name_circuits_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-g10-own-name.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:352', $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['ownNames']['opportunity:352'];
|
||||
$this->assertCount(4, $book['rows']);
|
||||
$this->assertSame(['goods_sale_commission','goods_purchase_commission','service_sale_mandate','service_purchase_mandate'],array_column($book['rows'],'treatment'));
|
||||
foreach ($book['result']['rows'] as $row) {
|
||||
$this->assertCount(2,$row['legs']);
|
||||
$this->assertSame(['1000.25','1100.75'],array_column($row['legs'],'consideration'));
|
||||
$this->assertNull($row['baseAmount']);$this->assertNull($row['vat']);$this->assertNull($row['rate']);
|
||||
}
|
||||
$this->assertSame($book['rows'], $book['result']['inputs']);
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertSame('1000.25',$book['history'][0]['rows'][0]['legs'][0]['consideration']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g10-own-name-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, ['ownNames' => 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-g10-own-name-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,24 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {newOwnName,switchOwnNameTreatment,ownNameConditions,ownNameSpecificConditions,ownNameTreatments,evaluateOwnName,recordOwnNames,saveOwnNameBook,ownNameBook,ownNameCurrent,ownNameReportRows} from '../../resources/js/lib/iva-own-name.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-g10-own-name-preserved.json','utf8'));const draft={...frozen,currency:'MZN',context_opportunity:'opportunity:352',project_title:'SYNTHETIC QA — G10 nome próprio / Manica'};
|
||||
function row(t){return{...newOwnName(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',principalRef:'SYNTHETIC QA comitente/mandante A',agentRef:'SYNTHETIC QA agente B',thirdPartyRef:'SYNTHETIC QA terceiro C',contractRef:'SYNTHETIC QA contrato/mandato',subjectRef:'SYNTHETIC QA objecto da operação',firstAmount:'1000.25',secondAmount:'1100.75',firstLegRef:'SYNTHETIC QA relação1',secondLegRef:'SYNTHETIC QA relação2',existingBaseRef:'SYNTHETIC QA referênciaP091preservada',scope:'SYNTHETIC QA partes/objecto/operação/período',evidence:'SYNTHETIC QA pressupostos expressos',conditions:Object.fromEntries([...ownNameConditions,...ownNameSpecificConditions(t)].map(([key])=>[key,'yes']))};}
|
||||
for(const rule of ownNameTreatments)test(rule.branch+'/'+rule.id+' direction,own facts and original separate amounts',()=>{
|
||||
const r=row(rule.id),result=evaluateOwnName(r,'MZN');assert.equal(result.status,'determined');assert.equal(result.legs.length,2);assert.deepEqual(result.legs.map(l=>[l.supplier,l.acquirer]),rule.direction==='sale'?[[r.principalRef,r.agentRef],[r.agentRef,r.thirdPartyRef]]:[[r.thirdPartyRef,r.agentRef],[r.agentRef,r.principalRef]]);assert.deepEqual(result.legs.map(l=>l.consideration),['1000.25','1100.75']);assert.deepEqual(['baseAmount','vat','rate'].map(k=>result[k]),[null,null,null]);
|
||||
for(const [key] of [...ownNameConditions,...ownNameSpecificConditions(rule.id)]){const missing=structuredClone(r);delete missing.conditions[key];assert.equal(evaluateOwnName(missing,'MZN').status,'indeterminate');missing.conditions[key]='no';assert.equal(evaluateOwnName(missing,'MZN').status,'not_applicable');assert.equal(evaluateOwnName(missing,'MZN').legs.length,0);}
|
||||
for(const field of ['description','operationRef','documentRef','contractRef','subjectRef','principalRef','agentRef','thirdPartyRef','firstLegRef','secondLegRef','firstAmount','secondAmount','scope','evidence'])assert.equal(evaluateOwnName({...r,[field]:''},'MZN').status,'indeterminate',field);
|
||||
for(const patch of [{operationDate:'2026-11-12'},{documentDate:'2026-02-30'},{period:'2025-12'},{currency:'USD'},{firstAmount:'1e3'},{secondAmount:'-1'}])assert.equal(evaluateOwnName({...r,...patch},'MZN').status,'indeterminate');
|
||||
assert.equal(evaluateOwnName({...r,currency:'USD'},'USD').legs[0].currency,'USD');assert.equal(evaluateOwnName({...r,firstAmount:'0'},'MZN').legs[0].consideration,'0');assert.equal(evaluateOwnName({...r,existingBaseRef:''},'MZN').status,'determined');
|
||||
assert.match(result.reason,rule.kind==='goods'?/comissário considerado comprador|comitente considerado comprador/:/adquirente eprestador/);
|
||||
});
|
||||
test('Changing direction/kind keeps references and values but requires own confirmations',()=>{
|
||||
const original=row('goods_sale_commission');const changed=switchOwnNameTreatment(original,'service_purchase_mandate');assert.equal(changed.firstAmount,original.firstAmount);assert.equal(changed.secondAmount,original.secondAmount);assert.equal(changed.existingBaseRef,original.existingBaseRef);assert.equal(changed.principalRef,original.principalRef);assert.equal(changed.conditions.directionConfirmed,undefined);assert.equal(evaluateOwnName(changed,'MZN').status,'indeterminate');
|
||||
changed.conditions={...changed.conditions,...Object.fromEntries(ownNameSpecificConditions(changed.treatment).map(([k])=>[k,'yes']))};const result=evaluateOwnName(changed,'MZN');assert.equal(result.status,'determined');assert.equal(result.legs[0].supplier,original.thirdPartyRef);assert.match(result.reason,/Não transformar/);
|
||||
});
|
||||
test('Four circuits save/recover/history/report;P091 and all prior books unchanged',()=>{
|
||||
const rows=ownNameTreatments.map(t=>row(t.id));let book=recordOwnNames(draft,{rows});book=recordOwnNames(draft,book);assert.equal(book.history.length,1);assert.notEqual(book.result.inputs,book.rows);
|
||||
const next={...draft,modelo_a_snapshot:saveOwnNameBook(draft,book)};assert.deepEqual(ownNameBook(next),book);assert.ok(ownNameCurrent(next,book));assert.equal(ownNameCurrent({...next,currency:'USD'},book),false);assert.equal(ownNameCurrent({...next,context_opportunity:'other'},book),false);assert.equal(ownNameReportRows(next).length,4);assert.ok(ownNameReportRows(next).every(r=>r[2].includes('sem soma das duas relações')));
|
||||
const store=JSON.parse(next.modelo_a_snapshot);delete store.ownNames;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordOwnNames(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>ownNameBook({...next,modelo_a_snapshot:JSON.stringify({...JSON.parse(next.modelo_a_snapshot),ownNames:[]})}));
|
||||
if(process.env.IVA_OWN_NAME_FIXTURE)fs.writeFileSync(process.env.IVA_OWN_NAME_FIXTURE,JSON.stringify(next,null,2)+'\n');
|
||||
});
|
||||
Reference in New Issue
Block a user