since3-portal/modules/shared/schemas/invoice.ts
2026-09-29 13:46:50 +02:00

75 lines
2.8 KiB
TypeScript

import * as z from "zod";
export const INVOICE_STATUSES = ["pending", "paid", "cancelled"] as const;
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, { error: "Ungültiges Datum." });
// Grenzen und Nachkommastellen passend zu invoice_items: quantity numeric(10,2), unit_price numeric(12,2), tax_rate numeric(5,2)
const twoDecimals = (label: string) => z.number().multipleOf(0.01, { error: `${label}: maximal 2 Nachkommastellen.` });
export const InvoiceItemSchema = z.object({
productTemplateId: z.string().uuid().nullable(),
description: z.string().trim().min(1, { error: "Jede Position braucht eine Bezeichnung." }),
quantity: twoDecimals("Menge")
.positive({ error: "Menge muss größer als 0 sein." })
.lt(1e8, { error: "Menge ist zu groß." }),
unitPrice: twoDecimals("Preis")
.min(0, { error: "Preis darf nicht negativ sein." })
.lt(1e10, { error: "Preis ist zu groß." }),
taxRate: twoDecimals("MwSt").min(0).max(100),
});
export const InvoiceDraftSchema = z
.object({
profileId: z.string().uuid(),
companyName: z.string().trim().min(1, { error: "Firmenname ist erforderlich." }),
billingStreet: z.string().trim().min(1, { error: "Straße ist erforderlich." }),
billingPostalCode: z.string().trim().min(1, { error: "PLZ ist erforderlich." }),
billingCity: z.string().trim().min(1, { error: "Stadt ist erforderlich." }),
billingCountry: z.string().trim().length(2, { error: "Land muss ein 2-stelliger Code sein." }),
invoiceDate: isoDate,
dueDate: isoDate,
items: z.array(InvoiceItemSchema).min(1, { error: "Mindestens eine Position hinzufügen." }),
})
.refine((data) => data.dueDate >= data.invoiceDate, {
path: ["dueDate"],
error: "Fälligkeit darf nicht vor dem Rechnungsdatum liegen.",
});
export type InvoiceItemDraft = z.infer<typeof InvoiceItemSchema>;
export type InvoiceDraft = z.infer<typeof InvoiceDraftSchema>;
export type InvoicePdfData = {
invoiceNumber: string;
invoiceDate: string;
dueDate: string;
companyName: string;
billingStreet: string;
billingPostalCode: string;
billingCity: string;
billingCountry: string;
items: { description: string; quantity: number; unitPrice: number; taxRate: number }[];
};
export type InvoiceListItem = {
id: string;
invoiceNumber: string;
status: InvoiceStatus;
displayStatus: InvoiceStatus | "overdue";
grossTotal: number;
dueDate: string;
invoiceDate: string;
};
export type ProductTemplate = {
id: string;
name: string;
description: string | null;
unitPrice: number;
taxRate: number;
};
export function firstIssueMessage(error: z.ZodError) {
return error.issues[0]?.message ?? "Bitte alle Pflichtfelder ausfüllen.";
}