133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
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<string, number>();
|
||
|
||
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 };
|