@venulog/phasing-engine-schemas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,890 @@
1
+ // packages/phasing-schemas/src/phaseBooking.ts
2
+ import { paginationSchema } from './pagination.js';
3
+ import { BookingStatus } from './enums/bookingStatus.js';
4
+ import { createSuccessResponseSchema, createMessageDataResponseSchema } from './common.js';
5
+ import { z } from './zod.js';
6
+ import { SlotStatus } from './enums/slotStatus.js';
7
+ // ------------------------------
8
+ // Query parameters schema
9
+ // ------------------------------
10
+ export const getEventBookingsQuerySchema = z
11
+ .object({
12
+ event_code: z.string().min(1, 'Event code is required').openapi({
13
+ description: 'Unique code identifying the event',
14
+ example: 'COEC2025'
15
+ }),
16
+ seller_id: z
17
+ .string()
18
+ .min(1, 'Seller ID is required')
19
+ .transform(val => parseInt(val, 10))
20
+ .refine(val => !isNaN(val) && val > 0, { message: 'Seller ID must be positive' })
21
+ .openapi({
22
+ description: 'ID of the seller (must be a positive integer)',
23
+ example: '1'
24
+ })
25
+ })
26
+ .extend(paginationSchema.shape)
27
+ .openapi('GetEventBookingsQuery');
28
+ // ------------------------------
29
+ // Path parameters schema
30
+ // ------------------------------
31
+ export const cancelPhaseSlotParamsSchema = z
32
+ .object({
33
+ slotId: z
34
+ .string()
35
+ .min(1, 'Slot ID is required')
36
+ .transform(val => parseInt(val, 10))
37
+ .refine(val => !isNaN(val) && val > 0, { message: 'Slot ID must be positive' })
38
+ .openapi({
39
+ description: 'The ID of the phase slot to cancel',
40
+ example: '1'
41
+ })
42
+ })
43
+ .openapi('CancelPhaseSlotParams');
44
+ export const closeEventParamsSchema = z
45
+ .object({
46
+ eventId: z
47
+ .string()
48
+ .min(1, 'Event ID is required')
49
+ .transform(val => parseInt(val, 10))
50
+ .refine(val => !isNaN(val) && val > 0, { message: 'Event ID must be positive' })
51
+ .openapi({
52
+ description: 'The ID of the event to close',
53
+ example: '1'
54
+ })
55
+ })
56
+ .openapi('CloseEventParams');
57
+ // ------------------------------
58
+ // Request body schemas
59
+ // ------------------------------
60
+ export const cancelPhaseSlotBodySchema = z
61
+ .object({
62
+ reason: z.string().optional().openapi({
63
+ description: 'Optional reason for cancellation (for audit purposes)',
64
+ example: 'Event postponed'
65
+ })
66
+ })
67
+ .openapi('CancelPhaseSlotBody');
68
+ export const closeEventBodySchema = z
69
+ .object({
70
+ reason: z.string().optional().openapi({
71
+ description: 'Optional reason for closing the event phase (for audit purposes)',
72
+ example: 'Event phase completed - moving to next stage'
73
+ })
74
+ })
75
+ .openapi('CloseEventBody');
76
+ // ------------------------------
77
+ // Response schemas
78
+ // ------------------------------
79
+ export const vehicleTypeSchema = z.object({
80
+ id: z.number(),
81
+ name: z.string()
82
+ });
83
+ export const phaseSlotSchema = z.object({
84
+ id: z.number(),
85
+ event_id: z.number(),
86
+ slot_number: z.number(),
87
+ vehicle_type_id: z.number().nullable(),
88
+ company_role: z.string().nullable(),
89
+ status: z.enum(BookingStatus),
90
+ is_active: z.boolean(),
91
+ created_at: z.string(),
92
+ updated_at: z.string(),
93
+ created_by: z.string().nullable(),
94
+ updated_by: z.string().nullable(),
95
+ vehicle_type: vehicleTypeSchema.nullable().optional()
96
+ });
97
+ export const companySchema = z.object({
98
+ id: z.number(),
99
+ company_name: z.string(),
100
+ vat_number: z.string().nullable(),
101
+ siret_code: z.string().nullable(),
102
+ tva_intracom: z.string().nullable(),
103
+ type: z.enum(['E', 'S']),
104
+ company_role: z.string().nullable(),
105
+ company_street: z.string().nullable(),
106
+ company_city: z.string().nullable(),
107
+ company_postal_code: z.string().nullable(),
108
+ company_country: z.string().nullable(),
109
+ company_address: z.string().nullable(),
110
+ contact_first_name: z.string().nullable(),
111
+ contact_last_name: z.string().nullable(),
112
+ contact_email: z.string().nullable(),
113
+ contact_phone: z.string().nullable(),
114
+ is_active: z.boolean(),
115
+ created_at: z.string(),
116
+ updated_at: z.string(),
117
+ created_by: z.string().nullable(),
118
+ updated_by: z.string().nullable()
119
+ });
120
+ export const phaseBookingSchema = z.object({
121
+ id: z.number(),
122
+ company_id: z.number(),
123
+ phase_slot_id: z.number(),
124
+ status: z.enum(BookingStatus),
125
+ is_active: z.boolean(),
126
+ created_at: z.string(),
127
+ updated_at: z.string(),
128
+ created_by: z.string().nullable(),
129
+ updated_by: z.string().nullable(),
130
+ company: companySchema,
131
+ phase_slot: phaseSlotSchema
132
+ });
133
+ export const createBookingBodySchema = z
134
+ .object({
135
+ company_id: z
136
+ .number()
137
+ .int()
138
+ .positive({
139
+ message: 'Company ID must be a positive integer'
140
+ })
141
+ .openapi({
142
+ description: 'ID of the company making the booking',
143
+ example: 123
144
+ }),
145
+ phase_slot_id: z
146
+ .number()
147
+ .int()
148
+ .positive({
149
+ message: 'Phase slot ID must be a positive integer'
150
+ })
151
+ .openapi({
152
+ description: 'ID of the phase slot to book',
153
+ example: 456
154
+ })
155
+ })
156
+ .openapi('CreateBookingBody');
157
+ export const createBookingDataSchema = z
158
+ .object({
159
+ booking_id: z.number().openapi({
160
+ description: 'ID of the created booking',
161
+ example: 789
162
+ }),
163
+ company_id: z.number().openapi({
164
+ description: 'ID of the company',
165
+ example: 123
166
+ }),
167
+ phase_slot_id: z.number().openapi({
168
+ description: 'ID of the booked phase slot',
169
+ example: 456
170
+ }),
171
+ slot_number: z.number().openapi({
172
+ description: 'Slot number',
173
+ example: 42
174
+ }),
175
+ status: z.enum(BookingStatus).openapi({
176
+ description: 'Booking status',
177
+ example: BookingStatus.BOOKED
178
+ }),
179
+ created_at: z.string().openapi({
180
+ description: 'Timestamp when booking was created',
181
+ example: '2025-12-05T10:30:00.000Z'
182
+ }),
183
+ created_by: z.string().nullable().openapi({
184
+ description: 'ID of the user who created the booking',
185
+ example: 'user-123'
186
+ })
187
+ })
188
+ .openapi('CreateBookingData');
189
+ export const createBookingResponseSchema = createMessageDataResponseSchema(createBookingDataSchema, 'CreateBookingResponse', 'Booking created successfully', 'Details of the created booking');
190
+ export const eventBookingsDataSchema = z
191
+ .object({
192
+ event_id: z.number(),
193
+ event_code: z.string(),
194
+ event_name: z.string(),
195
+ bookings: z.array(phaseBookingSchema),
196
+ total_count: z.number()
197
+ })
198
+ .openapi('EventBookingsData');
199
+ export const eventBookingsResponseSchema = createSuccessResponseSchema(eventBookingsDataSchema, 'EventBookingsResponse', 'Event bookings data with phase slot details');
200
+ export const cancelPhaseSlotDataSchema = z
201
+ .object({
202
+ slot_id: z.number().openapi({
203
+ description: 'ID of the cancelled slot',
204
+ example: 1
205
+ }),
206
+ status: z.enum(SlotStatus).openapi({
207
+ description: 'New status of the slot',
208
+ example: SlotStatus.AVAILABLE
209
+ }),
210
+ cancelled_at: z.string().openapi({
211
+ description: 'Timestamp when the slot was cancelled',
212
+ example: '2025-12-03T20:47:00.000Z'
213
+ }),
214
+ cancelled_by: z.string().nullable().openapi({
215
+ description: 'ID of the user who cancelled the slot',
216
+ example: null
217
+ }),
218
+ reason: z.string().nullable().openapi({
219
+ description: 'Reason for cancellation',
220
+ example: 'Event postponed'
221
+ })
222
+ })
223
+ .openapi('CancelPhaseSlotData');
224
+ export const cancelPhaseSlotResponseSchema = createMessageDataResponseSchema(cancelPhaseSlotDataSchema, 'CancelPhaseSlotResponse', 'Phase slot cancelled successfully', 'Details of the cancelled phase slot');
225
+ export const closeEventDataSchema = z
226
+ .object({
227
+ event_id: z.number().openapi({
228
+ description: 'ID of the close event',
229
+ example: 1
230
+ }),
231
+ event_code: z.string().nullable().openapi({
232
+ description: 'Code of the close event',
233
+ example: 'EVENT2025'
234
+ }),
235
+ event_name: z.string().openapi({
236
+ description: 'Name of the close event',
237
+ example: 'Annual Trade Show 2025'
238
+ }),
239
+ is_active: z.boolean().openapi({
240
+ description: 'Active status of the event',
241
+ example: false
242
+ }),
243
+ close_at: z.string().openapi({
244
+ description: 'Timestamp when the event was closed',
245
+ example: '2025-12-04T10:30:00.000Z'
246
+ }),
247
+ close_by: z.string().nullable().openapi({
248
+ description: 'ID of the user who closed the event',
249
+ example: null
250
+ }),
251
+ reason: z.string().nullable().openapi({
252
+ description: 'Reason for closing the event',
253
+ example: 'Event cancelled due to logistical issues'
254
+ })
255
+ })
256
+ .openapi('CloseEventData');
257
+ export const closeEventResponseSchema = createMessageDataResponseSchema(closeEventDataSchema, 'CloseEventResponse', 'Event phase closed successfully', 'Details of the closed event phase');
258
+ export const confirmBookingParamsSchema = z.object({
259
+ bookingId: z.coerce.number().int().positive({
260
+ message: 'Booking ID must be a positive integer'
261
+ })
262
+ });
263
+ // Response schema
264
+ export const confirmBookingResponseSchema = z.object({
265
+ success: z.boolean(),
266
+ message: z.string(),
267
+ data: z.object({
268
+ booking_id: z.number(),
269
+ booking_status: z.enum(BookingStatus),
270
+ slot_id: z.number(),
271
+ slot_status: z.enum(SlotStatus),
272
+ confirmed_at: z.string(),
273
+ confirmed_by: z.string().nullable()
274
+ })
275
+ });
276
+ export const refuseBookingParamsSchema = z
277
+ .object({
278
+ bookingId: z.coerce
279
+ .number()
280
+ .int()
281
+ .positive({
282
+ message: 'Booking ID must be a positive integer'
283
+ })
284
+ .openapi({
285
+ description: 'The ID of the booking to refuse',
286
+ example: 1
287
+ })
288
+ })
289
+ .openapi('RefuseBookingParams');
290
+ // Response data schema for refuse endpoint
291
+ export const refuseBookingDataSchema = z
292
+ .object({
293
+ booking_id: z.number().openapi({
294
+ description: 'ID of the refused booking',
295
+ example: 1
296
+ }),
297
+ booking_status: z.enum([BookingStatus.REFUSED]).openapi({
298
+ description: 'New status of the booking',
299
+ example: BookingStatus.REFUSED
300
+ }),
301
+ slot_id: z.number().openapi({
302
+ description: 'ID of the associated phase slot',
303
+ example: 10
304
+ }),
305
+ slot_status: z.enum([SlotStatus.AVAILABLE]).openapi({
306
+ description: 'New status of the phase slot',
307
+ example: SlotStatus.AVAILABLE
308
+ }),
309
+ refused_at: z.string().openapi({
310
+ description: 'ISO 8601 timestamp when the booking was refused',
311
+ example: '2025-12-08T10:30:00.000Z'
312
+ }),
313
+ refused_by: z.string().nullable().openapi({
314
+ description: 'ID of the user who refused the booking',
315
+ example: 'user-123'
316
+ })
317
+ })
318
+ .openapi('RefuseBookingData');
319
+ // Response schema for refuse endpoint
320
+ export const refuseBookingResponseSchema = createMessageDataResponseSchema(refuseBookingDataSchema, 'RefuseBookingResponse', 'Phase booking refused successfully', 'Details of the refused booking');
321
+ // ------------------------------
322
+ // Create Phase Slots schemas
323
+ // ------------------------------
324
+ export const createPhaseSlotsBodySchema = z
325
+ .object({
326
+ event_id: z
327
+ .number()
328
+ .int()
329
+ .positive({
330
+ message: 'Event ID must be a positive integer'
331
+ })
332
+ .openapi({
333
+ description: 'ID of the event to create the slots for',
334
+ example: 1
335
+ }),
336
+ slots: z
337
+ .array(z.object({
338
+ slot_number: z
339
+ .number()
340
+ .int()
341
+ .positive({
342
+ message: 'Slot number must be a positive integer'
343
+ })
344
+ .openapi({
345
+ description: 'Unique slot number within the event',
346
+ example: 42
347
+ }),
348
+ vehicle_type_id: z
349
+ .number()
350
+ .int()
351
+ .positive({
352
+ message: 'Vehicle type ID must be a positive integer'
353
+ })
354
+ .nullable()
355
+ .optional()
356
+ .openapi({
357
+ description: 'Optional vehicle type ID for the slot',
358
+ example: 1
359
+ }),
360
+ company_role: z.string().min(1).nullable().optional().openapi({
361
+ description: 'Optional company role for the slot',
362
+ example: 'buyer'
363
+ }),
364
+ status: z.enum(SlotStatus).optional().openapi({
365
+ description: 'Initial status of the slot (defaults to available)',
366
+ example: SlotStatus.AVAILABLE
367
+ })
368
+ }))
369
+ .min(1, 'At least one slot must be provided')
370
+ .openapi({
371
+ description: 'Array of slots to create',
372
+ example: [
373
+ { slot_number: 42, vehicle_type_id: 1, company_role: 'buyer' },
374
+ { slot_number: 43, vehicle_type_id: 2, company_role: 'seller' }
375
+ ]
376
+ })
377
+ })
378
+ .openapi('CreatePhaseSlotsBody');
379
+ export const createPhaseSlotDataSchema = z
380
+ .object({
381
+ slot_id: z.number().openapi({
382
+ description: 'ID of the created phase slot',
383
+ example: 123
384
+ }),
385
+ event_id: z.number().openapi({
386
+ description: 'ID of the event',
387
+ example: 1
388
+ }),
389
+ slot_number: z.number().openapi({
390
+ description: 'Slot number',
391
+ example: 42
392
+ }),
393
+ vehicle_type_id: z.number().nullable().openapi({
394
+ description: 'Vehicle type ID',
395
+ example: 1
396
+ }),
397
+ company_role: z.string().nullable().openapi({
398
+ description: 'Company role',
399
+ example: 'buyer'
400
+ }),
401
+ status: z.enum(SlotStatus).openapi({
402
+ description: 'Slot status',
403
+ example: SlotStatus.AVAILABLE
404
+ }),
405
+ is_active: z.boolean().openapi({
406
+ description: 'Whether the slot is active',
407
+ example: true,
408
+ examples: [true, false]
409
+ }),
410
+ created_at: z.string().openapi({
411
+ description: 'Timestamp when slot was created',
412
+ example: '2025-12-09T10:30:00.000Z'
413
+ }),
414
+ created_by: z.string().nullable().openapi({
415
+ description: 'ID of the user who created the slot',
416
+ example: 'user-123'
417
+ })
418
+ })
419
+ .openapi('CreatePhaseSlotData');
420
+ export const createPhaseSlotsDataSchema = z
421
+ .object({
422
+ event_id: z.number().openapi({
423
+ description: 'ID of the event',
424
+ example: 1
425
+ }),
426
+ total_created: z.number().openapi({
427
+ description: 'Total number of slots successfully created',
428
+ example: 2
429
+ }),
430
+ created_slots: z.array(createPhaseSlotDataSchema).openapi({
431
+ description: 'Array of successfully created slots',
432
+ example: []
433
+ }),
434
+ failed_slots: z
435
+ .array(z.object({
436
+ slot_number: z.number().openapi({
437
+ description: 'Slot number that failed to create',
438
+ example: 44
439
+ }),
440
+ error: z.string().openapi({
441
+ description: 'Error message explaining why the slot creation failed',
442
+ example: 'Slot number 44 already exists for event 1'
443
+ })
444
+ }))
445
+ .openapi({
446
+ description: 'Array of slots that failed to create with error details',
447
+ example: []
448
+ })
449
+ })
450
+ .openapi('CreatePhaseSlotsData');
451
+ export const createPhaseSlotsResponseSchema = createMessageDataResponseSchema(createPhaseSlotsDataSchema, 'CreatePhaseSlotsResponse', 'Bulk phase slots operation completed', 'Details of the bulk creation operation including successes and failures');
452
+ // ------------------------------
453
+ // Update Phase Slot schemas
454
+ // ------------------------------
455
+ export const updatePhaseSlotParamsSchema = z
456
+ .object({
457
+ slotId: z
458
+ .string()
459
+ .min(1, 'Slot ID is required')
460
+ .transform(val => parseInt(val, 10))
461
+ .refine(val => !isNaN(val) && val > 0, { message: 'Slot ID must be positive' })
462
+ .openapi({
463
+ description: 'The ID of the phase slot to update',
464
+ example: '123'
465
+ })
466
+ })
467
+ .openapi('UpdatePhaseSlotParams');
468
+ export const updatePhaseSlotBodySchema = z
469
+ .object({
470
+ slot_number: z
471
+ .number()
472
+ .int()
473
+ .positive({
474
+ message: 'Slot number must be a positive integer'
475
+ })
476
+ .optional()
477
+ .openapi({
478
+ description: 'New slot number (must be unique within the event)',
479
+ example: 43
480
+ }),
481
+ vehicle_type_id: z
482
+ .number()
483
+ .int()
484
+ .positive({
485
+ message: 'Vehicle type ID must be a positive integer'
486
+ })
487
+ .nullable()
488
+ .optional()
489
+ .openapi({
490
+ description: 'New vehicle type ID for the slot',
491
+ example: 2
492
+ }),
493
+ company_role: z.string().min(1).nullable().optional().openapi({
494
+ description: 'New company role for the slot',
495
+ example: 'seller'
496
+ }),
497
+ status: z.enum(SlotStatus).optional().openapi({
498
+ description: 'New status for the slot',
499
+ example: SlotStatus.RESERVED
500
+ })
501
+ })
502
+ .refine(data => Object.keys(data).length > 0, {
503
+ message: 'At least one field must be provided for update'
504
+ })
505
+ .openapi('UpdatePhaseSlotBody');
506
+ export const updatePhaseSlotDataSchema = z
507
+ .object({
508
+ slot_id: z.number().openapi({
509
+ description: 'ID of the updated phase slot',
510
+ example: 123
511
+ }),
512
+ event_id: z.number().openapi({
513
+ description: 'ID of the event',
514
+ example: 1
515
+ }),
516
+ slot_number: z.number().openapi({
517
+ description: 'Updated slot number',
518
+ example: 43
519
+ }),
520
+ vehicle_type_id: z.number().nullable().openapi({
521
+ description: 'Updated vehicle type ID',
522
+ example: 2
523
+ }),
524
+ company_role: z.string().nullable().openapi({
525
+ description: 'Updated company role',
526
+ example: 'seller'
527
+ }),
528
+ status: z.enum(SlotStatus).openapi({
529
+ description: 'Updated slot status',
530
+ example: SlotStatus.RESERVED
531
+ }),
532
+ is_active: z.boolean().openapi({
533
+ description: 'Whether the slot is active',
534
+ example: true
535
+ }),
536
+ updated_at: z.string().openapi({
537
+ description: 'Timestamp when slot was updated',
538
+ example: '2025-12-09T10:35:00.000Z'
539
+ }),
540
+ updated_by: z.string().nullable().openapi({
541
+ description: 'ID of the user who updated the slot',
542
+ example: 'user-123'
543
+ })
544
+ })
545
+ .openapi('UpdatePhaseSlotData');
546
+ export const updatePhaseSlotResponseSchema = createMessageDataResponseSchema(updatePhaseSlotDataSchema, 'UpdatePhaseSlotResponse', 'Phase slot updated successfully', 'Details of the updated phase slot');
547
+ // ------------------------------
548
+ // Phase Slot Assemblies & Dismantlings Schemas
549
+ // ------------------------------
550
+ export const phaseSlotAssemblySchema = z
551
+ .object({
552
+ id: z.number().int().positive().openapi({
553
+ description: 'Unique identifier for the assembly',
554
+ example: 1
555
+ }),
556
+ date: z.string().openapi({
557
+ description: 'Date of the assembly (YYYY-MM-DD format)',
558
+ example: '2025-12-15'
559
+ }),
560
+ start_time: z.string().openapi({
561
+ description: 'Start time of the assembly (HH:MM format)',
562
+ example: '06:00'
563
+ }),
564
+ end_time: z.string().openapi({
565
+ description: 'End time of the assembly (HH:MM format)',
566
+ example: '09:00'
567
+ }),
568
+ duration: z.number().openapi({
569
+ description: 'Duration of the assembly in minutes',
570
+ example: 180
571
+ }),
572
+ phase_slot_id: z.number().int().positive().openapi({
573
+ description: 'ID of the associated phase slot',
574
+ example: 101
575
+ }),
576
+ created_at: z.string().openapi({
577
+ description: 'Timestamp when assembly was created',
578
+ example: '2025-12-09T10:00:00.000Z'
579
+ }),
580
+ updated_at: z.string().openapi({
581
+ description: 'Timestamp when assembly was updated',
582
+ example: '2025-12-09T10:30:00.000Z'
583
+ })
584
+ })
585
+ .openapi('PhaseSlotAssembly');
586
+ export const phaseSlotDismantlingSchema = z
587
+ .object({
588
+ id: z.number().int().positive().openapi({
589
+ description: 'Unique identifier for the dismantling',
590
+ example: 1
591
+ }),
592
+ date: z.string().openapi({
593
+ description: 'Date of the dismantling (YYYY-MM-DD format)',
594
+ example: '2025-12-15'
595
+ }),
596
+ start_time: z.string().openapi({
597
+ description: 'Start time of the dismantling (HH:MM format)',
598
+ example: '06:00'
599
+ }),
600
+ end_time: z.string().openapi({
601
+ description: 'End time of the dismantling (HH:MM format)',
602
+ example: '09:00'
603
+ }),
604
+ duration: z.number().openapi({
605
+ description: 'Duration of the dismantling in minutes',
606
+ example: 180
607
+ }),
608
+ phase_slot_id: z.number().int().positive().openapi({
609
+ description: 'ID of the associated phase slot',
610
+ example: 101
611
+ }),
612
+ created_at: z.string().openapi({
613
+ description: 'Timestamp when dismantling was created',
614
+ example: '2025-12-09T10:00:00.000Z'
615
+ }),
616
+ updated_at: z.string().openapi({
617
+ description: 'Timestamp when dismantling was updated',
618
+ example: '2025-12-09T10:30:00.000Z'
619
+ })
620
+ })
621
+ .openapi('PhaseSlotDismantling');
622
+ export const upsertPhaseSlotAssemblySchema = z
623
+ .object({
624
+ date: z
625
+ .string()
626
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
627
+ .openapi({
628
+ description: 'Date of the assembly (YYYY-MM-DD format)',
629
+ example: '2025-12-15'
630
+ }),
631
+ start_time: z
632
+ .string()
633
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
634
+ .optional()
635
+ .openapi({
636
+ description: 'Start time of the assembly (HH:MM format). Defaults to 06:00',
637
+ example: '06:00'
638
+ }),
639
+ end_time: z
640
+ .string()
641
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
642
+ .optional()
643
+ .openapi({
644
+ description: 'End time of the assembly (HH:MM format). Defaults to 09:00',
645
+ example: '09:00'
646
+ }),
647
+ duration: z.number().positive().optional().openapi({
648
+ description: 'Duration of the assembly in minutes. Defaults to 60',
649
+ example: 180
650
+ }),
651
+ phase_slot_id: z.number().int().positive().openapi({
652
+ description: 'ID of the associated phase slot',
653
+ example: 101
654
+ })
655
+ })
656
+ .openapi('UpsertPhaseSlotAssembly');
657
+ export const upsertPhaseSlotDismantlingSchema = z
658
+ .object({
659
+ date: z
660
+ .string()
661
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
662
+ .openapi({
663
+ description: 'Date of the dismantling (YYYY-MM-DD format)',
664
+ example: '2025-12-15'
665
+ }),
666
+ start_time: z
667
+ .string()
668
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
669
+ .optional()
670
+ .openapi({
671
+ description: 'Start time of the dismantling (HH:MM format). Defaults to 06:00',
672
+ example: '06:00'
673
+ }),
674
+ end_time: z
675
+ .string()
676
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
677
+ .optional()
678
+ .openapi({
679
+ description: 'End time of the dismantling (HH:MM format). Defaults to 09:00',
680
+ example: '09:00'
681
+ }),
682
+ duration: z.number().positive().optional().openapi({
683
+ description: 'Duration of the dismantling in minutes. Defaults to 60',
684
+ example: 180
685
+ }),
686
+ phase_slot_id: z.number().int().positive().openapi({
687
+ description: 'ID of the associated phase slot',
688
+ example: 101
689
+ })
690
+ })
691
+ .openapi('UpsertPhaseSlotDismantling');
692
+ // ------------------------------
693
+ // Combined Phase Slot Schedules Schemas (Assembly + Dismantling)
694
+ // ------------------------------
695
+ export const phaseSlotScheduleOperationSchema = z
696
+ .object({
697
+ id: z.number().int().positive().optional().openapi({
698
+ description: 'Unique identifier for existing records (required for updates and deletes)',
699
+ example: 1
700
+ }),
701
+ start_time: z
702
+ .string()
703
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
704
+ .optional()
705
+ .openapi({
706
+ description: 'Start time (HH:MM format)',
707
+ example: '06:00'
708
+ }),
709
+ end_time: z
710
+ .string()
711
+ .regex(/^\d{2}:\d{2}$/, 'Time must be in HH:MM format')
712
+ .optional()
713
+ .openapi({
714
+ description: 'End time (HH:MM format)',
715
+ example: '09:00'
716
+ }),
717
+ duration: z.number().positive().optional().openapi({
718
+ description: 'Duration in minutes',
719
+ example: 180
720
+ }),
721
+ date: z
722
+ .string()
723
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in YYYY-MM-DD format')
724
+ .optional()
725
+ .openapi({
726
+ description: 'Date for assembly (YYYY-MM-DD format)',
727
+ example: '2025-12-15'
728
+ }),
729
+ _delete: z.boolean().optional().openapi({
730
+ description: 'Flag to mark this record for deletion. Requires existing id.',
731
+ example: false
732
+ })
733
+ })
734
+ .check(data => {
735
+ // If delete is true, id is required
736
+ if (data.value._delete === true && !data.value.id) {
737
+ data.issues.push({
738
+ code: 'custom',
739
+ message: 'ID is required when delete is true',
740
+ path: ['id'],
741
+ input: data.value
742
+ });
743
+ }
744
+ // If delete is false or undefined, date is required
745
+ if ((data.value._delete === false || data.value._delete === undefined) && !data.value.date) {
746
+ data.issues.push({
747
+ code: 'custom',
748
+ message: 'Date is required when delete is false or undefined',
749
+ path: ['date'],
750
+ input: data.value
751
+ });
752
+ }
753
+ })
754
+ .openapi('PhaseSlotScheduleOperation');
755
+ export const phaseSlotScheduleSchema = z
756
+ .object({
757
+ phase_slot_id: z.number().int().positive().openapi({
758
+ description: 'ID of the associated phase slot',
759
+ example: 101
760
+ }),
761
+ assembly: phaseSlotScheduleOperationSchema.openapi({
762
+ description: 'Assembly operation details with date'
763
+ }),
764
+ dismantling: phaseSlotScheduleOperationSchema.openapi({
765
+ description: 'Dismantling operation details with date'
766
+ })
767
+ })
768
+ .openapi('PhaseSlotSchedule');
769
+ export const upsertPhaseSlotSchedulesBodySchema = z
770
+ .object({
771
+ schedules: z
772
+ .array(phaseSlotScheduleSchema)
773
+ .min(1, 'At least one schedule is required')
774
+ .openapi({
775
+ description: 'Array of phase slot schedules to upsert',
776
+ example: [
777
+ {
778
+ phase_slot_id: 101,
779
+ assembly: {
780
+ date: '2025-12-15',
781
+ start_time: '06:00',
782
+ end_time: '09:00',
783
+ duration: 180
784
+ },
785
+ dismantling: {
786
+ date: '2025-12-16',
787
+ start_time: '15:00',
788
+ end_time: '18:00',
789
+ duration: 180
790
+ }
791
+ },
792
+ {
793
+ phase_slot_id: 102,
794
+ assembly: {
795
+ id: 15,
796
+ _delete: true
797
+ }
798
+ },
799
+ {
800
+ phase_slot_id: 103,
801
+ dismantling: {
802
+ id: 42,
803
+ _delete: true
804
+ }
805
+ }
806
+ ]
807
+ })
808
+ })
809
+ .openapi('UpsertPhaseSlotSchedulesBody');
810
+ export const upsertPhaseSlotSchedulesDataSchema = z
811
+ .object({
812
+ total_processed: z.number().int().nonnegative().openapi({
813
+ description: 'Total number of schedules processed',
814
+ example: 2
815
+ }),
816
+ total_assemblies_upserted: z.number().int().nonnegative().openapi({
817
+ description: 'Total number of assemblies successfully upserted',
818
+ example: 2
819
+ }),
820
+ total_dismantlings_upserted: z.number().int().nonnegative().openapi({
821
+ description: 'Total number of dismantlings successfully upserted',
822
+ example: 1
823
+ }),
824
+ total_assemblies_deleted: z.number().int().nonnegative().openapi({
825
+ description: 'Total number of assemblies successfully deleted',
826
+ example: 0
827
+ }),
828
+ total_dismantlings_deleted: z.number().int().nonnegative().openapi({
829
+ description: 'Total number of dismantlings successfully deleted',
830
+ example: 0
831
+ }),
832
+ upserted_assemblies: z.array(phaseSlotAssemblySchema).openapi({
833
+ description: 'Array of successfully upserted assemblies'
834
+ }),
835
+ upserted_dismantlings: z.array(phaseSlotDismantlingSchema).openapi({
836
+ description: 'Array of successfully upserted dismantlings'
837
+ }),
838
+ deleted_assemblies: z
839
+ .array(z.object({
840
+ id: z.number().int().positive(),
841
+ phase_slot_id: z.number().int().positive()
842
+ }))
843
+ .openapi({
844
+ description: 'Array of successfully deleted assemblies',
845
+ example: []
846
+ }),
847
+ deleted_dismantlings: z
848
+ .array(z.object({
849
+ id: z.number().int().positive(),
850
+ phase_slot_id: z.number().int().positive()
851
+ }))
852
+ .openapi({
853
+ description: 'Array of successfully deleted dismantlings',
854
+ example: []
855
+ }),
856
+ failed_operations: z
857
+ .array(z.object({
858
+ phase_slot_id: z.number().int().positive(),
859
+ operation: z.enum(['assembly', 'dismantling']),
860
+ action: z.enum(['upsert', 'delete']),
861
+ error: z.string(),
862
+ data: z.union([
863
+ z.object({
864
+ id: z.number().int().positive().optional(),
865
+ date: z.string().optional(),
866
+ start_time: z.string().optional(),
867
+ end_time: z.string().optional(),
868
+ duration: z.number().positive().optional(),
869
+ _delete: z.boolean().optional()
870
+ }),
871
+ z.object({
872
+ id: z.number().int().positive()
873
+ })
874
+ ])
875
+ }))
876
+ .openapi({
877
+ description: 'Array of operations that failed to process',
878
+ example: [
879
+ {
880
+ phase_slot_id: 999,
881
+ operation: 'assembly',
882
+ action: 'upsert',
883
+ error: 'Phase slot 999 not found or inactive',
884
+ data: { date: '2025-12-15', start_time: '06:00' }
885
+ }
886
+ ]
887
+ })
888
+ })
889
+ .openapi('UpsertPhaseSlotSchedulesData');
890
+ export const upsertPhaseSlotSchedulesResponseSchema = createMessageDataResponseSchema(upsertPhaseSlotSchedulesDataSchema, 'UpsertPhaseSlotSchedulesResponse', 'Bulk phase slot schedules upsert completed', 'Results of the combined upsert operation');