refactor: simplify finance view models
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
export { toInstrumentEventRows } from "./model/operation-rows";
|
||||||
export { section, type SectionState } from "./model/section";
|
export { section, type SectionState } from "./model/section";
|
||||||
export { useInstrumentParameters } from "./model/useInstrumentParameters";
|
export { useInstrumentParameters } from "./model/useInstrumentParameters";
|
||||||
export { InstrumentChart } from "./ui/InstrumentChart/InstrumentChart.view";
|
export { InstrumentChart } from "./ui/InstrumentChart/InstrumentChart.view";
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
|
import type { BrokerageOperation } from "@/entities/broker-operation";
|
||||||
|
|
||||||
|
import { toInstrumentEventRows } from "./operation-rows";
|
||||||
|
|
||||||
|
const operation: BrokerageOperation = {
|
||||||
|
accruedInterest: null,
|
||||||
|
category: "trade",
|
||||||
|
commission: null,
|
||||||
|
description: "Покупка",
|
||||||
|
figi: null,
|
||||||
|
id: "first",
|
||||||
|
instrumentType: "stock",
|
||||||
|
instrumentUid: "uid",
|
||||||
|
logoName: null,
|
||||||
|
occurredAt: "2026-01-01T00:00:00Z",
|
||||||
|
payment: null,
|
||||||
|
price: null,
|
||||||
|
quantity: null,
|
||||||
|
ticker: null,
|
||||||
|
type: "OPERATION_TYPE_BUY",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("fills only missing identity without changing rows or their order", () => {
|
||||||
|
const items = [operation, {
|
||||||
|
...operation,
|
||||||
|
id: "second",
|
||||||
|
logoName: "",
|
||||||
|
ticker: "own",
|
||||||
|
}];
|
||||||
|
|
||||||
|
const original = structuredClone(items);
|
||||||
|
expect(toInstrumentEventRows(items, {
|
||||||
|
logoName: "fallback.png",
|
||||||
|
ticker: "fallback",
|
||||||
|
})).toEqual([
|
||||||
|
{ ...operation, logoName: "fallback.png", ticker: "fallback" },
|
||||||
|
items[1],
|
||||||
|
]);
|
||||||
|
expect(items).toEqual(original);
|
||||||
|
expect(toInstrumentEventRows(items)).toEqual(items);
|
||||||
|
expect(toInstrumentEventRows()).toEqual([]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { BrokerageOperation } from "@/entities/broker-operation";
|
||||||
|
|
||||||
|
function toInstrumentEventRows(
|
||||||
|
items: BrokerageOperation[] = [],
|
||||||
|
instrument?: Pick<BrokerageOperation, "logoName" | "ticker">,
|
||||||
|
): BrokerageOperation[] {
|
||||||
|
return items.map(item => ({
|
||||||
|
...item,
|
||||||
|
logoName: item.logoName ?? instrument?.logoName ?? null,
|
||||||
|
ticker: item.ticker ?? instrument?.ticker ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toInstrumentEventRows };
|
||||||
@@ -203,6 +203,37 @@ describe("AccountsPage", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["idle", "failed"] as const)(
|
||||||
|
"keeps partial empty membership distinct from no accounts (sync=%s)",
|
||||||
|
async (state) => {
|
||||||
|
server.use(
|
||||||
|
http.get("*/api/v1/brokerage-accounts", () =>
|
||||||
|
HttpResponse.json(resource([], {
|
||||||
|
availability: "partial",
|
||||||
|
sync: { errorCode: null, id: null, state },
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const client = createTestQueryClient();
|
||||||
|
renderPage(client);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject({
|
||||||
|
data: [],
|
||||||
|
meta: { availability: "partial" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("Нет инвестиционных счетов"))
|
||||||
|
.not.toBeInTheDocument();
|
||||||
|
if (state === "failed")
|
||||||
|
// eslint-disable-next-line vitest/no-conditional-expect -- Each fixed state has a different observable recovery contract.
|
||||||
|
expect(await screen.findByRole("button", { name: "Повторить" }))
|
||||||
|
.toBeVisible();
|
||||||
|
else
|
||||||
|
// eslint-disable-next-line vitest/no-conditional-expect -- Each fixed state has a different observable recovery contract.
|
||||||
|
expect(screen.getByRole("status")).toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("keeps unknown membership loading instead of declaring no accounts", async () => {
|
it("keeps unknown membership loading instead of declaring no accounts", async () => {
|
||||||
server.use(
|
server.use(
|
||||||
http.get("*/api/v1/brokerage-accounts", () =>
|
http.get("*/api/v1/brokerage-accounts", () =>
|
||||||
|
|||||||
@@ -15,31 +15,31 @@ function useAccountsPageViewModel() {
|
|||||||
) {
|
) {
|
||||||
if (!accounts.isError && accounts.meta?.sync.state !== "failed")
|
if (!accounts.isError && accounts.meta?.sync.state !== "failed")
|
||||||
return { state: "loading" as const };
|
return { state: "loading" as const };
|
||||||
if (accounts.isError || accounts.meta?.sync.state === "failed")
|
|
||||||
return {
|
return {
|
||||||
code: apiErrorCode(accounts.error),
|
code: apiErrorCode(accounts.error),
|
||||||
retry: sync.retry,
|
retry: sync.retry,
|
||||||
state: "error" as const,
|
state: "error" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accounts.data?.length)
|
const refresh = {
|
||||||
return {
|
|
||||||
lastSuccessfulAt: accounts.meta?.lastSuccessfulAt,
|
|
||||||
refreshError: accounts.isError || sync.transportError || sync.sourceError,
|
|
||||||
refreshing: accounts.isFetching || sync.refreshing,
|
|
||||||
retry: sync.retry,
|
|
||||||
sourceError: sync.sourceError,
|
|
||||||
state: "empty" as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
accounts: brokerageAccountCards(accounts.data, accounts.meta?.dependencies),
|
|
||||||
lastSuccessfulAt: accounts.meta?.lastSuccessfulAt,
|
lastSuccessfulAt: accounts.meta?.lastSuccessfulAt,
|
||||||
refreshError: accounts.isError || sync.transportError || sync.sourceError,
|
refreshError: accounts.isError || sync.transportError || sync.sourceError,
|
||||||
refreshing: accounts.isFetching || sync.refreshing,
|
refreshing: accounts.isFetching || sync.refreshing,
|
||||||
retry: sync.retry,
|
retry: sync.retry,
|
||||||
sourceError: sync.sourceError,
|
sourceError: sync.sourceError,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (accounts.data.length === 0)
|
||||||
|
return {
|
||||||
|
...refresh,
|
||||||
|
state: "empty" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts: brokerageAccountCards(accounts.data, accounts.meta?.dependencies),
|
||||||
|
...refresh,
|
||||||
state: "loaded" as const,
|
state: "loaded" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {
|
|||||||
useBondQuote,
|
useBondQuote,
|
||||||
} from "@/entities/bond";
|
} from "@/entities/bond";
|
||||||
import { useBrokerageOperationHistory } from "@/entities/broker-operation";
|
import { useBrokerageOperationHistory } from "@/entities/broker-operation";
|
||||||
import { section, useInstrumentParameters } from "@/features/instrument-details";
|
import {
|
||||||
|
section,
|
||||||
|
toInstrumentEventRows,
|
||||||
|
useInstrumentParameters,
|
||||||
|
} from "@/features/instrument-details";
|
||||||
import { useScreenSync } from "@/features/tbank-connection";
|
import { useScreenSync } from "@/features/tbank-connection";
|
||||||
import { apiErrorCode } from "@/shared/api";
|
import { apiErrorCode } from "@/shared/api";
|
||||||
import { compareDateDesc, instrumentLogoUrl } from "@/shared/lib";
|
import { compareDateDesc, instrumentLogoUrl } from "@/shared/lib";
|
||||||
@@ -62,20 +66,7 @@ function useBondPageVm() {
|
|||||||
code: apiErrorCode(bond.error),
|
code: apiErrorCode(bond.error),
|
||||||
coupons: coupons.data,
|
coupons: coupons.data,
|
||||||
couponsState: section(coupons, sync.retry),
|
couponsState: section(coupons, sync.retry),
|
||||||
eventRows:
|
eventRows: toInstrumentEventRows(operations.data?.items, bond.data),
|
||||||
operations.data?.items.map(item => ({
|
|
||||||
...item,
|
|
||||||
logoName:
|
|
||||||
item.logoName
|
|
||||||
?? bond.data?.logoName
|
|
||||||
|
|
||||||
?? null,
|
|
||||||
ticker:
|
|
||||||
item.ticker
|
|
||||||
?? bond.data?.ticker
|
|
||||||
|
|
||||||
?? null,
|
|
||||||
})) ?? [],
|
|
||||||
filter,
|
filter,
|
||||||
hasPrevious: history.hasPrevious,
|
hasPrevious: history.hasPrevious,
|
||||||
logoSrc: instrumentLogoUrl(bond.data?.logoName),
|
logoSrc: instrumentLogoUrl(bond.data?.logoName),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/entities/stock";
|
} from "@/entities/stock";
|
||||||
import {
|
import {
|
||||||
section,
|
section,
|
||||||
|
toInstrumentEventRows,
|
||||||
useInstrumentParameters,
|
useInstrumentParameters,
|
||||||
} from "@/features/instrument-details";
|
} from "@/features/instrument-details";
|
||||||
import { useScreenSync } from "@/features/tbank-connection";
|
import { useScreenSync } from "@/features/tbank-connection";
|
||||||
@@ -65,20 +66,7 @@ function useStockPageVm() {
|
|||||||
companyState: section(company, sync.retry),
|
companyState: section(company, sync.retry),
|
||||||
dividends: dividends.data,
|
dividends: dividends.data,
|
||||||
dividendsState: section(dividends, sync.retry),
|
dividendsState: section(dividends, sync.retry),
|
||||||
eventRows:
|
eventRows: toInstrumentEventRows(operations.data?.items, stock.data),
|
||||||
operations.data?.items.map(item => ({
|
|
||||||
...item,
|
|
||||||
logoName:
|
|
||||||
item.logoName
|
|
||||||
?? stock.data?.logoName
|
|
||||||
|
|
||||||
?? null,
|
|
||||||
ticker:
|
|
||||||
item.ticker
|
|
||||||
?? stock.data?.ticker
|
|
||||||
|
|
||||||
?? null,
|
|
||||||
})) ?? [],
|
|
||||||
filter,
|
filter,
|
||||||
hasPrevious: history.hasPrevious,
|
hasPrevious: history.hasPrevious,
|
||||||
logoSrc: instrumentLogoUrl(stock.data?.logoName),
|
logoSrc: instrumentLogoUrl(stock.data?.logoName),
|
||||||
|
|||||||
Reference in New Issue
Block a user