fix(account): show source metadata on empty account and income (WealthLedger/frontend#55) (#133)
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 10s
Frontend verify / verify (push) Failing after 6m34s

This commit was merged in pull request #133.
This commit is contained in:
2026-11-03 06:44:13 +03:00
parent 71fbe7774d
commit 9619dc9791
12 changed files with 131 additions and 33 deletions
+8 -7
View File
@@ -3,7 +3,12 @@ import createQueryClient from "openapi-react-query";
import type { paths } from "./generated/schema";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
import {
ApiErrorCode,
apiErrorCode,
isApiErrorCode,
isBrokerageAccessLoss,
} from "./problem-details";
const HTTP_UNAUTHORIZED = 401;
const HTTP_REQUEST_TIMEOUT = 408;
@@ -165,12 +170,8 @@ function isNetworkFailure(error: unknown): boolean {
}
function isPermanentCode(code?: string): boolean {
return [
ApiErrorCode.InvalidTbankToken,
ApiErrorCode.NotFound,
ApiErrorCode.TbankNotConnected,
ApiErrorCode.Unauthorized,
].some(permanent => isApiErrorCode(code, permanent));
return isBrokerageAccessLoss(code)
|| isApiErrorCode(code, ApiErrorCode.Unauthorized);
}
function isTransientReadFailure(
+6 -1
View File
@@ -7,7 +7,12 @@ export {
configureAuthClient,
} from "./client";
export type { paths } from "./generated/schema";
export { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
export {
ApiErrorCode,
apiErrorCode,
isApiErrorCode,
isBrokerageAccessLoss,
} from "./problem-details";
export {
type InvestmentResourceMeta,
+13 -1
View File
@@ -1,6 +1,11 @@
import { expect, it } from "vitest";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
import {
ApiErrorCode,
apiErrorCode,
isApiErrorCode,
isBrokerageAccessLoss,
} from "./problem-details";
it("matches known codes and preserves unknown wire codes for fallback handling", () => {
expect(
@@ -16,3 +21,10 @@ it("matches known codes and preserves unknown wire codes for fallback handling",
expect(apiErrorCode(null)).toBeUndefined();
expect(apiErrorCode({})).toBeUndefined();
});
it("separates brokerage access loss from UNAUTHORIZED, temporary and unknown codes", () => {
for (const code of ["INVALID_TBANK_TOKEN", "NOT_FOUND", "TBANK_NOT_CONNECTED"])
expect(isBrokerageAccessLoss(code)).toBe(true);
for (const code of ["UNAUTHORIZED", "TBANK_UNAVAILABLE", "FUTURE_PROVIDER_ERROR", undefined])
expect(isBrokerageAccessLoss(code)).toBe(false);
});
+11 -1
View File
@@ -41,4 +41,14 @@ function isApiErrorCode(
return value === expected;
}
export { ApiErrorCode, apiErrorCode, isApiErrorCode };
// Confirmed loss of access to the brokerage data. UNAUTHORIZED is a session
// failure with its own refresh/sign-out path, so it is deliberately excluded.
function isBrokerageAccessLoss(code?: string): boolean {
return [
ApiErrorCode.InvalidTbankToken,
ApiErrorCode.NotFound,
ApiErrorCode.TbankNotConnected,
].some(loss => isApiErrorCode(code, loss));
}
export { ApiErrorCode, apiErrorCode, isApiErrorCode, isBrokerageAccessLoss };
+2 -10
View File
@@ -10,7 +10,7 @@ import type { components, paths } from "./generated/schema";
import { useAuthenticatedQuery } from "./authenticated-query";
import { getAuthenticatedSessionSnapshot } from "./client";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
import { apiErrorCode, isBrokerageAccessLoss } from "./problem-details";
type InvestmentResourceMeta = components["schemas"]["InvestmentResourceMetaDto"];
@@ -39,15 +39,7 @@ function invalidateResourceAccess(
group: string,
error: unknown,
) {
const errorCode = apiErrorCode(error);
if (
[
ApiErrorCode.InvalidTbankToken,
ApiErrorCode.NotFound,
ApiErrorCode.TbankNotConnected,
].every(code => !isApiErrorCode(errorCode, code))
)
return;
if (!isBrokerageAccessLoss(apiErrorCode(error))) return;
const affected = {
predicate: (cached: { meta?: Record<string, unknown> }) =>