feat(shopping): alışverişi tamamla transaction'ı ve karşılaştırma motoru
- tek transaction: stok alımı, fiş kaynaklı fiyat, mağaza başına bölünmüş gider, fiş ekleri, kalanların taşınması - muadil, markasız ürün tipi ve plansız ekstra satırları; stok → gider kategorisi eşlemesi ve yedek kategori - compareShopping saf fonksiyonu: aynı/muadil/alınmadı/ekstra, farkın nedenlere tam ayrışması - tamamlanan listede rapor görüntüsü; testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { tr } from "../../i18n/tr";
|
||||
import { parseMoney } from "../money";
|
||||
import { parseQuantity } from "../quantity";
|
||||
|
||||
const s = tr.shopping.validation;
|
||||
@@ -72,3 +73,91 @@ export const shoppingItemUpdateSchema = z.object({
|
||||
export type ShoppingItemUpdate = z.output<typeof shoppingItemUpdateSchema>;
|
||||
|
||||
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<typeof shoppingCompletionSchema>;
|
||||
export type ShoppingCompletionForm = z.output<typeof shoppingCompletionSchema>;
|
||||
|
||||
Reference in New Issue
Block a user