04a17d2775
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
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];
|