From f00fcf95fdb3fa3828204b2831339734dec113c1 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 9 Jun 2026 11:09:50 +0200 Subject: [PATCH] feat(orders): issued-order Zod schemas Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/issued-orders.test.ts | 16 ++++++++++ src/schemas/issued-orders.schema.ts | 48 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/schemas/issued-orders.schema.ts diff --git a/src/__tests__/issued-orders.test.ts b/src/__tests__/issued-orders.test.ts index cae0596..b6cdcfd 100644 --- a/src/__tests__/issued-orders.test.ts +++ b/src/__tests__/issued-orders.test.ts @@ -5,6 +5,7 @@ import { previewIssuedOrderNumber, releaseIssuedOrderNumber, } from "../services/numbering.service"; +import { CreateIssuedOrderSchema } from "../schemas/issued-orders.schema"; afterEach(async () => { await prisma.number_sequences.deleteMany({ where: { type: "issued_order" } }); @@ -39,3 +40,18 @@ describe("issued-order numbering", () => { expect((await previewIssuedOrderNumber(year)).number).toBe(c.number); }); }); + +describe("CreateIssuedOrderSchema", () => { + it("coerces string form numbers and rejects an out-of-range VAT", () => { + const ok = CreateIssuedOrderSchema.safeParse({ + customer_id: "5", + vat_rate: "21", + items: [{ description: "X", quantity: "2", unit_price: "100" }], + }); + expect(ok.success).toBe(true); + if (ok.success) expect(ok.data.customer_id).toBe(5); + + const bad = CreateIssuedOrderSchema.safeParse({ vat_rate: "200" }); + expect(bad.success).toBe(false); + }); +}); diff --git a/src/schemas/issued-orders.schema.ts b/src/schemas/issued-orders.schema.ts new file mode 100644 index 0000000..e5f975b --- /dev/null +++ b/src/schemas/issued-orders.schema.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { + numberInRange, + nonNegativeNumberFromForm, + positiveNumberFromForm, + nullableIntIdFromForm, + booleanFromForm, + isoDateString, +} from "./common"; + +export const IssuedOrderItemSchema = z.object({ + description: z.string().max(500).nullish(), + item_description: z.string().max(5000).nullish(), + quantity: positiveNumberFromForm.optional(), + unit: z.string().max(20).nullish(), + unit_price: nonNegativeNumberFromForm.optional(), + vat_rate: numberInRange(0, 100).optional(), + position: z.number().int().nonnegative().optional(), +}); + +const ISSUED_ORDER_STATUSES = [ + "draft", + "sent", + "confirmed", + "completed", + "cancelled", +] as const; + +export const CreateIssuedOrderSchema = z.object({ + po_number: z.string().max(50).nullish(), + customer_id: nullableIntIdFromForm.nullish(), + status: z.enum(ISSUED_ORDER_STATUSES).optional(), + currency: z.string().max(10).optional(), + vat_rate: numberInRange(0, 100).optional(), + apply_vat: booleanFromForm.optional(), + exchange_rate: nonNegativeNumberFromForm.optional(), + order_date: isoDateString.nullish(), + delivery_date: isoDateString.nullish(), + language: z.string().max(5).optional(), + delivery_terms: z.string().max(500).nullish(), + payment_terms: z.string().max(500).nullish(), + issued_by: z.string().max(255).nullish(), + notes: z.string().nullish(), + internal_notes: z.string().nullish(), + items: z.array(IssuedOrderItemSchema).optional(), +}); + +export const UpdateIssuedOrderSchema = CreateIssuedOrderSchema.partial();