import { z } from "zod"; import { tr } from "../../i18n/tr"; import { parseMoney } from "../money"; import { FREQUENCIES, MONTHLY_MODES } from "../recurrence"; import { normalizeTags, TRANSACTION_TYPES } from "../transactions"; const v = tr.validation; const t = tr.transactions.validation; const r = tr.recurring.validation; const ymd = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date); const optionalId = z .string() .regex(/^([a-f0-9]{24})?$/i) .nullable() .optional() .transform((s) => (s ? s : null)); 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; }); /** Formdan gelen boş alanlar (""), seçilmemiş isteğe bağlı değer sayılır. */ const emptyToNull = (v: unknown) => (v === "" || v === undefined ? null : v); export const END_KINDS = ["never", "count", "date"] as const; /** Düzenli işlem kuralı formu (web + mobil). Bölme yok; onaylarken tutar düzeltilebilir. */ export const recurringRuleFormSchema = z .object({ type: z.enum(TRANSACTION_TYPES), amount: positiveMoney, isEstimate: z.boolean().default(false), accountId: z.string().regex(/^[a-f0-9]{24}$/i, t.selectAccount), toAccountId: optionalId, categoryId: optionalId, 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)), startDate: ymd, frequency: z.enum(FREQUENCIES), interval: z.coerce.number().int().min(1, r.interval).max(12, r.interval).default(1), monthlyMode: z.enum(MONTHLY_MODES).default("day"), endKind: z.enum(END_KINDS).default("never"), count: z.preprocess( emptyToNull, z.coerce.number().int().min(1, r.count).max(600, r.count).nullable().optional(), ), endDate: z.preprocess(emptyToNull, ymd.nullable().optional()), }) .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 }); } else if (!x.categoryId) { ctx.addIssue({ code: "custom", path: ["categoryId"], message: t.selectCategory }); } if (x.endKind === "count" && !x.count) ctx.addIssue({ code: "custom", path: ["count"], message: r.count }); if (x.endKind === "date") { if (!x.endDate) ctx.addIssue({ code: "custom", path: ["endDate"], message: v.date }); else if (x.endDate < x.startDate) ctx.addIssue({ code: "custom", path: ["endDate"], message: r.endBeforeStart }); } }) .transform((x) => ({ ...x, count: x.endKind === "count" ? (x.count ?? null) : null, endDate: x.endKind === "date" ? (x.endDate ?? null) : null, })); export type RecurringRuleFormInput = z.input; export type RecurringRuleForm = z.output; /** Planlı kaydı onaylama: tutar ve tarih düzeltilebilir. */ export const confirmOccurrenceSchema = z.object({ amount: positiveMoney, date: ymd, }); export type ConfirmOccurrenceForm = z.output; export const SERIES_SCOPES = ["one", "following", "all"] as const; export type SeriesScope = (typeof SERIES_SCOPES)[number];