feat(shared): işlem şeması, bakiye etkisi, etiket ve CSV yardımcıları
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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>;
|
||||
Reference in New Issue
Block a user