import { describe, it, expect } from "vitest"; import { currencySchema, taxRateSchema, ibanSchema, taxIdSchema, vatIdSchema, invoiceSchema, companySchema, customerSchema, } from "@/lib/schemas"; describe("schemas.ts - Zod Validation", () => { describe("currencySchema", () => { it("should accept valid currency values", () => { expect(currencySchema.parse(0)).toBe(0); expect(currencySchema.parse(100)).toBe(100); expect(currencySchema.parse(99.99)).toBe(99.99); expect(currencySchema.parse(1000.5)).toBe(1000.5); }); it("should reject negative values", () => { expect(() => currencySchema.parse(-1)).toThrow("Geldbeträge dürfen nicht negativ sein"); }); it("should reject more than 2 decimal places", () => { expect(() => currencySchema.parse(1.999)).toThrow( "Geldbeträge dürfen maximal 2 Dezimalstellen haben" ); }); it("should accept exactly 2 decimal places", () => { expect(currencySchema.parse(1.99)).toBe(1.99); }); }); describe("taxRateSchema", () => { it("should accept valid German tax rates", () => { expect(taxRateSchema.parse(0)).toBe(0); expect(taxRateSchema.parse(7)).toBe(7); expect(taxRateSchema.parse(19)).toBe(19); }); it("should reject invalid tax rates", () => { expect(() => taxRateSchema.parse(5)).toThrow( "Steuersatz muss 0 (steuerfrei), 7 (ermäßigt) oder 19 (Standard) sein" ); expect(() => taxRateSchema.parse(20)).toThrow(); expect(() => taxRateSchema.parse(15)).toThrow(); }); it("should reject non-integer values", () => { expect(() => taxRateSchema.parse(7.5)).toThrow("Steuersatz muss eine ganze Zahl sein"); }); }); describe("ibanSchema", () => { it("should accept valid IBANs", () => { expect(ibanSchema.parse("DE89370400440532013000")).toBe("DE89370400440532013000"); expect(ibanSchema.parse("AT611904300234573201")).toBe("AT611904300234573201"); expect(ibanSchema.parse("CH9300762011623852957")).toBe("CH9300762011623852957"); }); it("should accept empty string", () => { expect(ibanSchema.parse("")).toBe(""); }); it("should reject invalid IBAN format", () => { // No country code - starts with numbers expect(() => ibanSchema.parse("1234567890")).toThrow(); // Too short - only 4 chars (need at least 5: 2 letters + 2 digits + 1) expect(() => ibanSchema.parse("DE1")).toThrow(); }); }); describe("taxIdSchema", () => { it("should accept valid 10-digit tax IDs", () => { expect(taxIdSchema.parse("1234567890")).toBe("1234567890"); }); it("should accept empty string", () => { expect(taxIdSchema.parse("")).toBe(""); }); it("should reject invalid tax IDs", () => { expect(() => taxIdSchema.parse("123")).toThrow("Steuernummer muss 10 Ziffern haben"); expect(() => taxIdSchema.parse("12345678901")).toThrow(); expect(() => taxIdSchema.parse("abcdefghij")).toThrow(); }); }); describe("vatIdSchema", () => { it("should accept valid DE VAT IDs", () => { expect(vatIdSchema.parse("DE123456789")).toBe("DE123456789"); }); it("should accept empty string", () => { expect(vatIdSchema.parse("")).toBe(""); }); it("should reject invalid VAT IDs", () => { expect(() => vatIdSchema.parse("DE123")).toThrow( "USt-IdNr. muss im Format DE + 9 Ziffern sein" ); expect(() => vatIdSchema.parse("1234567890")).toThrow(); expect(() => vatIdSchema.parse("DE12345678")).toThrow(); }); }); describe("invoiceSchema", () => { const validInvoice = { companyId: "cm123abc", customerId: "cm456def", issueDate: "2026-05-08", dueDate: "2026-06-08", kleinunternehmer: false, items: [ { position: 1, description: "Web Development", quantity: 10, unit: "Stunden", unitPrice: 100, taxRate: 19, netAmount: 1000, taxAmount: 190, grossAmount: 1190, }, ], netTotal: 1000, taxTotal: 190, grossTotal: 1190, }; it("should accept valid invoice", () => { const result = invoiceSchema.parse(validInvoice); expect(result.companyId).toBe("cm123abc"); expect(result.items).toHaveLength(1); }); it("should require at least one item", () => { const invalid = { ...validInvoice, items: [] }; expect(() => invoiceSchema.parse(invalid)).toThrow( "Mindestens ein Rechnungsposition erforderlich" ); }); it("should accept optional deliveryDate", () => { const withDelivery = { ...validInvoice, deliveryDate: "2026-05-07" }; expect(() => invoiceSchema.parse(withDelivery)).not.toThrow(); }); it("should reject invalid dates", () => { const invalid = { ...validInvoice, issueDate: "not-a-date" }; expect(() => invoiceSchema.parse(invalid)).toThrow("Ungültiges Datum"); }); it("should accept optional notes with max length", () => { const withNotes = { ...validInvoice, notes: "Test notes" }; expect(() => invoiceSchema.parse(withNotes)).not.toThrow(); }); }); describe("companySchema", () => { const validCompany = { name: "Test GmbH", taxId: null, vatId: null, address: "Hauptstraße 1", zip: "12345", city: "Berlin", country: "DE", invoicePrefix: "RE", kleinunternehmer: false, bankIban: null, // bankBic is optional, can be omitted or empty string }; it("should accept valid company", () => { const result = companySchema.parse(validCompany); expect(result.name).toBe("Test GmbH"); }); it("should require name and address", () => { const invalid = { ...validCompany, name: "" }; expect(() => companySchema.parse(invalid)).toThrow("Firmenname erforderlich"); }); it("should validate email format", () => { const withEmail = { ...validCompany, email: "test@example.com" }; expect(() => companySchema.parse(withEmail)).not.toThrow(); }); it("should reject invalid email", () => { const invalid = { ...validCompany, email: "not-an-email" }; expect(() => companySchema.parse(invalid)).toThrow("Ungültige E-Mail-Adresse"); }); it("should validate website starts with http/https", () => { const withWebsite = { ...validCompany, website: "https://example.com" }; expect(() => companySchema.parse(withWebsite)).not.toThrow(); }); it("should reject website without protocol", () => { const invalid = { ...validCompany, website: "example.com" }; expect(() => companySchema.parse(invalid)).toThrow( "Website muss mit http:// oder https:// beginnen" ); }); it("should accept valid IBAN", () => { const withIban = { ...validCompany, bankIban: "DE89370400440532013000" }; expect(() => companySchema.parse(withIban)).not.toThrow(); }); }); describe("customerSchema", () => { const validCustomer = { companyId: "cm123abc", name: "Max Mustermann", address: "Musterstraße 1", zip: "12345", city: "Berlin", country: "DE", }; it("should accept valid customer", () => { const result = customerSchema.parse(validCustomer); expect(result.name).toBe("Max Mustermann"); }); it("should require companyId", () => { const invalid = { ...validCustomer, companyId: "" }; expect(() => customerSchema.parse(invalid)).toThrow("Mandant erforderlich"); }); it("should validate zip code format", () => { const invalid = { ...validCustomer, zip: "abc" }; expect(() => customerSchema.parse(invalid)).toThrow( "PLZ darf nur Zahlen, Bindestriche und Leerzeichen enthalten" ); }); }); });