Files
butcem/src/shared/schemas/stock.ts
T
suphicakir 8ab2244755 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>
2026-11-06 23:28:54 +03:00

163 lines
5.6 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 { parseQuantity } from "../quantity";
import { WASTE_REASONS } from "../stock";
const v = tr.validation;
const s = tr.stock.validation;
const ymd = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, v.date);
const optionalId = z
.string()
.regex(/^([a-f0-9]{24})?$/i)
.transform((x) => (x === "" ? null : x));
const requiredId = z.string().regex(/^[a-f0-9]{24}$/i, s.selectCategory);
/** Miktar metni → binde bir tamsayı. `allowZero` sayım ve başlangıç için. */
const quantity = (allowZero = false) =>
z
.string()
.trim()
.transform((x, ctx) => {
const q = parseQuantity(x);
if (q === null || q < 0 || (!allowZero && q === 0)) {
ctx.addIssue({ code: "custom", message: s.quantity });
return z.NEVER;
}
return q;
});
const optionalQuantity = z.preprocess(
(x) => (x === undefined || x === null || String(x).trim() === "" ? null : x),
quantity(true).nullable(),
);
/** Boş → null; dolu → 1..365 gün. */
const countEveryDays = z.preprocess(
(x) => (x === undefined || x === null || String(x).trim() === "" ? null : x),
z
.string()
.trim()
.transform((x, ctx) => {
const n = Number(x);
if (!/^\d+$/.test(x) || n < 1 || n > 365) {
ctx.addIssue({ code: "custom", message: s.countDays });
return z.NEVER;
}
return n;
})
.nullable(),
);
export const stockCategoryFormSchema = z.object({
name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax),
parentId: optionalId,
expenseCategoryId: optionalId,
countEveryDays,
});
export type StockCategoryFormInput = z.input<typeof stockCategoryFormSchema>;
export type StockCategoryForm = z.output<typeof stockCategoryFormSchema>;
export const productTypeFormSchema = z.object({
name: z.string().trim().min(1, tr.common.required).max(80, v.nameMax),
categoryId: requiredId,
unit: z.string().trim().min(1, s.unit).max(20, s.unit),
targetQuantity: quantity(),
thresholdPct: z
.string()
.trim()
.transform((x, ctx) => {
const n = Number(x);
if (!/^\d+$/.test(x) || n > 100) {
ctx.addIssue({ code: "custom", message: s.thresholdPct });
return z.NEVER;
}
return n;
}),
location: z.string().trim().max(80).default(""),
step: quantity(),
countEveryDays,
note: z.string().trim().max(300).default(""),
/** Ağır/hacimli: akıllı mağaza önerisinde taşıma cezası. */
bulky: z.boolean().default(false),
/** Yalnızca yeni ürün tipinde: evdeki başlangıç miktarı. */
initialQuantity: optionalQuantity.default(null),
});
export type ProductTypeFormInput = z.input<typeof productTypeFormSchema>;
export type ProductTypeForm = z.output<typeof productTypeFormSchema>;
/** Alım, tüketim veya fire. Sayım düzeltmesi ayrı formdan (gerçek miktar). */
export const stockMovementFormSchema = z
.object({
kind: z.enum(["purchase", "consume", "waste"]),
date: ymd,
quantity: quantity(),
wasteReason: z.preprocess(
(x) => (x === "" || x === undefined ? null : x),
z.enum(WASTE_REASONS).nullable(),
),
expiryDate: z.preprocess((x) => (x === "" || x === undefined ? null : x), ymd.nullable()),
note: z.string().trim().max(300).default(""),
})
.superRefine((x, ctx) => {
if (x.kind === "waste" && !x.wasteReason)
ctx.addIssue({ code: "custom", path: ["wasteReason"], message: s.wasteReason });
if (x.expiryDate && x.expiryDate < x.date)
ctx.addIssue({ code: "custom", path: ["expiryDate"], message: s.expiryBeforeDate });
})
.transform((x) => ({
...x,
wasteReason: x.kind === "waste" ? x.wasteReason : null,
expiryDate: x.kind === "purchase" ? x.expiryDate : null,
}));
export type StockMovementFormInput = z.input<typeof stockMovementFormSchema>;
export type StockMovementForm = z.output<typeof stockMovementFormSchema>;
/** Tek ürün sayımı: gerçek miktar → fark sayım düzeltmesi olur. */
export const stockCountFormSchema = z.object({
date: ymd,
actualQuantity: quantity(true),
note: z.string().trim().max(300).default(""),
});
export type StockCountFormInput = z.input<typeof stockCountFormSchema>;
export type StockCountForm = z.output<typeof stockCountFormSchema>;
/**
* Toplu sayım satırı: sayılan miktar boşsa ürün sayılmamış sayılır (atlanır). Eksiğin bir kısmı
* fire olarak işaretlenebilir (neden zorunlu).
*/
const stockCountRowSchema = z
.object({
productTypeId: z.string().regex(/^[a-f0-9]{24}$/i),
actualQuantity: optionalQuantity,
wasteQuantity: optionalQuantity.default(null),
wasteReason: z.preprocess(
(x) => (x === "" || x === undefined ? null : x),
z.enum(WASTE_REASONS).nullable(),
),
})
.superRefine((x, ctx) => {
if (x.wasteQuantity && !x.wasteReason)
ctx.addIssue({ code: "custom", path: ["wasteReason"], message: s.wasteReason });
if (x.wasteQuantity && x.actualQuantity === null)
ctx.addIssue({ code: "custom", path: ["actualQuantity"], message: s.quantity });
})
.transform((x) => ({
...x,
wasteQuantity: x.wasteQuantity || null,
wasteReason: x.wasteQuantity ? x.wasteReason : null,
}));
export const stockCountSheetSchema = z
.object({
date: ymd,
categoryId: optionalId,
note: z.string().trim().max(300).default(""),
rows: z.array(stockCountRowSchema).max(500),
})
.superRefine((x, ctx) => {
if (!x.rows.some((r) => r.actualQuantity !== null))
ctx.addIssue({ code: "custom", path: ["rows"], message: s.nothingCounted });
});
export type StockCountSheetInput = z.input<typeof stockCountSheetSchema>;
export type StockCountSheet = z.output<typeof stockCountSheetSchema>;