Files
app/src/schemas/orders.schema.ts
BOHA 3ec512faf1 feat(projects/orders): project status machine + bidirectional project-order sync; order reopen
- projects.service: VALID_TRANSITIONS (aktivni->dokonceny/zruseny,
  terminal->aktivni reopen), tolerant of legacy statuses; getProject returns
  valid_transitions; updateProject validates transitions (invalid_transition
  token -> Czech 400 in the route).
- NEW project->order cascade, transactional with the project update: finishing/
  cancelling a project completes/cancels its linked received order — only while
  the order is still open (never resurrects storno, never cancels completed);
  reopen never cascades. Direct tx write, no service recursion; cascaded order
  change gets its own audit row.
- orders.service: reopen edges (dokoncena/stornovana -> v_realizaci);
  syncProjectStatus is reopen-aware (reopen skips project sync) and returns
  the cascaded projects for per-project audit rows in the route.
- orders.schema: fix phantom 'zrusena' enum token -> canonical 'stornovana'
  (every storno PUT through the route 400ed with a raw English Zod message;
  latent until now because no UI sent it) + route-level regression test.
- NEW project-order-status-sync.test.ts: 15 tests — both cascade directions,
  guards, reopen-no-cascade, legacy tolerance, forward-sync regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 04:33:58 +02:00

73 lines
2.5 KiB
TypeScript

import { z } from "zod";
import {
numberFromForm,
nonNegativeNumberFromForm,
positiveNumberFromForm,
intIdFromForm,
nullableIntIdFromForm,
booleanFromForm,
} from "./common";
const OrderItemSchema = z.object({
description: z.string().max(500).nullish(),
item_description: z.string().max(8000).nullish(),
quantity: positiveNumberFromForm.default(1),
unit: z.string().max(20).nullish(),
unit_price: numberFromForm.default(0),
is_included_in_total: booleanFromForm.optional().default(true),
position: nonNegativeNumberFromForm.optional(),
});
const OrderSectionSchema = z.object({
title: z.string().max(255).nullish(),
title_cz: z.string().max(255).nullish(),
content: z.string().max(8000).nullish(),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateOrderFromQuotationSchema = z.object({
quotationId: intIdFromForm,
customerOrderNumber: z.string().max(100).optional().default(""),
});
export const CreateOrderSchema = z.object({
order_number: z.string().max(50).nullish(),
customer_order_number: z.string().max(100).nullish(),
quotation_id: nullableIntIdFromForm.nullish(),
customer_id: nullableIntIdFromForm.nullish(),
status: z
.enum(["prijata", "v_realizaci", "dokoncena", "stornovana"])
.optional()
.default("prijata"),
currency: z.string().max(10).optional().default("CZK"),
language: z.string().max(5).optional().default("cs"),
exchange_rate: positiveNumberFromForm.optional().default(1.0),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
notes: z.string().max(8000).nullish(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
create_project: booleanFromForm.optional().default(true),
});
export const UpdateOrderSchema = z.object({
customer_order_number: z.string().max(100).nullish(),
status: z
.enum(["prijata", "v_realizaci", "dokoncena", "stornovana"])
.optional(),
currency: z.string().max(10).optional(),
language: z.string().max(5).optional(),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
notes: z.string().max(8000).nullish(),
customer_id: nullableIntIdFromForm.optional(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
});
export type CreateOrderFromQuotationInput = z.infer<
typeof CreateOrderFromQuotationSchema
>;
export type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>;