Files
Investidor-V2/resources/js/lib/iva-acquisition-bc.ts
T

656 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, Choice>;
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<string, string> = {
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<string, string[]> = {
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<string, unknown>) {
const store = readAcquisitionStore(
draft.modelo_a_snapshot,
) as unknown as Record<string, Record<string, { rows: ImportSource[] }>>;
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<string, unknown>,
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<string, unknown>,
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<string, unknown> | 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.');
}