import type { Acquisition, AcquisitionResult } from './iva-acquisitions.ts'; import type { AcquisitionEligibility } from './iva-acquisition-right.ts'; import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts'; import { linkedOperationRight, linkedBeverageDeductionRight, } from './iva-acquisition-right.ts'; import { acquisitionTreatmentIssue } from './iva-acquisition-continuity.ts'; import { decimalFraction, fraction, fractionPresentation, subtractFractions, } from './fiscal-rational.ts'; export const acquisitionBCVersion = 'acquisition-imports-exclusions/1'; export const acquisitionBCSource = 'L2-B/C: CIVA18(1)(a/b),18(2),19,20(1)(a–f)/20(2)/20(3)(a/b),21. Lei13/2016 PDF14–15,p1426(272–273);Lei22/2022 PDF3,p2252(27),art20(2–3);Lei10/2025 PDF13,p2180(13),18(2)/19(2). Bebidas13(2):Lei13/2016 PDF12,p1426(270). MZN/2026,mesmo mês documental;sem pro rata18(6),regularizações ou restituiçãoL3.'; type Choice = '' | 'yes' | 'no'; export const expenseOptions = [ ['ordinary', 'Despesa sem exclusão especial'], [ 'vehicle', 'Viatura de turismo,barco de recreio,helicóptero,avião,moto ou outro veículo', ], ['fuel', 'Combustível normalmente utilizável em viaturas automóveis'], ['travel', 'Transportes e viagens do sujeito passivo/pessoal'], ['lodging_food', 'Alojamento e alimentação'], ['beverages', 'Bebidas,tabaco e recepção'], ['telephone', 'Comunicações telefónicas'], ['luxury', 'Divertimento e luxo'], ['other', 'Outro caso — por enquadrar'], ] as const; export type AcquisitionBC = { version: 1; expense: string; reducedSupply: Choice; evidence: string; facts: Record; fuelKind: '' | 'diesel' | 'other'; dieselUse: | '' | 'heavy_passengers' | 'public_passengers' | 'machine' | 'agriculture' | 'other'; beveragePath: '' | 'deduction' | 'refund'; importer: string; belongs: Choice; ownImportVat: Choice; vatBasis: '' | 'due' | 'exempt' | 'zero' | 'unknown'; vatRef: string; goodsRef: string; source?: { map: string; id: string; fingerprint: string }; }; export function newAcquisitionBC(): AcquisitionBC { return { version: 1, expense: '', reducedSupply: '', evidence: '', facts: {}, fuelKind: '', dieselUse: '', beveragePath: '', importer: '', belongs: '', ownImportVat: '', vatBasis: '', vatRef: '', goodsRef: '', }; } export function acquisitionBCDetails(b: AcquisitionBC): string { const labels: Record = { exigibleInPeriod: 'IVA aduaneiro exigível no período de exercício confirmado pelo documento', tourism: 'Bem abrangido20(1)(a)', saleOrExploit: 'Venda/exploração como objecto da actividade', resale: 'Combustível para revenda', dieselScope: 'Âmbito específico do uso de gasóleo confirmado', commercialTraveller: 'Viajante comercial por conta própria', ownLandline: 'Telefone fixo em nome do sujeito passivo', beverageSpecial: 'Bebidas no âmbito13(2)', onlyDeduction: 'Parcela exclusivamente para dedução', notCoastalFishing: 'Sem provisões de pesca costeira', }; const keys: Record = { vehicle: [ 'tourism', ...(b.facts.tourism === 'yes' ? ['saleOrExploit'] : []), ], fuel: [ 'resale', ...(b.fuelKind === 'diesel' && b.dieselUse !== 'other' ? ['dieselScope'] : []), ], lodging_food: ['commercialTraveller'], telephone: ['ownLandline'], beverages: [ 'beverageSpecial', ...(b.facts.beverageSpecial === 'yes' ? [ ...(b.beveragePath === 'deduction' ? ['onlyDeduction'] : []), 'notCoastalFishing', ] : []), ], }; return [ ...(b.facts.exigibleInPeriod !== undefined ? ['exigibleInPeriod'] : []), ...(keys[b.expense] ?? []), ] .map( (k) => `${labels[k]}:${b.facts[k] === 'yes' ? 'Sim' : b.facts[k] === 'no' ? 'Não' : 'Por indicar'}`, ) .concat( b.expense === 'fuel' ? [ `Combustível:${b.fuelKind === 'diesel' ? 'Gasóleo' : b.fuelKind === 'other' ? 'Outro' : 'Por indicar'}`, `Utilização:${{ heavy_passengers: 'Pesado de passageiros', public_passengers: 'Transporte público licenciado,excepto rent a car', machine: 'Máquina,não veículo matriculado', agriculture: 'Tractor agrícola,emprego exclusivo/predominante', other: 'Outro uso50%', '': 'Por indicar' }[b.dieselUse]}`, ] : [], b.expense === 'beverages' && b.facts.beverageSpecial === 'yes' ? [ `Via:${b.beveragePath === 'deduction' ? 'DeduçãoL2' : b.beveragePath === 'refund' ? 'RestituiçãoL3' : 'Por indicar'}`, ] : [], ) .join('; '); } export function validAcquisitionBC(v: unknown): v is AcquisitionBC { if (!v || typeof v !== 'object' || Array.isArray(v)) return false; const b = v as AcquisitionBC; return ( b.version === 1 && [ 'expense', 'evidence', 'vatRef', 'goodsRef', 'importer', 'fuelKind', 'dieselUse', 'beveragePath', 'vatBasis', ].every((k) => typeof b[k as keyof AcquisitionBC] === 'string') && [b.reducedSupply, b.belongs, b.ownImportVat].every((v) => ['', 'yes', 'no'].includes(v), ) && !!b.facts && typeof b.facts === 'object' && !Array.isArray(b.facts) && Object.values(b.facts).every((v) => ['', 'yes', 'no'].includes(v)) && (!b.source || ['map', 'id', 'fingerprint'].every( (k) => typeof b.source?.[k as keyof typeof b.source] === 'string', )) ); } type ImportSource = { id: string; treatment?: string; description?: string; documentRef?: string; documentDate?: string; period?: string; importDate?: string; importer?: string; }; const importMaps = ['importExemptions', 'objectiveImports', 'remissionImports']; export function acquisitionImportSources(draft: Record) { const store = readAcquisitionStore( draft.modelo_a_snapshot, ) as unknown as Record>; return importMaps.flatMap((map) => (store[map]?.[acquisitionScope(draft)]?.rows ?? []).map((row) => ({ map, row, label: `${row.description || row.id} — ${row.period ?? ''} — ${row.documentRef || 'documento por indicar'}`, })), ); } export function reuseAcquisitionImport( draft: Record, row: Acquisition, map: string, id: string, ): Acquisition { const src = acquisitionImportSources(draft).find( (v) => v.map === map && v.row.id === id, ); if (!src) throw Error('Origem da importação indisponível;dados conservados.'); const bc = row.bc ?? newAcquisitionBC(); return { ...row, description: row.description || src.row.description || '', documentRef: row.documentRef || src.row.documentRef || '', documentDate: row.documentDate || src.row.documentDate || src.row.importDate || '', period: row.period || src.row.period || '', bc: { ...bc, importer: bc.importer || src.row.importer || '', source: { map, id, fingerprint: JSON.stringify(src.row) }, }, }; } export function acquisitionBCSourceKey( draft: Record, rows: Acquisition[], ): string { return rows.some((r) => r.bc) ? JSON.stringify({ version: acquisitionBCVersion, sources: rows .filter((r) => r.bc?.source) .map( (r) => acquisitionImportSources(draft).find( (s) => s.map === r.bc!.source!.map && s.row.id === r.bc!.source!.id, )?.row ?? null, ), }) : ''; } function validDate(value: string) { 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 evaluateAcquisitionBC( row: Acquisition, currency: string, registered: Choice, draft: Record | undefined, raw: AcquisitionResult, partial?: { right: AcquisitionEligibility; noExcludedPayments: Choice }, ): AcquisitionResult { const bc = row.bc!; let right = partial?.right ?? (row.deductionLink && draft ? linkedOperationRight(draft, row.deductionLink) : { status: 'indeterminate' as const, reason: 'Vínculo à operação de destino em falta.', source: acquisitionBCSource, }); const stop = ( reason: string, status: 'eligible' | 'no_right' | 'indeterminate' = 'indeterminate', percent?: number, ): AcquisitionResult => { const exact = percent !== undefined && raw.vat !== null ? decimalFraction(raw.vat) : null; const deductibleExact = exact ? fraction( BigInt(exact.numerator) * BigInt(percent!), BigInt(exact.denominator) * 100n, ) : undefined; return { ...raw, reason, deductible: deductibleExact ? fractionPresentation(deductibleExact).decimal : null, ...(deductibleExact ? { deductibleExact, nonDeductibleExact: subtractFractions( exact!, deductibleExact, ), } : {}), eligibility: { ...right, status, reason, source: acquisitionBCSource + '; ' + right.source, }, }; }; if (!validAcquisitionBC(bc)) return stop('CondiçõesB/C não recuperáveis;sem regra por defeito.'); if ( currency !== 'MZN' || row.sourceCurrency !== currency || !/^2026-(0[1-9]|1[0-2])$/.test(row.period) || raw.vat === null ) return stop(raw.reason || 'IVA/moeda/período em falta.'); if (row.domestic === 'no') { if (!/^\d{1,15}(\.\d{1,6})?$/.test(row.vat.trim())) return stop( 'Indique expressamente o IVA do bilhete;diferença entre custo líquido/bruto não constitui montante aduaneiro identificado.', ); if ( (row.net.trim() && raw.net === null) || (row.gross.trim() && raw.gross === null) ) return stop( 'Valores de custo preenchidos inválidos;conservar e resolver a divergência.', ); } else if (raw.net === null || raw.gross === null) return stop(raw.reason); if ( raw.net !== null && raw.gross !== null && fractionPresentation( subtractFractions( decimalFraction(raw.gross), decimalFraction(raw.net), ), ).decimal !== raw.vat ) return stop( 'Líquido + IVA diverge do bruto declarado;sem substituir valores.', ); const treatmentIssue = acquisitionTreatmentIssue(row.acquisitionTreatment); if (treatmentIssue) return stop(treatmentIssue); if (!['projected', 'effective'].includes(row.origin)) return stop('Indique dados efectivos ou pressupostos de projecção.'); if ( row.sourceMonth !== null && (!row.sourceBasis || row.sourceAmount === null || (row.sourceBasis === 'net' ? raw.net : raw.gross) === null || JSON.stringify(decimalFraction(row.sourceAmount)) !== JSON.stringify( decimalFraction( (row.sourceBasis === 'net' ? raw.net : raw.gross)!, ), )) ) return stop( 'Valor de origem divergente ou por classificar;sem sobrescrita.', ); if ( (row.domestic === 'no' && row.field !== '11') || (row.domestic === 'yes' && row.field === '11') || !['yes', 'no'].includes(row.domestic) ) return stop('Separe aquisição interna08–10 da importação de bens11.'); if (bc.source) { const src = draft ? acquisitionImportSources(draft).find( (s) => s.map === bc.source!.map && s.row.id === bc.source!.id, ) : undefined; if (!src || JSON.stringify(src.row) !== bc.source.fingerprint) return stop( 'OrigemL1 da importação alterada:adopte explicitamente os dados actuais,conservando edições e histórico.', ); if ( [ 'service_in_customs_value', 'accessory_service_first_destination', ].includes(src.row.treatment ?? '') ) return stop( 'Origem é serviço incluído na base aduaneira12(1)(e),não IVA próprio da importação de bens;identifique o bilhete dos bens,sem somar novamente IVA do serviço.', ); } if (registered !== 'yes') return stop( 'Enquadramento do adquirente no regime normal por confirmar.', ); if ( row.allocation === 'no_right' || row.integrity === 'no' || row.exclusion === 'excluded' ) return stop( 'Sem direito pela afectação,exclusão expressa ou impedimento18(3–5) declarado.', 'no_right', 0, ); if (row.allocation === 'mixed' && !partial) return stop( 'Afectação parcial/pro rata depende deD;sem dedução integral presumida.', ); if ( !partial && (!row.deductionLink || row.deductionLink.scopeConfirmed !== 'yes' || !row.deductionLink.allocationRef.trim() || row.allocation !== 'taxable_only') ) return stop( 'Confirme afectação exclusiva e referência à operação ligada.', ); if ( bc.expense === 'beverages' && bc.facts.beverageSpecial === 'yes' && ['deduction', 'refund'].includes(bc.beveragePath) && draft && row.deductionLink ) right = linkedBeverageDeductionRight( draft, row.deductionLink, bc.facts.notCoastalFishing === 'yes', ); if (right.status === 'no_right') return stop(right.reason, 'no_right', 0); if (right.status !== 'eligible') return stop(right.reason); const noExcludedPayments = partial?.noExcludedPayments ?? row.deductionLink?.noExcludedPayments; if (noExcludedPayments === 'no') return stop('Pagamentos15(6)(c)/19(2) excluídos.', 'no_right', 0); if (noExcludedPayments !== 'yes') return stop('Exclusão19(2) por esclarecer.'); if ( !validDate(row.documentDate) || row.documentDate.slice(0, 7) !== row.period ) return stop( 'Documento e exercício no mesmo mês neste incremento;dedução posterior não presumida.', ); if ( !row.documentRef.trim() || row.legalDocument !== 'yes' || row.held !== 'yes' || row.integrity !== 'yes' ) return stop( 'Referência,forma legal,posse e integridade do documento por completar.', ); if (row.domestic === 'no') { if ( row.documentType !== 'customs' || !bc.importer.trim() || !bc.goodsRef.trim() || !bc.vatRef.trim() || bc.belongs !== 'yes' || bc.ownImportVat !== 'yes' ) return stop( 'Bilhete de despacho,importador/titularidade,bens/linha e IVA próprio devido na importação por completar;fornecedor inscrito no IVA interno não é exigido.', ); if (bc.facts.exigibleInPeriod !== 'yes') return stop( 'Exigibilidade do IVA aduaneiro no período de exercício por confirmar pelo documento21(1);não inferida só da emissão do bilhete.Dedução posterior conserva percurso próprio.', ); if (!['due', 'exempt', 'zero'].includes(bc.vatBasis)) return stop( 'IVA da importação desconhecido;não inferir montante da classificaçãoL1 ou calcular direitosL13.', ); if ( ['exempt', 'zero'].includes(bc.vatBasis) && decimalFraction(raw.vat).numerator !== '0' ) return stop( 'IVA positivo incompatível com isenção/zero declarado;resolver divergência sem substituir montante.', ); } else if ( !['invoice', 'equivalent'].includes(row.documentType) || !row.supplier.trim() || row.supplierRegistered !== 'yes' ) return stop( 'Factura/documento equivalente e fornecedor interno inscrito por completar.', ); if (row.domestic === 'yes' && bc.reducedSupply === 'yes') return stop( 'IVA suportado em fornecimento17-A:exclusão20(2),Lei22/2022;distinta do destino19(2).', 'no_right', 0, ); if (row.domestic === 'yes' && bc.reducedSupply !== 'no') return stop( 'Indique se o IVA suportado respeita a fornecimento17-A;sem inferir pela taxa ou valor.', ); if ( !expenseOptions.some(([id]) => id === bc.expense) || bc.expense === 'other' || !bc.evidence.trim() ) return stop( 'Natureza da despesa e fundamento factual/documental por identificar.', ); const f = bc.facts; switch (bc.expense) { case 'ordinary': return stop( 'Elegível pelas condições declaradas18/19;sem exclusão especial20 identificada.', 'eligible', 100, ); case 'vehicle': if (f.tourism === '') return stop( 'Classificação do veículo na definição20(1)(a) em falta.', ); if (f.tourism === 'no') return stop( 'Veículo fora da definição20(1)(a),segundo características/utilização documentadas.', 'eligible', 100, ); if (f.tourism !== 'yes') return stop('Indique se o bem está abrangido por20(1)(a).'); if (f.saleOrExploit === 'yes') return stop( 'Excepção20(3)(a):venda/exploração do bem constitui objecto da actividade;combustível conserva20(1)(b).', 'eligible', 100, ); return f.saleOrExploit === 'no' ? stop( 'Bem abrangido20(1)(a),sem excepção20(3)(a).', 'no_right', 0, ) : stop( 'Venda/exploração como objecto da actividade por esclarecer.', ); case 'fuel': if (f.resale === 'yes') return stop( 'Combustível adquirido para revenda,conforme ressalva20(3)(a);fundamento específico documentado.', 'eligible', 100, ); if (f.resale !== 'no') return stop( 'Destino de revenda do combustível por esclarecer.', ); if (bc.fuelKind === 'other') return stop( 'Combustível normalmente utilizável em viaturas,fora da excepção gasóleo20(1)(b).', 'no_right', 0, ); if (bc.fuelKind !== 'diesel') return stop('Natureza do combustível por indicar.'); if (bc.dieselUse === 'other') return stop( 'Gasóleo20(1)(b):dedução50%,não taxa de IVA nem pro rata geral.', 'eligible', 50, ); if ( [ 'heavy_passengers', 'public_passengers', 'machine', 'agriculture', ].includes(bc.dieselUse) ) { if (f.dieselScope !== 'yes') return stop( 'Confirme o âmbito específico20(1)(b)(i–iv);não basta escolher a categoria.', ); return stop( 'Gasóleo20(1)(b)(i–iv):dedução integral no uso específico confirmado;transporte público exclui rent a car,máquina não é veículo matriculado,tractor agrícola tem emprego exclusivo/predominante.', 'eligible', 100, ); } return stop('Utilização do gasóleo por indicar.'); case 'travel': return stop( 'Transportes/viagens do sujeito passivo e pessoal excluídos20(1)(c).', 'no_right', 0, ); case 'lodging_food': return f.commercialTraveller === 'yes' ? stop( 'Excepção20(3)(b):viajante comercial por conta própria,no quadro da actividade profissional.', 'eligible', 100, ) : f.commercialTraveller === 'no' ? stop( 'Alojamento/alimentação excluídos20(1)(d),sem excepção20(3)(b).', 'no_right', 0, ) : stop( 'Âmbito de viajante comercial por conta própria por esclarecer.', ); case 'telephone': return f.ownLandline === 'yes' ? stop( 'Telefone fixo em nome do sujeito passivo:excepção expressa20(1)(e).', 'eligible', 100, ) : f.ownLandline === 'no' ? stop( 'Comunicações telefónicas fora da excepção20(1)(e).', 'no_right', 0, ) : stop('Telefone fixo e titularidade por esclarecer.'); case 'luxury': return stop( 'Despesa de divertimento/luxo20(1)(f),segundo natureza/montante e exploração normal declarados.', 'no_right', 0, ); case 'beverages': if (bc.facts.beverageSpecial === 'no') return stop( 'Bebidas/tabaco/recepção excluídos20(1)(d),fora do fundamento especial13(2).', 'no_right', 0, ); if ( bc.facts.beverageSpecial === 'yes' && bc.beveragePath === 'refund' && right.reason.startsWith('Via de dedução13(2)') ) return stop( 'Via restituição13(2):linha documental integral escolhida para acompanhamentoL3-E,sem dedução desta parcela. Não representa pedido apresentado,concessão ou recebimento.', 'no_right', 0, ); if ( bc.facts.beverageSpecial === 'yes' && bc.beveragePath === 'deduction' && bc.facts.onlyDeduction === 'yes' && right.reason.startsWith('Via de dedução13(2)') ) return stop( right.reason + ' IVA suportado identificado neste documento;mesma parcela não segue restituiçãoL3.', 'eligible', 100, ); return stop( 'Via especial13(2) por delimitar:dedução ou restituiçãoL3,sem dupla contagem;tabaco/recepção não herdam a excepção de bebidas.', ); } return stop('Tratamento não implementado;rascunho preservado.'); }