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