Implement P123 consignment and P126 rebuttable goods presumptions in QA
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import {useState} from 'react';
|
||||
import {goodsPresumptionBook,goodsPresumptionCurrent,goodsPresumptionReportRows,goodsPresumptionSource,goodsPresumptionTreatments,newGoodsPresumption,presumptionConditions,recordGoodsPresumptions,saveGoodsPresumptionBook,switchPresumptionTreatment,type GoodsPresumptionBook,type GoodsPresumptionRow} from '@/lib/iva-goods-presumptions';
|
||||
export function SimulationGoodsPresumptions({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:GoodsPresumptionBook;
|
||||
try{book=goodsPresumptionBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:GoodsPresumptionBook)=>{try{onChange('modelo_a_snapshot',saveGoodsPresumptionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
const change=(id:string,patch:Partial<GoodsPresumptionRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)});
|
||||
return <details className="rounded border p-3"><summary>Consignação e presunções de bens</summary>
|
||||
<p>Uma linha por conjunto identificado. Na consignação,180dias contam da entrega ao destinatário. As presunções admitem prova em contrário. Classificação não concede isenção,dedução ou elegibilidade para margem.</p>
|
||||
<p>Oportunidade: {String(draft.context_opportunity??'projecto')}. A data de conferência situa o cenário; este percurso não determina a exigibilidade,base ou IVA.</p>
|
||||
<p className="text-sm">{goodsPresumptionSource}</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" onClick={()=>save({...book,rows:[...book.rows,newGoodsPresumption(crypto.randomUUID())]})}>Acrescentar conjunto de bens</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Conjunto por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Percurso de consignação/presunção<select value={row.treatment} onChange={e=>change(row.id,switchPresumptionTreatment(row,e.target.value))}><option value="">Por indicar</option>{goodsPresumptionTreatments.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 GoodsPresumptionRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Conjunto/operação identificados'],['operationRef','Referência da operação / pressuposto'],['operationDate','Data de conferência / prevista'],['period','Mês da conferência'],['documentRef','Documento / referência da previsão'],['documentDate','Data do documento / prevista'],['goodsRef','Bens e elementos identificados'],['scope','Âmbito:conjunto,estabelecimentos,operação e perí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>)}
|
||||
{row.treatment==='consignment_180'?<><label>Data da entrega ao destinatário<input type="date" value={row.deliveryDate} onChange={e=>change(row.id,{deliveryDate:e.target.value})}/></label><label>Destinatário e referência da entrega<input value={row.recipientRef} onChange={e=>change(row.id,{recipientRef:e.target.value})}/></label><p>Identifique apenas o conjunto não devolvido. Separe devoluções parciais por conjunto; uma devolução posterior não apaga o cenário anteriormente guardado.</p></>:row.treatment&&<>
|
||||
<label>Levantamento/inventário e estabelecimentos abrangidos<input value={row.inventoryRef} onChange={e=>change(row.id,{inventoryRef:e.target.value})}/></label>
|
||||
{row.treatment==='excess_consumption'&&<>{([['quantity','Quantidade consumida'],['quantityUnit','Unidade da quantidade'],['productionRef','Volume de produção e comparação que fundamenta o excesso']] as const).map(([field,label])=><label key={field}>{label}<input value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}<p>Sem percentagem ou limiar de consumo por defeito.</p></>}
|
||||
<label>Existe prova em contrário para o âmbito declarado?<select value={row.contraryProof} onChange={e=>change(row.id,{contraryProof:e.target.value as GoodsPresumptionRow['contraryProof']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
|
||||
{row.contraryProof==='yes'&&<label>Referência e âmbito da prova em contrário<input value={row.contraryProofRef} onChange={e=>change(row.id,{contraryProofRef:e.target.value})}/></label>}
|
||||
</>}
|
||||
</div>
|
||||
<fieldset><legend>Factos próprios do percurso</legend>{presumptionConditions(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/referências ocultos; reveja as confirmações aplicáveis. Prova em contrário afasta a presunção no seu âmbito,sem transformar o resultado em isenção.</p>
|
||||
</details>)}
|
||||
{!!book.rows.length&&<button type="button" onClick={()=>{try{save(recordGoodsPresumptions(draft,book));}catch(cause){setError(String(cause));}}}>Conferir consignação/presunções e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{goodsPresumptionCurrent(draft,book)?'Classificações correspondentes às entradas guardadas.':'Entradas alteradas;resultado histórico conservado.Reveja e confira novamente.'} {book.history?.length??0} versões anteriores.Sem IVA,base,dedução ou margem calculados.</p><dl>{goodsPresumptionReportRows(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>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SimulationGoodsPresumptions } from './simulation-goods-presumptions';
|
||||
import { InvestmentContextAssessment } from './investment-context-assessment';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, recoverMonthlyIvaRecord, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
|
||||
@@ -121,6 +122,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationReducedBases draft={draft} onChange={onChange} />
|
||||
<SimulationOwnName draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
|
||||
<SimulationSecondhand draft={draft} onChange={onChange} />
|
||||
<SimulationOfferPreparations draft={draft} onChange={onChange} />
|
||||
<SimulationModeloA draft={draft} />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||||
export const goodsPresumptionVersion='goods-presumptions/1';
|
||||
export const goodsPresumptionSource='CIVA Lei13/2016,3(3)d PDF6/p1426(264):mercadorias enviadas à consignação não devolvidas em180dias da entrega;3(4) PDF7/p1426(265):salvo prova em contrário,presunção de transmissão dos bens adquiridos/importados/produzidos não encontrados nos estabelecimentos e dos consumidos em quantidades excessivas face ao volume de produção;presunção de aquisição dos bens encontrados nesses locais.Lei22/2022 não altera3;Lei10/2025 PDF12 mantém3(3)d/3(4).Não são embalagensP102 nem aplicação automática da margemP092/P093/P125.';
|
||||
export const goodsPresumptionTreatments=[{id:'consignment_180',branch:'P123',label:'Consignação — não devolução em180dias'},{id:'missing_goods',branch:'P126',label:'Presunção de transmissão — bens não localizados'},{id:'excess_consumption',branch:'P126',label:'Presunção de transmissão — consumo excessivo'},{id:'found_goods',branch:'P126',label:'Presunção de aquisição — bens encontrados'}] as const;
|
||||
export const goodsPresumptionConditions={consignment_180:[['consignment','Mercadorias enviadas à consignação,com entrega ao destinatário identificado'],['notReturned','O conjunto identificado não foi devolvido até à data de conferência']],missing_goods:[['acquiredImportedProduced','Bens adquiridos,importados ou produzidos pelo sujeito passivo'],['notLocated','Não se encontram em nenhum dos estabelecimentos do sujeito passivo,segundo o levantamento identificado']],excess_consumption:[['acquiredImportedProduced','Bens adquiridos,importados ou produzidos pelo sujeito passivo'],['excessive','Consumo em quantidades excessivas,tendo em conta o volume de produção identificado']],found_goods:[['found','Bens encontrados em estabelecimento do sujeito passivo identificado']]} as const;
|
||||
export function presumptionConditions(t:string):ReadonlyArray<readonly[string,string]>{return goodsPresumptionConditions[t as keyof typeof goodsPresumptionConditions]??[];}
|
||||
type Choice=''|'yes'|'no';
|
||||
export type GoodsPresumptionRow={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;deliveryDate:string;recipientRef:string;inventoryRef:string;productionRef:string;quantity:string;quantityUnit:string;contraryProof:Choice;contraryProofRef:string;conditions:Partial<Record<string,Choice>>};
|
||||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';classification:'consignment_transmission'|'presumed_transmission'|'presumed_acquisition'|'rebutted'|'not_applicable'|'indeterminate';deadline:string|null;elapsedDays:number|null;baseAmount:null;vat:null;rate:null;reason:string};
|
||||
export type GoodsPresumptionCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:GoodsPresumptionRow[];rows:LineResult[]};
|
||||
export type GoodsPresumptionBook={rows:GoodsPresumptionRow[];result?:GoodsPresumptionCalculation;history?:GoodsPresumptionCalculation[]};
|
||||
type Store=ReturnType<typeof readAcquisitionStore>&{goodsPresumptions?:Record<string,GoodsPresumptionBook>};
|
||||
export function newGoodsPresumption(id:string):GoodsPresumptionRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsRef:'',scope:'',evidence:'',deliveryDate:'',recipientRef:'',inventoryRef:'',productionRef:'',quantity:'',quantityUnit:'',contraryProof:'',contraryProofRef:'',conditions:{}};}
|
||||
export function switchPresumptionTreatment(row:GoodsPresumptionRow,treatment:string):GoodsPresumptionRow{return{...row,treatment,conditions:{},contraryProof:''};}
|
||||
function readStore(draft:Record<string,unknown>):Store{
|
||||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||||
if(store.goodsPresumptions!==undefined&&(!store.goodsPresumptions||typeof store.goodsPresumptions!=='object'||Array.isArray(store.goodsPresumptions)))throw new Error('Presunções não recuperáveis;originais preservados.');
|
||||
const fields=Object.keys(newGoodsPresumption('')).filter(k=>k!=='conditions');
|
||||
for(const book of Object.values(store.goodsPresumptions??{}))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 GoodsPresumptionRow]!=='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 goodsPresumptionBook(draft:Record<string,unknown>):GoodsPresumptionBook{return readStore(draft).goodsPresumptions?.[acquisitionScope(draft)]??{rows:[]};}
|
||||
export function saveGoodsPresumptionBook(draft:Record<string,unknown>,book:GoodsPresumptionBook):string{const store=readStore(draft),encoded=JSON.stringify({...store,goodsPresumptions:{...store.goodsPresumptions,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||||
function date(s:string):number|null{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?n:null;}
|
||||
export function evaluateGoodsPresumption(row:GoodsPresumptionRow):LineResult{
|
||||
const result:LineResult={id:row.id,status:'indeterminate',classification:'indeterminate',deadline:null,elapsedDays:null,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});
|
||||
const rule=goodsPresumptionTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique o percurso;sem presunção por defeito.');
|
||||
const operation=date(row.operationDate);
|
||||
if(operation===null||!/^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 de conferência/mês2026 e dados efectivos ou pressupostos expressos.');
|
||||
if(![row.description,row.operationRef,row.documentRef,row.goodsRef,row.scope,row.evidence].every(v=>!!v.trim())||date(row.documentDate)===null)return stop('Identifique bens,conjunto/âmbito,operação,documento/data e factos ou pressupostos;reutilize dados disponíveis,sem certificado/aprovador novo.');
|
||||
const conditions=presumptionConditions(row.treatment);
|
||||
if(conditions.some(([k])=>row.conditions[k]==='no'))return stop('Os factos declarados afastam este percurso específico;isso não determina isenção,IVA zero ou a classificação de outra operação.','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==='consignment_180'){
|
||||
const delivery=date(row.deliveryDate);if(delivery===null||delivery>operation||!row.recipientRef.trim())return stop('Identifique entrega ao destinatário e data anterior ou igual à conferência;sem prazo presumido.');
|
||||
result.elapsedDays=(operation-delivery)/86400000;result.deadline=new Date(delivery+180*86400000).toISOString().slice(0,10);
|
||||
if(result.elapsedDays<=180)return stop(`Prazo de180dias ainda não ultrapassado na conferência;limite ${result.deadline},decorridos ${result.elapsedDays}dias.Sem transmissão por não devolução nesta data. O dia180 conserva o prazo até ao fim do dia.`,`not_applicable`);
|
||||
return stop(`P123:mercadorias identificadas enviadas à consignação,não devolvidas após180dias desde entrega${row.deliveryDate};limite${result.deadline};decorridos${result.elapsedDays}dias. Transmissão3(3)d identificada no recorte de conferência;não são embalagensP102. Data de exigibilidade/base/IVA não calculadas neste percurso;devolução posterior requer tratamento próprio,sem apagar histórico.`,`determined`,'consignment_transmission');
|
||||
}
|
||||
if(!row.inventoryRef.trim())return stop('Identifique levantamento/inventário e os estabelecimentos abrangidos;ausência numa única localização não demonstra ausência em todos os estabelecimentos.');
|
||||
if(row.treatment==='excess_consumption'&&(!row.productionRef.trim()||!row.quantityUnit.trim()||!/^\d{1,15}(\.\d{1,6})?$/.test(row.quantity)||!/[1-9]/.test(row.quantity)))return stop('Identifique quantidade/unidade consumida e volume de produção/comparação que fundamenta o excesso;sem percentagem ou limiar inventado.');
|
||||
if(row.contraryProof==='yes')return row.contraryProofRef.trim()?stop('Prova em contrário identificada para o conjunto/âmbito declarado:presunção3(4) afastada,sem atribuir isenção ou IVA zero e sem apagar histórico.','not_applicable','rebutted'):stop('Identifique o âmbito e referência da prova em contrário declarada;sem afastar presunção só por seleccionar Sim.');
|
||||
if(row.contraryProof!=='no')return stop('Existência de prova em contrário por esclarecer;presunção ilidível,não conclusão definitiva nem infracção presumida.');
|
||||
return stop(`P126:presunção ilidível3(4) de ${row.treatment==='found_goods'?'aquisição dos bens encontrados':'transmissão dos bens '+(row.treatment==='missing_goods'?'não localizados nos estabelecimentos abrangidos':'consumidos em quantidades excessivas face ao volume de produção')};segundo factos/pressupostos e âmbito identificados,sem prova em contrário declarada. Não comprova factura,aquisição efectiva,isenção ou dedução.Nenhuma base/taxa/IVA calculada;manter a possibilidade de prova em contrário.`,`determined`,row.treatment==='found_goods'?'presumed_acquisition':'presumed_transmission');
|
||||
}
|
||||
function key(draft:Record<string,unknown>,book:GoodsPresumptionBook):string{return JSON.stringify({version:goodsPresumptionVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},rows:book.rows});}
|
||||
export function goodsPresumptionCurrent(draft:Record<string,unknown>,book:GoodsPresumptionBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||||
export function recordGoodsPresumptions(draft:Record<string,unknown>,book:GoodsPresumptionBook):GoodsPresumptionBook{
|
||||
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:GoodsPresumptionCalculation={version:goodsPresumptionVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:goodsPresumptionSource,inputs:structuredClone(book.rows),rows:book.rows.map(evaluateGoodsPresumption)};
|
||||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveGoodsPresumptionBook(draft,next);return next;
|
||||
}
|
||||
export function goodsPresumptionReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||||
try{const book=goodsPresumptionBook(draft),r=book.result;if(!r)return[];return r.inputs.map((row,i)=>['IVA mensal',`${goodsPresumptionTreatments.find(t=>t.id===row.treatment)?.branch??'Presunção'} — ${row.period} — ${row.id}`,`${row.description};${r.rows[i].reason};base/IVA:Indeterminados`,`${r.opportunity};${r.location};${goodsPresumptionCurrent(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};${row.treatment==='consignment_180'?`entrega${row.deliveryDate},destinatário${row.recipientRef},limite${r.rows[i].deadline??'indeterminado'}`:`inventário ${row.inventoryRef};${row.treatment==='excess_consumption'?`consumo ${row.quantity} ${row.quantityUnit},produção/comparação ${row.productionRef};`:''}provaemcontrário ${row.contraryProof},${row.contraryProofRef}`};condições${JSON.stringify(row.conditions)};fundamento ${row.evidence};sem nova dedução/campo11/IVA/margem automática.`]);}
|
||||
catch{return[['IVA mensal','Consignação/presunções','Dados não recuperáveis','Originais conservados;sem classificação por defeito.']];}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { goodsPresumptionReportRows } from './iva-goods-presumptions.ts';
|
||||
import { displayValue } from './display-value.ts';
|
||||
import { investmentFieldOrigin } from './context-inheritance.ts';
|
||||
import { investmentLandPreparation } from './investment-land-preparation.ts';
|
||||
@@ -652,6 +653,7 @@ export function buildProjectDossierRows(
|
||||
rows.push(...ownNameReportRows(draft));
|
||||
rows.push(...goodsAllocationReportRows(draft));
|
||||
rows.push(...secondhandReportRows(draft));
|
||||
rows.push(...goodsPresumptionReportRows(draft));
|
||||
rows.push(...offerPreparationReportRows(draft));
|
||||
const operationRows = salesReportRows(draft);
|
||||
rows.push(...operationRows);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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 IvaGoodsPresumptionDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_goodspresumptions_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-goods-presumptions.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:355', $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['goodsPresumptions']['opportunity:355'];
|
||||
$this->assertCount(4, $book['rows']);
|
||||
$this->assertSame(['consignment_180','missing_goods','excess_consumption','found_goods'],array_column($book['rows'],'treatment'));
|
||||
$this->assertSame(['consignment_transmission','presumed_transmission','presumed_transmission','presumed_acquisition'],array_column($book['result']['rows'],'classification'));
|
||||
foreach ($book['result']['rows'] as $row) {
|
||||
$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('2026-11-28',$book['history'][0]['rows'][0]['deadline']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-goods-presumptions-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, ['goodsPresumptions' => 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-goods-presumptions-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,31 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {newGoodsPresumption,presumptionConditions,goodsPresumptionTreatments,evaluateGoodsPresumption,recordGoodsPresumptions,saveGoodsPresumptionBook,goodsPresumptionBook,goodsPresumptionCurrent,goodsPresumptionReportRows,switchPresumptionTreatment} from '../../resources/js/lib/iva-goods-presumptions.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-goods-presumptions-preserved.json','utf8'));
|
||||
const draft={...frozen,context_opportunity:'opportunity:355',project_title:'SYNTHETIC QA — Consignação e presunções / Manica'};
|
||||
function row(t){return{...newGoodsPresumption(t),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',origin:'projected',operationDate:'2026-12-12',operationRef:'SYNTHETIC QA operação/conferência',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',goodsRef:'SYNTHETIC QA conjunto individual',scope:'SYNTHETIC QA todos os estabelecimentos/período/conjunto',evidence:'SYNTHETIC QA factos/pressupostos expressos',deliveryDate:'2026-06-01',recipientRef:'SYNTHETIC QA destinatário/entrega',inventoryRef:'SYNTHETIC QA levantamento todos estabelecimentos',productionRef:'SYNTHETIC QA volume de produção/comparação',quantity:'125.123456',quantityUnit:'kg',contraryProof:'no',contraryProofRef:'',conditions:Object.fromEntries(presumptionConditions(t).map(([k])=>[k,'yes']))};}
|
||||
for(const rule of goodsPresumptionTreatments)test(rule.branch+' '+rule.id+' own facts/references and no fiscal amount',()=>{
|
||||
const r=row(rule.id),result=evaluateGoodsPresumption(r);assert.equal(result.status,'determined');assert.equal(result.classification,rule.id==='consignment_180'?'consignment_transmission':rule.id==='found_goods'?'presumed_acquisition':'presumed_transmission');assert.equal(result.baseAmount,null);assert.equal(result.vat,null);assert.equal(result.rate,null);
|
||||
for(const[k]of presumptionConditions(rule.id)){const missing=structuredClone(r);delete missing.conditions[k];assert.equal(evaluateGoodsPresumption(missing).status,'indeterminate');missing.conditions[k]='no';assert.equal(evaluateGoodsPresumption(missing).status,'not_applicable');}
|
||||
for(const field of ['description','operationRef','documentRef','goodsRef','scope','evidence',...(rule.id==='consignment_180'?['deliveryDate','recipientRef']:['inventoryRef']),...(rule.id==='excess_consumption'?['productionRef','quantity','quantityUnit']:[])])assert.equal(evaluateGoodsPresumption({...r,[field]:''}).status,'indeterminate',field);
|
||||
for(const patch of [{operationDate:'2026-02-30'},{documentDate:'2026-02-30'},{period:'2025-12'},{period:'2026-11'}])assert.equal(evaluateGoodsPresumption({...r,...patch}).status,'indeterminate');
|
||||
});
|
||||
test('P123 exact180-day boundary,yearcrossing and chronology',()=>{
|
||||
const r=row('consignment_180');const day=n=>new Date(Date.parse(r.deliveryDate+'T00:00:00Z')+n*86400000).toISOString().slice(0,10);
|
||||
for(const n of [179,180,181]){const d=day(n),v=evaluateGoodsPresumption({...r,operationDate:d,period:d.slice(0,7)});assert.equal(v.elapsedDays,n);assert.equal(v.deadline,day(180));assert.equal(v.status,n>180?'determined':'not_applicable');}
|
||||
assert.equal(evaluateGoodsPresumption({...r,deliveryDate:'2025-12-31',operationDate:'2026-06-30',period:'2026-06'}).elapsedDays,181);
|
||||
assert.equal(evaluateGoodsPresumption({...r,deliveryDate:'2026-12-13'}).status,'indeterminate');
|
||||
const returned=structuredClone(r);returned.conditions.notReturned='no';assert.equal(evaluateGoodsPresumption(returned).status,'not_applicable');
|
||||
});
|
||||
test('P126 rebuttable presumption,unknown proof and scope retained',()=>{
|
||||
for(const t of ['missing_goods','excess_consumption','found_goods']){const r=row(t);assert.equal(evaluateGoodsPresumption({...r,contraryProof:''}).status,'indeterminate');assert.equal(evaluateGoodsPresumption({...r,contraryProof:'yes'}).status,'indeterminate');const v=evaluateGoodsPresumption({...r,contraryProof:'yes',contraryProofRef:'SYNTHETIC QA prova/conjunto/período'});assert.equal(v.classification,'rebutted');assert.equal(v.baseAmount,null);assert.equal(v.vat,null);}
|
||||
for(const quantity of ['','0','-1','1e3','1.1234567'])assert.equal(evaluateGoodsPresumption({...row('excess_consumption'),quantity}).status,'indeterminate');
|
||||
const switched=switchPresumptionTreatment({...row('excess_consumption'),contraryProof:'yes',contraryProofRef:'retained'},'consignment_180');assert.equal(switched.productionRef,row('excess_consumption').productionRef);assert.equal(switched.contraryProofRef,'retained');assert.equal(switched.contraryProof,'');assert.deepEqual(switched.conditions,{});
|
||||
});
|
||||
test('private draft/report/history preserve allpreviousbooks and opportunity',()=>{
|
||||
const rows=goodsPresumptionTreatments.map(t=>row(t.id)),first=recordGoodsPresumptions(draft,{rows});const next={...draft,modelo_a_snapshot:saveGoodsPresumptionBook(draft,first)};const second=recordGoodsPresumptions(next,first);next.modelo_a_snapshot=saveGoodsPresumptionBook(next,second);
|
||||
assert.equal(second.history.length,1);assert.deepEqual(second.history[0].inputs,rows);assert.equal(goodsPresumptionCurrent(next,second),true);assert.equal(goodsPresumptionCurrent(next,{...second,rows:rows.map((r,i)=>i? r:{...r,scope:'changed'})}),false);assert.equal(goodsPresumptionReportRows(next).length,4);assert.deepEqual(goodsPresumptionBook({...next,context_opportunity:'opportunity:356'}),{rows:[]});
|
||||
const store=JSON.parse(next.modelo_a_snapshot);delete store.goodsPresumptions;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordGoodsPresumptions(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>goodsPresumptionBook({...next,modelo_a_snapshot:JSON.stringify({...store,goodsPresumptions:[]})}));
|
||||
if(process.env.IVA_GOODS_PRESUMPTIONS_FIXTURE)fs.writeFileSync(process.env.IVA_GOODS_PRESUMPTIONS_FIXTURE,JSON.stringify(next,null,2)+'\n');
|
||||
});
|
||||
Reference in New Issue
Block a user