feat: show persisted investment data and sync state

Consume the provided resource-envelope API snapshot. Preserve financial snapshots during
refresh, observe synchronization without importing, and fence late responses across
account and connection changes.
This commit is contained in:
2026-09-26 21:22:49 +03:00
parent b987fd8e78
commit 4e5f564fa1
81 changed files with 4714 additions and 1420 deletions
+65 -54
View File
@@ -16,6 +16,7 @@ import type { IncomeAnalytics } from "@/entities/income";
import type { Portfolio } from "@/entities/portfolio";
import { AuthProvider } from "@/features/auth";
import { createTestQueryClient } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
import { AppRoutes } from "./AppRoutes";
@@ -132,50 +133,56 @@ beforeEach(() => {
),
http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json(
accounts.map((account) => ({
...account,
type: "BROKERAGE",
openedOn: null,
portfolioSummary: null,
})),
resource(
accounts.map((account) => ({
...account,
type: "BROKERAGE",
openedOn: null,
portfolioSummary: null,
})),
),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(portfolio),
HttpResponse.json(resource(portfolio)),
),
http.get(
"*/api/v1/brokerage-accounts/:accountId/operations",
({ params }) =>
HttpResponse.json({
accountId: params.accountId,
items,
hasNext: false,
nextCursor: null,
observedAt: receipt.occurredAt,
}),
HttpResponse.json(
resource({
accountId: params.accountId,
items,
hasNext: false,
nextCursor: null,
observedAt: receipt.occurredAt,
}),
),
),
http.get("*/api/v1/brokerage-accounts/:accountId/income-analytics", () =>
HttpResponse.json(analytics),
HttpResponse.json(resource(analytics)),
),
http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
HttpResponse.json({
accountId: "all",
calculatedAt: receipt.occurredAt,
incomeTotalsByCurrency: [],
events: accounts.map((account) => ({
account,
type: "coupon",
date: "2026-09-15",
dateKind: "event",
figi: null,
instrumentUid: null,
ticker: "BOND",
displayName: "Облигация",
isin: null,
logoName: null,
projectedAmount: { currency: "RUB", value: "2" },
})),
}),
HttpResponse.json(
resource({
accountId: "all",
calculatedAt: receipt.occurredAt,
incomeTotalsByCurrency: [],
events: accounts.map((account) => ({
account,
type: "coupon",
date: "2026-09-15",
dateKind: "event",
figi: null,
instrumentUid: null,
ticker: "BOND",
displayName: "Облигация",
isin: null,
logoName: null,
projectedAmount: { currency: "RUB", value: "2" },
})),
}),
),
),
);
});
@@ -224,20 +231,22 @@ describe("All accounts routes", () => {
({ request, params }) => {
const url = new URL(request.url);
requests.push(url);
return HttpResponse.json({
accountId: params.accountId,
items: [
{
...receipt,
description: url.searchParams.has("cursor")
? "Вторая страница"
: "Первая страница",
},
],
hasNext: !url.searchParams.has("cursor"),
nextCursor: url.searchParams.has("cursor") ? null : "next",
observedAt: receipt.occurredAt,
});
return HttpResponse.json(
resource({
accountId: params.accountId,
items: [
{
...receipt,
description: url.searchParams.has("cursor")
? "Вторая страница"
: "Первая страница",
},
],
hasNext: !url.searchParams.has("cursor"),
nextCursor: url.searchParams.has("cursor") ? null : "next",
observedAt: receipt.occurredAt,
}),
);
},
),
);
@@ -274,13 +283,15 @@ describe("All accounts routes", () => {
{ code: "INVALID_OPERATIONS_CURSOR" },
{ status: 400 },
)
: HttpResponse.json({
accountId: "all",
items,
hasNext: true,
nextCursor: "expired",
observedAt: receipt.occurredAt,
}),
: HttpResponse.json(
resource({
accountId: "all",
items,
hasNext: true,
nextCursor: "expired",
observedAt: receipt.occurredAt,
}),
),
),
);
renderPage("/accounts/all/operations?from=2026-08-01&to=2026-08-31");
@@ -12,6 +12,7 @@ import { MemoryRouter, useNavigate } from "react-router";
import { beforeEach, describe, expect, it } from "vitest";
import { AuthProvider } from "@/features/auth";
import { createTestQueryClient } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
import { ProtectedDocumentTitle } from "./DocumentTitle";
@@ -62,7 +63,7 @@ describe("document titles", () => {
server.use(
http.get("*/api/v1/brokerage-accounts", async () => {
await accountsReady;
return HttpResponse.json([{ id: "a", name: "ИИС" }]);
return HttpResponse.json(resource([{ id: "a", name: "ИИС" }]));
}),
);
renderTitle("/accounts/a/income?from=2026-01-01&to=2026-08-31");
@@ -85,7 +86,7 @@ describe("document titles", () => {
async (kind, id, fallback, name) => {
server.use(
http.get(`*/api/v1/${kind}/by-ticker/:ticker`, () =>
HttpResponse.json({ name }),
HttpResponse.json(resource({ name })),
),
);
renderTitle(`/${kind}/${id}`);