import { inputText } from './input-text.ts'; import { irpcSpecialCombinationsVersion, qualifySpecialCombinations, validateSpecialCombinations, specialCombinationsSource, type SpecialCombinationsInput, } from './irpc-special-combinations.ts'; import type { NormalBook, NormalModel } from './irpc-normal.ts'; import { calculateNormalBase } from './irpc-normal-base.ts'; import { calculateNormalAssessment } from './irpc-normal-assessment.ts'; import { calculateNormalDepreciation, type NormalDepreciationModel, } from './irpc-normal-depreciation.ts'; import { addFractions, subtractFractions, decimalFraction, fraction, fractionPresentation, type FiscalFraction, } from './fiscal-rational.ts'; export const irpcSpecialVersion = 'IRPC-normal-adjustments/10'; export const irpcSpecialMonthlyVersion = 'IRPC-normal-adjustments/11'; export const irpcSpecialStartPeriodVersion = 'IRPC-normal-adjustments/12'; export const irpcSpecialCessationPriorVersion = 'IRPC-normal-adjustments/13'; export const specialCessationPriorSource = 'CBF3/9/10/16;CIRPC7(4);Decreto72/2013 art3(3),9(1–3) e10:residente,ano civil2026 ou período curto de cessação com meses completosL6. Activo novo a custo histórico,entrada2025/2026,método constante/taxa normal máxima e duodécimos desde a entrada. Taxa ×1,5,limites contabilizado elegível e saldo fiscal;quotas anteriores e parcela temporariamente tributada documentadas emL6,sem recalcular o passado nem consumir saldos oficiais. Saída do activo só quando declarada,excluindo o mês do evento;cessação não presume transmissão. Sem cumulação.'; export const specialStartPeriodSource = 'CBF3/9/10/16;CIRPC7(4);Decreto72/2013 art3(3)/9(3):período curto por início de actividade de residente em2026. Taxa normal máxima ×1,5 ×meses efectivos/12 reutilizados de L6,sem anualização;activo novo emuso no período,método constante,sem saída/quotas anteriores/cumulação. Limites contabilizado elegível e saldo fiscal;apenas diferença adicional.'; export const specialMonthlySource = 'CBF16 com Decreto72/2013 art9:taxa normal máxima ×1,5 ×duodécimos/12,meses reutilizados do mapaL6. Exercício anual civil2026,entrada emuso no ano,método constante,sem saída/quotas anteriores/cumulação. Dedução limitada ao contabilizado elegível e saldo fiscal;apenas diferença adicional,não nova amortização contabilística.'; export const specialSources = { expenses: 'CIRPC,Lei34/2007,art61(4),PDF17/p768(71):35% sobre encargos não devidamente documentados e despesas confidenciais/ilícitas,sem prejuízo do36(1)g. Exercício2026;sem regra inferida da falta de anexo.', cooperative: 'CIRPC,Lei34/2007,art12/14,PDF5/p768(59),e61(1):cooperativas agrárias,de artesanato e culturais;redução de50% da taxa geral32%=16%;rendimentos sujeitos a retenção excluídos deste benefício. Recorte2026,residente,parcela ordinária integralmente elegível.', acceleration: 'CBF,Lei4/2009,art3/4/9/10/16;compilação oficial APIEX pp66–68/72:incremento50% das taxas normais nos imóveis novos do projecto e,nas mesmas condições,imóveis reabilitados/máquinas e equipamentos industriais ou agro-industriais. Decreto72/2013 art3(3):limite contabilizado e saldo fiscal. Recorte constante pela taxa normal máxima da tabela,primeiro exercício anual2026,sem cumulação presumida.', } as const; export type Qualification = { state: '' | 'known' | 'absent' | 'not_applicable'; eligible: '' | 'yes' | 'no'; from: string; to: string; reference: string; periodReference: string; }; export const newQualification = (): Qualification => ({ state: '', eligible: '', from: '', to: '', reference: '', periodReference: '', }); export type SpecialExpense = { id: string; scheduleId: string; nature: '' | 'undocumented' | 'confidential' | 'illicit' | 'other'; eligible: '' | 'yes' | 'no'; reference: string; }; export type AcceleratedAsset = { id: string; assetId: string; kind: | '' | 'new_project_building' | 'rehabilitated_project_building' | 'industrial_equipment' | 'agroindustrial_equipment' | 'other'; eligible: '' | 'yes' | 'no'; reference: string; }; export type SpecialContinuationInput = { version: 1; period: '' | 'annual' | 'cessation'; reference: string; historyReference: string; monthlyFromStart: '' | 'yes' | 'no'; }; export type SpecialMonthlyInput = { version: 1; reference: string; startPeriod?: { version: 1; reference: string }; continuation?: SpecialContinuationInput; }; export type SpecialInput = { version: 1; combinations?: SpecialCombinationsInput; expenses: { qualification: Qualification; rows: SpecialExpense[] }; cooperative: { qualification: Qualification; kind: '' | 'agrarian' | 'craft' | 'cultural' | 'other'; allIncomeEligible: '' | 'yes' | 'no'; withheldIncome: '' | 'absent' | 'known'; }; acceleration: { qualification: Qualification; cbfRequirements: '' | 'yes' | 'no'; recognition: '' | 'documented' | 'not_required'; recognitionReference: string; noOtherBenefits: '' | 'yes' | 'no'; rows: AcceleratedAsset[]; monthly?: SpecialMonthlyInput; }; }; export const specialCalculationVersion = (i: SpecialInput) => i.combinations ? irpcSpecialCombinationsVersion : i.acceleration.monthly?.continuation ? irpcSpecialCessationPriorVersion : i.acceleration.monthly?.startPeriod ? irpcSpecialStartPeriodVersion : i.acceleration.monthly ? irpcSpecialMonthlyVersion : irpcSpecialVersion; export function specialAccelerationSource(i: SpecialInput) { const source = i.acceleration.monthly?.continuation ? specialCessationPriorSource : i.acceleration.monthly?.startPeriod ? specialStartPeriodSource : i.acceleration.monthly ? specialMonthlySource : specialSources.acceleration; return i.combinations ? source .replace('/cumulação', '') .replace( 'Sem cumulação.', 'Cumulação qualificada no mapa próprio.', ) .replace( 'sem cumulação presumida.', 'cumulação qualificada no mapa próprio.', ) + ' ' + specialCombinationsSource : source; } export const newSpecialInput = (): SpecialInput => ({ version: 1, expenses: { qualification: newQualification(), rows: [] }, cooperative: { qualification: newQualification(), kind: '', allIncomeEligible: '', withheldIncome: '', }, acceleration: { qualification: newQualification(), cbfRequirements: '', recognition: '', recognitionReference: '', noOtherBenefits: '', rows: [], }, }); export const newSpecialExpense = (id: string): SpecialExpense => ({ id, scheduleId: '', nature: '', eligible: '', reference: '', }); export const newAcceleratedAsset = (id: string): AcceleratedAsset => ({ id, assetId: '', kind: '', eligible: '', reference: '', }); const choices = (v: unknown, allowed: string[]) => typeof v === 'string' && allowed.includes(v); export function validateSpecial(i: SpecialInput) { if ( !i || i.version !== 1 || !i.expenses || !i.cooperative || !i.acceleration ) throw Error('Qualificação L7 não recuperável;originais conservados.'); if (i.combinations !== undefined) validateSpecialCombinations(i.combinations); if ( i.acceleration.monthly !== undefined && (!i.acceleration.monthly || i.acceleration.monthly.version !== 1 || typeof i.acceleration.monthly.reference !== 'string') ) throw Error( 'Preparação CBF16 por duodécimos não recuperável;históricos conservados.', ); if ( i.acceleration.monthly?.startPeriod !== undefined && (!i.acceleration.monthly.startPeriod || i.acceleration.monthly.startPeriod.version !== 1 || typeof i.acceleration.monthly.startPeriod.reference !== 'string') ) throw Error( 'Preparação CBF16 para início de actividade não recuperável;históricos conservados.', ); const continuation = i.acceleration.monthly?.continuation; if ( continuation !== undefined && (!continuation || continuation.version !== 1 || !choices(continuation.period, ['', 'annual', 'cessation']) || typeof continuation.reference !== 'string' || typeof continuation.historyReference !== 'string' || !choices(continuation.monthlyFromStart, ['', 'yes', 'no']) || i.acceleration.monthly?.startPeriod !== undefined) ) throw Error( 'Preparação CBF16 de cessação/quotas anteriores não recuperável ou incompatível com início de actividade;históricos conservados.', ); for (const q of [ i.expenses.qualification, i.cooperative.qualification, i.acceleration.qualification, ]) if ( !q || !choices(q.state, ['', 'known', 'absent', 'not_applicable']) || !choices(q.eligible, ['', 'yes', 'no']) || ['from', 'to', 'reference', 'periodReference'].some( (k) => typeof q[k as 'from'] !== 'string', ) ) throw Error('Condição/período L7 não recuperável.'); for (const [rows, keys] of [ [i.expenses.rows, Object.keys(newSpecialExpense(''))], [i.acceleration.rows, Object.keys(newAcceleratedAsset(''))], ] as const) { if ( !Array.isArray(rows) || new Set(rows.map((r) => r.id)).size !== rows.length || rows.some( (r) => !r || keys.some( (k) => typeof (r as unknown as Record)[ k ] !== 'string', ) || !choices(r.eligible, ['', 'yes', 'no']), ) ) throw Error( 'Parcela L7 não recuperável ou identificador repetido.', ); } if ( i.expenses.rows.some( (r) => !choices(r.nature, [ '', 'undocumented', 'confidential', 'illicit', 'other', ]), ) || i.acceleration.rows.some( (r) => !choices(r.kind, [ '', 'new_project_building', 'rehabilitated_project_building', 'industrial_equipment', 'agroindustrial_equipment', 'other', ]), ) ) throw Error('Natureza L7 desconhecida.'); if ( !choices(i.cooperative.kind, [ '', 'agrarian', 'craft', 'cultural', 'other', ]) || !choices(i.cooperative.allIncomeEligible, ['', 'yes', 'no']) || !choices(i.cooperative.withheldIncome, ['', 'absent', 'known']) || !choices(i.acceleration.cbfRequirements, ['', 'yes', 'no']) || !choices(i.acceleration.noOtherBenefits, ['', 'yes', 'no']) || !choices(i.acceleration.recognition, [ '', 'documented', 'not_required', ]) || typeof i.acceleration.recognitionReference !== 'string' ) throw Error('Parâmetro L7 não recuperável.'); } type Status = 'calculated' | 'absent' | 'not_applicable' | 'indeterminate'; type Decision = { status: Status; reason: string; pending: string[] }; const z = () => fraction(0n), mul = (v: FiscalFraction, n: bigint, d = 1n) => fraction(BigInt(v.numerator) * n, BigInt(v.denominator) * d); const cmp = (a: FiscalFraction, b: FiscalFraction) => BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator); const iso = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; export function specialContext(d: Record) { return Object.fromEntries( [ 'context_opportunity', 'context_opportunity_snapshot', 'context_applicability_snapshot', 'context_reuse', 'sector_id', 'sector_id_label', 'province_id', 'province_id_label', 'district_id', 'district_id_label', 'admin_post_id', 'admin_post_id_label', 'locality_id', 'locality_id_label', 'selection_labels', ].map((k) => [k, d[k] ?? null]), ); } export const specialDependency = (d: Record) => JSON.stringify(specialContext(d)); const specialPeriod = (b: NormalBook, m: NormalModel) => b.periodsFx ? { from: b.periodsFx.from, to: b.periodsFx.to, pending: m.periodsFx?.pending ?? [], } : (m.fiscalPeriod ?? { from: b.year + '-01-01', to: b.year + '-12-31', pending: [], }); function qualify(q: Qualification, b: NormalBook, m: NormalModel): Decision { const pending: string[] = [], p = specialPeriod(b, m); if (!q.state) pending.push('Informação em falta:identifique o estado do tratamento.'); if (!q.reference.trim()) pending.push( 'Falta prova/declaração das condições ou da ausência/não aplicação.', ); if ( !iso(q.from) || !iso(q.to) || q.from !== p.from || q.to !== p.to || !q.periodReference.trim() ) pending.push( 'Período do tratamento deve corresponder ao período fiscal documentado,com suporte próprio;sem transpor vigência.', ); if ( b.year !== '2026' || m.currency !== 'MZN' || !b.base?.subjectNuit.match(/^\d{9}$/) || !b.base.identityReference.trim() || !b.scopeReference.trim() || !['resident_general', 'ep_general'].includes(b.scope) || p.pending.length ) pending.push( 'Sujeito/NUIT,âmbito ou período2026 emMZN por reconciliar na base existente.', ); if (q.state === 'known' && !q.eligible) pending.push( 'Condições documentadas por qualificar:sim,não ou informação em falta.', ); if (q.state !== 'known' && q.eligible) pending.push( 'Estado de ausência/não aplicação/informação em falta não admite condição de benefício preenchida.', ); if (pending.length) return { status: 'indeterminate', pending, reason: pending.join(' ') }; const status = q.state === 'absent' ? 'absent' : q.state === 'not_applicable' || q.eligible === 'no' ? 'not_applicable' : 'calculated'; return { status, pending, reason: status === 'absent' ? 'Ausência declarada com suporte;efeito zero neste período.' : status === 'not_applicable' ? 'Tratamento não aplicável/condição negativa documentada;percurso normal conservado.' : 'Condições e período documentados no recorte.', }; } function fail(d: Decision, reason: string) { d.status = 'indeterminate'; d.pending.push(reason); d.reason = d.pending.join(' '); } export function calculateSpecial( draft: Record, b: NormalBook, m: NormalModel, source: Parameters[1], ) { const i = b.special!; validateSpecial(i); const expenseDecision = qualify(i.expenses.qualification, b, m), coopDecision = qualify(i.cooperative.qualification, b, m), accDecision = qualify(i.acceleration.qualification, b, m); const combinations = i.combinations ? qualifySpecialCombinations( i.combinations, accDecision.status === 'calculated' && i.acceleration.rows.some((r) => r.eligible === 'yes'), ) : undefined; if (combinations && b.base?.benefits.state === 'known') { let extra = true; try { extra = cmp(decimalFraction(b.base.benefits.amount), z()) !== 0n; } catch { /*L6 conserva a própria pendência.*/ } if (extra) { combinations.ready = false; if (combinations.status !== 'incompatible') combinations.status = 'indeterminate'; combinations.pending.push( 'Dedução/benefício à matéria colectável já registado emL6 sem correspondência a esta combinação:identifique parcela,origem e limite antes de reaplicarCBF16 ou reduzir novamente;valor original conservado.', ); } } const existing = (b.schedules?.rows ?? []).filter( (r) => r.rule === 'undocumented', ); if ( expenseDecision.status === 'absent' && (existing.length || i.expenses.rows.length) ) fail( expenseDecision, 'Ausência declarada contradiz despesas36g ou linhasL7 presentes;reconcilie sem apagar o ajustamento.', ); if ( expenseDecision.status !== 'calculated' && i.expenses.rows.length && expenseDecision.status !== 'indeterminate' ) fail( expenseDecision, 'Estado global e parcelas da despesa contraditórios;qualifique as parcelas ou conserve apenas a declaração.', ); const expenses = i.expenses.rows.map((r) => { const origin = existing.find((x) => x.id === r.scheduleId), effect = m.schedules?.effects.find((x) => x.id === r.scheduleId); let amount: FiscalFraction | null = effect?.amount ?? null, tax: FiscalFraction | null = null, status: Status = 'indeterminate'; const reasons: string[] = []; if (expenseDecision.status !== 'calculated') reasons.push(expenseDecision.reason); if ( !origin || !effect?.effect || !effect.amount || cmp(effect.effect, effect.amount) !== 0n ) reasons.push( 'Seleccione ajustamento36g calculado emL6;imposto autónomo não acrescenta novamente a despesa.', ); if ( i.expenses.rows.filter((x) => x.scheduleId === r.scheduleId) .length !== 1 ) reasons.push( 'Despesa L6 repetida no imposto autónomo;uma referência por parcela.', ); if (!r.reference.trim() || !r.eligible || !r.nature) reasons.push( 'Falta classificação legal documentada da despesa;ausência de anexo não prova61(4).', ); if (!reasons.length) { status = r.eligible === 'no' ? 'not_applicable' : r.nature === 'other' ? 'indeterminate' : 'calculated'; if (status === 'indeterminate') reasons.push( 'Outra natureza por corresponder;sem taxa provisória.', ); else tax = status === 'calculated' ? mul(amount!, 35n, 100n) : z(); } return { ...structuredClone(r), document: origin?.document ?? '', line: origin?.line ?? '', sourceCode: origin?.sourceCode ?? '', amount, l6Adjustment: effect?.effect ?? null, additionalProfitAdjustment: z(), rate: status === 'calculated' ? fraction(35n) : null, tax, status, reason: reasons.join(' ') || (status === 'calculated' ? 'CIRPC61(4):despesa ×35%;ajustamento36g reutilizado,sem duplicação.' : 'Condição negativa documentada;sem imposto autónomo aplicado à parcela.'), }; }); if (expenseDecision.status === 'calculated') { if (!existing.length && !expenses.length) fail( expenseDecision, 'Não há parcelas conhecidas;declare ausência com suporte ou seleccione despesas36g.', ); if (existing.some((r) => !expenses.some((x) => x.scheduleId === r.id))) fail( expenseDecision, 'Há despesa36g sem qualificação autónoma;corresponda cada parcela ou justifique a sua não aplicação.', ); for (const r of expenses) if (r.status === 'indeterminate') fail(expenseDecision, (r.document || r.id) + ': ' + r.reason); } const knownAutonomous = addFractions( expenses.flatMap((r) => (r.tax ? [r.tax] : [])), ), autonomousTax = expenseDecision.status === 'indeterminate' ? null : expenseDecision.status === 'calculated' ? knownAutonomous : z(); if ( coopDecision.status === 'absent' && ['agrarian', 'craft', 'cultural'].includes(i.cooperative.kind) && i.cooperative.allIncomeEligible === 'yes' && i.cooperative.withheldIncome === 'absent' ) fail( coopDecision, 'Ausência de redução contradiz os dados positivos de cooperativa integralmente elegível;reconcilie a declaração sem apagar os dados.', ); if (coopDecision.status === 'calculated') { if ( b.scope !== 'resident_general' || !['agrarian', 'craft', 'cultural'].includes(i.cooperative.kind) ) fail( coopDecision, 'Art12:tipo de cooperativa abrangida e residência por documentar;outras entidades têm correspondência própria.', ); if ( i.cooperative.allIncomeEligible !== 'yes' || i.cooperative.withheldIncome !== 'absent' ) fail( coopDecision, 'Recorte exige base ordinária integralmente elegível e ausência documentada de rendimentos sujeitos a retenção;parcelas mistas/retidas exigem segregação própria,sem redução global presumida.', ); } if (accDecision.status === 'absent' && i.acceleration.rows.length) fail( accDecision, 'Ausência de amortização acelerada contradiz activos seleccionados.', ); if ( accDecision.status !== 'calculated' && i.acceleration.rows.length && accDecision.status !== 'indeterminate' ) fail( accDecision, 'Não aplicação global com activos seleccionados:reconcilie o estado e as parcelas.', ); if (accDecision.status === 'calculated') { if (combinations && !combinations.ready) fail(accDecision, combinations.pending.join(' ')); if (i.acceleration.monthly && !i.acceleration.monthly.reference.trim()) fail( accDecision, 'Falta suporte da reutilização dos meses e da opção por duodécimosCBF16 no mapaL6.', ); if ( i.acceleration.monthly?.startPeriod && !i.acceleration.monthly.startPeriod.reference.trim() ) fail( accDecision, 'Falta suporte do período curto por início de actividadeCBF16;período e activosL6 conservados.', ); const continuation = i.acceleration.monthly?.continuation; if (continuation?.monthlyFromStart === 'no') fail( accDecision, 'Duodécimos desde a entrada emuso não aplicados:esta combinação de quotas/saída exige correspondência própria;L6 e os saldos declarados ficam preservados.', ); if ( continuation && (!continuation.period || !continuation.reference.trim() || !continuation.historyReference.trim() || !continuation.monthlyFromStart) ) fail( accDecision, 'CBF16 cessação/quotas anteriores:identifique período,suporte da vigência/condições e das quotas anteriores ou ausência;duodécimos desde a entrada emuso por documentar. Sem recalcular ou presumir saldos anteriores.', ); if ( i.acceleration.cbfRequirements !== 'yes' || !i.acceleration.recognition || !i.acceleration.recognitionReference.trim() ) fail( accDecision, 'Falta enquadramentoCBF3/9 e reconhecimento10 quando exigido;excepção deve ter norma/suporte,não localização.', ); if (!i.combinations && i.acceleration.noOtherBenefits !== 'yes') fail( accDecision, 'CumulaçãoCBF4 por corresponder;este recorte não combina outros benefícios positivos.', ); if (!i.combinations && coopDecision.status === 'calculated') fail( accDecision, 'Combinação de redução cooperativa comCBF16 ainda não correspondida;cada caso independente continua demonstrável.', ); if (!b.depreciation || !m.depreciation || !i.acceleration.rows.length) fail( accDecision, 'Seleccione activos do mapaL6 já reconciliado;sem criar activo ou quota paralelos.', ); } const accelerations = i.acceleration.rows.map((r) => { const a = b.depreciation?.assets.find((x) => x.id === r.assetId), base = m.depreciation?.assets.find((x) => x.id === r.assetId), issues: string[] = []; let status: Status = 'indeterminate'; if (accDecision.status !== 'calculated') issues.push(accDecision.reason); if (!r.reference.trim() || !r.eligible || !r.kind) issues.push( 'Condição/afectação do activo ao projecto por documentar.', ); if ( i.acceleration.rows.filter((x) => x.assetId === r.assetId) .length !== 1 ) issues.push('Activo repetido no benefício.'); if (!a || base?.status !== 'calculated') issues.push( 'ActivoL6 inexistente ou cálculo de origem indeterminado.', ); if (!issues.length && r.eligible === 'no') status = 'not_applicable'; else if (!issues.length && a) { const building = r.kind === 'new_project_building' && base?.catalog?.kind === 'building', equipment = [ 'industrial_equipment', 'agroindustrial_equipment', ].includes(r.kind) && ['machine', 'laundry_machine'].includes(a.category); if (!building && !equipment) issues.push( 'Categoria/afectação fora do recorte:imóvel novo ou máquina industrial/agro-industrial correspondida;reabilitados e outras categorias/métodos exigem regra própria.', ); if ( base?.catalog && cmp( decimalFraction(a.rate), decimalFraction(base.catalog.rate), ) !== 0n ) issues.push( 'RecorteCBF16 usa a taxa normal máxima da tabela;opção por quota inferior exige correspondência própria,sem a acelerar por aproximação.', ); if (i.acceleration.monthly) { const p = specialPeriod(b, m), priorZero = [ a.previousAccounting, a.previousFiscal, a.priorAvailable, ].every((v) => cmp(decimalFraction(v), z()) === 0n); if (i.acceleration.monthly.continuation) { const c = i.acceleration.monthly.continuation, annual = c.period === 'annual' && p.from === '2026-01-01' && p.to === '2026-12-31' && !m.periodsFx?.short, cessation = c.period === 'cessation' && b.periodsFx?.kind === 'cessation' && m.periodsFx?.short && p.from === '2026-01-01' && iso(p.to) && p.to < '2026-12-01'; if ( b.scope !== 'resident_general' || p.pending.length || (!annual && !cessation) || a.method !== 'constant' || a.timing !== 'months' || !iso(a.start) || a.start < '2025-01-01' || a.start > p.to || (a.start >= '2026-01-01' && !priorZero) || (a.exitState !== 'absent' && !( a.exitState === 'known' && iso(a.exitDate) && a.exitDate >= p.from && a.exitDate <= p.to )) || base?.months === null || base?.months === undefined ) issues.push( 'CBF16 cessação/quotas anteriores:residente,ano civil2026 ou cessaçãoL6 de meses completos desde1Janeiro,activo novo com entrada2025/2026 e saldosL6 reconciliados,método constante e duodécimos. Quotas anteriores positivas exigem entrada2025;saída só por evento documentado dentro do período. Outros anos/períodos/métodos mantêm correspondência própria.', ); } else if (i.acceleration.monthly.startPeriod) { if ( b.scope !== 'resident_general' || b.periodsFx?.kind !== 'start' || !m.periodsFx?.short || p.pending.length || !iso(p.from) || p.from < '2026-02-01' || p.from > '2026-12-01' || p.from.slice(-2) !== '01' || p.to !== '2026-12-31' || a.method !== 'constant' || a.timing !== 'months' || !iso(a.start) || a.start < p.from || a.start > p.to || a.exitState !== 'absent' || !priorZero || base?.months === null || base?.months === undefined ) issues.push( 'CBF16 início de actividade:residente,período curtoL6 de meses completos em2026 até31/12,activo novo emuso dentro do período,método constante,opção mensal e ausência documentada de saída/quotas anteriores;cessação/transição e demais situações permanecem separadas.', ); } else if ( a.method !== 'constant' || a.timing !== 'months' || !iso(a.start) || a.start < '2026-01-01' || a.start > '2026-12-31' || a.exitState !== 'absent' || !priorZero || p.from !== '2026-01-01' || p.to !== '2026-12-31' || base?.months === null || base?.months === undefined ) issues.push( 'CBF16 por duodécimos:exercício anual civil2026,método constante,entrada no ano,opção mensalL6 e ausência documentada de saída/quotas anteriores;outros períodos/casos permanecem separados.', ); } else if ( a.method !== 'constant' || a.timing !== 'annual' || a.start !== '2026-01-01' || a.exitState !== 'absent' || a.previousAccounting !== '0' || a.previousFiscal !== '0' || a.priorAvailable !== '0' || m.fiscalPeriod?.from !== '2026-01-01' || m.fiscalPeriod?.to !== '2026-12-31' ) issues.push( 'CBF16 inicial:primeiro exercício anual civil2026,método constante,sem quotas anteriores ou saída;duodécimos e recuperação anterior continuam separados.', ); if (!issues.length) status = 'calculated'; } return { ...structuredClone(r), assetKey: a?.assetKey ?? '', description: a?.description ?? '', status, reason: issues.join(' ') || (status === 'calculated' ? 'Activo existente elegível;taxa normal ×1,5,com limite contabilístico e saldo fiscal.' : 'Condição negativa documentada;quota normal mantida.'), }; }); let acceleratedDepreciation: NormalDepreciationModel | null = null, additionalDeduction: FiscalFraction | null = accDecision.status === 'indeterminate' ? null : z(); if (accDecision.status === 'calculated') { for (const r of accelerations) if (r.status === 'indeterminate') fail(accDecision, (r.description || r.id) + ': ' + r.reason); if ( accDecision.status === 'calculated' && b.depreciation && m.depreciation?.adjustment ) { const startPeriod = !!i.acceleration.monthly?.startPeriod, continuation = i.acceleration.monthly?.continuation; acceleratedDepreciation = calculateNormalDepreciation( b.depreciation, source, startPeriod || continuation ? specialPeriod(b, m) : { from: '2026-01-01', to: '2026-12-31' }, [ ...b.rows, ...(b.schedules?.rows ?? []), ...(b.costs?.rows ?? []), ], startPeriod || continuation?.period === 'cessation', new Set( accelerations .filter((r) => r.status === 'calculated') .map((r) => r.assetId), ), ); if (startPeriod || continuation) acceleratedDepreciation = { ...acceleratedDepreciation, source: continuation ? specialCessationPriorSource : specialStartPeriodSource, }; if (i.combinations) acceleratedDepreciation = { ...acceleratedDepreciation, source: specialAccelerationSource(i), }; if (acceleratedDepreciation.adjustment === null) fail(accDecision, acceleratedDepreciation.pending.join(' ')); else additionalDeduction = subtractFractions( m.depreciation.adjustment, acceleratedDepreciation.adjustment, ); } else if (accDecision.status === 'calculated') fail(accDecision, 'Reconciliação da amortização normal em falta.'); } if (accDecision.status === 'indeterminate') additionalDeduction = null; const taxableProfit = m.taxableProfit && additionalDeduction ? subtractFractions(m.taxableProfit, additionalDeduction) : null; const base = b.base ? calculateNormalBase( b.base, taxableProfit, b.year, b.schedules?.currentLossReference, !!m.periodsFx?.short, ) : null; const assessmentInput = coopDecision.status === 'calculated' ? { version: 1 as const, treatment: 'general_61_1' as const, reference: i.cooperative.qualification.reference, } : b.assessment; const assessment = assessmentInput ? calculateNormalAssessment( assessmentInput, base?.taxableBase ?? null, { year: b.year, scope: b.scope, currency: m.currency, extended: !!b.schedules || !!b.periodsFx, short: !!m.periodsFx?.short, period: specialPeriod(b, m), }, ) : null; const normalColecta = assessment?.colecta ?? null, colecta = coopDecision.status === 'indeterminate' ? null : normalColecta ? coopDecision.status === 'calculated' ? mul(normalColecta, 1n, 2n) : normalColecta : null; const combinedColecta = colecta && autonomousTax ? addFractions([colecta, autonomousTax]) : null; const monthlySource = i.combinations ? specialAccelerationSource(i) : i.acceleration.monthly?.continuation ? specialCessationPriorSource : i.acceleration.monthly?.startPeriod ? specialStartPeriodSource : specialMonthlySource; const monthlyModel = i.acceleration.monthly ? { input: structuredClone(i.acceleration.monthly), period: specialPeriod(b, m), source: monthlySource, assets: accelerations.map((r) => { const input = b.depreciation?.assets.find( (a) => a.id === r.assetId, ), normal = m.depreciation?.assets.find( (a) => a.id === r.assetId, ), accelerated = acceleratedDepreciation?.assets.find( (a) => a.id === r.assetId, ), applied = accDecision.status === 'calculated' && r.status === 'calculated' && accelerated?.status === 'calculated', rate = normal?.status === 'calculated' && input ? decimalFraction(input.rate) : null; return { ...(i.acceleration.monthly?.continuation ? { prior: { previousAccounting: input?.previousAccounting ?? '', previousFiscal: input?.previousFiscal ?? '', available: input?.priorAvailable ?? '', reference: input?.previousReference ?? '', normalUsed: normal?.priorUsed ?? null, normalRemaining: normal?.priorRemaining ?? null, acceleratedUsed: applied ? accelerated.priorUsed : null, acceleratedRemaining: applied ? accelerated.priorRemaining : null, normalFiscalRemaining: normal?.remainingFiscal ?? null, acceleratedFiscalRemaining: applied ? accelerated.remainingFiscal : null, }, exit: { state: input?.exitState ?? '', date: input?.exitDate ?? '', }, } : {}), id: r.id, assetId: r.assetId, assetKey: r.assetKey, description: r.description, start: input?.start ?? '', months: normal?.months ?? null, taxBasis: normal?.taxBasis ?? null, normalRate: rate, acceleratedRate: applied && rate ? mul(rate, 3n, 2n) : null, normalQuota: normal?.quota ?? null, acceleratedQuota: applied ? accelerated.quota : null, normalDeduction: normal?.deduction ?? null, acceleratedDeduction: applied ? accelerated.deduction : null, additionalDeduction: applied && normal?.deduction && accelerated.deduction ? subtractFractions( accelerated.deduction, normal.deduction, ) : r.status === 'not_applicable' ? z() : null, applied, reason: r.reason, }; }), } : undefined; const combinationsModel = combinations ? { ...combinations, input: structuredClone(i.combinations!), period: specialPeriod(b, m), subjectNuit: b.base?.subjectNuit ?? '', reusedL6Benefits: b.base ? structuredClone(b.base.benefits) : null, order: [ 'Diferença adicionalCBF16 aplicada uma vez ao lucroL6', 'Matéria colectável recalculada com as mesmas deduções/perdasL6', 'Cooperativa12:50% da taxa geral32%=16% apenas na base integralmente elegível', 'Imposto autónomo61(4) separado a35%,sem redução cooperativa', 'Saldo final a pagar/recuperar permaneceL8', ], assetClaims: accelerations.map((r) => { const normal = m.depreciation?.assets.find( (a) => a.id === r.assetId, ), accelerated = acceleratedDepreciation?.assets.find( (a) => a.id === r.assetId, ); return { assetId: r.assetId, document: normal?.document ?? '', line: normal?.line ?? '', normalDeduction: normal?.deduction ?? null, acceleratedDeduction: accelerated?.deduction ?? null, priorRecoveredIncluded: accelerated?.priorUsed ?? null, additionalDeduction: normal?.deduction && accelerated?.deduction ? subtractFractions( accelerated.deduction, normal.deduction, ) : null, remainingFiscal: accelerated?.remainingFiscal ?? null, }; }), limits: { additionalDeduction, cooperativeRate: coopDecision.status === 'calculated' ? fraction(16n) : null, autonomousRate: fraction(35n), autonomousTax, ordinaryColecta: colecta, combinedColecta, }, } : undefined; const combinationsReady = combinations?.ready ?? true; return { ...(combinationsModel ? { combinations: combinationsModel } : {}), input: structuredClone(i), context: structuredClone(specialContext(draft)), subjectNuit: b.base?.subjectNuit ?? '', period: specialPeriod(b, m), expenses: { ...expenseDecision, rows: expenses, knownAutonomous, autonomousTax, source: specialSources.expenses, }, cooperative: { ...coopDecision, rate: coopDecision.status === 'calculated' ? fraction(16n) : null, source: specialSources.cooperative, }, acceleration: { ...accDecision, rows: accelerations, additionalDeduction, normal: m.depreciation ?? null, accelerated: acceleratedDepreciation, ...(monthlyModel ? { monthly: monthlyModel } : {}), source: i.combinations ? specialAccelerationSource(i) : i.acceleration.monthly?.continuation ? specialCessationPriorSource : i.acceleration.monthly?.startPeriod ? specialStartPeriodSource : i.acceleration.monthly ? specialSources.acceleration + ' ' + specialMonthlySource : specialSources.acceleration, }, reference: { taxableProfit: m.taxableProfit, taxableBase: m.taxableBase, colecta: m.colecta ?? null, }, taxableProfit: combinationsReady ? taxableProfit : null, taxableBase: combinationsReady ? (base?.taxableBase ?? null) : null, colecta: combinationsReady ? colecta : null, combinedColecta: combinationsReady ? combinedColecta : null, pending: [ ...(combinations?.pending ?? []), ...expenseDecision.pending, ...coopDecision.pending, ...accDecision.pending, ...(base?.pending ?? []), ...(assessment?.pending ?? []), ], limitations: [ 'L7 parcial:apenas61(4),cooperativa integralmente elegível12 eCBF16 no recorte documentado.', 'Localização/sector só contextualizam;não activam benefícios.', i.combinations ? 'Parcelas mistas,outras cumulações/excepções,mais-valias e demais famílias têm correspondência própria.' : 'Parcelas mistas,cumulação,mais-valias e demais famílias têm correspondência própria.', 'M20/M22 permanecem preparaçãoL6 de referência;efeitosL7 sem campo oficial presumido.', 'Colecta combinada não é saldo final a pagar/recuperar;retenções,pagamentos e créditos anuais emL8.', ], finalBalance: { scope: 'L8', payable: null, recoverable: null }, }; } export type SpecialModel = ReturnType; const money = (v: FiscalFraction | null) => { if (!v) return 'Indeterminado'; const p = fractionPresentation(v); return ( (p.approximate ? '≈' : '') + p.decimal + ' MZN (exacto ' + v.numerator + '/' + v.denominator + ')' ); }; const status = (s: Status) => ({ calculated: 'Calculado no recorte', absent: 'Ausência declarada', not_applicable: 'Não aplicável', indeterminate: 'Informação em falta', })[s]; const contextDescription = (c: Record) => [ ['Sector', 'sector_id'], ['Província', 'province_id'], ['Distrito', 'district_id'], ['Posto administrativo', 'admin_post_id'], ['Localidade', 'locality_id'], ] .map( ([label, key]) => label + ': ' + inputText(c[key + '_label'] || c[key] || 'Por identificar'), ) .join(';'); export function specialReportRows( m: SpecialModel, ): Array<[string, string, string, string]> { return [ [ 'IRPC — L7', 'Qualificação comum', `NUIT:${m.subjectNuit};período:${m.period.from}–${m.period.to};${contextDescription(m.context)};${m.limitations.join(' ')}`, 'Origem herdada conservada;fonte/condições/período por tratamento;sem concessão pela localização.', ], [ 'IRPC — L7', 'Despesas — imposto autónomo', `${status(m.expenses.status)};parcela conhecida:${money(m.expenses.knownAutonomous)};imposto autónomo reconciliado:${money(m.expenses.autonomousTax)};${m.expenses.reason}`, `${m.input.expenses.qualification.reference};${m.input.expenses.qualification.periodReference};${m.expenses.source}`, ], ...m.expenses.rows.map( (r) => [ 'IRPC — L7', r.document + '/' + r.line, `${status(r.status)};natureza:${r.nature || 'Por identificar'};despesa:${money(r.amount)};ajustamento36g já emL6:${money(r.l6Adjustment)};novo ajustamento ao lucro:${money(r.additionalProfitAdjustment)};taxa:${r.rate ? '35%' : 'Não aplicada'};imposto autónomo:${money(r.tax)};${r.reason}`, r.reference, ] as [string, string, string, string], ), [ 'IRPC — L7', 'Cooperativa — redução da taxa', `${status(m.cooperative.status)};tipo:${m.input.cooperative.kind || 'Por identificar'};taxa:${m.cooperative.rate ? '16%' : 'Não aplicada'};${m.cooperative.reason}`, `${m.input.cooperative.qualification.reference};${m.input.cooperative.qualification.periodReference};${m.cooperative.source}`, ], [ 'IRPC — L7', 'Amortizações aceleradas', `${status(m.acceleration.status)};dedução adicional conciliada:${money(m.acceleration.additionalDeduction)};${m.acceleration.reason}`, `${m.input.acceleration.qualification.reference};${m.input.acceleration.qualification.periodReference};${m.input.acceleration.recognitionReference};${m.acceleration.source}`, ], ...m.acceleration.rows.map((r) => { const a = m.acceleration.accelerated?.assets.find( (a) => a.id === r.assetId, ), n = m.acceleration.normal?.assets.find( (a) => a.id === r.assetId, ); return [ 'IRPC — L7', r.description || r.assetId, `${status(r.status)};quota normal:${money(n?.quota ?? null)};quota acelerada:${money(a?.quota ?? null)};dedução normal:${money(n?.deduction ?? null)};dedução acelerada:${money(a?.deduction ?? null)};${r.reason}`, r.reference, ] as [string, string, string, string]; }), [ 'IRPC — L7', 'Efeitos nos totais', `ReferênciaL6:lucro:${money(m.reference.taxableProfit)};base:${money(m.reference.taxableBase)};colecta:${money(m.reference.colecta)}. Após tratamentosL7:lucro:${money(m.taxableProfit)};base:${money(m.taxableBase)};colecta sobre a base:${money(m.colecta)};imposto autónomo:${money(m.expenses.autonomousTax)};colecta combinada:${money(m.combinedColecta)}. Saldo final a pagar/recuperar:Indeterminado (L8).`, m.pending.join(' | ') || 'Regras/parcelas reconciliadas neste recorte;simulação,não liquidação final.', ], ...(m.combinations ? specialCombinationsReport(m) : []), ...(m.acceleration.monthly ? specialMonthlyReport( m.acceleration.monthly, m.acceleration.additionalDeduction, ) : []), ]; } function specialMonthlyReport( m: NonNullable, deduction: FiscalFraction | null, ): Array<[string, string, string, string]> { return [ [ 'IRPC — L7', 'CBF16 — duodécimos reutilizados de L6', 'Período:' + m.period.from + '–' + m.period.to + ';dedução adicional conciliada:' + money(deduction) + ';sem nova dedução do activo de origem.', m.input.reference + (m.input.startPeriod ? ';início de actividade:' + m.input.startPeriod.reference : '') + (m.input.continuation ? ';período:' + (m.input.continuation.period === 'cessation' ? 'Cessação da actividade' : m.input.continuation.period === 'annual' ? 'Ano civil2026' : 'Informação em falta') + ';suporte:' + m.input.continuation.reference + ';histórico:' + m.input.continuation.historyReference : '') + ';' + m.source, ], ...m.assets.map( (a) => [ 'IRPC — L7', 'CBF16 por duodécimos — ' + a.description, 'Entrada emuso:' + (a.start || 'Informação em falta') + ';duodécimosL6:' + (a.months ?? 'Indeterminados') + ';base fiscal:' + money(a.taxBasis) + ';taxa normal:' + (a.normalRate ? fractionPresentation(a.normalRate).decimal + '%' : 'Indeterminada') + ';taxa CBF16:' + (a.acceleratedRate ? fractionPresentation(a.acceleratedRate).decimal + '%' : 'Não aplicada') + ';quota normal:' + money(a.normalQuota) + ';quota CBF16:' + money(a.acceleratedQuota) + ';dedução normal:' + money(a.normalDeduction) + ';dedução CBF16:' + money(a.acceleratedDeduction) + ';diferença adicional:' + money(a.additionalDeduction) + (a.prior ? ';quotas contabilísticas anteriores:' + a.prior.previousAccounting + ';quotas fiscais anteriores:' + a.prior.previousFiscal + ';parcela anterior recuperável declarada:' + a.prior.available + ';recuperada normal:' + money(a.prior.normalUsed) + ';recuperada CBF16:' + money(a.prior.acceleratedUsed) + ';saldo anterior aritmético normal:' + money(a.prior.normalRemaining) + ';saldo anterior aritmético CBF16:' + money(a.prior.acceleratedRemaining) + ';saldo fiscal normal:' + money(a.prior.normalFiscalRemaining) + ';saldo fiscal CBF16:' + money(a.prior.acceleratedFiscalRemaining) + ';saída:' + (a.exit?.state === 'absent' ? 'Ausência declarada' : a.exit?.state === 'known' ? 'Evento documentado' : 'Informação em falta') + ' ' + (a.exit?.date ?? '') + ';sem consumo de saldos oficiais' : '') + ';' + a.reason, m.source + (a.prior ? ';suporte das quotas anteriores:' + a.prior.reference : ''), ] as [string, string, string, string], ), ]; } function specialCombinationsReport( m: SpecialModel, ): Array<[string, string, string, string]> { const c = m.combinations!; return [ [ 'IRPC — L7', 'Cumulações — qualificação e ordem', 'Estado:' + (c.status === 'compatible' ? 'Combinação permitida no recorte' : c.status === 'incompatible' ? 'Combinação incompatível' : 'Informação em falta') + ';mesmo sujeito:' + c.subjectNuit + ';período:' + c.period.from + '–' + c.period.to + ';' + c.order.join(' → ') + ';' + c.pending.join(' | '), c.input.reference + ';benefícios específicos:' + c.input.specificReference + ';outros benefícios:' + c.input.otherReference + ';' + c.source, ], ...c.assetClaims.map( (a) => [ 'IRPC — L7', 'Cumulações — activo ' + a.assetId, 'Documento/linha:' + a.document + '/' + a.line + ';dedução normal:' + money(a.normalDeduction) + ';dedução CBF16:' + money(a.acceleratedDeduction) + ';parcela anterior já incluída na dedução CBF16:' + money(a.priorRecoveredIncluded) + ';única diferença adicional:' + money(a.additionalDeduction) + ';saldo fiscal aritmético:' + money(a.remainingFiscal) + ';sem nova dedução do custo nem consumo de saldo oficial.', c.source, ] as [string, string, string, string], ), [ 'IRPC — L7', 'Cumulações — limites e totais', 'Dedução adicional conciliada:' + money(c.limits.additionalDeduction) + ';taxa cooperativa:' + (c.limits.cooperativeRate ? '16%' : 'Não aplicada') + ';imposto autónomo independente:' + money(c.limits.autonomousTax) + ';colecta sobre a base:' + money(m.colecta) + ';colecta combinada:' + money(m.combinedColecta) + ';saldo final a pagar/recuperar:Indeterminado (L8).', c.source, ], ]; } export { qualify as qualifySpecialTreatment };