d18c8a0d76
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
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>;
|