import { z } from "zod"; import { tr } from "../../i18n/tr"; import { parseMoney } from "../money"; import { parseQuantity } from "../quantity"; const s = tr.shopping.validation; const id = z.string().regex(/^[a-f0-9]{24}$/i); const optionalId = z .string() .regex(/^([a-f0-9]{24})?$/i) .transform((x) => (x === "" ? null : x)); /** Boş → null (1 paket / belirtilmemiş); dolu → binde bir pozitif tamsayı. */ const optionalQuantity = z.preprocess( (x) => (x === undefined || x === null || String(x).trim() === "" ? null : x), z .string() .trim() .transform((x, ctx) => { const q = parseQuantity(x); if (q === null || q <= 0) { ctx.addIssue({ code: "custom", message: s.quantity }); return z.NEVER; } return q; }) .nullable(), ); export const shoppingListFormSchema = z.object({ name: z.string().trim().min(1, tr.common.required).max(80, tr.validation.nameMax), note: z.string().trim().max(300).default(""), }); export type ShoppingListFormInput = z.input; export type ShoppingListForm = z.output; /** Kalem: ürün tipi, belirli ürün (marka/paket) ya da serbest metin. */ export const shoppingItemFormSchema = z .object({ kind: z.enum(["type", "product", "text"]), productTypeId: optionalId.default(""), productId: optionalId.default(""), text: z.string().trim().max(120).default(""), quantity: optionalQuantity.default(null), unit: z.string().trim().max(20).default(""), note: z.string().trim().max(200).default(""), }) .superRefine((x, ctx) => { if (x.kind === "type" && !x.productTypeId) ctx.addIssue({ code: "custom", path: ["productTypeId"], message: s.productType }); if (x.kind === "product" && !x.productId) ctx.addIssue({ code: "custom", path: ["productId"], message: s.product }); if (x.kind === "text" && !x.text) ctx.addIssue({ code: "custom", path: ["text"], message: s.text }); }) .transform((x) => ({ ...x, productTypeId: x.kind === "type" ? x.productTypeId : null, productId: x.kind === "product" ? x.productId : null, text: x.kind === "text" ? x.text : "", unit: x.kind === "text" ? x.unit : "", })); export type ShoppingItemFormInput = z.input; export type ShoppingItemForm = z.output; /** Kalem düzenleme: miktar ve not (serbest metinde metin ve birim de). */ export const shoppingItemUpdateSchema = z.object({ quantity: optionalQuantity.default(null), text: z.string().trim().max(120).default(""), unit: z.string().trim().max(20).default(""), note: z.string().trim().max(200).default(""), }); export type ShoppingItemUpdate = z.output; export const shoppingItemRefSchema = z.object({ listId: id, itemId: id }); const positiveQuantity = z .string() .trim() .transform((x, ctx) => { const q = parseQuantity(x); if (q === null || q <= 0) { ctx.addIssue({ code: "custom", message: s.quantity }); return z.NEVER; } return q; }); const amount = z .string() .trim() .transform((x, ctx) => { const minor = parseMoney(x); if (minor === null || minor <= 0) { ctx.addIssue({ code: "custom", message: s.amount }); return z.NEVER; } return minor; }); /** * Alışverişi tamamla. Satır: liste kalemi (itemId) ya da plansız ekstra (itemId boş). Ürün * kaleminde/ekstrada ürün zorunlu; serbest metin kaleminde ürün yok. Tutar satırın ödenen tutarı. */ export const shoppingCompletionSchema = z .object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, tr.validation.date), lines: z .array( z.object({ itemId: optionalId.default(""), productId: optionalId.default(""), storeId: id, packages: positiveQuantity, amount, }), ) .min(1, s.noLines) .max(200), stores: z .array( z.object({ storeId: id, accountId: z.string().regex(/^[a-f0-9]{24}$/i, s.account), attachmentIds: z.array(id).max(10).default([]), }), ) .max(10), /** Gider kategorisine eşlenmemiş stok kategorilerindeki kalemler için. */ fallbackCategoryId: optionalId.default(""), leftover: z.object({ action: z.enum(["move", "new", "drop"]), targetListId: optionalId.default(""), newListName: z.string().trim().max(80).default(""), }), }) .superRefine((x, ctx) => { const items = x.lines.map((l) => l.itemId).filter(Boolean); if (new Set(items).size !== items.length) ctx.addIssue({ code: "custom", path: ["lines"], message: s.duplicateLine }); x.lines.forEach((l, i) => { if (!l.itemId && !l.productId) ctx.addIssue({ code: "custom", path: ["lines", i, "productId"], message: s.product }); }); const stores = new Set(x.stores.map((st) => st.storeId)); if (stores.size !== x.stores.length) ctx.addIssue({ code: "custom", path: ["stores"], message: s.duplicateStore }); for (const l of x.lines) if (!stores.has(l.storeId)) { ctx.addIssue({ code: "custom", path: ["stores"], message: s.account }); break; } if (x.leftover.action === "move" && !x.leftover.targetListId) ctx.addIssue({ code: "custom", path: ["leftover", "targetListId"], message: s.targetList }); if (x.leftover.action === "new" && !x.leftover.newListName) ctx.addIssue({ code: "custom", path: ["leftover", "newListName"], message: tr.common.required, }); }); export type ShoppingCompletionInput = z.input; export type ShoppingCompletionForm = z.output;