refactor: unify bank form types and actions
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BankCard } from "@/entities/bank-card";
|
||||
|
||||
import type { OperationValues } from "./OperationForm.types";
|
||||
|
||||
import { createOperationSchema } from "./OperationForm.schema";
|
||||
|
||||
const card: BankCard = {
|
||||
archivedAt: null,
|
||||
balance: { currency: "RUB", value: "0" },
|
||||
bankName: null,
|
||||
id: "card",
|
||||
lastFour: null,
|
||||
name: "Карта",
|
||||
openingBalance: { currency: "RUB", value: "0" },
|
||||
openingDate: "2026-01-01",
|
||||
paymentSystem: null,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const values: OperationValues = {
|
||||
amount: "1",
|
||||
cardId: card.id,
|
||||
categoryId: "",
|
||||
date: "2026-01-02",
|
||||
description: "",
|
||||
kind: "expense",
|
||||
originalExpenseId: "",
|
||||
targetCardId: "",
|
||||
};
|
||||
|
||||
describe("expense category validation", () => {
|
||||
it.each([
|
||||
["", [{ id: "default", isDefault: true }], true],
|
||||
["chosen", [{ id: "chosen", isDefault: false }], true],
|
||||
["", [{ id: "chosen", isDefault: false }], false],
|
||||
["unknown", [{ id: "default", isDefault: true }], false],
|
||||
["", undefined, false],
|
||||
] as const)("checks category %s with current categories %j", (
|
||||
categoryId, categories, valid,
|
||||
) => {
|
||||
const schema = createOperationSchema({
|
||||
cards: [card],
|
||||
categories: categories ? [...categories] : undefined,
|
||||
});
|
||||
|
||||
const result = schema.safeParse({ ...values, categoryId });
|
||||
expect(result.success).toBe(valid);
|
||||
if (!result.success)
|
||||
// eslint-disable-next-line vitest/no-conditional-expect -- Invalid table cases retain the category field error boundary.
|
||||
expect(result.error.issues.map(issue => issue.path))
|
||||
.toEqual([["categoryId"]]);
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
} from "@/entities/bank-card";
|
||||
import { validationMessage } from "@/shared/i18n";
|
||||
|
||||
import type { OperationValues } from "./OperationForm.types";
|
||||
|
||||
// ZodIssueCode is deprecated; this is the custom refinement code accepted by Zod.
|
||||
enum FormIssueCode {
|
||||
Custom = "custom",
|
||||
@@ -43,6 +41,8 @@ const operationSchema = z.object({
|
||||
targetCardId: z.string(),
|
||||
});
|
||||
|
||||
type OperationValues = z.infer<typeof operationSchema>;
|
||||
|
||||
function createOperationSchema({
|
||||
cards,
|
||||
categories,
|
||||
@@ -83,16 +83,19 @@ function createOperationSchema({
|
||||
path: ["date"],
|
||||
...validationMessage("bank.validation.start")(undefined),
|
||||
});
|
||||
if (values.kind === "refund")
|
||||
|
||||
if (values.kind === "refund") {
|
||||
validateRefund(values, context, expense, limit);
|
||||
if (
|
||||
values.kind === "expense"
|
||||
&& !categories?.some(
|
||||
c =>
|
||||
c.id
|
||||
=== (values.categoryId || categories.find(c => c.isDefault)?.id),
|
||||
)
|
||||
)
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.kind !== "expense") return;
|
||||
|
||||
const effectiveCategoryId
|
||||
= values.categoryId || categories?.find(c => c.isDefault)?.id;
|
||||
|
||||
if (!categories?.some(c => c.id === effectiveCategoryId))
|
||||
context.addIssue({
|
||||
code: FormIssueCode.Custom,
|
||||
path: ["categoryId"],
|
||||
@@ -128,4 +131,4 @@ function validateRefund(
|
||||
});
|
||||
}
|
||||
|
||||
export { createOperationSchema };
|
||||
export { createOperationSchema, type OperationValues };
|
||||
|
||||
@@ -4,6 +4,8 @@ import type {
|
||||
BankOperation,
|
||||
} from "@/entities/bank-card";
|
||||
|
||||
import type { OperationValues } from "./OperationForm.schema";
|
||||
|
||||
type Choice = { label: string; value: string };
|
||||
|
||||
type OperationFieldsProps = {
|
||||
@@ -26,20 +28,11 @@ type OperationFormProps = {
|
||||
operation?: BankOperation;
|
||||
};
|
||||
|
||||
type OperationValues = {
|
||||
amount: string;
|
||||
cardId: string;
|
||||
categoryId: string;
|
||||
date: string;
|
||||
description: string;
|
||||
kind: BankOperation["kind"];
|
||||
originalExpenseId: string;
|
||||
targetCardId: string;
|
||||
};
|
||||
|
||||
export type {
|
||||
Choice,
|
||||
OperationFieldsProps,
|
||||
OperationFormProps,
|
||||
OperationValues,
|
||||
|
||||
};
|
||||
|
||||
export { type OperationValues } from "./OperationForm.schema";
|
||||
|
||||
@@ -43,6 +43,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
const [submittedValues, setSubmittedValues] = useState<OperationValues>();
|
||||
const [error, setError] = useState<unknown>();
|
||||
const categories = useBankCategories();
|
||||
const defaultCategoryId = categories.data?.find(c => c.isDefault)?.id;
|
||||
const operationCreation = useCreateBankOperation();
|
||||
const updateOperation = useUpdateBankOperation();
|
||||
|
||||
@@ -98,7 +99,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
}),
|
||||
...(values.kind === "expense" && {
|
||||
categoryId:
|
||||
values.categoryId || categories.data?.find(c => c.isDefault)?.id,
|
||||
values.categoryId || defaultCategoryId,
|
||||
}),
|
||||
...(values.kind === "refund" && {
|
||||
originalExpenseId: values.originalExpenseId,
|
||||
@@ -169,7 +170,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
cards: options,
|
||||
categories:
|
||||
categories.data?.map(c => ({ label: c.name, value: c.id })) ?? [],
|
||||
defaultCategory: categories.data?.find(c => c.isDefault)?.id ?? "",
|
||||
defaultCategory: defaultCategoryId ?? "",
|
||||
editing: Boolean(props.operation),
|
||||
kind,
|
||||
kinds: bankOperationKinds.map(value => ({
|
||||
|
||||
Reference in New Issue
Block a user