feat(iva): explain eliminated P086 exemption and preserve acquisition continuity
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, calculateAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
|
||||
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, recordAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
|
||||
import { acquisitionTreatmentIssue, continueWithoutFormerExploration, formerExplorationSource, type AcquisitionTreatment } from '@/lib/iva-acquisition-continuity';
|
||||
|
||||
export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
|
||||
const [error,setError]=useState('');
|
||||
const [manualPeriod,setManualPeriod]=useState('');
|
||||
let book:AcquisitionBook;
|
||||
try {book=acquisitionBook(draft);} catch(cause) {return <p role="alert">{String(cause)}</p>;}
|
||||
const save=(next:AcquisitionBook)=>{try {onChange('modelo_a_snapshot',saveAcquisitionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
|
||||
@@ -21,9 +23,20 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
|
||||
<p>Primeiro âmbito: aquisições internas em MZN, afectação exclusiva a operações tributadas, sem exclusões ou dedução parcial, documento e dedução no mesmo período de 2026. Nas projecções, cada confirmação é um pressuposto, sem certificar documento ou declaração efectiva. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido por diferença ou soma, sem taxa de compra presumida.</p>
|
||||
<label className="block">Adquirente sujeito passivo de IVA no regime normal (efectivo ou pressuposto da projecção)<select className="block rounded border p-2" value={book.taxpayerRegistered??''} onChange={e=>save({...book,taxpayerRegistered:e.target.value as AcquisitionBook['taxpayerRegistered']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / outro enquadramento — fora deste lote</option></select></label>
|
||||
{error&&<p role="alert">{error}</p>}
|
||||
<details className="rounded border p-2"><summary>Indicar mês da aquisição pelo documento ou pressuposto da projecção</summary>
|
||||
<p>Para compras introduzidas directamente, indique o mês aplicável sem depender de ter vendas nesse mês. A reutilização de valores do orçamento mantém a conferência da origem.</p>
|
||||
<label>Mês da aquisição<input type="month" min="2026-01" max="2026-12" value={manualPeriod} onChange={e=>setManualPeriod(e.target.value)}/></label>
|
||||
<button type="button" onClick={()=>run(()=>{if(!/^2026-(0[1-9]|1[0-2])$/.test(manualPeriod))throw new Error('Indique um mês válido de2026.');if(book.periods.some(p=>p.period===manualPeriod))throw new Error('Esse mês já consta da cobertura; valores conservados.');save({...book,periods:[...book.periods,{period:manualPeriod,complete:false,absent:[]}]});})}>Acrescentar mês indicado</button>
|
||||
</details>
|
||||
<button type="button" className="underline" onClick={()=>run(()=>save(reuseAcquisitions(draft,book)))}>Reutilizar compras e períodos existentes</button>
|
||||
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Aquisição adicional'} — {row.period} — campo{row.field}</summary>
|
||||
<details className="rounded border p-2"><summary>Enquadramento especial da aquisição / referência anterior</summary>
|
||||
<label>Tratamento da aquisição<select value={row.acquisitionTreatment??''} onChange={e=>row.acquisitionTreatment==='former_exploration_exemption'&&e.target.value!=='former_exploration_exemption'?run(()=>save({...book,rows:book.rows.map(item=>item.id===row.id?continueWithoutFormerExploration(item):item)})):change(row.id,{acquisitionTreatment:e.target.value as AcquisitionTreatment})}><option value="">Enquadramento normal; sem benefício especial indicado</option><option value="former_exploration_exemption">Antiga isenção de prospecção/pesquisa —14(2)(c),eliminada</option><option value="normal">Continuidade pelo enquadramento normal</option></select></label>
|
||||
{acquisitionTreatmentIssue(row.acquisitionTreatment)&&<p role="status">{acquisitionTreatmentIssue(row.acquisitionTreatment)}</p>}
|
||||
{row.acquisitionTreatment==='former_exploration_exemption'&&<><p>{formerExplorationSource}</p><p>A continuidade conserva montantes,moeda,mês,documento e respostas existentes. Não introduz taxa nem altera o IVA declarado; reveja as condições antes de calcular.</p><button type="button" onClick={()=>run(()=>save({...book,rows:book.rows.map(item=>item.id===row.id?continueWithoutFormerExploration(item):item)}))}>Continuar aquisição sem esta isenção e rever condições</button></>}
|
||||
{row.acquisitionTreatmentOrigin&&<p>Origem conservada: antiga referência14(2)(c),eliminada. {row.acquisitionTreatment==='former_exploration_exemption'?'A continuidade normal foi escolhida numa versão anterior; a referência antiga está agora seleccionada para análise.':'Continuidade normal escolhida expressamente; não é nova concessão de isenção.'}</p>}
|
||||
</details>
|
||||
<p>Origem: {row.sourceMonth===null?'Introdução directa':`Plano, mês ${row.sourceMonth} (${row.sourcePeriod??'por indicar'}): ${row.sourceAmount??'sem informação'} ${row.sourceCurrency}`}. O mês da encomenda/orçamento não determina a data do documento ou a dedução. Alterações não substituem o resultado anterior.</p>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label>Descrição<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
|
||||
@@ -39,12 +52,14 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
|
||||
<label>Data do documento / prevista<input className="block rounded border p-2" type="date" value={row.documentDate} onChange={e=>change(row.id,{documentDate:e.target.value})}/></label>
|
||||
<label>Fornecedor / previsto<input className="block rounded border p-2" value={row.supplier} onChange={e=>change(row.id,{supplier:e.target.value})}/></label>
|
||||
{select(row,'domestic','Aquisição interna',yesNo)}
|
||||
{row.acquisitionTreatment!=='former_exploration_exemption'&&<>
|
||||
{select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
|
||||
{select(row,'legalDocument','Documento emitido em forma legal',yesNo)}
|
||||
{select(row,'held','Documento na posse do adquirente / previsto na projecção',yesNo)}
|
||||
{select(row,'allocation','Afectação',[['taxable_only','Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
|
||||
{select(row,'exclusion','Exclusões de dedução',[['none','Sem exclusões ou casos especiais dos arts.18–20'],['excluded','Despesa excluída'],['special','Veículos, combustível, viagens, alojamento, comunicações, luxo ou outra situação especial — por enquadrar']])}
|
||||
{select(row,'integrity','Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',yesNo)}
|
||||
</>}
|
||||
</div>
|
||||
<p>Fundamento: CIVA arts.18–21, fontes e versão no resultado. Não se calcula dedução parcial, excepção de despesa ou dedução posterior neste lote.</p>
|
||||
{row.sourceMonth===null && <button type="button" className="mr-3 underline" onClick={()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id)})}>Remover esta aquisição introduzida directamente</button>}
|
||||
@@ -55,7 +70,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
|
||||
<p>Campos fora deste lote: assinale apenas quando não existiram as respectivas operações. Uma parcela não assinalada permanece indeterminada.</p>
|
||||
{acquisitionExcludedFields.map(field=><label className="block" key={field}><input type="checkbox" checked={period.absent.includes(field)} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,absent:e.target.checked?[...p.absent,field]:p.absent.filter(f=>f!==field)}:p)})}/> Sem operações no campo{field} — {labels[field]}</label>)}
|
||||
</details>)}
|
||||
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save({...book,result:calculateAcquisitions(draft,book)}))}>Calcular e guardar versão do apuramento parcial</button>
|
||||
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordAcquisitions(draft,book)))}>Calcular e guardar versão do apuramento parcial</button>
|
||||
<p>O preenchimento e o resultado seguem a gravação privada do simulador. Os campos19–27 e a submissão à AT permanecem fora deste lote; 17/18 são diferenças antes de créditos anteriores, sem ordem de pagamento ou promessa de reembolso.</p>
|
||||
{book.result&&<><p role="status">{acquisitionCurrent(draft,book)?'Resultado correspondente às entradas guardadas.':'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}</p><dl>{acquisitionReportRows(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></>}
|
||||
</section>;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Acquisition } from './iva-acquisitions.ts';
|
||||
|
||||
export type AcquisitionTreatment = '' | 'normal' | 'former_exploration_exemption';
|
||||
export const acquisitionContinuityVersion='acquisition-continuity/1';
|
||||
export const formerExplorationSource = 'Aviso da Assembleia da República,BR89/I Série,08/06/2017,PDF1/p.569:ordena eliminação de14(2)(c) e transcreve «c) eliminado». CIVA republicado Lei13/2016,PDF13/p.1426(271),continha aquisição de serviços de perfuração,pesquisa e construção de infra-estruturas na actividade mineira/petrolífera em fase de prospecção/pesquisa. Lei22/2022art.1 eLei10/2025art.1 não reintroduzem14(2)(c). Cobertura2026/MZN;sem cálculo retroactivo.';
|
||||
export const formerExplorationNotice = 'A antiga isenção da aquisição de serviços de perfuração, pesquisa e infra-estruturas de prospecção mineira/petrolífera do artigo14(2)(c) foi eliminada pela rectificação publicada no BR89 de08/06/2017. Não se aplica nesta cobertura2026. Não indique IVA zero nem dedução por esta referência. Valores e documento conservados; escolha expressamente continuar a aquisição sem esta isenção e reveja o enquadramento normal. Outra eventual base legal deve ser identificada, não presumida.';
|
||||
export function acquisitionTreatmentIssue(treatment:unknown):string|null {
|
||||
if(treatment===undefined||treatment===''||treatment==='normal')return null;
|
||||
return treatment==='former_exploration_exemption'?formerExplorationNotice:'Tratamento especial guardado desconhecido; dados conservados, sem isenção ou dedução presumidas. Identifique o enquadramento antes de continuar.';
|
||||
}
|
||||
export function continueWithoutFormerExploration(row:Acquisition):Acquisition {
|
||||
if(row.acquisitionTreatment!=='former_exploration_exemption')throw new Error('A continuidade exige a antiga referência14(2)(c) seleccionada; aquisição conservada.');
|
||||
return {...row,acquisitionTreatment:'normal',acquisitionTreatmentOrigin:{id:'former_exploration_exemption',source:formerExplorationSource}};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
|
||||
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
|
||||
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
||||
import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts';
|
||||
import { acquisitionTreatmentIssue, acquisitionContinuityVersion, formerExplorationSource, type AcquisitionTreatment } from './iva-acquisition-continuity.ts';
|
||||
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts';
|
||||
|
||||
export const acquisitionVersion = 'modelo-a-internal-acquisitions/1';
|
||||
@@ -15,10 +16,12 @@ export type Acquisition = {
|
||||
supplier: string; supplierRegistered: Choice; legalDocument: Choice; held: Choice; domestic: Choice;
|
||||
allocation: ''|'taxable_only'|'mixed'|'no_right'; exclusion: ''|'none'|'excluded'|'special';
|
||||
integrity: Choice; description: string;
|
||||
acquisitionTreatment?: AcquisitionTreatment;
|
||||
acquisitionTreatmentOrigin?: {id:'former_exploration_exemption';source:string};
|
||||
};
|
||||
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string};
|
||||
type PeriodInput = {period:string; complete:boolean; absent:string[]};
|
||||
export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation};
|
||||
export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation;history?:AcquisitionCalculation[]};
|
||||
export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string;
|
||||
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; exactFields?:Record<string,FiscalFraction|null>; reasons:string[]}>};
|
||||
type Store = {version:1; books:Record<string,AcquisitionBook>};
|
||||
@@ -36,6 +39,7 @@ export function readAcquisitionStore(raw:unknown):Store {
|
||||
const value=JSON.parse(String(raw));
|
||||
if (value?.version!==1 || !value.books || typeof value.books!=='object' || Array.isArray(value.books)) throw new Error('Compras fiscais guardadas não recuperáveis; dados preservados.');
|
||||
for(const book of Object.values(value.books) as AcquisitionBook[]) {
|
||||
if(book?.history!==undefined&&!Array.isArray(book.history))throw new Error('Histórico de aquisições não recuperável; originais preservados, sem substituição.');
|
||||
if(!book || !Array.isArray(book.rows) || !Array.isArray(book.periods) || typeof book.currency!=='string') throw new Error('Formato de compras não recuperável; sem substituição.');
|
||||
const stringKeys=Object.keys(newAcquisition('','','')).filter(key=>!['sourceMonth','sourceAmount'].includes(key));
|
||||
if(book.rows.some(row=>!row || stringKeys.some(key=>typeof row[key as keyof Acquisition]!=='string') || !(row.sourceMonth===null || Number.isInteger(row.sourceMonth)) || !(row.sourceAmount===null || typeof row.sourceAmount==='string')) || book.periods.some(p=>!p || typeof p.period!=='string' || typeof p.complete!=='boolean' || !Array.isArray(p.absent) || p.absent.some(f=>typeof f!=='string'))) throw new Error('Linhas ou períodos de compras não recuperáveis; os dados guardados foram preservados.');
|
||||
@@ -91,6 +95,8 @@ export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegi
|
||||
const stop=(reason:string,deductible:string|null=null)=>({...result,reason,deductible});
|
||||
if(currency!=='MZN' || row.sourceCurrency!==currency) return stop('Divergência ou moeda fora de MZN; sem conversão.');
|
||||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)) return stop('Período fora do âmbito 2026 ou em falta.');
|
||||
const treatmentIssue=acquisitionTreatmentIssue(row.acquisitionTreatment);
|
||||
if(treatmentIssue)return stop(treatmentIssue);
|
||||
if(n===null || v===null || g===null || n+v!==g) return stop('Indique dois valores válidos; líquido + IVA deve corresponder ao bruto.');
|
||||
if(row.sourceMonth!==null && (!row.sourceBasis || amount(row.sourceAmount)!==(row.sourceBasis==='net'?n:g))) return stop('Classifique ou resolva a divergência do valor de origem; não foi reinterpretado.');
|
||||
if(v===0n) return stop('IVA explicitamente indicado como zero; não existe imposto a deduzir nesta aquisição.','0.00');
|
||||
@@ -107,7 +113,7 @@ function sourceKey(draft:Record<string,unknown>,book:AcquisitionBook):string {
|
||||
// Change detection only, never an authorization or authenticity token.
|
||||
let hash=14695981039346656037n;for(let i=0;i<raw.length;i++)hash=BigInt.asUintN(64,(hash^BigInt(raw.charCodeAt(i)))*1099511628211n);
|
||||
const operations=(readAcquisitionStore(draft.modelo_a_snapshot) as Store & {salesOperations?:Record<string,unknown>}).salesOperations?.[acquisitionScope(draft)];
|
||||
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}`;
|
||||
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}${book.rows.some(r=>r.acquisitionTreatment||r.acquisitionTreatmentOrigin)?`:${acquisitionContinuityVersion}`:''}`;
|
||||
}
|
||||
function location(draft:Record<string,unknown>):string {
|
||||
const labels=draft.selection_labels as Record<string,unknown>|undefined;
|
||||
@@ -115,13 +121,13 @@ function location(draft:Record<string,unknown>):string {
|
||||
}
|
||||
export function acquisitionCurrent(draft:Record<string,unknown>,book:AcquisitionBook):boolean {return !!book.result && book.result.version===acquisitionVersion && book.result.opportunity===acquisitionScope(draft) && book.result.sourceKey===sourceKey(draft,book);}
|
||||
export function calculateAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionCalculation {
|
||||
const scope=acquisitionScope(draft),dates=periods(draft);
|
||||
const scope=acquisitionScope(draft),dates=book.periods.map(p=>p.period);
|
||||
if(book.currency!=='MZN' || draft.currency!==book.currency) throw new Error('Resolva a moeda de origem das compras; valores preservados sem conversão.');
|
||||
if(!dates.length) throw new Error('Período de vendas por determinar.');
|
||||
if(!dates.length||dates.some(period=>!/^2026-(0[1-9]|1[0-2])$/.test(period))||new Set(dates).size!==dates.length) throw new Error('Indique os meses aplicáveis às aquisições em2026,sem períodos duplicados; não inferidos do orçamento ou das vendas.');
|
||||
if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length) throw new Error('Existem compras com identidade repetida.');
|
||||
const documents=book.rows.filter(r=>r.documentRef.trim() && r.supplier.trim()).map(r=>JSON.stringify([r.origin,r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine?.trim()??'']));
|
||||
if(new Set(documents).size!==documents.length) throw new Error('Documento/linha repetido: discrimine linhas distintas ou remova a duplicação antes de deduzir.');
|
||||
if(book.rows.some(r=>!['08','09','10'].includes(r.field) || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 e num período da projecção.');
|
||||
if(book.rows.some(r=>!['08','09','10'].includes(r.field) || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 e num mês incluído na cobertura declarada das aquisições.');
|
||||
const references=acquisitionPlanReferences(draft);
|
||||
for(const row of book.rows.filter(r=>r.sourceMonth!==null)) {
|
||||
const current=references.find(p=>p.month===row.sourceMonth && p.kind===(row.sourceField??'inventory'));
|
||||
@@ -179,7 +185,11 @@ export function calculateAcquisitions(draft:Record<string,unknown>,book:Acquisit
|
||||
}
|
||||
return {period,fields,reasons};
|
||||
});
|
||||
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources,declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
|
||||
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources+(book.rows.some(r=>r.acquisitionTreatment==='former_exploration_exemption'||r.acquisitionTreatmentOrigin)?`; ${formerExplorationSource}`:''),declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
|
||||
}
|
||||
export function recordAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionBook {
|
||||
const result=calculateAcquisitions(draft,book,now);
|
||||
return {...book,...(book.result?{history:[...(book.history??[]),structuredClone(book.result)]}:{}),result};
|
||||
}
|
||||
export function acquisitionReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]> {
|
||||
if(!draft.modelo_a_snapshot) return [];
|
||||
@@ -193,6 +203,7 @@ export function acquisitionReportRows(draft:Record<string,unknown>):Array<[strin
|
||||
const source=row.sourceMonth===null?'Introduzido pelo utilizador':`Plano (${row.sourceField==='asset'?'orçamento de activos':row.sourceField==='operating'?'orçamento operacional':'compras/encomendas'}), mês ${row.sourceMonth}, ${row.sourcePeriod??'período de origem não indicado'}, ${row.sourceAmount??'valor em falta'} ${row.sourceCurrency}; ${row.sourceBasis==='net'?'sem IVA':row.sourceBasis==='gross'?'com IVA':'base por classificar'}`;
|
||||
const allocation={taxable_only:'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation];
|
||||
out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
|
||||
if(row.acquisitionTreatment==='former_exploration_exemption'||row.acquisitionTreatmentOrigin)out.push(['IVA mensal',`Continuidade14(2)(c) — ${row.id}`,row.acquisitionTreatment==='former_exploration_exemption'?'Antiga isenção eliminada; dedução indeterminada por esta referência':'Continuidade normal expressamente escolhida; montantes e documento conservados; condições de dedução da aquisição, sem aplicar antiga isenção',`${context}; ${formerExplorationSource}; origem conservada ${row.acquisitionTreatmentOrigin?.id??'former_exploration_exemption'}`]);
|
||||
}
|
||||
for(const period of result.periods) {
|
||||
const entries=Object.entries(period.fields).sort(([a],[b])=>Number(a)-Number(b));
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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 IvaAcquisitionContinuityDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_acquisition_continuity_recovers_with_document_history_and_previous_sales_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('outputs/iva-l1-g5b-acquisitions-20261024/calculated.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:329', $recovered['context_opportunity']);
|
||||
$this->assertSame('Manica', $recovered['selection_labels']['province_id']);
|
||||
$this->assertArrayNotHasKey('monthly_iva_settings', $recovered);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['books']['opportunity:329'];
|
||||
$this->assertCount(1, $book['rows']);
|
||||
$this->assertSame('normal', $book['rows'][0]['acquisitionTreatment']);
|
||||
$this->assertSame('former_exploration_exemption', $book['rows'][0]['acquisitionTreatmentOrigin']['id']);
|
||||
$this->assertSame('100', $book['rows'][0]['net']);
|
||||
$this->assertSame('16', $book['rows'][0]['vat']);
|
||||
$this->assertSame('116', $book['rows'][0]['gross']);
|
||||
$this->assertSame('SYNTHETIC QA G5B 001', $book['rows'][0]['documentRef']);
|
||||
$this->assertSame('2026-12', $book['rows'][0]['period']);
|
||||
$this->assertSame('MZN', $book['currency']);
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertNull($book['history'][0]['rows'][0]['deductible']);
|
||||
$this->assertSame('former_exploration_exemption', $book['history'][0]['inputs'][0]['acquisitionTreatment']);
|
||||
$this->assertSame('16.00', $book['result']['rows'][0]['deductible']);
|
||||
$this->assertSame('16.00', $book['result']['periods'][0]['fields']['10']);
|
||||
$this->assertNull($book['result']['periods'][0]['fields']['17']);
|
||||
$this->assertNull($book['result']['periods'][0]['fields']['18']);
|
||||
$this->assertStringContainsString('BR89', $book['result']['sources']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g5a-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
foreach (json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['salesOperations'] as $context => $original) {
|
||||
$this->assertSame($original, $store['salesOperations'][$context]);
|
||||
}
|
||||
file_put_contents(base_path('outputs/iva-l1-g5b-acquisitions-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { acquisitionTreatmentIssue,continueWithoutFormerExploration,formerExplorationSource } from '../../resources/js/lib/iva-acquisition-continuity.ts';
|
||||
import { newAcquisition,evaluateAcquisition,recordAcquisitions,acquisitionBook,acquisitionCurrent,saveAcquisitionBook,acquisitionReportRows } from '../../resources/js/lib/iva-acquisitions.ts';
|
||||
import { salesCurrent } from '../../resources/js/lib/iva-operations.ts';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g5a-inputs.json',import.meta.url),'utf8'));
|
||||
const row=()=>({...newAcquisition('g5b-0','2026-12','MZN'),field:'10',net:'100',vat:'16',gross:'116',documentType:'invoice',documentRef:'SYNTHETIC QA G5B 001',documentDate:'2026-12-10',supplier:'SYNTHETIC QA fornecedor',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes',description:'SYNTHETIC QA — serviço de pesquisa; antiga referência14(2)(c)',acquisitionTreatment:'former_exploration_exemption'});
|
||||
function fixture(){
|
||||
const draft={...structuredClone(frozen),context_opportunity:'opportunity:329',project_title:'SYNTHETIC QA — G5B / Continuidade da aquisição'};
|
||||
const prior=recordAcquisitions(draft,{currency:'MZN',taxpayerRegistered:'yes',rows:[row()],periods:[{period:'2026-12',complete:true,absent:[]}]});draft.modelo_a_snapshot=saveAcquisitionBook(draft,prior);return{draft,prior};
|
||||
}
|
||||
test('2017 rectification prevents former14(2)(c) exemption in2026,including explicit zero;unknown is not a normal treatment',()=>{
|
||||
assert.match(formerExplorationSource,/BR89/);assert.match(acquisitionTreatmentIssue('former_exploration_exemption'),/eliminada/);
|
||||
for(const vat of ['16','0','']){const input={...row(),vat,gross:vat==='0'?'100':'116'},result=evaluateAcquisition(input,'MZN','yes');assert.equal(result.deductible,null);assert.match(result.reason,/Não se aplica nesta cobertura2026/);assert.equal(input.net,'100');}
|
||||
assert.equal(evaluateAcquisition({...row(),acquisitionTreatment:'unknown'},'MZN','yes').deductible,null);
|
||||
assert.match(evaluateAcquisition({...row(),period:'2016-12'},'MZN','yes').reason,/fora do âmbito/);
|
||||
});
|
||||
test('explicit continuation keeps values,document,answers and source;normal deduction requires existing conditions',()=>{
|
||||
const input=row(),next=continueWithoutFormerExploration(input);for(const key of Object.keys(input).filter(k=>k!=='acquisitionTreatment'))assert.deepEqual(next[key],input[key],key);
|
||||
assert.equal(input.acquisitionTreatment,'former_exploration_exemption');assert.equal(next.acquisitionTreatment,'normal');assert.equal(next.acquisitionTreatmentOrigin.id,'former_exploration_exemption');
|
||||
assert.equal(evaluateAcquisition(next,'MZN','yes').deductible,'16.00');
|
||||
for(const patch of [{held:''},{allocation:'mixed'},{legalDocument:''},{sourceCurrency:'USD'},{vat:'',gross:''},{domestic:'no'},{documentDate:'2026-11-10'}])assert.equal(evaluateAcquisition({...next,...patch},'MZN','yes').deductible,null);
|
||||
assert.throws(()=>continueWithoutFormerExploration({...input,acquisitionTreatment:'normal'}),/antiga referência/);
|
||||
});
|
||||
test('manual purchase month does not require sales;previous classification remains in history and incomplete totals remain unknown',()=>{
|
||||
const{draft,prior}=fixture();assert.equal(draft.monthly_iva_settings,undefined);assert.equal(prior.result.rows[0].deductible,null);assert.equal(prior.result.periods[0].fields['10'],null);
|
||||
const next=recordAcquisitions(draft,{...prior,rows:[continueWithoutFormerExploration(prior.rows[0])]});assert.deepEqual(next.history,[prior.result]);assert.equal(next.result.rows[0].deductible,'16.00');assert.equal(next.result.periods[0].fields['10'],'16.00');
|
||||
for(const f of ['01','02','11','14','15','16','17','18'])assert.equal(next.result.periods[0].fields[f],null,f);
|
||||
assert.throws(()=>recordAcquisitions(draft,{...next,periods:[]}),/meses aplicáveis/);assert.throws(()=>recordAcquisitions(draft,{...next,periods:[...next.periods,...next.periods]}),/meses aplicáveis/);
|
||||
assert.throws(()=>recordAcquisitions(draft,{...next,rows:[{...next.rows[0],sourceMonth:1,sourceAmount:'100',sourceBasis:'net'}]}),/orçamentos do plano mudaram/);
|
||||
});
|
||||
test('save/recovery/report retain rectification,continuation and history;all20 prior sales books stay identical/current',()=>{
|
||||
const{draft,prior}=fixture();const book=recordAcquisitions(draft,{...prior,rows:[continueWithoutFormerExploration(prior.rows[0])]});draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);assert.ok(acquisitionCurrent(draft,book));assert.deepEqual(acquisitionBook(JSON.parse(JSON.stringify(draft))),JSON.parse(JSON.stringify(book)));
|
||||
const before=JSON.parse(frozen.modelo_a_snapshot),after=JSON.parse(draft.modelo_a_snapshot);assert.equal(Object.keys(before.salesOperations).length,20);assert.deepEqual(after.salesOperations,before.salesOperations);assert.deepEqual(Object.keys(after.books),['opportunity:329']);
|
||||
for(const[context,original]of Object.entries(before.salesOperations))assert.ok(salesCurrent({...draft,context_opportunity:context},original));
|
||||
const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance']),version:'QA G5B'}));for(const token of ['opportunity:329','Manica','2026-12','16.00','BR89','Continuidade14(2)(c)','eliminado','expressamente escolhida'])assert.ok(report.includes(token),token);
|
||||
assert.match(JSON.stringify(acquisitionReportRows(draft)),/antiga isenção/);
|
||||
if(process.env.IVA_G5B_CALCULATED)fs.writeFileSync(process.env.IVA_G5B_CALCULATED,JSON.stringify(draft));
|
||||
});
|
||||
Reference in New Issue
Block a user