feat(iva): prepare P111-P120 listed services as one compatible QA set

This commit is contained in:
2026-10-24 23:48:08 +02:00
parent 4853bc7284
commit 6cffa1188f
8 changed files with 230 additions and 0 deletions
@@ -0,0 +1,41 @@
import {useState} from 'react';
import {newListedService,switchListedTreatment,listedServiceBook,listedServiceConditions,ownListedConditions,listedServiceCurrent,listedServiceReportRows,listedServiceSource,listedServiceTreatments,recordListedServices,saveListedServiceBook,type ListedServiceBook,type ListedServiceRow} from '@/lib/iva-listed-services';
export function SimulationListedServices({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let book:ListedServiceBook;
try{book=listedServiceBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
const save=(next:ListedServiceBook)=>{try{onChange('modelo_a_snapshot',saveListedServiceBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<ListedServiceRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
return<details className="space-y-3 rounded border p-3">
<summary>Serviços enumerados — localização e responsável pelas obrigações</summary>
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Identifique a natureza do serviço,o estabelecimento a partir do qual o prestador o fornece e o estabelecimento estável ou domicílio do adquirente. Nacionalidade não substitui estes factos.</p>
<p>Adquirente no estrangeiro:conferir6(8). Prestador estrangeiro e adquirente nacional:6(7)inclui sujeitos passivos2(1)a/b que realizem exclusivamente operações isentas sem direito à dedução. Não exigir direito à dedução como condição de localização. Digital,imóveis e outras regras próprias têm os seus percursos.</p>
<p>A classificação não concede isenção nem calcula IVA. A identificação do responsável explica a regra aplicável;não apresenta declaração nem inicia pagamento.</p>
<p>{listedServiceSource}</p>{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newListedService(crypto.randomUUID())]})}>Acrescentar serviço enumerado</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Serviço por identificar'} — {row.period||'mês por indicar'}</summary>
<div className="grid gap-3 md:grid-cols-2">
<label>Tratamento do serviço<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,switchListedTreatment(row,e.target.value))}><option value="">Por indicar</option>{listedServiceTreatments.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 ListedServiceRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
{([['description','Serviço identificado'],['operationRef','Referência da operação / pressuposto'],['operationDate','Data do serviço / prevista'],['period','Mês do serviço'],['documentRef','Documento da operação / referência da previsão'],['documentDate','Data do documento / prevista'],['subjectRef','Objecto:direito,campanha,serviço,pessoal,bens ou contrato'],['useRef','Destinatário e utilização / pressuposto'],['scope','Âmbito:serviço,operação,período e condições'],['evidence','Fundamento factual / pressupostos'],['providerPlace','Sede,EP ou domicílio a partir do qual o prestador fornece'],['providerRef','Referência do estabelecimento prestador'],['buyerPlace','Estabelecimento estável ou domicílio do adquirente'],['buyerRef','Referência do estabelecimento ou domicílio adquirente']] 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>)}
{([['providerCountry','País do estabelecimento prestador'],['buyerCountry','País do estabelecimento ou domicílio adquirente']] as const).map(([field,label])=><label key={field}>{label}<select className="block rounded border p-2" value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}><option value="">Por indicar</option><option value="MZ">Moçambique</option><option value="foreign">Estrangeiro</option></select></label>)}
{row.providerCountry==='foreign'&&row.buyerCountry==='MZ'&&<>
<label>Qualidade do adquirente2(1)<select className="block rounded border p-2" value={row.recipientType} onChange={e=>change(row.id,{recipientType:e.target.value})}><option value="">Por esclarecer</option><option value="a">Alínea a — actividade independente habitual,incluindo operações isentas sem dedução</option><option value="b">Alínea b — operação independente nos pressupostos legais</option><option value="other">Não corresponde às alíneas a/b</option></select></label>
<label>Referência da qualidade fiscal do adquirente<input className="block rounded border p-2" value={row.recipientRef} onChange={e=>change(row.id,{recipientRef:e.target.value})}/></label>
</>}
{row.providerCountry==='MZ'&&row.buyerCountry==='MZ'&&<>
<label>Prestador sujeito passivo no regime geral<select className="block rounded border p-2" value={row.providerTaxable} onChange={e=>change(row.id,{providerTaxable:e.target.value})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
<label>Referência da qualidade fiscal do prestador<input className="block rounded border p-2" value={row.providerTaxRef} onChange={e=>change(row.id,{providerTaxRef:e.target.value})}/></label>
</>}
{['intermediation_service','abstention_service'].includes(row.treatment)&&<>
<label>Alínea6(7)do serviço,direito ou actividade subjacente<select className="block rounded border p-2" value={row.targetClause} onChange={e=>change(row.id,{targetClause:e.target.value})}><option value="">Por indicar</option>{listedServiceTreatments.filter(t=>t.id!==row.treatment).map(t=><option key={t.id} value={t.clause}>{t.clause} — {t.label}</option>)}</select></label>
<label>Referência do subjacente e relação da intermediação/obrigação<input className="block rounded border p-2" value={row.targetRef} onChange={e=>change(row.id,{targetRef:e.target.value})}/></label>
</>}
</div>
<p>Mudar tratamento conserva dados e referências ocultos,mas exige rever os factos próprios. Campos sem aplicação ficam ocultos e conservados.</p>
<fieldset><legend>Factos próprios deste tratamento</legend>{[...listedServiceConditions,...ownListedConditions(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(recordListedServices(draft,book));}catch(cause){setError(String(cause));}}}>Classificar serviços e guardar versão</button>}
{book.result&&<><p role="status">{listedServiceCurrent(draft,book)?'Resultados correspondentes às entradas guardadas.':'Entradas alteradas;resultados históricos conservados. Reveja e classifique novamente.'} {book.history?.length??0} versões anteriores. Localização e responsável não determinam isenção,IVA ou dedução/campo11.</p><dl>{listedServiceReportRows(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>;
}
@@ -17,6 +17,7 @@ import { SimulationValueReview } from './simulation-value-review';
import { SimulationServiceNormal } from './simulation-service-normal';
import { SimulationGoodsLocation } from './simulation-goods-location';
import { SimulationServiceLocation } from './simulation-service-location';
import { SimulationListedServices } from './simulation-listed-services';
import { SimulationOfferPreparations } from './simulation-offer-preparations';
import { SimulationOperations } from './simulation-operations';
@@ -112,6 +113,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
<SimulationServiceNormal draft={draft} onChange={onChange} />
<SimulationGoodsLocation draft={draft} onChange={onChange} />
<SimulationServiceLocation draft={draft} onChange={onChange} />
<SimulationListedServices draft={draft} onChange={onChange} />
<SimulationOfferPreparations draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>;
+81
View File
@@ -0,0 +1,81 @@
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
export const listedServiceVersion='listed-nondigital-services/1';
export const listedServiceSource='CIVA2(1)a/b,2(2),6(3),6(7)a–j e6(8),Lei13/2016PDF6–8/p1426(264–266);obrigações23(1)/(2),25(4),26(1–3),2016PDF16–17;Lei10/2025PDF12mantém alíneasa–j ealtera6(7)/(8)/(9),PDF13art23(2)excluik,PDF14mantém26(1–3),adita26(4)digital ealtera33(2). Recorte2026,não digital. Regime digital/P121 eoutros6(4)/(5)seguem percurso próprio. Localização/obrigações não confirmam isenção,taxa,IVA,dedução/campo11 ou montante a pagar.';
export const listedServiceTreatments=[
{id:'rights_service',branch:'P111',clause:'a',label:'P111 — Direitos de autor,patentes,licenças e marcas'},
{id:'advertising_service',branch:'P112',clause:'b',label:'P112 — Publicidade'},
{id:'telecom_service',branch:'P113',clause:'c',label:'P113 — Telecomunicações'},
{id:'consulting_service',branch:'P114',clause:'d',label:'P114 — Consultoria,engenharia,advocacia,economia e contabilidade'},
{id:'data_service',branch:'P115',clause:'e',label:'P115 — Tratamento de dados e informações'},
{id:'finance_service',branch:'P116',clause:'f',label:'P116 — Banca,finanças,seguro e resseguro'},
{id:'staff_service',branch:'P117',clause:'g',label:'P117 — Colocação de pessoal'},
{id:'intermediation_service',branch:'P118',clause:'h',label:'P118 — Intermediação em nome e por conta de outrem'},
{id:'abstention_service',branch:'P119',clause:'i',label:'P119 — Obrigação de não exercer actividade ou direito'},
{id:'movable_lease_service',branch:'P120',clause:'j',label:'P120 — Locação de bens móveis corpóreos e financeira'},
] as const;
export const listedServiceConditions=[['serviceSupply','Está identificada uma prestação de serviços desta operação e período'],['nonDigital','A natureza foi conferida:este caso não é bem/serviço digital abrangido por6(7)k/6(9)/26(4)'],['placeRulesExcluded','Foram conferidas as regras de imóveis,trabalhos em móveis,actividades e transporte6(4)/(5):não exigem outro percurso neste caso']] as const;
export const listedOwnConditions={
rights_service:[['rightsNature','Cessão ou autorização para utilizar direitos de autor,licenças,marcas de fábrica/comércio ou outros direitos análogos identificados']],
advertising_service:[['advertisingNature','Serviço de publicidade identificado']],
telecom_service:[['telecomNature','Serviço de telecomunicações identificado']],
consulting_service:[['consultingNature','Consultores,engenheiros,advogados,economistas,contabilistas ou gabinetes de estudo,organização,investigação e desenvolvimento ou domínio similar identificado']],
data_service:[['dataNature','Tratamento de dados ou fornecimento de informações identificado']],
finance_service:[['financeNature','Operação bancária,financeira,de seguro ou resseguro identificada;localização não determina a eventual isenção']],
staff_service:[['staffNature','Colocação de pessoal identificada']],
intermediation_service:[['intermediaryNature','Intermediário intervém em nome e por conta de outrem no fornecimento de serviço enumerado neste número']],
abstention_service:[['abstentionNature','Obrigação de não exercer,mesmo parcialmente,actividade profissional ou direito mencionado nas alíneas deste número']],
movable_lease_service:[['leaseNature','Locação de bens móveis corpóreos,incluindo locação financeira,com bens e contrato identificados']],
} as const;
export function ownListedConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return listedOwnConditions[treatment as keyof typeof listedOwnConditions]??[];}
type Choice=''|'yes'|'no';type Condition=typeof listedServiceConditions[number][0];
export type ListedServiceRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationDate:string;operationRef:string;documentRef:string;documentDate:string;providerPlace:string;providerCountry:string;providerRef:string;buyerPlace:string;buyerCountry:string;buyerRef:string;recipientType:string;recipientRef:string;providerTaxable:string;providerTaxRef:string;useRef:string;subjectRef:string;targetClause:string;targetRef:string;scope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';territory:'national'|'outside'|null;responsible:'acquirer'|'provider'|'outside'|'indeterminate';obligationReason:string;baseAmount:null;vat:null;rate:null;reason:string};
export type ListedServiceCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ListedServiceRow[];rows:LineResult[]};
export type ListedServiceBook={rows:ListedServiceRow[];result?:ListedServiceCalculation;history?:ListedServiceCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{listedServices?:Record<string,ListedServiceBook>};
export function newListedService(id:string):ListedServiceRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',providerPlace:'',providerCountry:'',providerRef:'',buyerPlace:'',buyerCountry:'',buyerRef:'',recipientType:'',recipientRef:'',providerTaxable:'',providerTaxRef:'',useRef:'',subjectRef:'',targetClause:'',targetRef:'',scope:'',evidence:'',conditions:{}};}
export function switchListedTreatment(row:ListedServiceRow,treatment:string):ListedServiceRow{return{...row,treatment,conditions:Object.fromEntries(listedServiceConditions.map(([key])=>[key,row.conditions[key]??'']))};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.listedServices!==undefined&&(!store.listedServices||typeof store.listedServices!=='object'||Array.isArray(store.listedServices)))throw new Error('Serviços enumerados não recuperáveis;originais preservados.');
const fields=Object.keys(newListedService('')).filter(key=>key!=='conditions');
for(const book of Object.values(store.listedServices??{}))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 ListedServiceRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de serviços enumerados não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function listedServiceBook(draft:Record<string,unknown>):ListedServiceBook{return readStore(draft).listedServices?.[acquisitionScope(draft)]??{rows:[]};}
export function saveListedServiceBook(draft:Record<string,unknown>,book:ListedServiceBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,listedServices:{...current.listedServices,[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 evaluateListedService(row:ListedServiceRow):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',territory:null,responsible:'indeterminate',obligationReason:'Responsável indeterminado;sem obrigação ou montante presumido.',baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
const rule=listedServiceTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique o serviço enumerado,não digital,e a natureza efectiva da operaçã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=[...listedServiceConditions,...ownListedConditions(row.treatment)];
if(required.some(([key])=>row.conditions[key]==='no'))return stop('Este percurso não se aplica:conferir natureza digital,regra territorial própria ou tratamento indicado;nenhum IVAzero/isenção presumido.','not_applicable');
if(required.some(([key])=>row.conditions[key]!=='yes'))return stop('Natureza ou regra própria por esclarecer;desconhecido não é fora do território,isenção ou IVAzero.');
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.scope.trim()||!row.evidence.trim()||!row.useRef.trim()||!row.subjectRef.trim())return stop('Identifique serviço,objecto,destinatário/utilização,operação,documento/data,âmbito e factos ou pressupostos expressos;reutilize referências,sem certificado/aprovador novo.');
if(['intermediation_service','abstention_service'].includes(row.treatment)&&(!'abcdefghij'.includes(row.targetClause)||row.targetClause.length!==1||row.targetClause===rule.clause||!row.targetRef.trim()))return stop('Identifique o serviço/direito/actividade subjacente e a sua alínea de6(7),com referência;digitalk exige percurso próprio,não presumir pelo título.');
if(!['MZ','foreign'].includes(row.buyerCountry)||!row.buyerPlace.trim()||!row.buyerRef.trim())return stop('Identifique o estabelecimento estável ou domicílio do adquirente,país e referência correspondentes,não apenas nacionalidade.');
if(row.buyerCountry==='foreign')return{...result,status:'determined',territory:'outside',responsible:'outside',obligationReason:'Artigo6(8):fora do âmbito territorial de Moçambique;sem obrigação nacional calculada por este percurso ou imposto estrangeiro presumido.',reason:`${rule.branch}:6(8),Lei10/2025PDF12,adquirente estabelecido ou domiciliado no estrangeiro;serviço6(7)${rule.clause} não tributável neste âmbito,ainda que prestador tenha sede/EP/domicílio nacional. Não é concessão de isenção nem IVAzero;sem dedução/campo11 ou imposto estrangeiro presumido.`};
if(!['MZ','foreign'].includes(row.providerCountry)||!row.providerPlace.trim()||!row.providerRef.trim())return stop('Identifique sede/EP/domicílio do prestador a partir do qual o serviço é prestado,país e referência.');
if(row.providerCountry==='foreign'){
if(row.recipientType==='other')return stop('Adquirente não corresponde a2(1)a/b:6(7)não demonstrado por este percurso;conferir outra regra aplicável,sem classificar automaticamente fora do território.','not_applicable');
if(!['a','b'].includes(row.recipientType)||!row.recipientRef.trim())return stop('Identifique qualidade do adquirente2(1)a/b e referência;inclui quem pratica exclusivamente operações isentas sem direito à dedução,não exigir direito à dedução.');
return{...result,status:'determined',territory:'national',responsible:'acquirer',obligationReason:'Adquirente sujeito passivo2(2)/6(7)/23(2),redacção2025:entrega declaração33(2)eimposto exigível,nos termos a regulamentar. Não se aplica26(4)digital,nem prazo32(1)c digital a este caso. Eventual isenção/taxa/montante eprocedimento de pagamento não são apurados aqui.',reason:`${rule.branch}:6(7)${rule.clause},prestador estrangeiro sem sede/EP/domicílio nacional a partir do qual presta;adquirente2(1)${row.recipientType} comEP/domicílio nacional,mesmo que pratique exclusivamente operações isentas sem dedução. Localização nacional não confirma isenção,IVA ou dedução/campo11.`};
}
const liable=row.providerTaxable==='yes'&&!!row.providerTaxRef.trim();
return{...result,status:'determined',territory:'national',responsible:liable?'provider':'indeterminate',obligationReason:liable?'Prestador sujeito passivo no regime geral:23(1),sem activação automática de cobrança;eventual isenção,taxa,IVA eapuramento independentes.':'Localização6(3)determinada;qualidade fiscal do prestador ou referência por esclarecer,sem atribuir obrigação automaticamente.',reason:`${rule.branch}:6(3),prestador comsede/EP/domicílio nacional a partir do qual presta eadquirente nacional,sem regras6(4)/(5)ou digitais aplicáveis. Qualidade do adquirente2(1)a/b não é pré-condição deste percurso doméstico. Localização não concede isenção nem calculaIVA/dedução.`};
}
function key(draft:Record<string,unknown>,book:ListedServiceBook):string{return JSON.stringify({version:listedServiceVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},rows:book.rows});}
export function listedServiceCurrent(draft:Record<string,unknown>,book:ListedServiceBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordListedServices(draft:Record<string,unknown>,book:ListedServiceBook):ListedServiceBook{
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:ListedServiceCalculation={version:listedServiceVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:listedServiceSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateListedService(row))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveListedServiceBook(draft,next);return next;
}
export function listedServiceReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=listedServiceBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>{const line=result.rows[index];return['IVA mensal',`Serviço ${listedServiceTreatments.find(t=>t.id===row.treatment)?.branch??'enumerado'} — ${row.period} — ${row.id}`,`${row.description};classificação territorial: ${line.territory==='national'?'Moçambique':line.territory==='outside'?'Fora do território nacional':'Indeterminada'};IVA: Indeterminado;${line.reason};responsável: ${line.responsible==='acquirer'?'Adquirente':line.responsible==='provider'?'Prestador':line.responsible==='outside'?'Fora deste âmbito':'Indeterminado'};${line.obligationReason}`,`${result.opportunity};${result.location};${listedServiceCurrent(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'};${listedServiceTreatments.find(t=>t.id===row.treatment)?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};prestador ${row.providerPlace},${row.providerCountry},${row.providerRef};adquirente ${row.buyerPlace},${row.buyerCountry},${row.buyerRef};${row.buyerCountry==='MZ'&&row.providerCountry==='foreign'?`qualidade adquirente2(1)${row.recipientType},${row.recipientRef};`:''}${row.buyerCountry==='MZ'&&row.providerCountry==='MZ'?`qualidade prestador ${row.providerTaxable},${row.providerTaxRef};`:''}objecto ${row.subjectRef};destinatário/utilização ${row.useRef};${['intermediation_service','abstention_service'].includes(row.treatment)?`subjacente6(7)${row.targetClause},${row.targetRef};`:''}âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...listedServiceConditions,...ownListedConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};sem isenção/IVA/campo11/totaisModeloA.`];});}
catch{return[['IVA mensal','Serviços enumerados','Dados não recuperáveis','Originais preservados;sem classificação por defeito.']];}
}
+2
View File
@@ -16,6 +16,7 @@ import { valueReviewReportRows } from './iva-value-review.ts';
import { serviceNormalReportRows } from './iva-service-normal.ts';
import { goodsLocationReportRows } from './iva-goods-location.ts';
import { serviceLocationReportRows } from './iva-service-location.ts';
import { listedServiceReportRows } from './iva-listed-services.ts';
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
import { salesReportRows } from './iva-operations.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
@@ -642,6 +643,7 @@ export function buildProjectDossierRows(
rows.push(...serviceNormalReportRows(draft));
rows.push(...goodsLocationReportRows(draft));
rows.push(...serviceLocationReportRows(draft));
rows.push(...listedServiceReportRows(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 IvaListedServicesDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_listed_services_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-g9-nondigital.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:350', $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['listedServices']['opportunity:350'];
$this->assertCount(30, $book['rows']);
$this->assertCount(10,array_unique(array_column($book['rows'],'treatment')));
$this->assertSame(array_merge(...array_fill(0,10,['national','outside','national'])),array_column($book['result']['rows'],'territory'));
$this->assertSame(array_merge(...array_fill(0,10,['acquirer','outside','provider'])),array_column($book['result']['rows'],'responsible'));
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->assertNull($book['history'][0]['rows'][0]['baseAmount']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g9-nondigital-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, ['listedServices' => 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-g9-nondigital-20261024/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,33 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newListedService,switchListedTreatment,listedServiceConditions,ownListedConditions,listedServiceTreatments,evaluateListedService,recordListedServices,saveListedServiceBook,listedServiceBook,listedServiceCurrent,listedServiceReportRows} from '../../resources/js/lib/iva-listed-services.ts';
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-g9-nondigital-preserved.json','utf8'));
const draft={...frozen,context_opportunity:'opportunity:350',project_title:'SYNTHETIC QA — G9 não digital / Manica'};
function row(t,route='inbound') {const rule=listedServiceTreatments.find(x=>x.id===t);return {...newListedService(t+'-'+route),treatment:t,description:'SYNTHETIC QA '+rule.branch+' '+route,period:'2026-12',origin:'projected',operationDate:'2026-12-12',operationRef:'SYNTHETIC QA operação',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',scope:'SYNTHETIC QA operação/serviço/dezembro2026',evidence:'SYNTHETIC QA pressupostos expressos',providerPlace:'SYNTHETIC QA estabelecimento prestador',providerCountry:route==='inbound'?'foreign':'MZ',providerRef:'SYNTHETIC QA referência estabelecimento prestador',buyerPlace:'SYNTHETIC QA adquirente EP/domicílio',buyerCountry:route==='outbound'?'foreign':'MZ',buyerRef:'SYNTHETIC QA referência adquirente',recipientType:'a',recipientRef:'SYNTHETIC QA SP2(1)a,inclui actividade exclusivamente isenta sem dedução',providerTaxable:'yes',providerTaxRef:'SYNTHETIC QA SPprestador',subjectRef:'SYNTHETIC QA objecto próprio '+rule.branch,useRef:'SYNTHETIC QA destinatário/utilização e âmbito',targetClause:'a',targetRef:'SYNTHETIC QA relação comdireito6(7)a',conditions:Object.fromEntries([...listedServiceConditions,...ownListedConditions(t)].map(([key])=>[key,'yes']))};}
for(const rule of listedServiceTreatments)test(rule.branch+' ownnature,three routes,quality and no deduction prerequisite',()=>{
for(const route of ['inbound','outbound','domestic']){
const r=row(rule.id,route),result=evaluateListedService(r);assert.equal(result.status,'determined');assert.equal(result.territory,route==='outbound'?'outside':'national');assert.equal(result.responsible,route==='outbound'?'outside':route==='inbound'?'acquirer':'provider');assert.deepEqual(['baseAmount','vat','rate'].map(k=>result[k]),[null,null,null]);
for(const [key] of [...listedServiceConditions,...ownListedConditions(rule.id)]){const missing=structuredClone(r);delete missing.conditions[key];assert.equal(evaluateListedService(missing).status,'indeterminate');missing.conditions[key]='no';assert.equal(evaluateListedService(missing).status,'not_applicable');assert.equal(evaluateListedService(missing).territory,null);}
for(const field of ['operationRef','documentRef','scope','evidence','subjectRef','useRef','buyerRef','buyerPlace','buyerCountry'])assert.equal(evaluateListedService({...r,[field]:''}).status,'indeterminate',field);
assert.equal(evaluateListedService({...r,operationDate:'2026-11-12'}).status,'indeterminate');assert.equal(evaluateListedService({...r,documentDate:'2026-02-30'}).status,'indeterminate');assert.equal(evaluateListedService({...r,period:'2025-12'}).status,'indeterminate');
}
const r=row(rule.id);for(const field of ['providerCountry','providerPlace','providerRef','recipientType','recipientRef'])assert.equal(evaluateListedService({...r,[field]:''}).status,'indeterminate',field);
assert.equal(evaluateListedService({...r,recipientType:'b'}).responsible,'acquirer');assert.equal(evaluateListedService({...r,recipientType:'other'}).status,'not_applicable');
assert.equal(evaluateListedService({...r,deductionRight:'no',exemptOnly:'yes'}).territory,'national');assert.match(evaluateListedService(r).obligationReason,/23\(2\)/);assert.match(evaluateListedService(r).obligationReason,/Não se aplica26\(4\)/);
const domestic=row(rule.id,'domestic');assert.equal(evaluateListedService({...domestic,recipientType:'',recipientRef:''}).responsible,'provider');assert.equal(evaluateListedService({...domestic,providerTaxable:''}).territory,'national');assert.equal(evaluateListedService({...domestic,providerTaxable:''}).responsible,'indeterminate');
const outbound=row(rule.id,'outbound');assert.equal(evaluateListedService({...outbound,providerCountry:'',providerPlace:'',providerRef:'',recipientType:''}).territory,'outside');assert.match(evaluateListedService(outbound).reason,/6\(8\)/);
});
test('P118/P119 require identified underlying service/right and preserve hidden references',()=>{
for(const t of ['intermediation_service','abstention_service']){const r=row(t);for(const targetClause of ['',t==='intermediation_service'?'h':'i','k','aa'])assert.equal(evaluateListedService({...r,targetClause}).status,'indeterminate');assert.equal(evaluateListedService({...r,targetRef:''}).status,'indeterminate');}
const r=row('intermediation_service');const next=switchListedTreatment(r,'advertising_service');assert.equal(next.targetRef,r.targetRef);assert.equal(next.recipientRef,r.recipientRef);assert.equal(next.conditions.advertisingNature,undefined);next.conditions.advertisingNature='yes';assert.equal(evaluateListedService({...next,targetClause:'',targetRef:''}).status,'determined');
});
test('Ten branches persist together with previous books,immutable history and report',()=>{
const rows=listedServiceTreatments.flatMap(t=>['inbound','outbound','domestic'].map(route=>row(t.id,route)));
let book=recordListedServices(draft,{rows});book=recordListedServices(draft,book);assert.equal(book.history.length,1);assert.notEqual(book.result.inputs,book.rows);
const next={...draft,modelo_a_snapshot:saveListedServiceBook(draft,book)};assert.deepEqual(listedServiceBook(next),book);assert.ok(listedServiceCurrent(next,book));assert.equal(listedServiceCurrent({...next,context_opportunity:'other'},book),false);assert.equal(listedServiceCurrent({...next,currency:'USD',customer_nationality:'foreign'},book),true);
assert.equal(listedServiceReportRows(next).length,30);for(const t of listedServiceTreatments)assert.ok(listedServiceReportRows(next).some(r=>r[1].includes(t.branch)));assert.ok(listedServiceReportRows(next).some(r=>r[2].includes('responsável: Adquirente')));
const store=JSON.parse(next.modelo_a_snapshot);delete store.listedServices;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));
assert.throws(()=>recordListedServices(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>listedServiceBook({...next,modelo_a_snapshot:JSON.stringify({...JSON.parse(next.modelo_a_snapshot),listedServices:[]})}));
if(process.env.IVA_G9_FIXTURE)fs.writeFileSync(process.env.IVA_G9_FIXTURE,JSON.stringify(next,null,2)+'\n');
});