feat: add manual bank card screens

Add cards, operations, refunds, categories and monthly summaries with Mantine date controls.

Derive banking types from runtime OpenAPI. Cover recovery and session isolation.
This commit is contained in:
2026-09-12 18:34:41 +03:00
parent 427ca23a43
commit ff4e88edbe
50 changed files with 7795 additions and 10 deletions
+13
View File
@@ -8,6 +8,10 @@ import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import { AuthProvider, useAuth } from "./auth";
const bankCardsKey = apiQueryClient.queryOptions("get", "/api/v1/bank-cards", {
params: { query: { includeArchived: "false" } },
}).queryKey;
describe("AuthProvider", () => {
it("authorizes requests after restoring the initial session", async () => {
let authorization: string | null = null;
@@ -54,6 +58,7 @@ describe("AuthProvider", () => {
server.use(http.post("*/api/v1/auth/refresh", () => HttpResponse.error()));
const queryClient = createTestQueryClient();
queryClient.setQueryData(["cached-query"], { value: "stale" });
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
const wrapper: FC<PropsWithChildren> = (props) => {
const { children } = props;
@@ -70,6 +75,7 @@ describe("AuthProvider", () => {
expect(result.current.user).toBeNull();
expect(result.current.accessToken).toBeNull();
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
});
it("keeps the original protected 401 when serialized refresh has a transport error", async () => {
@@ -90,6 +96,7 @@ describe("AuthProvider", () => {
);
const queryClient = createTestQueryClient();
queryClient.setQueryData(["cached-query"], { value: "stale" });
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
const wrapper: FC<PropsWithChildren> = (props) => {
const { children } = props;
@@ -110,6 +117,7 @@ describe("AuthProvider", () => {
await waitFor(() => expect(result.current.user).toBeNull());
expect(result.current.accessToken).toBeNull();
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
});
it("clears connection status and financial cache independently on logout", async () => {
@@ -136,6 +144,7 @@ describe("AuthProvider", () => {
connectedAt: "2026-08-14T00:00:00Z",
});
queryClient.setQueryData(portfolioKey, { source: "current-connection" });
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
const wrapper: FC<PropsWithChildren> = (props) => {
const { children } = props;
return (
@@ -147,11 +156,15 @@ describe("AuthProvider", () => {
const { result } = renderHook(() => useAuth(), { wrapper });
await waitFor(() => expect(result.current.accessToken).toBe("test-token"));
expect(queryClient.getQueryData(bankCardsKey)).toEqual([
{ id: "bank-card-1" },
]);
await act(() => result.current.logout());
expect(result.current.user).toBeNull();
expect(result.current.accessToken).toBeNull();
expect(queryClient.getQueryData(statusKey)).toBeUndefined();
expect(queryClient.getQueryData(portfolioKey)).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
});
});
@@ -32,6 +32,13 @@ const bondQueryKey = apiQueryClient.queryOptions(
"/api/v1/bonds/{uid}/position",
{ params: { path: { uid: "e6123145-9665-43e0-8413-cd61b8aa9b13" } } },
).queryKey;
const bankCardsQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/bank-cards",
{
params: { query: { includeArchived: "false" } },
},
).queryKey;
const unrelatedQueryKey = apiQueryClient.queryOptions(
"get",
"/api/v1/auth/me",
@@ -66,6 +73,7 @@ function seededClient() {
client.setQueryData(stockQueryKey, { source: "previous-connection" });
client.setQueryData(bondQueryKey, { source: "previous-connection" });
client.setQueryData(unrelatedQueryKey, { id: "user-1" });
client.setQueryData(bankCardsQueryKey, [{ id: "bank-card-1" }]);
return client;
}
@@ -86,6 +94,10 @@ describe("TbankConnectionSection", () => {
).toBeInTheDocument(),
);
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(bankCardsQueryKey)).toEqual([
{ id: "bank-card-1" },
]);
expect(client.getQueryState(bankCardsQueryKey)?.isInvalidated).toBe(false);
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
expect(client.getQueryData(stockQueryKey)).toBeUndefined();
expect(client.getQueryData(bondQueryKey)).toBeUndefined();
@@ -114,6 +126,10 @@ describe("TbankConnectionSection", () => {
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined(),
);
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(bankCardsQueryKey)).toEqual([
{ id: "bank-card-1" },
]);
expect(client.getQueryState(bankCardsQueryKey)?.isInvalidated).toBe(false);
});
it("commits disconnected status and resets financial queries after deleting", async () => {
@@ -139,6 +155,10 @@ describe("TbankConnectionSection", () => {
).toBeInTheDocument(),
);
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
expect(client.getQueryData(bankCardsQueryKey)).toEqual([
{ id: "bank-card-1" },
]);
expect(client.getQueryState(bankCardsQueryKey)?.isInvalidated).toBe(false);
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
expect(client.getQueryData(stockQueryKey)).toBeUndefined();
expect(client.getQueryData(bondQueryKey)).toBeUndefined();