1405 lines
60 KiB
TypeScript
1405 lines
60 KiB
TypeScript
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<string, unknown>)[
|
||
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<string, unknown>) {
|
||
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<string, unknown>) =>
|
||
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<string, unknown>,
|
||
b: NormalBook,
|
||
m: NormalModel,
|
||
source: Parameters<typeof calculateNormalDepreciation>[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<typeof calculateSpecial>;
|
||
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<string, unknown>) =>
|
||
[
|
||
['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<SpecialModel['acceleration']['monthly']>,
|
||
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 };
|