feat: align investment client with hourly sync
This commit is contained in:
@@ -497,7 +497,6 @@ describe("AccountOperationsPage", () => {
|
||||
it("keeps UTC operation boundaries in GET and source sync requests", async () => {
|
||||
configureTestAuth();
|
||||
const requested: Record<string, string>[] = [];
|
||||
const syncRequests: unknown[] = [];
|
||||
server.use(
|
||||
http.get(
|
||||
"*/api/v1/brokerage-accounts/:accountId/operations",
|
||||
@@ -518,11 +517,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
|
||||
);
|
||||
},
|
||||
),
|
||||
http.post("*/api/v1/tbank-sync", async ({ request }) => {
|
||||
syncRequests.push(await request.json());
|
||||
|
||||
return HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 });
|
||||
}),
|
||||
);
|
||||
renderPage("?from=2024-02-29&to=2024-03-01&categories=trade");
|
||||
await screen.findByRole("button", { name: "Сбросить" });
|
||||
@@ -534,7 +528,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
|
||||
to: "2024-03-01T23:59:59.999Z",
|
||||
},
|
||||
]);
|
||||
expect(syncRequests).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not label incomplete empty operations as a confirmed empty period", async () => {
|
||||
|
||||
@@ -770,28 +770,6 @@ describe("AccountPage states", () => {
|
||||
let requestCount = 0;
|
||||
let resolveRetry: ((response: Response) => void) | undefined;
|
||||
server.use(
|
||||
http.post("*/api/v1/tbank-sync", () =>
|
||||
HttpResponse.json(
|
||||
{
|
||||
id: "overview",
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
scope: { accountId: "account-1", screen: "account" },
|
||||
},
|
||||
{ status: 202 },
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
id: "overview",
|
||||
now: "2026-09-25T00:00:00Z",
|
||||
scope: { accountId: "account-1", screen: "account" },
|
||||
state: "succeeded",
|
||||
},
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1)
|
||||
|
||||
@@ -130,17 +130,6 @@ describe("AccountsPage", () => {
|
||||
});
|
||||
|
||||
it("keeps the cached account list quietly after a refresh error", async () => {
|
||||
const requests: unknown[] = [];
|
||||
server.use(
|
||||
http.post("*/api/v1/tbank-sync", async ({ request }) => {
|
||||
requests.push(await request.json());
|
||||
|
||||
return HttpResponse.json(
|
||||
{ code: "TBANK_UNAVAILABLE" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
let requestCount = 0;
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts", () => {
|
||||
@@ -169,7 +158,6 @@ describe("AccountsPage", () => {
|
||||
expect(screen.queryByRole("button", { name: "Повторить" }))
|
||||
.not.toBeInTheDocument();
|
||||
expect(screen.getByText(account.name)).toBeInTheDocument();
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a cached empty account list visible after a refresh error", async () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ const AccountsPage: FC = () => {
|
||||
return (
|
||||
<AccountsPageLoaded
|
||||
accounts={viewModel.accounts}
|
||||
sync={viewModel.sync}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,15 +4,17 @@ import {
|
||||
useBrokerageAccounts,
|
||||
} from "@/entities/brokerage-account";
|
||||
import { useScreenSync } from "@/features/tbank-connection";
|
||||
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
import { ApiErrorCode, apiErrorCode, isApiErrorCode, type paths } from "@/shared/api";
|
||||
import { useInitialAvailability } from "@/shared/lib";
|
||||
|
||||
type AccountsPageViewModel
|
||||
= | { accounts: BrokerageAccountCard[]; state: "loaded" }
|
||||
= | { accounts: BrokerageAccountCard[]; state: "loaded"; sync: AccountsSync }
|
||||
| { code?: string; state: "error" }
|
||||
| { state: "empty" }
|
||||
| { state: "loading" };
|
||||
|
||||
type AccountsSync = paths["/api/v1/brokerage-accounts"]["get"]["responses"][200]["content"]["application/json"]["sync"];
|
||||
|
||||
function isAccountsAccessFailure(code?: string) {
|
||||
return isApiErrorCode(code, ApiErrorCode.InvalidTbankToken)
|
||||
|| isApiErrorCode(code, ApiErrorCode.TbankNotConnected);
|
||||
@@ -50,6 +52,7 @@ function useAccountsPageViewModel(): AccountsPageViewModel {
|
||||
return {
|
||||
accounts: brokerageAccountCards(accounts.data, accounts.meta?.dependencies),
|
||||
state: "loaded" as const,
|
||||
sync: accounts.sync,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Button, Stack, Text, Title } from "@mantine/core";
|
||||
import { Badge, Button, Stack, Text, Title } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
@@ -11,10 +11,35 @@ import {
|
||||
|
||||
type AccountsPageLoadedProps = {
|
||||
accounts: BrokerageAccountCard[];
|
||||
sync:
|
||||
| null
|
||||
| undefined
|
||||
| {
|
||||
finishedAt: null | string;
|
||||
sections: {
|
||||
accountId?: string;
|
||||
errorCode: null | string;
|
||||
section: string;
|
||||
}[];
|
||||
startedAt: string;
|
||||
state: string;
|
||||
};
|
||||
};
|
||||
|
||||
const AccountsPageLoaded: FC<AccountsPageLoadedProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const sync = props.sync;
|
||||
|
||||
const syncLabel = sync
|
||||
? new Intl.DateTimeFormat("ru-RU", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(sync.finishedAt ?? sync.startedAt))
|
||||
: null;
|
||||
|
||||
const syncColor = sync?.state === "failed" || sync?.state === "partial"
|
||||
? "red"
|
||||
: "gray";
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
@@ -24,6 +49,21 @@ const AccountsPageLoaded: FC<AccountsPageLoadedProps> = (props) => {
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("accounts.subtitle")}
|
||||
</Text>
|
||||
|
||||
{sync && (
|
||||
<Text c="dimmed" size="xs">
|
||||
Последний обход:
|
||||
{" "}
|
||||
{syncLabel}
|
||||
{" "}
|
||||
<Badge
|
||||
color={syncColor}
|
||||
size="xs"
|
||||
>
|
||||
{sync.state}
|
||||
</Badge>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user