import type { IncomeAnalytics, IncomeAnalyticsMeta } 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; }; 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: IncomeAnalyticsMeta["dependencies"][number] | undefined, unavailable: "previousUnavailable" | "yearUnavailable", ): ComparisonNotice { if (!dependency || dependency.unavailableReason) return unavailable; return dependency.lastAttempt.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?: IncomeAnalyticsMeta, ): IncomeDisplayModel { const entries: IncomeDisplayModel["entries"] = []; const occurrences = new Map(); for (const entry of analytics.entries) { const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]); const occurrence = occurrences.get(occurrenceKey) ?? 0; occurrences.set(occurrenceKey, occurrence + 1); entries.push({ key: JSON.stringify([ entry.instrumentKey, entry.id, occurrence, entry.logoName, ]), operation: entry, }); } const dependencies = meta?.dependencies ?? []; const previous = dependencies.find(({ role }) => role === "previous"); const year = 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 };