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 { return ( categoryConditions[category as keyof typeof categoryConditions] ?? [] ); } type Choice = '' | 'yes' | 'no'; 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>; }; 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 & { regulatedSecondhands?: Record; }; 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): 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): SecondhandBook { return ( readStore(draft).regulatedSecondhands?.[acquisitionScope(draft)] ?? { rows: [], } ); } export function saveSecondhandBook( draft: Record, 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, 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, book: SecondhandBook, ): boolean { return !!book.result && book.result.sourceKey === key(draft, book); } export function recordSecondhands( draft: Record, 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, ): 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.', ], ]; } }