refactor: align frontend ownership boundaries
Close audit gaps across API drift, query policy, UI, and tests.
This commit is contained in:
@@ -20,12 +20,26 @@ type IncomeForecast =
|
||||
events: BrokerageEvent[];
|
||||
};
|
||||
|
||||
type IncomeDisplayModel = {
|
||||
trend: {
|
||||
currency: string;
|
||||
buckets: {
|
||||
period: IncomeAnalytics["buckets"][number]["period"];
|
||||
total: IncomeAnalytics["totalsByCurrency"][number];
|
||||
}[];
|
||||
}[];
|
||||
instruments: (IncomeAnalytics["instruments"][number] & {
|
||||
entries: IncomeAnalytics["entries"];
|
||||
})[];
|
||||
};
|
||||
|
||||
type AccountIncomePageViewModel =
|
||||
| { state: "loading" }
|
||||
| { state: "error"; retry: () => void; period: IncomePeriod }
|
||||
| {
|
||||
state: "loaded";
|
||||
analytics: IncomeAnalytics;
|
||||
display: IncomeDisplayModel;
|
||||
forecast: IncomeForecast;
|
||||
form: ReturnType<typeof useForm<IncomeForm>>;
|
||||
onReset: () => void;
|
||||
@@ -81,6 +95,7 @@ const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
|
||||
return {
|
||||
state: "loaded",
|
||||
analytics: analytics.data,
|
||||
display: toIncomeDisplayModel(analytics.data),
|
||||
forecast: toForecast(events),
|
||||
form,
|
||||
onSubmit,
|
||||
@@ -126,12 +141,59 @@ function validPeriod(period: IncomePeriod): period is IncomePeriod {
|
||||
}
|
||||
|
||||
function isDate(value: string): boolean {
|
||||
if (!datePattern.test(value)) return false;
|
||||
const parsed = new Date(`${value}T00:00:00Z`);
|
||||
return (
|
||||
datePattern.test(value) &&
|
||||
new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value
|
||||
Number.isFinite(parsed.valueOf()) &&
|
||||
parsed.toISOString().slice(0, 10) === value
|
||||
);
|
||||
}
|
||||
|
||||
function toIncomeDisplayModel(analytics: IncomeAnalytics): IncomeDisplayModel {
|
||||
const entriesByInstrument = new Map<string, IncomeAnalytics["entries"]>();
|
||||
for (const entry of analytics.entries) {
|
||||
const key = incomeEntryKey(entry);
|
||||
entriesByInstrument.set(key, [
|
||||
...(entriesByInstrument.get(key) ?? []),
|
||||
entry,
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
trend: analytics.totalsByCurrency.map(({ currency }) => ({
|
||||
currency,
|
||||
buckets: analytics.buckets.map((bucket) => ({
|
||||
period: bucket.period,
|
||||
total:
|
||||
bucket.totalsByCurrency.find((item) => item.currency === currency) ??
|
||||
zeroSummary(currency),
|
||||
})),
|
||||
})),
|
||||
instruments: analytics.instruments.map((instrument) => ({
|
||||
...instrument,
|
||||
entries: entriesByInstrument.get(instrument.key) ?? [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function incomeEntryKey(entry: IncomeAnalytics["entries"][number]): string {
|
||||
const base = entry.instrumentUid
|
||||
? `uid:${entry.instrumentUid}`
|
||||
: entry.figi
|
||||
? `figi:${entry.figi}`
|
||||
: entry.ticker || entry.instrumentType
|
||||
? `ticker:${entry.ticker ?? ""}:${entry.instrumentType ?? ""}`
|
||||
: `operation:${entry.id}`;
|
||||
return `${base}:${entry.payment?.currency ?? ""}`;
|
||||
}
|
||||
|
||||
function zeroSummary(
|
||||
currency: string,
|
||||
): IncomeAnalytics["totalsByCurrency"][number] {
|
||||
const zero = { currency, value: "0" };
|
||||
return { currency, coupon: zero, dividend: zero, total: zero };
|
||||
}
|
||||
|
||||
function moscowDate(value: Date): string {
|
||||
const parts = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Europe/Moscow",
|
||||
@@ -144,4 +206,9 @@ function moscowDate(value: Date): string {
|
||||
return `${part("year")}-${part("month")}-${part("day")}`;
|
||||
}
|
||||
|
||||
export { type AccountIncomePageViewModel, useAccountIncomePageViewModel };
|
||||
export {
|
||||
type AccountIncomePageViewModel,
|
||||
isDate,
|
||||
toIncomeDisplayModel,
|
||||
useAccountIncomePageViewModel,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user