import { z } from "zod"; // Gemeinsame, gehaertete Basisfelder: Laengenlimits verhindern uebergrosse // Payloads in der Datenbank, trim/lowercase normalisieren E-Mail-Adressen, // damit "Max@Example.de" und "max@example.de " dasselbe Konto treffen. const emailSchema = z .string() .trim() .toLowerCase() .max(254, "Die E-Mail-Adresse ist zu lang.") .email("Bitte gib eine gültige E-Mail-Adresse ein."); const nameSchema = (message: string) => z.string().trim().min(1, message).max(100); export const newsletterSchema = z.object({ email: emailSchema, }); export type NewsletterInput = z.infer; export const stockNotificationSchema = z.object({ productId: z.string().min(1).max(64), email: emailSchema, }); export type StockNotificationInput = z.infer; export const returnRequestSchema = z.object({ orderId: z.string().min(1).max(64), reason: z.string().trim().min(3, "Bitte gib einen Grund für die Rücksendung an.").max(2000), }); export type ReturnRequestInput = z.infer; export const businessInquirySchema = z.object({ companyName: z.string().trim().min(2, "Bitte gib den Firmennamen an.").max(200), contactPerson: z.string().trim().min(2, "Bitte gib einen Ansprechpartner an.").max(100), email: emailSchema, phone: z.string().trim().max(40).optional(), desiredQuantity: z.coerce .number() .int() .min(1, "Bitte gib eine Menge von mindestens 1 an.") .max(100_000), desiredDeliveryDate: z.string().max(30).optional(), package: z.enum(["KLASSIK", "GESCHENK", "INDIVIDUELL"]), message: z.string().trim().max(5000).optional(), }); export type BusinessInquiryInput = z.infer; export const registerSchema = z.object({ firstName: nameSchema("Bitte gib deinen Vornamen an."), lastName: nameSchema("Bitte gib deinen Nachnamen an."), email: emailSchema, // bcrypt verarbeitet maximal 72 Bytes — laengere Passwoerter wuerden sonst // stillschweigend abgeschnitten, deshalb hartes Limit mit klarer Meldung. password: z .string() .min(8, "Das Passwort muss mindestens 8 Zeichen lang sein.") .max(72, "Das Passwort darf höchstens 72 Zeichen lang sein."), }); export type RegisterInput = z.infer; export const loginSchema = z.object({ email: emailSchema, password: z.string().min(1, "Bitte gib dein Passwort ein.").max(72), }); export type LoginInput = z.infer; export const requestPasswordResetSchema = z.object({ email: emailSchema, }); export type RequestPasswordResetInput = z.infer; export const resetPasswordSchema = z.object({ token: z.string().trim().min(1, "Ungültiger oder abgelaufener Link."), password: z .string() .min(8, "Das Passwort muss mindestens 8 Zeichen lang sein.") .max(72, "Das Passwort darf höchstens 72 Zeichen lang sein."), }); export type ResetPasswordInput = z.infer; export const adminCreateCustomerSchema = z.object({ firstName: nameSchema("Bitte gib den Vornamen an."), lastName: nameSchema("Bitte gib den Nachnamen an."), email: emailSchema, role: z.enum(["KUNDE", "ADMIN"]), }); export type AdminCreateCustomerInput = z.infer; export const adminUpdateCustomerSchema = z.object({ firstName: nameSchema("Bitte gib den Vornamen an."), lastName: nameSchema("Bitte gib den Nachnamen an."), email: emailSchema, phone: z.string().trim().max(40).optional(), role: z.enum(["KUNDE", "ADMIN"]), }); export type AdminUpdateCustomerInput = z.infer; // Nur die Zahlenfelder aus dem Admin-Produktformular — name/description/ // tasteTags/category/consistency laufen weiterhin ueber die einfachen // str()/strOrNull()-Helfer in admin/actions/products.ts, da dort keine // Vorzeichen-/Bereichspruefung noetig ist. num() dort prueft nur NaN/ // Infinity, nicht Vorzeichen — ohne dieses Schema waeren negative Preise/ // Gewichte/Intensitaeten ueber ein manipuliertes Formularfeld moeglich. export const adminProductNumbersSchema = z.object({ intensityMild: z.coerce.number().int().min(1).max(5), intensityFloral: z.coerce.number().int().min(1).max(5), intensityKraeftig: z.coerce.number().int().min(1).max(5), weightGrams: z.coerce.number().int().min(1, "Das Gewicht muss mindestens 1 g betragen."), priceCents: z.coerce.number().int().min(0, "Der Preis darf nicht negativ sein."), pricePerKgCents: z.coerce.number().int().min(0, "Der Kilopreis darf nicht negativ sein."), quantity: z.coerce.number().int().min(0, "Der Lagerbestand darf nicht negativ sein."), /** * Umsatzsteuersatz als Ausnahme zur Kategorie. Leeres Feld = null = "Satz * der Kategorie" (der Normalfall, siehe lib/vat.ts) — deshalb ein eigener * Zweig fuer den Leerstring: z.coerce.number() machte daraus sonst eine 0, * und 0 bedeutet hier "steuerfrei nach § 19 UStG", also etwas voellig * anderes als "nicht gesetzt". */ vatRatePercent: z .union([ z.literal("").transform(() => null), z.coerce .number() .int("Der Steuersatz muss eine ganze Zahl sein.") .min(0, "Der Steuersatz darf nicht negativ sein.") .max(100, "Der Steuersatz darf höchstens 100 betragen."), ]) .default(null), }); export type AdminProductNumbersInput = z.infer; export const shippingTierSchema = z .object({ minQuantity: z.coerce.number().int().min(0, "Die Mindestmenge darf nicht negativ sein."), maxQuantity: z.coerce.number().int().min(1, "Die Höchstmenge muss mindestens 1 sein.").optional(), priceCents: z.coerce.number().int().min(0, "Der Preis darf nicht negativ sein."), }) .refine((data) => data.maxQuantity === undefined || data.maxQuantity >= data.minQuantity, { message: "Die Höchstmenge muss größer oder gleich der Mindestmenge sein.", path: ["maxQuantity"], }); export type ShippingTierInput = z.infer; export const addressSchema = z.object({ label: z.string().trim().min(1, "Bitte gib eine Bezeichnung an.").max(60), firstName: nameSchema("Bitte gib den Vornamen an."), lastName: nameSchema("Bitte gib den Nachnamen an."), street: z.string().trim().min(1, "Bitte gib die Straße an.").max(200), zip: z.string().trim().min(4, "Bitte gib eine gültige Postleitzahl an.").max(10), city: z.string().trim().min(1, "Bitte gib den Ort an.").max(100), country: z.string().trim().max(2).default("DE"), isDefault: z.boolean().default(false), }); export type AddressInput = z.infer; export const checkoutSchema = z.object({ email: emailSchema, firstName: nameSchema("Bitte gib deinen Vornamen an."), lastName: nameSchema("Bitte gib deinen Nachnamen an."), phone: z.string().trim().max(40).optional(), fulfillmentMethod: z.enum(["VERSAND", "ABHOLUNG"]), street: z.string().trim().max(200).optional(), zip: z.string().trim().max(10).optional(), city: z.string().trim().max(100).optional(), country: z.string().trim().max(2).default("DE"), items: z .array( z.object({ productId: z.string().min(1).max(64), quantity: z.coerce.number().int().min(1).max(20), }) ) .min(1, "Der Warenkorb ist leer.") .max(50, "Zu viele verschiedene Positionen im Warenkorb."), discountCode: z.string().trim().max(50).optional(), giftMessage: z.string().trim().max(500, "Die Geschenknachricht darf höchstens 500 Zeichen lang sein.").optional(), /** Nummer der ueber die DHL-Standortsuche gewaehlten Packstation (z.B. "145"). */ packstationNumber: z .string() .trim() .regex(/^\d{1,10}$/, "Die Packstation-Nummer besteht nur aus Ziffern.") .optional(), /** DHL-Postnummer der empfangenden Person — bei Packstation-Lieferung Pflicht. */ postNumber: z .string() .trim() .regex(/^\d{6,10}$/, "Die DHL-Postnummer besteht aus 6 bis 10 Ziffern.") .optional(), }) // DHL verlangt bei Lieferung an eine Packstation zwingend die Postnummer // der empfangenden Person — ohne sie laesst sich spaeter keine // Versandmarke erstellen, die Bestellung waere also nicht zustellbar. .refine((data) => !data.packstationNumber || Boolean(data.postNumber), { message: "Für die Lieferung an eine Packstation wird deine DHL-Postnummer benötigt.", path: ["postNumber"], }) // Eine Packstation ist ein Versandziel — bei Abholung ergibt sie keinen Sinn // und wuerde sonst still mitgespeichert. .refine((data) => !data.packstationNumber || data.fulfillmentMethod === "VERSAND", { message: "Eine Packstation ist nur bei Versand möglich.", path: ["packstationNumber"], }); export type CheckoutInput = z.infer; export const reviewSchema = z.object({ productId: z.string().min(1).max(64), rating: z.coerce.number().int().min(1, "Bitte wähle eine Bewertung von 1 bis 5 Sternen.").max(5), comment: z.string().trim().max(2000).optional(), }); export type ReviewInput = z.infer; export const discountCodeSchema = z .object({ code: z .string() .trim() .min(3, "Der Code muss mindestens 3 Zeichen lang sein.") .max(30) .toUpperCase(), type: z.enum(["PERCENT", "FIXED"]), value: z.coerce.number().int().min(1, "Der Wert muss mindestens 1 sein."), minOrderCents: z.coerce.number().int().min(0, "Der Mindestbestellwert darf nicht negativ sein.").default(0), minQuantity: z.coerce.number().int().min(1, "Die Mindestmenge muss mindestens 1 sein.").optional(), maxUses: z.coerce.number().int().min(1, "Das Nutzungslimit muss mindestens 1 sein.").optional(), isActive: z.boolean().default(true), validFrom: z.coerce.date().optional(), validUntil: z.coerce.date().optional(), }) .refine((data) => data.type !== "PERCENT" || data.value <= 100, { message: "Ein Prozent-Rabatt darf höchstens 100 sein.", path: ["value"], }) .refine((data) => !data.validFrom || !data.validUntil || data.validFrom <= data.validUntil, { message: "Das Enddatum muss nach dem Startdatum liegen.", path: ["validUntil"], }); export type DiscountCodeInput = z.infer;