feat(web): Zod DTO schemas mirroring backend + fixtures
This commit is contained in:
parent
0275aeaaaf
commit
6454bc0a51
16 changed files with 189 additions and 0 deletions
9
src/api/schemas/analyze.ts
Normal file
9
src/api/schemas/analyze.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const NamedSchema = z.object({ name: z.string(), count: z.number(), total: z.string() });
|
||||||
|
export const MonthSchema = z.object({ month: z.string(), income: z.string(), expense: z.string() });
|
||||||
|
export const AnalyzeSchema = z.object({ from: z.string(), to: z.string(), income: z.string(), expense: z.string(),
|
||||||
|
discretionaryExpense: z.string().nullish(), net: z.string(), months: z.array(MonthSchema).nullish(),
|
||||||
|
expenseCategories: z.array(NamedSchema).nullish(), incomePayees: z.array(NamedSchema).nullish(),
|
||||||
|
expensePayees: z.array(NamedSchema).nullish() });
|
||||||
|
export type Named = z.infer<typeof NamedSchema>;
|
||||||
|
export type Analyze = z.infer<typeof AnalyzeSchema>;
|
||||||
13
src/api/schemas/budget.ts
Normal file
13
src/api/schemas/budget.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const LimitRowSchema = z.object({ category: z.string(), spent: z.string(), limit: z.string() });
|
||||||
|
export const HorizonReportSchema = z.object({ overallSpent: z.string(), overallLimit: z.string(), rows: z.array(LimitRowSchema) });
|
||||||
|
export const CategoryLimitSchema = z.object({ name: z.string(), day: z.string(), week: z.string(), month: z.string() });
|
||||||
|
export const SavingsGoalSchema = z.object({ name: z.string(), target: z.string(), monthlyContribution: z.string(),
|
||||||
|
accountId: z.number(), accountName: z.string(), saved: z.string(), targetDate: z.string() });
|
||||||
|
export const BudgetSchema = z.object({ dayLimit: z.string(), weekLimit: z.string(), monthLimit: z.string(),
|
||||||
|
plannedIncome: z.string(), plannedIncomeManual: z.string(), loanObligations: z.string(),
|
||||||
|
savingsContributions: z.string().nullish(), subscriptionContributions: z.string().nullish(),
|
||||||
|
availableIncome: z.string(), categories: z.array(CategoryLimitSchema), savingsGoals: z.array(SavingsGoalSchema).nullish(),
|
||||||
|
report: z.object({ day: HorizonReportSchema, week: HorizonReportSchema, month: HorizonReportSchema }) });
|
||||||
|
export type Budget = z.infer<typeof BudgetSchema>;
|
||||||
|
export type SavingsGoal = z.infer<typeof SavingsGoalSchema>;
|
||||||
8
src/api/schemas/category.ts
Normal file
8
src/api/schemas/category.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const CategorySchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
kind: z.string(),
|
||||||
|
depth: z.number(),
|
||||||
|
icon: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export type Category = z.infer<typeof CategorySchema>;
|
||||||
7
src/api/schemas/connection.ts
Normal file
7
src/api/schemas/connection.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const ConnectionSchema = z.object({
|
||||||
|
bank: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
courierManaged: z.boolean(),
|
||||||
|
});
|
||||||
|
export type Connection = z.infer<typeof ConnectionSchema>;
|
||||||
11
src/api/schemas/index.ts
Normal file
11
src/api/schemas/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export * from "./networth";
|
||||||
|
export * from "./analyze";
|
||||||
|
export * from "./budget";
|
||||||
|
export * from "./transaction";
|
||||||
|
export * from "./lending";
|
||||||
|
export * from "./settings";
|
||||||
|
export * from "./connection";
|
||||||
|
export * from "./category";
|
||||||
|
export * from "./subscription";
|
||||||
|
export * from "./manualAsset";
|
||||||
|
export * from "./user";
|
||||||
7
src/api/schemas/lending.ts
Normal file
7
src/api/schemas/lending.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const LendingRepaymentSchema = z.object({ id: z.number(), amount: z.string(), paidOn: z.string(),
|
||||||
|
note: z.string(), txnId: z.number().nullish() });
|
||||||
|
export const LendingSchema = z.object({ id: z.number(), person: z.string(), principal: z.string(), lentOn: z.string(),
|
||||||
|
dueOn: z.string(), note: z.string(), repaid: z.string(), remaining: z.string(), status: z.string(), overdue: z.boolean(),
|
||||||
|
txnId: z.number().nullish(), repayments: z.array(LendingRepaymentSchema) });
|
||||||
|
export type Lending = z.infer<typeof LendingSchema>;
|
||||||
50
src/api/schemas/manualAsset.ts
Normal file
50
src/api/schemas/manualAsset.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ManualAssetSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
category: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
acquiredValue: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
condition: z.string(),
|
||||||
|
isLiability: z.boolean(),
|
||||||
|
change: z.string(),
|
||||||
|
});
|
||||||
|
export type ManualAsset = z.infer<typeof ManualAssetSchema>;
|
||||||
|
|
||||||
|
export const AssetPointSchema = z.object({
|
||||||
|
value: z.string(),
|
||||||
|
recordedAt: z.string(),
|
||||||
|
low: z.string().nullish(),
|
||||||
|
high: z.string().nullish(),
|
||||||
|
count: z.number().nullish(),
|
||||||
|
source: z.string().nullish(),
|
||||||
|
kind: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export type AssetPoint = z.infer<typeof AssetPointSchema>;
|
||||||
|
|
||||||
|
export const ListingFieldSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
});
|
||||||
|
export type ListingField = z.infer<typeof ListingFieldSchema>;
|
||||||
|
|
||||||
|
export const AssetListingSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
price: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
description: z.string().nullish(),
|
||||||
|
fields: z.array(ListingFieldSchema).nullish(),
|
||||||
|
images: z.array(z.string()).nullish(),
|
||||||
|
source: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export type AssetListing = z.infer<typeof AssetListingSchema>;
|
||||||
|
|
||||||
|
export const RevalueResultSchema = z.object({
|
||||||
|
value: z.string(),
|
||||||
|
low: z.string(),
|
||||||
|
high: z.string(),
|
||||||
|
source: z.string(),
|
||||||
|
count: z.number(),
|
||||||
|
});
|
||||||
|
export type RevalueResult = z.infer<typeof RevalueResultSchema>;
|
||||||
7
src/api/schemas/networth.ts
Normal file
7
src/api/schemas/networth.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const AccountSchema = z.object({ accountId: z.number(), bank: z.string(), accountNumber: z.string(),
|
||||||
|
currency: z.string(), isLiability: z.boolean(), balance: z.string(), asOf: z.string().nullish() });
|
||||||
|
export const NetWorthSchema = z.object({ assets: z.string(), liabilities: z.string(), total: z.string(),
|
||||||
|
accounts: z.array(AccountSchema).nullish() });
|
||||||
|
export type Account = z.infer<typeof AccountSchema>;
|
||||||
|
export type NetWorth = z.infer<typeof NetWorthSchema>;
|
||||||
12
src/api/schemas/schemas.test.ts
Normal file
12
src/api/schemas/schemas.test.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { NetWorthSchema, AnalyzeSchema, BudgetSchema, TxnSchema, LendingSchema } from "./index";
|
||||||
|
import * as fx from "../../test/fixtures";
|
||||||
|
|
||||||
|
describe("schemas parse backend fixtures", () => {
|
||||||
|
it("networth", () => expect(() => NetWorthSchema.parse(fx.netWorth)).not.toThrow());
|
||||||
|
it("analyze", () => expect(() => AnalyzeSchema.parse(fx.analyzeMonth)).not.toThrow());
|
||||||
|
it("budget", () => expect(() => BudgetSchema.parse(fx.budget)).not.toThrow());
|
||||||
|
it("transaction", () => expect(() => TxnSchema.parse(fx.txn)).not.toThrow());
|
||||||
|
it("lending", () => expect(() => LendingSchema.parse(fx.lending)).not.toThrow());
|
||||||
|
it("rejects wrong shape", () => expect(() => NetWorthSchema.parse({})).toThrow());
|
||||||
|
});
|
||||||
11
src/api/schemas/settings.ts
Normal file
11
src/api/schemas/settings.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const SettingsSchema = z.object({
|
||||||
|
holderName: z.string(),
|
||||||
|
employer: z.string(),
|
||||||
|
salaryKeywords: z.array(z.string()),
|
||||||
|
payDays: z.array(z.number()),
|
||||||
|
ownAccounts: z.array(z.string()),
|
||||||
|
peerAccounts: z.array(z.string()),
|
||||||
|
hideAmounts: z.boolean().nullish(),
|
||||||
|
});
|
||||||
|
export type Settings = z.infer<typeof SettingsSchema>;
|
||||||
15
src/api/schemas/subscription.ts
Normal file
15
src/api/schemas/subscription.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
// Wire JSON field is "id" (Swift-side property name is `subId`, mapped via
|
||||||
|
// CodingKeys); the schema key stays "id" to match the actual JSON payload.
|
||||||
|
export const SubscriptionSchema = z.object({
|
||||||
|
label: z.string(),
|
||||||
|
amount: z.string(),
|
||||||
|
monthly: z.string(),
|
||||||
|
cadence: z.string(),
|
||||||
|
nextDue: z.string().nullish(),
|
||||||
|
matchKey: z.string().nullish(),
|
||||||
|
id: z.number().nullish(),
|
||||||
|
manual: z.boolean().nullish(),
|
||||||
|
category: z.string().nullish(),
|
||||||
|
});
|
||||||
|
export type Subscription = z.infer<typeof SubscriptionSchema>;
|
||||||
5
src/api/schemas/transaction.ts
Normal file
5
src/api/schemas/transaction.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const TxnSchema = z.object({ date: z.string(), amount: z.string(), direction: z.string(), category: z.string(),
|
||||||
|
title: z.string(), balanceAfter: z.string().nullish(), accountId: z.number(), transfer: z.boolean().nullish(),
|
||||||
|
txnId: z.number().nullish(), note: z.string().nullish(), matchKey: z.string().nullish(), salary: z.boolean().nullish() });
|
||||||
|
export type Txn = z.infer<typeof TxnSchema>;
|
||||||
13
src/api/schemas/user.ts
Normal file
13
src/api/schemas/user.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
email: z.string(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
});
|
||||||
|
export type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
export const AuthResponseSchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
user: UserSchema,
|
||||||
|
});
|
||||||
|
export type AuthResponse = z.infer<typeof AuthResponseSchema>;
|
||||||
14
src/test/fixtures.ts
Normal file
14
src/test/fixtures.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const netWorth = { assets: "452200", liabilities: "0", total: "452200",
|
||||||
|
accounts: [{ accountId: 1, bank: "khan", accountNumber: "***1", currency: "MNT", isLiability: false, balance: "452200", asOf: "2026-08-01" }] };
|
||||||
|
export const analyzeMonth = { from: "2026-08-01", to: "2026-08-31", income: "1345634", expense: "1200000",
|
||||||
|
discretionaryExpense: "800000", net: "145634", months: null, expenseCategories: [{ name: "Хоол", count: 12, total: "23700" }],
|
||||||
|
incomePayees: [{ name: "Цалин", count: 1, total: "1345634" }], expensePayees: [{ name: "худалдан авалт", count: 3, total: "52000" }] };
|
||||||
|
export const budget = { dayLimit: "35500", weekLimit: "0", monthLimit: "900000", plannedIncome: "2036795",
|
||||||
|
plannedIncomeManual: "0", loanObligations: "0", savingsContributions: "0", subscriptionContributions: "0", availableIncome: "2036795",
|
||||||
|
categories: [{ name: "Хоол", day: "27000", week: "0", month: "0" }], savingsGoals: [],
|
||||||
|
report: { day: { overallSpent: "15500", overallLimit: "35500", rows: [{ category: "Хоол", spent: "23700", limit: "27000" }] },
|
||||||
|
week: { overallSpent: "0", overallLimit: "0", rows: [] }, month: { overallSpent: "1200000", overallLimit: "900000", rows: [] } } };
|
||||||
|
export const txn = { date: "2026-08-01", amount: "52000", direction: "debit", category: "Хоол", title: "худалдан авалт",
|
||||||
|
balanceAfter: "400200", accountId: 1, transfer: false, txnId: 10, note: "", matchKey: "mk1", salary: false };
|
||||||
|
export const lending = { id: 1, person: "Бат", principal: "100000", lentOn: "2026-07-01", dueOn: "2026-09-01", note: "",
|
||||||
|
repaid: "40000", remaining: "60000", status: "partial", overdue: false, txnId: null, repayments: [{ id: 1, amount: "40000", paidOn: "2026-08-01", note: "", txnId: null }] };
|
||||||
6
vitest.config.ts
Normal file
6
vitest.config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"], globals: true },
|
||||||
|
});
|
||||||
1
vitest.setup.ts
Normal file
1
vitest.setup.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
Loading…
Add table
Reference in a new issue