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

95 lines
3.5 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 { 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<typeof recurringRuleFormSchema>;
export type RecurringRuleForm = z.output<typeof recurringRuleFormSchema>;
/** Planlı kaydı onaylama: tutar ve tarih düzeltilebilir. */
export const confirmOccurrenceSchema = z.object({
amount: positiveMoney,
date: ymd,
});
export type ConfirmOccurrenceForm = z.output<typeof confirmOccurrenceSchema>;
export const SERIES_SCOPES = ["one", "following", "all"] as const;
export type SeriesScope = (typeof SERIES_SCOPES)[number];