677 lines
26 KiB
TypeScript
677 lines
26 KiB
TypeScript
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
|
||
export const travelVersion = 'travel-period-margin/1';
|
||
export const travelSource =
|
||
'CIVA4(5),Lei13/2016PDF7/p1426(265); RIVA Decreto7/2008,38–46,PDF6–8/p104–106: âmbito, prestação única, margem global por período, custos directos de terceiros, excesso negativo e facturação IVA incluído. CIVA17(1),Lei22/2022PDF3:16% usado apenas no divisor da base em2026;Lei10/2025 não altera4/7/8/17. Sem IVA devido, dedução/campo11, regularização ou totaisModeloA calculados. O cálculo fica limitado ao registo e âmbito declarados, não fecha apuramento de toda a organização.';
|
||
type Choice = '' | 'yes' | 'no';
|
||
export const travelConditions = [
|
||
[
|
||
'operator',
|
||
'Agência de viagens ou organizador de circuitos turísticos identificado, actuando nessa qualidade',
|
||
],
|
||
[
|
||
'singlePrice',
|
||
'Conjunto combinado de serviços por preço único, sem discriminar preços finais por componente',
|
||
],
|
||
['ownName', 'Actua em nome próprio perante o cliente'],
|
||
] as const;
|
||
export const travelCostKinds = [
|
||
['direct', 'Bem/serviço de terceiro para benefício directo do cliente'],
|
||
['exchange', 'Compra/venda/troca de divisas'],
|
||
['advertising', 'Publicidade/promoção'],
|
||
['communications', 'Comunicações e correspondência da agência'],
|
||
['sales_commission', 'Comissão paga à agência vendedora'],
|
||
['other', 'Outro custo com natureza a esclarecer'],
|
||
] as const;
|
||
export type TravelCost = {
|
||
id: string;
|
||
kind: string;
|
||
description: string;
|
||
amount: string;
|
||
ref: string;
|
||
thirdParty: Choice;
|
||
directBenefit: Choice;
|
||
};
|
||
export type TravelRow = {
|
||
id: string;
|
||
description: string;
|
||
period: string;
|
||
origin: 'projected' | 'effective';
|
||
currency: string;
|
||
operatorRef: string;
|
||
operationRef: string;
|
||
documentRef: string;
|
||
documentDate: string;
|
||
scope: string;
|
||
evidence: string;
|
||
agencyCountry: string;
|
||
agencyRef: string;
|
||
resources: string;
|
||
retailer: Choice;
|
||
newPackage: Choice;
|
||
packageRef: string;
|
||
grossPrice: string;
|
||
thirdPartyGross: string;
|
||
allocationRef: string;
|
||
domesticTaxable: Choice;
|
||
taxRef: string;
|
||
conditions: Partial<Record<string, Choice>>;
|
||
costs: TravelCost[];
|
||
};
|
||
export type TravelPeriod = {
|
||
id: string;
|
||
period: string;
|
||
operatorRef: string;
|
||
scope: string;
|
||
complete: Choice;
|
||
registerRef: string;
|
||
priorMode: string;
|
||
priorAmount: string;
|
||
priorRef: string;
|
||
priorOverride: Choice;
|
||
};
|
||
export type TravelLine = {
|
||
id: string;
|
||
status: 'determined' | 'not_applicable' | 'indeterminate';
|
||
regime: 'special' | 'mixed' | 'general' | 'outside' | 'indeterminate';
|
||
territory: 'national' | 'outside' | null;
|
||
specialGross: string | null;
|
||
generalGross: string | null;
|
||
eligibleCosts: string | null;
|
||
excludedCosts: string | null;
|
||
reason: string;
|
||
};
|
||
export type TravelPeriodResult = {
|
||
id: string;
|
||
period: string;
|
||
operatorRef: string;
|
||
status: 'determined' | 'indeterminate';
|
||
grossReceipts: string | null;
|
||
directCosts: string | null;
|
||
excludedCosts: string | null;
|
||
priorExcess: string | null;
|
||
grossDifference: string | null;
|
||
carryForward: string | null;
|
||
baseAmount: string | null;
|
||
baseDivisorRate: string | null;
|
||
vat: null;
|
||
deduction: null;
|
||
reason: string;
|
||
};
|
||
export type TravelCalculation = {
|
||
version: string;
|
||
revision: number;
|
||
calculatedAt: string;
|
||
opportunity: string;
|
||
sourceKey: string;
|
||
source: string;
|
||
inputs: TravelRow[];
|
||
periodInputs: TravelPeriod[];
|
||
rows: TravelLine[];
|
||
periods: TravelPeriodResult[];
|
||
};
|
||
export type TravelBook = {
|
||
rows: TravelRow[];
|
||
periods: TravelPeriod[];
|
||
result?: TravelCalculation;
|
||
history?: TravelCalculation[];
|
||
};
|
||
type Store = ReturnType<typeof readAcquisitionStore> & {
|
||
travelOperations?: Record<string, TravelBook>;
|
||
};
|
||
export function newTravelCost(id: string): TravelCost {
|
||
return {
|
||
id,
|
||
kind: '',
|
||
description: '',
|
||
amount: '',
|
||
ref: '',
|
||
thirdParty: '',
|
||
directBenefit: '',
|
||
};
|
||
}
|
||
export function newTravel(id: string, currency: string): TravelRow {
|
||
return {
|
||
id,
|
||
description: '',
|
||
period: '',
|
||
origin: 'projected',
|
||
currency,
|
||
operatorRef: '',
|
||
operationRef: '',
|
||
documentRef: '',
|
||
documentDate: '',
|
||
scope: '',
|
||
evidence: '',
|
||
agencyCountry: '',
|
||
agencyRef: '',
|
||
resources: '',
|
||
retailer: '',
|
||
newPackage: '',
|
||
packageRef: '',
|
||
grossPrice: '',
|
||
thirdPartyGross: '',
|
||
allocationRef: '',
|
||
domesticTaxable: '',
|
||
taxRef: '',
|
||
conditions: {},
|
||
costs: [],
|
||
};
|
||
}
|
||
export function newTravelPeriod(id: string): TravelPeriod {
|
||
return {
|
||
id,
|
||
period: '',
|
||
operatorRef: '',
|
||
scope: '',
|
||
complete: '',
|
||
registerRef: '',
|
||
priorMode: '',
|
||
priorAmount: '',
|
||
priorRef: '',
|
||
priorOverride: '',
|
||
};
|
||
}
|
||
export function switchTravelResources(
|
||
row: TravelRow,
|
||
resources: string,
|
||
): TravelRow {
|
||
return {
|
||
...row,
|
||
resources,
|
||
conditions: { ...row.conditions, singlePrice: '', ownName: '' },
|
||
};
|
||
}
|
||
function readStore(draft: Record<string, unknown>): Store {
|
||
const s = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if (
|
||
s.travelOperations !== undefined &&
|
||
(!s.travelOperations ||
|
||
typeof s.travelOperations !== 'object' ||
|
||
Array.isArray(s.travelOperations))
|
||
)
|
||
throw new Error('Viagens não recuperáveis; originais conservados.');
|
||
const rf = Object.keys(newTravel('', '')).filter(
|
||
(k) => !['conditions', 'costs'].includes(k),
|
||
),
|
||
pf = Object.keys(newTravelPeriod('')),
|
||
cf = Object.keys(newTravelCost(''));
|
||
for (const b of Object.values(s.travelOperations ?? {}))
|
||
if (
|
||
!b ||
|
||
!Array.isArray(b.rows) ||
|
||
!Array.isArray(b.periods) ||
|
||
(b.history !== undefined && !Array.isArray(b.history)) ||
|
||
b.rows.some(
|
||
(r) =>
|
||
!r ||
|
||
rf.some(
|
||
(k) => typeof r[k as keyof TravelRow] !== 'string',
|
||
) ||
|
||
!r.conditions ||
|
||
typeof r.conditions !== 'object' ||
|
||
Array.isArray(r.conditions) ||
|
||
!Array.isArray(r.costs) ||
|
||
r.costs.some(
|
||
(c) =>
|
||
!c ||
|
||
cf.some(
|
||
(k) =>
|
||
typeof c[k as keyof TravelCost] !==
|
||
'string',
|
||
),
|
||
),
|
||
) ||
|
||
b.periods.some(
|
||
(p) =>
|
||
!p ||
|
||
pf.some(
|
||
(k) => typeof p[k as keyof TravelPeriod] !== 'string',
|
||
),
|
||
)
|
||
)
|
||
throw new Error(
|
||
'Registo de viagens não recuperável; sem substituição por lista vazia.',
|
||
);
|
||
return s;
|
||
}
|
||
export function travelBook(draft: Record<string, unknown>): TravelBook {
|
||
return (
|
||
readStore(draft).travelOperations?.[acquisitionScope(draft)] ?? {
|
||
rows: [],
|
||
periods: [],
|
||
}
|
||
);
|
||
}
|
||
export function saveTravelBook(
|
||
draft: Record<string, unknown>,
|
||
book: TravelBook,
|
||
): string {
|
||
const s = readStore(draft),
|
||
encoded = JSON.stringify({
|
||
...s,
|
||
travelOperations: {
|
||
...s.travelOperations,
|
||
[acquisitionScope(draft)]: book,
|
||
},
|
||
});
|
||
readStore({ ...draft, modelo_a_snapshot: encoded });
|
||
return encoded;
|
||
}
|
||
export function travelDecimal(s: string): bigint | null {
|
||
if (!/^\d{1,15}(\.\d{1,6})?$/.test(s.trim())) return null;
|
||
const [w, f = ''] = s.trim().split('.');
|
||
return BigInt(w) * 1000000n + BigInt(f.padEnd(6, '0'));
|
||
}
|
||
function money(n: bigint): string {
|
||
if (n < 0n) return '-' + money(-n);
|
||
return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')}`;
|
||
}
|
||
export { money as travelMoney };
|
||
function validDate(s: string): boolean {
|
||
const d = new Date(s + 'T00:00:00Z');
|
||
return (
|
||
/^\d{4}-\d{2}-\d{2}$/.test(s) &&
|
||
Number.isFinite(d.getTime()) &&
|
||
d.toISOString().slice(0, 10) === s
|
||
);
|
||
}
|
||
export function evaluateTravel(row: TravelRow, currency: unknown): TravelLine {
|
||
const result: TravelLine = {
|
||
id: row.id,
|
||
status: 'indeterminate',
|
||
regime: 'indeterminate',
|
||
territory: null,
|
||
specialGross: null,
|
||
generalGross: null,
|
||
eligibleCosts: null,
|
||
excludedCosts: null,
|
||
reason: '',
|
||
};
|
||
const stop = (
|
||
reason: string,
|
||
status: TravelLine['status'] = 'indeterminate',
|
||
) => ({ ...result, status, reason });
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
|
||
!['projected', 'effective'].includes(row.origin) ||
|
||
![
|
||
row.description,
|
||
row.operatorRef,
|
||
row.operationRef,
|
||
row.documentRef,
|
||
row.scope,
|
||
row.evidence,
|
||
row.agencyRef,
|
||
].every((v) => !!v.trim()) ||
|
||
!validDate(row.documentDate)
|
||
)
|
||
return stop(
|
||
'Identifique operador, operação, período do registo2026, documento ou previsão, estabelecimento, âmbito e factos. O período do registo não é inferido da data da viagem.',
|
||
);
|
||
if (row.conditions.operator === 'no')
|
||
return stop(
|
||
'Qualidade de agência/organizador não demonstrada; aplicar enquadramento próprio, sem benefício presumido.',
|
||
'not_applicable',
|
||
);
|
||
if (row.conditions.operator !== 'yes')
|
||
return stop('Qualidade em que o operador actua por esclarecer.');
|
||
if (
|
||
row.conditions.singlePrice === 'no' ||
|
||
row.conditions.ownName === 'no' ||
|
||
row.resources === 'own'
|
||
)
|
||
return {
|
||
...stop(
|
||
'RIVA38(2)/(3): operação avulsa/em nome de outrem ou meios próprios segue regime geral, sem margem especial. Reutilize o percurso de serviço/localização aplicável.',
|
||
'not_applicable',
|
||
),
|
||
regime: 'general',
|
||
};
|
||
if (
|
||
!['third_party', 'mixed'].includes(row.resources) ||
|
||
row.conditions.singlePrice !== 'yes' ||
|
||
row.conditions.ownName !== 'yes' ||
|
||
!['yes', 'no'].includes(row.retailer)
|
||
)
|
||
return stop(
|
||
'Esclareça pacote a preço único, nome próprio, meios próprios/terceiros e venda retalhista.',
|
||
);
|
||
if (row.retailer === 'yes') {
|
||
if (row.newPackage === 'no')
|
||
return {
|
||
...stop(
|
||
'RIVA38(4): revenda retalhista sem novo conjunto com serviços não meramente acessórios/complementares segue regime geral.',
|
||
'not_applicable',
|
||
),
|
||
regime: 'general',
|
||
};
|
||
if (row.newPackage !== 'yes' || !row.packageRef.trim())
|
||
return stop(
|
||
'Na revenda retalhista, identifique novo conjunto em nome próprio e serviços acrescidos não meramente acessórios/complementares.',
|
||
);
|
||
}
|
||
if (!['MZ', 'foreign'].includes(row.agencyCountry))
|
||
return stop(
|
||
'Identifique sede/estabelecimento a partir do qual a agência presta o serviço.',
|
||
);
|
||
if (row.agencyCountry === 'foreign')
|
||
return {
|
||
...stop(
|
||
'RIVA39(2): sede/estabelecimento prestador fora do território; esta margem nacional não se aplica. Conferir localização/regime próprio, sem isenção ou IVA estrangeiro presumido.',
|
||
'not_applicable',
|
||
),
|
||
regime: 'outside',
|
||
territory: 'outside',
|
||
};
|
||
result.regime = row.resources === 'mixed' ? 'mixed' : 'special';
|
||
result.territory = 'national';
|
||
if (currency !== 'MZN' || row.currency !== currency)
|
||
return stop(
|
||
'Natureza especial identificada; cálculo em moeda estrangeira ou divergente não apurado, sem conversão fiscal presumida. P104 mantém âmbito próprio.',
|
||
);
|
||
if (row.domesticTaxable !== 'yes' || !row.taxRef.trim())
|
||
return stop(
|
||
'Localização39(2) identificada; enquadramento tributável desta parcela/período por esclarecer antes do divisor17(1). Não presumir isenção, taxa ou benefício pela categoria turística.',
|
||
);
|
||
const gross = travelDecimal(row.grossPrice);
|
||
if (gross === null)
|
||
return stop(
|
||
'Identifique contraprestação com IVA incluído registada neste período; desconhecido não é zero.',
|
||
);
|
||
const special =
|
||
row.resources === 'mixed' ? travelDecimal(row.thirdPartyGross) : gross;
|
||
if (
|
||
special === null ||
|
||
special > gross ||
|
||
(row.resources === 'mixed' && !row.allocationRef.trim())
|
||
)
|
||
return stop(
|
||
'Meios mistos: delimite contraprestação da parte de terceiros e fundamento de repartição; não aplicar margem ao conjunto nem repartir por percentagem inventada.',
|
||
);
|
||
let eligible = 0n,
|
||
excluded = 0n;
|
||
if (new Set(row.costs.map((c) => c.id)).size !== row.costs.length)
|
||
return stop('Identifique custos sem duplicar identidades.');
|
||
for (const cost of row.costs) {
|
||
const value = travelDecimal(cost.amount);
|
||
if (
|
||
value === null ||
|
||
!cost.ref.trim() ||
|
||
!cost.description.trim() ||
|
||
!travelCostKinds.some(([k]) => k === cost.kind)
|
||
)
|
||
return stop(
|
||
'Identifique natureza, montante com IVA e referência de cada custo registado no período. Lista vazia corresponde apenas à ausência declarada de custos no âmbito completo.',
|
||
);
|
||
if (
|
||
[
|
||
'exchange',
|
||
'advertising',
|
||
'communications',
|
||
'sales_commission',
|
||
].includes(cost.kind)
|
||
) {
|
||
excluded += value;
|
||
continue;
|
||
}
|
||
if (cost.thirdParty === 'no' || cost.directBenefit === 'no') {
|
||
excluded += value;
|
||
continue;
|
||
}
|
||
if (cost.thirdParty !== 'yes' || cost.directBenefit !== 'yes')
|
||
return stop(
|
||
'Benefício directo e fornecimento por terceiro por esclarecer neste custo; não o excluir ou incluir por defeito.',
|
||
);
|
||
eligible += value;
|
||
}
|
||
return {
|
||
...result,
|
||
status: 'determined',
|
||
specialGross: money(special),
|
||
generalGross: money(gross - special),
|
||
eligibleCosts: money(eligible),
|
||
excludedCosts: money(excluded),
|
||
reason: 'RIVA38/39: prestação única no âmbito da parte de terceiros; registo43 de contraprestações e custos directos com IVA incluído. Custos41(3) excluídos da margem, não transformados em dedução. A base é apurada globalmente por operador/período, não por pacote. Meios próprios/parte geral conservados fora da margem; factura45 sem discriminar IVA, menção IVA incluído; registo especial46. Sem factura oficial emitida, dedução ou regularização automática.',
|
||
};
|
||
}
|
||
function previousMonth(period: string): string {
|
||
const [y, m] = period.split('-').map(Number);
|
||
return new Date(Date.UTC(y, m - 2, 1)).toISOString().slice(0, 7);
|
||
}
|
||
export function evaluateTravelPeriods(
|
||
book: TravelBook,
|
||
lines: TravelLine[],
|
||
): TravelPeriodResult[] {
|
||
const results: TravelPeriodResult[] = [];
|
||
for (const input of [...book.periods].sort((a, b) =>
|
||
a.period.localeCompare(b.period),
|
||
)) {
|
||
const r: TravelPeriodResult = {
|
||
id: input.id,
|
||
period: input.period,
|
||
operatorRef: input.operatorRef,
|
||
status: 'indeterminate',
|
||
grossReceipts: null,
|
||
directCosts: null,
|
||
excludedCosts: null,
|
||
priorExcess: null,
|
||
grossDifference: null,
|
||
carryForward: null,
|
||
baseAmount: null,
|
||
baseDivisorRate: null,
|
||
vat: null,
|
||
deduction: null,
|
||
reason: '',
|
||
};
|
||
const stop = (reason: string) => {
|
||
r.reason = reason;
|
||
results.push(r);
|
||
};
|
||
if (
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(input.period) ||
|
||
!input.operatorRef.trim() ||
|
||
!input.scope.trim() ||
|
||
!input.registerRef.trim() ||
|
||
input.complete !== 'yes'
|
||
) {
|
||
stop(
|
||
'Identifique período, operador e âmbito completo do registo simulado, incluindo contraprestações/custos e referência. Não apresentar soma parcial como apuramento global da organização.',
|
||
);
|
||
continue;
|
||
}
|
||
const rows = book.rows
|
||
.map((row, i) => ({ row, line: lines[i] }))
|
||
.filter(
|
||
({ row }) =>
|
||
row.operatorRef === input.operatorRef &&
|
||
row.period === input.period,
|
||
);
|
||
if (
|
||
book.rows.some(
|
||
(row) => !row.operatorRef.trim() || !row.period.trim(),
|
||
) ||
|
||
!rows.length ||
|
||
rows.some(({ line }) => line.status === 'indeterminate')
|
||
) {
|
||
stop(
|
||
'Linhas por esclarecer ou âmbito sem registo: período indeterminado; não omitir silenciosamente operação/custo da margem global.',
|
||
);
|
||
continue;
|
||
}
|
||
const selected = rows.filter(
|
||
({ line }) =>
|
||
line.status === 'determined' &&
|
||
['special', 'mixed'].includes(line.regime),
|
||
);
|
||
if (new Set(selected.map(({ row }) => row.origin)).size > 1) {
|
||
stop(
|
||
'Não somar dados efectivos e pressupostos projectados no mesmo registo global sem distinguir o cenário. Conservar as linhas e delimitar o âmbito.',
|
||
);
|
||
continue;
|
||
}
|
||
let prior: bigint | null = null;
|
||
if (input.priorMode === 'none') prior = 0n;
|
||
else if (input.priorMode === 'manual' && input.priorRef.trim())
|
||
prior = travelDecimal(input.priorAmount);
|
||
else if (input.priorMode === 'previous') {
|
||
const prev = results.find(
|
||
(x) =>
|
||
x.period === previousMonth(input.period) &&
|
||
x.operatorRef === input.operatorRef &&
|
||
x.status === 'determined',
|
||
);
|
||
if (prev?.carryForward !== null && prev?.carryForward !== undefined)
|
||
prior = travelDecimal(prev.carryForward);
|
||
}
|
||
if (prior === null) {
|
||
stop(
|
||
'Excesso negativo anterior por esclarecer: indicar inexistência, montante/referência ou reutilizar período imediatamente anterior determinado do mesmo operador.',
|
||
);
|
||
continue;
|
||
}
|
||
const existingPrevious = results.find(
|
||
(x) =>
|
||
x.period === previousMonth(input.period) &&
|
||
x.operatorRef === input.operatorRef &&
|
||
x.status === 'determined',
|
||
);
|
||
if (
|
||
input.priorMode !== 'previous' &&
|
||
existingPrevious?.carryForward !== null &&
|
||
existingPrevious?.carryForward !== undefined &&
|
||
travelDecimal(existingPrevious.carryForward) !== prior &&
|
||
(input.priorMode !== 'manual' ||
|
||
!input.priorRef.trim() ||
|
||
input.priorOverride !== 'yes')
|
||
) {
|
||
stop(
|
||
'Excesso indicado ' +
|
||
money(prior) +
|
||
' diverge do período anterior calculado ' +
|
||
existingPrevious.carryForward +
|
||
'. A alteração muda os custos/base e o excesso seguinte; indique montante manual e referência da correcção e confirme a utilização, ou reutilize o anterior. Nada foi sobrescrito.',
|
||
);
|
||
continue;
|
||
}
|
||
let gross = 0n,
|
||
costs = 0n,
|
||
excluded = 0n;
|
||
for (const { line } of selected) {
|
||
gross += travelDecimal(line.specialGross!)!;
|
||
costs += travelDecimal(line.eligibleCosts!)!;
|
||
excluded += travelDecimal(line.excludedCosts!)!;
|
||
}
|
||
const difference = gross - costs - prior,
|
||
positive = difference > 0n ? difference : 0n,
|
||
baseWhole = (positive * 100n + 58000000n) / 116000000n;
|
||
Object.assign(r, {
|
||
status: 'determined',
|
||
grossReceipts: money(gross),
|
||
directCosts: money(costs),
|
||
excludedCosts: money(excluded),
|
||
priorExcess: money(prior),
|
||
grossDifference: money(difference),
|
||
carryForward: money(difference < 0n ? -difference : 0n),
|
||
baseAmount: money(baseWhole * 1000000n),
|
||
baseDivisorRate: '16%',
|
||
reason: 'RIVA41(4)/(5),43a/b: soma global das parcelas especiais registadas no âmbito declarado, custos directos de terceiros e excesso anterior. Diferença positiva dividida por1.16, base arredondada à unidade; diferença negativa transportada como custo para o período seguinte, não dedução nem imposto a reembolsar. Taxa16% apenas no divisor da base; IVA devido e dedução43d não calculados. Alteração de custos/contraprestação44 exige versão referenciada, sem reescrever histórico ou gerar regularização automática.',
|
||
});
|
||
results.push(r);
|
||
}
|
||
return results;
|
||
}
|
||
function key(draft: Record<string, unknown>, book: TravelBook): string {
|
||
return JSON.stringify({
|
||
version: travelVersion,
|
||
scope: acquisitionScope(draft),
|
||
location: draft.selection_labels ?? {},
|
||
currency: draft.currency,
|
||
rows: book.rows,
|
||
periods: book.periods,
|
||
});
|
||
}
|
||
export function travelCurrent(
|
||
draft: Record<string, unknown>,
|
||
book: TravelBook,
|
||
): boolean {
|
||
return !!book.result && book.result.sourceKey === key(draft, book);
|
||
}
|
||
export function recordTravel(
|
||
draft: Record<string, unknown>,
|
||
book: TravelBook,
|
||
): TravelBook {
|
||
const costIds = book.rows.flatMap((r) => r.costs.map((c) => c.id));
|
||
if (
|
||
!book.rows.length ||
|
||
new Set(book.rows.map((r) => r.id)).size !== book.rows.length ||
|
||
new Set(
|
||
book.rows.map(
|
||
(r) => r.operatorRef + '|' + r.period + '|' + r.operationRef,
|
||
),
|
||
).size !== book.rows.length ||
|
||
new Set(costIds).size !== costIds.length ||
|
||
new Set(book.periods.map((p) => p.id)).size !== book.periods.length ||
|
||
new Set(book.periods.map((p) => p.operatorRef + '|' + p.period))
|
||
.size !== book.periods.length
|
||
)
|
||
throw new Error(
|
||
'Identidades de operações/custos/períodos devem ser distintas; evite dupla soma, originais conservados.',
|
||
);
|
||
const rows = book.rows.map((row) => evaluateTravel(row, draft.currency));
|
||
const result: TravelCalculation = {
|
||
version: travelVersion,
|
||
revision: (book.result?.revision ?? 0) + 1,
|
||
calculatedAt: new Date().toISOString(),
|
||
opportunity: acquisitionScope(draft),
|
||
sourceKey: key(draft, book),
|
||
source: travelSource,
|
||
inputs: structuredClone(book.rows),
|
||
periodInputs: structuredClone(book.periods),
|
||
rows,
|
||
periods: evaluateTravelPeriods(book, rows),
|
||
};
|
||
const next = {
|
||
...book,
|
||
result,
|
||
history: [
|
||
...(book.history ?? []),
|
||
...(book.result ? [book.result] : []),
|
||
],
|
||
};
|
||
saveTravelBook(draft, next);
|
||
return next;
|
||
}
|
||
export function travelReportRows(
|
||
draft: Record<string, unknown>,
|
||
): Array<[string, string, string, string]> {
|
||
try {
|
||
const b = travelBook(draft),
|
||
r = b.result;
|
||
if (!r) return [];
|
||
const state = `${r.opportunity};${travelCurrent(draft, b) ? 'Resultado actual' : 'Entradas alteradas; resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source}`;
|
||
return [
|
||
...r.inputs.map((row, i): [string, string, string, string] => [
|
||
'IVA mensal',
|
||
`P129 — ${row.period} — ${row.id}`,
|
||
`${row.description};regime:${r.rows[i].regime};${r.rows[i].reason};contraprestação parte terceiros:${r.rows[i].specialGross ?? 'Indeterminada'};parte geral:${r.rows[i].generalGross ?? 'Indeterminada'};custos elegíveis:${r.rows[i].eligibleCosts ?? 'Indeterminados'};custos excluídos:${r.rows[i].excludedCosts ?? 'Indeterminados'};base apenas no período global`,
|
||
`${state};${row.origin};operação${row.operationRef};operador${row.operatorRef};sede/estabelecimento${row.agencyCountry},${row.agencyRef};âmbito${row.scope};documento${row.documentRef},${row.documentDate};meios${row.resources};retalhista${row.retailer};novo conjunto${row.newPackage},${row.packageRef};preço comIVA${row.grossPrice}${row.currency};parte terceiros${row.thirdPartyGross},repartição${row.allocationRef};enquadramento${row.domesticTaxable},${row.taxRef};condições${JSON.stringify(row.conditions)};custos${JSON.stringify(row.costs)};factos${row.evidence}`,
|
||
]),
|
||
...r.periods.map((p, i): [string, string, string, string] => [
|
||
'IVA mensal',
|
||
`P129 — margem global ${p.period} — ${p.operatorRef}`,
|
||
`contraprestações:${p.grossReceipts ?? 'Indeterminadas'};custos directos:${p.directCosts ?? 'Indeterminados'};excesso anterior:${p.priorExcess ?? 'Indeterminado'};diferença:${p.grossDifference ?? 'Indeterminada'};base:${p.baseAmount ?? 'Indeterminada'}MZN;excesso seguinte:${p.carryForward ?? 'Indeterminado'};${p.reason};IVA/dedução:Indeterminados`,
|
||
`${state};registo${JSON.stringify(r.periodInputs.find((x) => x.id === p.id) ?? r.periodInputs[i])};sem total organizacional presumido, nova dedução/campo11, regularização ou totaisModeloA.`,
|
||
]),
|
||
];
|
||
} catch {
|
||
return [
|
||
[
|
||
'IVA mensal',
|
||
'P129',
|
||
'Dados não recuperáveis',
|
||
'Originais preservados; sem margem por defeito.',
|
||
],
|
||
];
|
||
}
|
||
}
|