refactor: enforce enum machine codes #18
Use owner enums for known codes; preserve wire values and unknown fallbacks. Verified npm ci and verify: 379 tests, 5 lint checks, app and Storybook builds.
This commit was merged in pull request #41.
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
useBrokerageOperations,
|
||||
} from "@/entities/broker-operation";
|
||||
import { useScreenSync } from "@/features/tbank-connection";
|
||||
import { apiErrorCode } from "@/shared/api";
|
||||
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
type AccountOperationsPageViewModel
|
||||
= | {
|
||||
@@ -114,7 +114,7 @@ const useAccountOperationsPageVm = (
|
||||
|
||||
const resetCursors = () => setPagination({ cursors: [], scopeKey });
|
||||
const retry = () => {
|
||||
if (apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR") {
|
||||
if (isApiErrorCode(apiErrorCode(operations.error), ApiErrorCode.InvalidOperationsCursor)) {
|
||||
void queryClient.invalidateQueries({
|
||||
exact: true,
|
||||
queryKey: brokerageOperationsQueryOptions(accountId, firstPageQuery)
|
||||
@@ -136,7 +136,7 @@ const useAccountOperationsPageVm = (
|
||||
if (
|
||||
(operations.isError || operations.meta?.sync.state === "failed")
|
||||
&& (!operations.data
|
||||
|| apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR")
|
||||
|| isApiErrorCode(apiErrorCode(operations.error), ApiErrorCode.InvalidOperationsCursor))
|
||||
)
|
||||
return { onReset, retry, state: "error" };
|
||||
if (!operations.data) return { state: "loading" };
|
||||
|
||||
@@ -5,12 +5,14 @@ import { StateView } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
type AccountPageErrorProps = { code?: string; retry: () => void };
|
||||
|
||||
const AccountPageError: FC<AccountPageErrorProps> = (props) => {
|
||||
const { code, retry } = props;
|
||||
const { t } = useTranslation();
|
||||
if (code === "NOT_FOUND")
|
||||
if (isApiErrorCode(code, ApiErrorCode.NotFound))
|
||||
return (
|
||||
<StateView
|
||||
action={(
|
||||
@@ -23,7 +25,7 @@ const AccountPageError: FC<AccountPageErrorProps> = (props) => {
|
||||
title={t("portfolio.accountUnavailableTitle")}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED" || code === "INVALID_TBANK_TOKEN")
|
||||
if (isApiErrorCode(code, ApiErrorCode.TbankNotConnected) || isApiErrorCode(code, ApiErrorCode.InvalidTbankToken))
|
||||
return (
|
||||
<StateView
|
||||
action={(
|
||||
@@ -36,7 +38,7 @@ const AccountPageError: FC<AccountPageErrorProps> = (props) => {
|
||||
title={t("portfolio.connectionErrorTitle")}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
if (isApiErrorCode(code, ApiErrorCode.TbankUnavailable))
|
||||
return (
|
||||
<StateView
|
||||
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { StateView } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
type AccountsPageErrorProps = { code?: string; retry?: () => void };
|
||||
|
||||
const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
|
||||
@@ -15,7 +17,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
|
||||
{t("accounts.openSettings")}
|
||||
</Button>
|
||||
);
|
||||
if (code === "TBANK_NOT_CONNECTED")
|
||||
if (isApiErrorCode(code, ApiErrorCode.TbankNotConnected))
|
||||
return (
|
||||
<StateView
|
||||
action={settingsAction}
|
||||
@@ -24,7 +26,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
|
||||
title={t("accounts.notConnectedTitle")}
|
||||
/>
|
||||
);
|
||||
if (code === "INVALID_TBANK_TOKEN")
|
||||
if (isApiErrorCode(code, ApiErrorCode.InvalidTbankToken))
|
||||
return (
|
||||
<StateView
|
||||
action={settingsAction}
|
||||
@@ -33,7 +35,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
|
||||
title={t("accounts.invalidTokenTitle")}
|
||||
/>
|
||||
);
|
||||
if (code === "TBANK_UNAVAILABLE")
|
||||
if (isApiErrorCode(code, ApiErrorCode.TbankUnavailable))
|
||||
return (
|
||||
<StateView
|
||||
action={(
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, Navigate } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
import { BondPageView } from "./BondPage.view";
|
||||
import { useBondPageVm } from "./BondPage.vm";
|
||||
|
||||
@@ -22,15 +24,15 @@ const BondPage: FC = () => {
|
||||
</Stack>
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
const unavailableMessage = vm.code === "BOND_TICKER_AMBIGUOUS" ? "bond.ambiguousTicker" : "bond.unavailableText";
|
||||
const unavailableMessage = isApiErrorCode(vm.code, ApiErrorCode.BondTickerAmbiguous) ? "bond.ambiguousTicker" : "bond.unavailableText";
|
||||
if (!vm.bond)
|
||||
return (
|
||||
<Stack role="alert">
|
||||
<Title order={1}>{t("bond.unavailable")}</Title>
|
||||
<Text>
|
||||
{t(
|
||||
vm.code === "TBANK_NOT_CONNECTED"
|
||||
|| vm.code === "INVALID_TBANK_TOKEN"
|
||||
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|
||||
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
|
||||
? "bond.connect"
|
||||
: unavailableMessage,
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
import {
|
||||
formatDate,
|
||||
formatDecimal,
|
||||
@@ -188,8 +189,8 @@ const BondsPageView: FC<BondsPageViewProps> = (props) => {
|
||||
<Stack gap="xs">
|
||||
<Text>
|
||||
{t(
|
||||
vm.code === "TBANK_NOT_CONNECTED"
|
||||
|| vm.code === "INVALID_TBANK_TOKEN"
|
||||
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|
||||
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
|
||||
? "bond.connect"
|
||||
: "stock.sectionError",
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useBankOperations,
|
||||
useBankSummary,
|
||||
} from "@/entities/bank-card";
|
||||
import { apiErrorCode } from "@/shared/api";
|
||||
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
import { formatDate, formatMoney } from "@/shared/lib";
|
||||
|
||||
const PAGE_SIZES = new Set(["5", "10", "20", "50", "100", "all"]);
|
||||
@@ -81,7 +81,7 @@ function useCardPageVm() {
|
||||
deleting,
|
||||
editingCard,
|
||||
history,
|
||||
notFound: apiErrorCode(card.error) === "BANK_NOT_FOUND",
|
||||
notFound: isApiErrorCode(apiErrorCode(card.error), ApiErrorCode.BankNotFound),
|
||||
operation,
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
@@ -27,6 +27,9 @@ import type {
|
||||
OperationValues,
|
||||
} from "./OperationForm.types";
|
||||
|
||||
// ZodIssueCode is deprecated; this is the custom refinement code accepted by Zod.
|
||||
enum FormIssueCode { Custom = "custom" }
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
const operationSchema = z.object({
|
||||
@@ -49,13 +52,13 @@ function validateRefund(values: OperationValues, context: z.RefinementCtx,
|
||||
expense: BankOperation | undefined, limit: BankOperation["availableRefund"] | undefined) {
|
||||
if (!expense || expense.cardId !== values.cardId)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["originalExpenseId"],
|
||||
...validationMessage("bank.validation.required")(undefined),
|
||||
});
|
||||
if (expense && values.date < expense.date)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["date"],
|
||||
...validationMessage("bank.validation.start")(undefined),
|
||||
});
|
||||
@@ -66,7 +69,7 @@ function validateRefund(values: OperationValues, context: z.RefinementCtx,
|
||||
&& amount > (moneyCents(limit.value) ?? 0n)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["amount"],
|
||||
...validationMessage("bank.validation.refund")(undefined),
|
||||
});
|
||||
@@ -116,7 +119,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
const target = props.cards.find(c => c.id === values.targetCardId);
|
||||
if (!source || source.archivedAt)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["cardId"],
|
||||
...validationMessage("bank.validation.required")(undefined),
|
||||
});
|
||||
@@ -125,7 +128,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
&& (!target || target.archivedAt || target.id === values.cardId)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["targetCardId"],
|
||||
...validationMessage("bank.validation.destination")(undefined),
|
||||
});
|
||||
@@ -136,7 +139,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
&& values.date < target.openingDate)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["date"],
|
||||
...validationMessage("bank.validation.start")(undefined),
|
||||
});
|
||||
@@ -152,7 +155,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["categoryId"],
|
||||
...validationMessage("bank.validation.required")(undefined),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, Navigate } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
import { StockPageView } from "./StockPage.view";
|
||||
import { useStockPageVm } from "./StockPage.vm";
|
||||
|
||||
@@ -22,15 +24,15 @@ const StockPage: FC = () => {
|
||||
</Stack>
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
const unavailableMessage = vm.code === "STOCK_TICKER_AMBIGUOUS" ? "stock.ambiguousTicker" : "stock.unavailableText";
|
||||
const unavailableMessage = isApiErrorCode(vm.code, ApiErrorCode.StockTickerAmbiguous) ? "stock.ambiguousTicker" : "stock.unavailableText";
|
||||
if (!vm.stock)
|
||||
return (
|
||||
<Stack role="alert">
|
||||
<Title order={1}>{t("stock.unavailable")}</Title>
|
||||
<Text>
|
||||
{t(
|
||||
vm.code === "TBANK_NOT_CONNECTED"
|
||||
|| vm.code === "INVALID_TBANK_TOKEN"
|
||||
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|
||||
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
|
||||
? "stock.connect"
|
||||
: unavailableMessage,
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
import { formatMoney, formatSnapshotTime } from "@/shared/lib";
|
||||
|
||||
import type { StocksPageVm } from "./StocksPage.vm";
|
||||
@@ -156,8 +157,8 @@ const StocksPageView: FC<StocksPageViewProps> = (props) => {
|
||||
<Stack gap="xs">
|
||||
<Text>
|
||||
{t(
|
||||
vm.code === "TBANK_NOT_CONNECTED"
|
||||
|| vm.code === "INVALID_TBANK_TOKEN"
|
||||
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|
||||
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
|
||||
? "stock.connect"
|
||||
: "stock.sectionError",
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user