213 lines
8.2 KiB
TypeScript
213 lines
8.2 KiB
TypeScript
import { acquisitionScope } from './iva-acquisitions.ts';
|
||
import {
|
||
operationTreatments,
|
||
salesBook,
|
||
salesCurrent,
|
||
type SalesOperation,
|
||
type SalesLineResult,
|
||
} from './iva-operations.ts';
|
||
export const acquisitionRightVersion = 'acquisition-operation-right/1';
|
||
export const acquisitionRightSource =
|
||
'L2-A — CIVA19(1)(a)/(b)(i/iv/v),19(2),Lei13/2016 PDF15/p1426(273);Lei10/2025 PDF13/p2180(13):18(2),19(2),exclusão17-A. Condições da aquisição18/20/21 continuam próprias. Sem resolverP00105/06,importações,afectação parcial,excepções ou regularizações.';
|
||
export type AcquisitionRightLink = {
|
||
version: 1;
|
||
scopeId: string;
|
||
operationId: string;
|
||
fingerprint: string;
|
||
scopeConfirmed: '' | 'yes' | 'no';
|
||
allocationRef: string;
|
||
noExcludedPayments: '' | 'yes' | 'no';
|
||
};
|
||
export type AcquisitionEligibility = {
|
||
status: 'eligible' | 'no_right' | 'indeterminate';
|
||
reason: string;
|
||
source: string;
|
||
operationId?: string;
|
||
operationPeriod?: string;
|
||
operationTreatment?: string;
|
||
operationDescription?: string;
|
||
operationArticle?: string;
|
||
scopeId?: string;
|
||
};
|
||
export function validAcquisitionRightLink(
|
||
value: unknown,
|
||
): value is AcquisitionRightLink {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||
return false;
|
||
const v = value as AcquisitionRightLink;
|
||
return (
|
||
v.version === 1 &&
|
||
['scopeId', 'operationId', 'fingerprint', 'allocationRef'].every(
|
||
(k) => typeof v[k as keyof AcquisitionRightLink] === 'string',
|
||
) &&
|
||
['', 'yes', 'no'].includes(v.scopeConfirmed) &&
|
||
['', 'yes', 'no'].includes(v.noExcludedPayments)
|
||
);
|
||
}
|
||
function fingerprint(row: SalesOperation, result: SalesLineResult | undefined) {
|
||
return JSON.stringify({
|
||
version: acquisitionRightVersion,
|
||
input: row,
|
||
classification: result
|
||
? {
|
||
article: result.article,
|
||
source: result.source,
|
||
status: result.status,
|
||
}
|
||
: null,
|
||
});
|
||
}
|
||
export function linkedBeverageDeductionRight(
|
||
draft: Record<string, unknown>,
|
||
link: AcquisitionRightLink,
|
||
notCoastalFishing: boolean,
|
||
): AcquisitionEligibility {
|
||
const base = linkedOperationRight(draft, link),
|
||
b = salesBook(draft),
|
||
row = b.rows.find((r) => r.id === link.operationId),
|
||
line = b.result?.rows.find((r) => r.id === link.operationId),
|
||
rule = operationTreatments.find((t) => t.id === row?.treatment);
|
||
if (
|
||
link.scopeId !== acquisitionScope(draft) ||
|
||
!salesCurrent(draft, b) ||
|
||
!row ||
|
||
!line ||
|
||
!rule ||
|
||
link.fingerprint !== fingerprint(row, line) ||
|
||
row.supplyCategory !== 'beverages' ||
|
||
![
|
||
'onboard_seagoing_ship',
|
||
'onboard_international_aircraft',
|
||
'onboard_rescue_coastal_ship',
|
||
].includes(row.treatment) ||
|
||
!row.description.trim() ||
|
||
!row.establishment.trim() ||
|
||
!row.evidence.trim() ||
|
||
rule.conditions.some((_, i) => !row.confirmed.includes(i)) ||
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
||
row.sourceCurrency !== 'MZN' ||
|
||
!/^\d{1,15}(\.\d{1,6})?$/.test(row.amount) ||
|
||
!['net', 'included'].includes(row.basis) ||
|
||
(row.treatment === 'onboard_rescue_coastal_ship' && !notCoastalFishing)
|
||
)
|
||
return {
|
||
...base,
|
||
status: 'indeterminate',
|
||
reason: 'Correspondência própria13(2) por demonstrar em origemL1 actual:c/d/h,bebidas,condições e documento;pesca costeira conserva exclusão de provisões.',
|
||
};
|
||
return {
|
||
...base,
|
||
status: 'eligible',
|
||
reason: 'Via de dedução13(2) de bebidas em13(1)(c/d/h),nas condições demonstradas;20(1)(d) não se aplica para este efeito. Não concede isenção directa nem restituição.',
|
||
source: base.source + ';CIVA13(2–3),Lei13/2016 PDF12/p1426(270)',
|
||
};
|
||
}
|
||
export function acquisitionOperationChoices(draft: Record<string, unknown>) {
|
||
const book = salesBook(draft);
|
||
return book.rows.map((row) => ({
|
||
id: row.id,
|
||
label: `${row.description || row.id} — ${row.period} — ${operationTreatments.find((t) => t.id === row.treatment)?.label ?? row.treatment}`,
|
||
row,
|
||
current: salesCurrent(draft, book),
|
||
}));
|
||
}
|
||
export function createAcquisitionRightLink(
|
||
draft: Record<string, unknown>,
|
||
operationId: string,
|
||
): AcquisitionRightLink {
|
||
const b = salesBook(draft),
|
||
row = b.rows.find((r) => r.id === operationId);
|
||
if (!row)
|
||
throw Error(
|
||
'Operação de origem não encontrada nesta oportunidade;dados preservados.',
|
||
);
|
||
return {
|
||
version: 1,
|
||
scopeId: acquisitionScope(draft),
|
||
operationId,
|
||
fingerprint: fingerprint(
|
||
row,
|
||
b.result?.rows.find((r) => r.id === operationId),
|
||
),
|
||
scopeConfirmed: '',
|
||
allocationRef: '',
|
||
noExcludedPayments: '',
|
||
};
|
||
}
|
||
export function linkedOperationRight(
|
||
draft: Record<string, unknown>,
|
||
link: AcquisitionRightLink,
|
||
): AcquisitionEligibility {
|
||
const base: AcquisitionEligibility = {
|
||
status: 'indeterminate',
|
||
reason: '',
|
||
source: acquisitionRightSource,
|
||
operationId: link.operationId,
|
||
scopeId: link.scopeId,
|
||
};
|
||
const stop = (
|
||
reason: string,
|
||
status: AcquisitionEligibility['status'] = 'indeterminate',
|
||
) => ({ ...base, status, reason });
|
||
if (
|
||
!validAcquisitionRightLink(link) ||
|
||
link.scopeId !== acquisitionScope(draft)
|
||
)
|
||
return stop(
|
||
'Vínculo inválido ou pertencente a outra oportunidade;sem direito presumido.',
|
||
);
|
||
const b = salesBook(draft),
|
||
row = b.rows.find((r) => r.id === link.operationId),
|
||
line = b.result?.rows.find((r) => r.id === link.operationId);
|
||
if (!row)
|
||
return stop(
|
||
'Operação ligada removida/não recuperável;escolha uma origem disponível,sem apagar o histórico.',
|
||
);
|
||
base.operationPeriod = row.period;
|
||
base.operationTreatment = row.treatment;
|
||
base.operationDescription = row.description;
|
||
base.operationArticle = line?.article;
|
||
base.source +=
|
||
';' + (line?.source ?? 'Fonte da classificação por recuperar');
|
||
if (!salesCurrent(draft, b) || !line || line.status === 'indeterminate')
|
||
return stop(
|
||
'Classificação L1 de origem incompleta ou alterada;recupere/actualize a classificação,sem dedução por rótulo.',
|
||
);
|
||
if (link.fingerprint !== fingerprint(row, line))
|
||
return stop(
|
||
'Operação de origem alterada:reveja e adopte explicitamente a origem actual;montantes e histórico da aquisição conservados.',
|
||
);
|
||
const rule = operationTreatments.find((t) => t.id === row.treatment);
|
||
if (!rule)
|
||
return stop(
|
||
'Correspondência do direito ainda não identificada para esta operação.',
|
||
);
|
||
if (/17-A/.test(rule.article))
|
||
return stop(
|
||
'Operação abrangida por17-A:19(2),redacçãoLei10/2025,não permite dedução apesar de ter IVA liquidado.',
|
||
'no_right',
|
||
);
|
||
if (/19\(1\)\(b\)\((i|iv|v)\)/.test(rule.article))
|
||
return stop(
|
||
'Direito da operação identificado em' +
|
||
rule.article +
|
||
';a aquisição continua sujeita às suas condições próprias.',
|
||
'eligible',
|
||
);
|
||
if (line.status === 'calculated' && rule.rate === 16)
|
||
return stop(
|
||
'Operação sujeita e não isenta:direito19(1)(a),sujeito a19(2) e condições próprias da aquisição.',
|
||
'eligible',
|
||
);
|
||
// This is a bounded match to the already implemented article9 catalogue, after
|
||
// checking its express article19 exceptions. Never use the ModeloA box as law.
|
||
if (line.status === 'exempt' && /\b9\(/.test(rule.article))
|
||
return stop(
|
||
'Operação isenta do artigo9 fora das correspondências com direito identificadas19(1)(b);não confere dedução neste fundamento.',
|
||
'no_right',
|
||
);
|
||
return stop(
|
||
'Classificação disponível,mas correspondência do direito não implementada neste percurso;não inferir direito pela caixa05/06/07 ou por IVAzero.',
|
||
);
|
||
}
|