85 lines
17 KiB
TypeScript
85 lines
17 KiB
TypeScript
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||
|
||
export const secondhandVersion='secondhand-individual-margin/1';
|
||
export const secondhandSource='CIVA3(3)g/3(6),15(2)f/g,Lei13/2016PDF7/13;Lei22/2022PDF3mantém15f/g efixa17(1)16%;Lei10/2025não altera15/17,revoga regimes35/42. RIVADecreto7/2008arts47–49,BR16-I16/04/2008PDF8–9/p106–107:revenda porbem,margem positiva dividida por1.16 ebase arredondada à unidade;sem compensar perdas entrebens. Leilão:facturado−líquidocomitente−IVApróprio identificado,líquido=adjudicação−comissão. ClassificaçãoP125 separada da baseP092/P093. Sem nova dedução/campo11,isenção automática,IVA calculado ou totaisModeloA.';
|
||
export const secondhandTreatments=[{id:'reseller_margin',branch:'P092',label:'Revendedor — margem individual'},{id:'auction_margin',branch:'P093',label:'Organizador emnome próprio — venda em leilão'}] as const;
|
||
export const secondhandCategories=[{id:'used_goods',label:'Bem móvel usado3(6)a'},{id:'art',label:'Objecto de arte3(6)b'},{id:'collection',label:'Objecto de colecção3(6)c'},{id:'antique',label:'Antiguidade3(6)d'}] as const;
|
||
export const secondhandConditions=[['goodsIdentity','Bem individual identificado,com aquisição,transmissão e referências correspondentes'],['operatorRole','Sujeito passivo revendedor ou organizador de leilão identificado,no exercício da actividade epercurso escolhido']] as const;
|
||
export const categoryConditions={
|
||
used_goods:[['reusableUsed','Bem móvel usado susceptível de reutilização no estado ou após reparação'],['notRenewedTransformed','Não é renovado nem transformado:renovado se materiais de reparação excedem aquisição+mão-de-obra;transformado se reparação modifica características essenciais'],['notPreciousExcluded','Não são pedras/metais preciosos excluídos3(6)a;moedas/artefactos desses materiais não são excluídos só por esse motivo']],
|
||
art:[['artistOriginal','Obra da autoria do artista,no âmbito3(6)b;sem desenho industrial,ourivesaria ou joalharia']],
|
||
collection:[['collectionNature','Objecto de colecção no âmbito3(6)c,com natureza e características identificadas']],
|
||
antique:[['notArtCollection','Não é objecto de arte ou de colecção já classificado nessas categorias']],
|
||
} as const;
|
||
export function secondhandCategoryConditions(category:string):ReadonlyArray<readonly[string,string]>{return categoryConditions[category as keyof typeof categoryConditions]??[];}
|
||
type Choice=''|'yes'|'no';type Condition=typeof secondhandConditions[number][0];
|
||
export type SecondhandRow={id:string;treatment:string;category:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationDate:string;operationRef:string;documentRef:string;documentDate:string;goodsRef:string;operatorRef:string;categoryRef:string;artSubtype:string;editionCount:string;ceramicUniqueSigned:string;collectionSubtype:string;postalNotCirculating:string;ageYears:string;sourceType:string;sourceRef:string;domesticPurchase:string;regimeChoice:string;domesticTaxable:string;grossSale:string;purchasePrice:string;purchaseRef:string;grossInvoice:string;hammerPrice:string;principalCommission:string;auctionRef:string;principalRef:string;ownVat:string;ownVatRef:string;scope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
|
||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';classification:'qualified_goods'|'not_applicable'|'indeterminate';baseAmount:string|null;grossMargin:string|null;principalNet:string|null;baseDivisorRate:string|null;vat:null;rate:null;reason:string};
|
||
export type SecondhandCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:SecondhandRow[];rows:LineResult[]};
|
||
export type SecondhandBook={rows:SecondhandRow[];result?:SecondhandCalculation;history?:SecondhandCalculation[]};
|
||
type Store=ReturnType<typeof readAcquisitionStore>&{regulatedSecondhands?:Record<string,SecondhandBook>};
|
||
export function newSecondhand(id:string,currency:string):SecondhandRow{return{id,treatment:'',category:'',description:'',period:'',origin:'projected',currency,operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsRef:'',operatorRef:'',categoryRef:'',artSubtype:'',editionCount:'',ceramicUniqueSigned:'',collectionSubtype:'',postalNotCirculating:'',ageYears:'',sourceType:'',sourceRef:'',domesticPurchase:'',regimeChoice:'',domesticTaxable:'',grossSale:'',purchasePrice:'',purchaseRef:'',grossInvoice:'',hammerPrice:'',principalCommission:'',auctionRef:'',principalRef:'',ownVat:'',ownVatRef:'',scope:'',evidence:'',conditions:{}};}
|
||
export function switchSecondhandTreatment(row:SecondhandRow,treatment:string):SecondhandRow{return{...row,treatment,conditions:{...row.conditions,regimeConfirmed:'',compositionConfirmed:''}};}
|
||
export function switchSecondhandCategory(row:SecondhandRow,category:string):SecondhandRow{return{...row,category,conditions:Object.fromEntries(secondhandConditions.map(([key])=>[key,row.conditions[key]??'']))};}
|
||
|
||
function readStore(draft:Record<string,unknown>):Store{
|
||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if(store.regulatedSecondhands!==undefined&&(!store.regulatedSecondhands||typeof store.regulatedSecondhands!=='object'||Array.isArray(store.regulatedSecondhands)))throw new Error('Afectações de bens não recuperáveis;originais preservados.');
|
||
const fields=Object.keys(newSecondhand('','')).filter(key=>key!=='conditions');
|
||
for(const book of Object.values(store.regulatedSecondhands??{}))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 SecondhandRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de afectações de bens não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
||
return store;
|
||
}
|
||
export function secondhandBook(draft:Record<string,unknown>):SecondhandBook{return readStore(draft).regulatedSecondhands?.[acquisitionScope(draft)]??{rows:[]};}
|
||
export function saveSecondhandBook(draft:Record<string,unknown>,book:SecondhandBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,regulatedSecondhands:{...current.regulatedSecondhands,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||
function decimal(raw:unknown):bigint|null{if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[whole,fraction='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(fraction.padEnd(6,'0'));}
|
||
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
|
||
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 evaluateSecondhand(row:SecondhandRow,currency:unknown):LineResult{
|
||
const result:LineResult={id:row.id,status:'indeterminate',classification:'indeterminate',baseAmount:null,grossMargin:null,principalNet:null,baseDivisorRate:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||
if(!secondhandTreatments.some(t=>t.id===row.treatment)||!secondhandCategories.some(c=>c.id===row.category))return stop('Identifique revendedor/leilão e categoria do bem;sem regime ou base por defeito.');
|
||
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 natureza efectiva/projectada dos dados.');
|
||
const required=[...secondhandConditions,...secondhandCategoryConditions(row.category)];if(required.some(([k])=>row.conditions[k]==='no'))return{...stop('Definição/estatuto deste percurso não correspondem aos factos;rever classificação3(6),sem basezero ou isenção presumida.','not_applicable'),classification:'not_applicable'};
|
||
if(required.some(([k])=>row.conditions[k]!=='yes'))return stop('Natureza do bem ou estatuto do operador por esclarecer;desconhecido não é zero.');
|
||
if(![row.description,row.operationRef,row.documentRef,row.goodsRef,row.operatorRef,row.categoryRef,row.scope,row.evidence].every(v=>!!v.trim())||!validDate(row.documentDate))return stop('Identifique bem individual,operador,categoria,operação,documento/data,âmbito e factos ou pressupostos expressos;sem certificado/aprovador novo.');
|
||
if(row.category==='art'){
|
||
if(!['original','print','sculpture','ceramic'].includes(row.artSubtype))return stop('Identifique pintura/desenho original,gravura/estampa/litografia,escultura/estatuária ou cerâmica.');
|
||
if(row.artSubtype==='print'&&(!/^\d+$/.test(row.editionCount)||BigInt(row.editionCount)<1n))return stop('Identifique tiragem da gravura/estampa/litografia.');
|
||
if(row.artSubtype==='print'&&BigInt(row.editionCount)>200n)return{...stop('Tiragem superior a200 não corresponde a esta definição de arte3(6)b.','not_applicable'),classification:'not_applicable'};
|
||
if(row.artSubtype==='ceramic'&&row.ceramicUniqueSigned!=='yes')return stop('Esclareça se a cerâmica é exemplar único executado eassinado pelo artista3(6)b.');
|
||
}
|
||
if(row.category==='collection'&&(!['postal','scientific','historical'].includes(row.collectionSubtype)||(row.collectionSubtype==='postal'&&row.postalNotCirculating!=='yes')))return stop('Identifique colecção postal não emcirculação/nem destinada acircular,colecções/espécimes científicos ou interesse histórico/arqueológico/paleontológico/etnográfico/numismático3(6)c.');
|
||
if(row.category==='antique'){
|
||
const age=decimal(row.ageYears);if(age===null)return stop('Identifique idade emanos do bem.');if(age<=100000000n)return{...stop('A definição de antiguidade3(6)d exige mais de100anos.','not_applicable'),classification:'not_applicable'};
|
||
}
|
||
result.classification='qualified_goods';
|
||
if(row.domesticPurchase!=='yes'||!row.sourceRef.trim())return stop('P125identificado;P092/P093 por esclarecer:origem nacional equalidade do fornecedor/comitente comreferência48(1)/49(1).');
|
||
if(row.sourceType==='legacy_35_42')return stop('P125identificado;referência histórica aos regimes35/42 não activa margem em2026:Lei10/2025revogou essas subsecções. Conferir situação aplicável sem reactivar regime revogado.');
|
||
if(!['private','margin_reseller','exempt_9_12_e'].includes(row.sourceType))return stop('P125identificado;origem elegível particular,revendedor na margem ou transmissão9(12)e não demonstrada. Outro regime permanece por esclarecer.');
|
||
if(row.treatment==='reseller_margin'&&row.regimeChoice==='general')return stop('P125identificado;opção pelo regime geral47(1)não aplica esta fórmula de margem.','not_applicable');
|
||
if(row.treatment==='reseller_margin'&&row.regimeChoice!=='margin')return stop('P125identificado;esclareça opção margem/regime geral do revendedor47(1).');
|
||
if(row.domesticTaxable!=='yes'||row.conditions.regimeConfirmed!=='yes'||row.conditions.compositionConfirmed!=='yes')return stop('P125identificado;base por esclarecer:transmissão interna tributável,natureza/regime elegível ecomposição confirmados;exportação13/48(7)tem percurso próprio.');
|
||
if(currency!=='MZN'||row.currency!==currency)return stop('P125identificado;base fora do recorteMZN ou moeda divergente,sem conversão silenciosa.');
|
||
if(row.treatment==='reseller_margin'){
|
||
const sale=decimal(row.grossSale),purchase=decimal(row.purchasePrice);if(sale===null||purchase===null||!row.purchaseRef.trim())return stop('Identifique contraprestação total devida pelocliente comIVA incluído epreço compra justificado do mesmo bem;desconhecido não é zero.');
|
||
const diff=sale-purchase,positive=diff>0n?diff:0n,denominator=116000000n;const whole=(positive*100n+denominator/2n)/denominator;
|
||
return{...result,status:'determined',baseAmount:money(whole*1000000n),grossMargin:diff<0n?'-'+money(-diff):money(diff),baseDivisorRate:'16%',reason:`P092:48(2)/(3)/(6),margem individual=${money(sale)}−${money(purchase)}=${diff<0n?'-'+money(-diff):money(diff)}MZN;parcela positiva÷1.16,base arredondada à unidade=${money(whole*1000000n)}MZN. ${diff<0n?'Excesso do preço compra registado neste bem,sem compensar outras transmissões;sem margem positiva tributável.':''} Taxa16% utilizada só no divisor da base17(1)/48(6)b;não há IVA ou dedução calculados. Factura com menção IVA-Bens emSegundaMão,sem discriminarIVA48(8)/(9);registos separados48(10)/(11).`};
|
||
}
|
||
const invoice=decimal(row.grossInvoice),hammer=decimal(row.hammerPrice),commission=decimal(row.principalCommission),tax=decimal(row.ownVat);
|
||
if(invoice===null||hammer===null||commission===null||tax===null||![row.auctionRef,row.principalRef,row.ownVatRef].every(v=>!!v.trim()))return stop('Identifique montante facturado,adjudicação,comissão do comitente eIVApróprio relativo à transmissão,comreferências;IVA desconhecido não é zero.');
|
||
if(commission>hammer)return stop('Comissão excede adjudicação;conferir componentes antes de determinar líquido comitente49(3).');
|
||
const net=hammer-commission;if(invoice<net+tax)return stop('Facturado inferior ao líquido comitente+IVAindicado;conferir composição,sem base negativa ou zero substituto.');
|
||
return{...result,status:'determined',baseAmount:money(invoice-net-tax),principalNet:money(net),reason:`P093:15(2)g/49(2)/(3),líquido comitente=${money(hammer)}−${money(commission)}=${money(net)}MZN;base=${money(invoice)}facturado−${money(net)}líquido−${money(tax)}IVApróprio identificado=${money(invoice-net-tax)}MZN. IVAindicado é componente referenciada,não calculada aqui. Factura IVA-Regime específico devendas emleilão,sem discriminarIVA49(5);relatório ao comitente em5dias úteis49(6)/(7),sem calendário de feriados presumido;contas deterceiros49(8). Sem nova dedução/campo11 ou totaisModeloA.`};
|
||
}
|
||
|
||
function key(draft:Record<string,unknown>,book:SecondhandBook):string{return JSON.stringify({version:secondhandVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||
export function secondhandCurrent(draft:Record<string,unknown>,book:SecondhandBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||
export function recordSecondhands(draft:Record<string,unknown>,book:SecondhandBook):SecondhandBook{
|
||
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:SecondhandCalculation={version:secondhandVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:secondhandSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateSecondhand(row,draft.currency))};
|
||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveSecondhandBook(draft,next);return next;
|
||
}
|
||
export function secondhandReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||
try{const book=secondhandBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,i)=>{const line=result.rows[i],rule=secondhandTreatments.find(t=>t.id===row.treatment);return['IVA mensal',`Bem P125/base${rule?.branch??'margem'} — ${row.period} — ${row.id}`,`${row.description};P125:${line.classification==='qualified_goods'?'Categoria/estatuto identificados':line.classification==='not_applicable'?'Percurso não aplicável':'Indeterminado'};base:${line.baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;${line.reason}`,`${result.opportunity};${result.location};${secondhandCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};${rule?.label};categoria ${row.category},${row.categoryRef};critérios ${row.category==='art'?`${row.artSubtype},tiragem ${row.editionCount},cerâmica única/assinada ${row.ceramicUniqueSigned}`:row.category==='collection'?`${row.collectionSubtype},postal fora de circulação ${row.postalNotCirculating}`:row.category==='antique'?`idade ${row.ageYears} anos`:JSON.stringify(row.conditions)};bem ${row.goodsRef};operador ${row.operatorRef};origem ${row.sourceType},${row.sourceRef};operação ${row.operationRef},${row.operationDate};documento ${row.documentRef},${row.documentDate};${row.treatment==='reseller_margin'?`venda comIVA ${row.grossSale};compra ${row.purchasePrice},${row.purchaseRef};margem ${line.grossMargin??'indeterminada'};divisor ${line.baseDivisorRate??'indeterminado'}`:`facturado ${row.grossInvoice};adjudicação ${row.hammerPrice};comissão comitente ${row.principalCommission};líquido ${line.principalNet??'indeterminado'},${row.principalRef};IVAcomponente ${row.ownVat},${row.ownVatRef};leilão ${row.auctionRef}`};âmbito ${row.scope};fundamento ${row.evidence};sem nova dedução/campo11/totaisModeloA.`];});}
|
||
catch{return[['IVA mensal','Bens usados/margem','Dados não recuperáveis','Originais preservados;sem base por defeito.']];}
|
||
}
|