feat(shopping): alışveriş listeleri, stok eşiği önerileri ve akıllı mağaza önerisi motoru
- shoppingLists koleksiyonu (ürün tipi / marka / serbest metin kalemleri), migration 015 - planShopping saf fonksiyonu: paket yuvarlama, zahmet ve hacimli cezası, teslimat, yemek kartı önceliği, ek mağaza eşiği - eşik önerileri onay/red (red stok yenilenene kadar), ürün tipine hacimli işareti - fiyat servisinde ortak loadTypeOffers; seed listeleri ve testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
import { tr } from "../../i18n/tr";
|
||||
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<typeof shoppingListFormSchema>;
|
||||
export type ShoppingListForm = z.output<typeof shoppingListFormSchema>;
|
||||
|
||||
/** 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<typeof shoppingItemFormSchema>;
|
||||
export type ShoppingItemForm = z.output<typeof shoppingItemFormSchema>;
|
||||
|
||||
/** 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<typeof shoppingItemUpdateSchema>;
|
||||
|
||||
export const shoppingItemRefSchema = z.object({ listId: id, itemId: id });
|
||||
Reference in New Issue
Block a user