Files
butcem/src/shared/schemas/transaction.ts
T

121 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import { tr } from "../../i18n/tr";
import { parseMoney } from "../money";
import { MAX_SPLITS, normalizeTags, TRANSACTION_TYPES } from "../transactions";
const v = tr.validation;
const t = tr.transactions.validation;
const objectId = z.string().regex(/^[a-f0-9]{24}$/i, t.selectAccount);
const optionalId = z
.string()
.regex(/^([a-f0-9]{24})?$/i)
.nullable()
.optional()
.transform((s) => (s ? s : null));
/** Pozitif tutar ("1.250,50" → 125050). */
const positiveMoney = z
.string()
.trim()
.transform((s, ctx) => {
const minor = parseMoney(s);
if (minor === null || minor <= 0) {
ctx.addIssue({ code: "custom", message: minor === null ? v.money : t.amountPositive });
return z.NEVER;
}
return minor;
});
const splitSchema = z.object({
categoryId: objectId.refine(Boolean, t.selectCategory),
amount: positiveMoney,
note: z.string().trim().max(120).default(""),
});
/**
* İşlem formu (web + mobil). Kurallar:
* - transfer: hedef hesap zorunlu ve kaynaktan farklı; kategori/bölme yok.
* - gelir/gider: ya tek kategori ya da en az 2 kalemli bölme; kalemler toplamı = tutar.
*/
export const transactionFormSchema = z
.object({
type: z.enum(TRANSACTION_TYPES),
amount: positiveMoney,
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date),
accountId: objectId,
toAccountId: optionalId,
categoryId: optionalId,
splits: z.array(splitSchema).max(MAX_SPLITS).default([]),
payee: z.string().trim().max(80).default(""),
description: z.string().trim().max(500).default(""),
tags: z
.union([z.string(), z.array(z.string())])
.default("")
.transform((x) => normalizeTags(x)),
})
.superRefine((x, ctx) => {
if (x.type === "transfer") {
if (!x.toAccountId)
ctx.addIssue({ code: "custom", path: ["toAccountId"], message: t.selectToAccount });
else if (x.toAccountId === x.accountId)
ctx.addIssue({ code: "custom", path: ["toAccountId"], message: t.sameAccount });
return;
}
if (x.splits.length === 1) {
ctx.addIssue({ code: "custom", path: ["splits"], message: t.splitMin });
} else if (x.splits.length >= 2) {
const sum = x.splits.reduce((a, s) => a + s.amount, 0);
if (sum !== x.amount) ctx.addIssue({ code: "custom", path: ["splits"], message: t.splitSum });
} else if (!x.categoryId) {
ctx.addIssue({ code: "custom", path: ["categoryId"], message: t.selectCategory });
}
});
export type TransactionFormInput = z.input<typeof transactionFormSchema>;
export type TransactionForm = z.output<typeof transactionFormSchema>;
/** Liste filtreleri (URL arama parametrelerinden). Geçersiz değerler yok sayılır. */
export const transactionFilterSchema = z.object({
from: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.catch(undefined),
to: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.catch(undefined),
type: z.enum(TRANSACTION_TYPES).optional().catch(undefined),
hesap: z
.string()
.regex(/^[a-f0-9]{24}$/i)
.optional()
.catch(undefined),
kategori: z
.string()
.regex(/^[a-f0-9]{24}$/i)
.optional()
.catch(undefined),
kisi: z
.string()
.regex(/^[a-f0-9]{24}$/i)
.optional()
.catch(undefined),
durum: z.enum(["posted", "planned", "void"]).optional().catch(undefined),
min: z
.string()
.optional()
.catch(undefined)
.transform((s) => (s ? (parseMoney(s) ?? undefined) : undefined)),
max: z
.string()
.optional()
.catch(undefined)
.transform((s) => (s ? (parseMoney(s) ?? undefined) : undefined)),
etiket: z.string().trim().max(30).optional().catch(undefined),
q: z.string().trim().max(80).optional().catch(undefined),
sayfa: z.coerce.number().int().min(1).max(10_000).catch(1).default(1),
});
export type TransactionFilters = z.output<typeof transactionFilterSchema>;