import type { IncomeAnalytics, useIncomeAnalytics } from "@/entities/income"; import { formatDate } from "@/shared/lib"; import type { IncomePeriod } from "./period"; type ComparisonNotice = | "comparisonFailed" | "comparisonPending" | "previousUnavailable" | "yearUnavailable"; type IncomeDisplayModel = { comparison: null | { comparedPeriod: string; selectedPeriod: string; values: (NonNullable< IncomeAnalytics["yearComparison"] >["totalsByCurrency"][number] & { percentages: Record<"coupon" | "dividend" | "total", null | string>; })[]; }; entries: { key: string; operation: IncomeAnalytics["entries"][number] }[]; previousComparison: IncomeDisplayModel["comparison"]; previousNotice: ComparisonNotice; yearNotice: ComparisonNotice; }; type IncomeMeta = ReturnType["meta"]; function comparisonDisplay( period: IncomePeriod, comparison: IncomeAnalytics["yearComparison"], ): IncomeDisplayModel["comparison"] { if (!comparison) return null; return { comparedPeriod: formatComparisonPeriod(comparison.period), selectedPeriod: formatComparisonPeriod(period), values: comparison.totalsByCurrency.map(value => ({ ...value, percentages: { coupon: formatIncomeChange( value.difference.coupon.value, value.compared.coupon.value, ), dividend: formatIncomeChange( value.difference.dividend.value, value.compared.dividend.value, ), total: formatIncomeChange( value.difference.total.value, value.compared.total.value, ), }, })), }; } function comparisonNotice( dependency: NonNullable["dependencies"]>[number] | undefined, unavailable: "previousUnavailable" | "yearUnavailable", ): ComparisonNotice { if (!dependency || dependency.unavailableReason) return unavailable; return dependency.sync.state === "failed" ? "comparisonFailed" : "comparisonPending"; } function formatComparisonPeriod(period: IncomePeriod): string { return `${formatDate(period.from)} – ${formatDate(period.to)}`; } function formatIncomeChange( difference: string, compared: string, ): null | string { // Display-only ratio; server-provided Money values remain exact decimal strings. const base = Number(compared); if (!Number.isFinite(base) || base <= 0) return null; const ratio = Number(difference) / base; if (!Number.isFinite(ratio)) return null; return new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 1, signDisplay: "exceptZero", style: "percent", }).format(ratio); } function toIncomeDisplayModel( analytics: IncomeAnalytics, meta?: IncomeMeta, ): IncomeDisplayModel | null { const instruments = new Map< string, { currency: string; includedCount: number; receiptCount: number; } >(); const currencies = new Set( analytics.totalsByCurrency.map(({ currency }) => currency), ); for (const instrument of analytics.instruments) { if ( typeof instrument.key !== "string" || !instrument.key.trim() || instruments.has(instrument.key) || !currencies.has(instrument.total.currency) ) return null; instruments.set(instrument.key, { currency: instrument.total.currency, includedCount: 0, receiptCount: instrument.receiptCount, }); } const entries: IncomeDisplayModel["entries"] = []; const occurrences = new Map(); for (const entry of analytics.entries) { if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim()) return null; const instrument = instruments.get(entry.instrumentKey); if (!instrument || entry.payment?.currency !== instrument.currency) return null; const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]); const occurrence = occurrences.get(occurrenceKey) ?? 0; occurrences.set(occurrenceKey, occurrence + 1); instrument.includedCount += 1; entries.push({ key: JSON.stringify([ entry.instrumentKey, entry.id, occurrence, entry.logoName, ]), operation: entry, }); } for (const instrument of instruments.values()) { if (instrument.includedCount !== instrument.receiptCount) return null; } const previous = meta?.dependencies?.find(({ role }) => role === "previous"); const year = meta?.dependencies?.find(({ role }) => role === "year"); return { comparison: comparisonDisplay(analytics.period, analytics.yearComparison), entries, previousComparison: comparisonDisplay(analytics.period, analytics.previous), previousNotice: comparisonNotice(previous, "previousUnavailable"), yearNotice: comparisonNotice(year, "yearUnavailable"), }; } export { type IncomeDisplayModel, toIncomeDisplayModel };