import { z } from "zod"; import { tr } from "../../i18n/tr"; import { ASSET_KEYS } from "../assets"; import { parseMoney } from "../money"; import { parseQuantity } from "../quantity"; const v = tr.validation; const k = tr.inKind.validation; const ymd = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date); const objectId = z.string().regex(/^[a-f0-9]{24}$/i, tr.transactions.validation.selectAccount); const emptyToNull = (x: unknown) => (x === "" || x === undefined ? null : x); /** Boş → null; dolu → pozitif kuruş. */ const optionalMoney = z.preprocess( emptyToNull, z .string() .trim() .transform((s, ctx) => { const minor = parseMoney(s); if (minor === null || minor < 0) { ctx.addIssue({ code: "custom", message: v.money }); return z.NEVER; } return minor; }) .nullable(), ); const quantity = z .string() .trim() .transform((s, ctx) => { const q = parseQuantity(s); if (q === null || q <= 0) { ctx.addIssue({ code: "custom", message: k.quantity }); return z.NEVER; } return q; }); /** Fiyat girişi: satış boşsa alışla aynı (tek fiyat girilirse ikisine de yazılır — Faz 0 kararı). */ export const assetPriceFormSchema = z .object({ assetKey: z.enum(ASSET_KEYS), date: ymd, buy: optionalMoney, sell: optionalMoney, }) .superRefine((x, ctx) => { if (x.buy === null && x.sell === null) ctx.addIssue({ code: "custom", path: ["buy"], message: k.price }); }) .transform((x) => ({ assetKey: x.assetKey, date: x.date, buyMinor: (x.buy ?? x.sell)!, sellMinor: (x.sell ?? x.buy)!, })); export type AssetPriceForm = z.output; /** Aynî borç/alacak kaydı. Birim fiyat boşsa fiyat tablosundan (o tarihteki son fiyat) alınır. */ export const inKindFormSchema = z.object({ direction: z.enum(["debt", "receivable"]), assetKey: z.enum(ASSET_KEYS), quantity, date: ymd, unitPrice: optionalMoney, note: z.string().trim().max(500).default(""), savingsAccountId: z.preprocess(emptyToNull, objectId.nullable()), monthlyPlan: optionalMoney, }); export type InKindForm = z.output; /** Birikim katkısı: hesaptan borcun birikim hesabına transfer. */ export const contributionSchema = z.object({ fromAccountId: objectId, amount: z .string() .trim() .transform((s, ctx) => { const minor = parseMoney(s); if (minor === null || minor <= 0) { ctx.addIssue({ code: "custom", message: tr.transactions.validation.amountPositive }); return z.NEVER; } return minor; }), date: ymd, }); export type ContributionForm = z.output; /** * "Ödeme gerçekleşti". Aynı varlıkla: miktar zorunlu, TL (varlığı almak için harcanan) isteğe * bağlı. TL karşılığıyla: tutar ve hesap zorunlu; miktar boşsa güncel fiyattan hesaplanır. */ export const paymentFormSchema = z .object({ method: z.enum(["asset", "cash"]), date: ymd, quantity: z.preprocess(emptyToNull, quantity.nullable()), amount: optionalMoney, accountId: z.preprocess(emptyToNull, objectId.nullable()), /** Aynı varlıkla ödemede bağlanacak varlık (Faz 9b): borçta varlıktan düşer, alacakta eklenir. */ assetId: z.preprocess(emptyToNull, objectId.nullable()).default(null), note: z.string().trim().max(300).default(""), }) .superRefine((x, ctx) => { if (x.assetId && x.method !== "asset") ctx.addIssue({ code: "custom", path: ["assetId"], message: k.assetOnlyWithAsset }); if (x.assetId && x.amount) ctx.addIssue({ code: "custom", path: ["amount"], message: k.noAmountWithAsset }); if (x.method === "asset" && x.quantity === null) ctx.addIssue({ code: "custom", path: ["quantity"], message: k.quantity }); if (x.method === "cash") { if (!x.amount) ctx.addIssue({ code: "custom", path: ["amount"], message: k.amount }); if (!x.accountId) ctx.addIssue({ code: "custom", path: ["accountId"], message: k.account }); } if (x.amount && !x.accountId) ctx.addIssue({ code: "custom", path: ["accountId"], message: k.account }); }); export type PaymentForm = z.output;