@shopickup/adapters-foxpost 0.0.1

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.
Files changed (47) hide show
  1. package/README.md +48 -0
  2. package/dist/capabilities/index.d.ts +9 -0
  3. package/dist/capabilities/index.d.ts.map +1 -0
  4. package/dist/capabilities/index.js +8 -0
  5. package/dist/capabilities/label.d.ts +27 -0
  6. package/dist/capabilities/label.d.ts.map +1 -0
  7. package/dist/capabilities/label.js +370 -0
  8. package/dist/capabilities/parcels.d.ts +21 -0
  9. package/dist/capabilities/parcels.d.ts.map +1 -0
  10. package/dist/capabilities/parcels.js +233 -0
  11. package/dist/capabilities/pickup-points.d.ts +38 -0
  12. package/dist/capabilities/pickup-points.d.ts.map +1 -0
  13. package/dist/capabilities/pickup-points.js +225 -0
  14. package/dist/capabilities/track.d.ts +16 -0
  15. package/dist/capabilities/track.d.ts.map +1 -0
  16. package/dist/capabilities/track.js +99 -0
  17. package/dist/client/index.d.ts +17 -0
  18. package/dist/client/index.d.ts.map +1 -0
  19. package/dist/client/index.js +30 -0
  20. package/dist/errors.d.ts +34 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +165 -0
  23. package/dist/index.d.ts +119 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +151 -0
  26. package/dist/mappers/index.d.ts +108 -0
  27. package/dist/mappers/index.d.ts.map +1 -0
  28. package/dist/mappers/index.js +270 -0
  29. package/dist/mappers/trackStatus.d.ts +58 -0
  30. package/dist/mappers/trackStatus.d.ts.map +1 -0
  31. package/dist/mappers/trackStatus.js +290 -0
  32. package/dist/types/generated.d.ts +177 -0
  33. package/dist/types/generated.d.ts.map +1 -0
  34. package/dist/types/generated.js +9 -0
  35. package/dist/types/index.d.ts +7 -0
  36. package/dist/types/index.d.ts.map +1 -0
  37. package/dist/types/index.js +6 -0
  38. package/dist/utils/httpUtils.d.ts +18 -0
  39. package/dist/utils/httpUtils.d.ts.map +1 -0
  40. package/dist/utils/httpUtils.js +33 -0
  41. package/dist/utils/resolveBaseUrl.d.ts +23 -0
  42. package/dist/utils/resolveBaseUrl.d.ts.map +1 -0
  43. package/dist/utils/resolveBaseUrl.js +19 -0
  44. package/dist/validation.d.ts +1723 -0
  45. package/dist/validation.d.ts.map +1 -0
  46. package/dist/validation.js +799 -0
  47. package/package.json +68 -0
@@ -0,0 +1,799 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schemas for runtime validation
4
+ */
5
+ /**
6
+ * Money schema (ISO currency amount in smallest unit)
7
+ */
8
+ const MoneySchema = z.object({
9
+ amount: z.number().int().nonnegative().describe('Amount must be non-negative'),
10
+ currency: z.string().length(3).describe('Currency must be ISO 4217 code (3 chars)'),
11
+ });
12
+ /**
13
+ * Contact schema (phone/email optional)
14
+ */
15
+ const ContactSchema = z.object({
16
+ name: z.string().min(1).describe('Name is required'),
17
+ phone: z.string().optional(),
18
+ email: z.string().optional(),
19
+ company: z.string().optional(),
20
+ });
21
+ /**
22
+ * Address schema (full shipping address)
23
+ */
24
+ const AddressSchema = z.object({
25
+ name: z.string().min(1).describe('Address name is required'),
26
+ street: z.string().min(1).describe('Street is required'),
27
+ city: z.string().min(1).describe('City is required'),
28
+ postalCode: z.string().min(1).describe('Postal code is required'),
29
+ country: z.string().min(1).describe('Country is required'),
30
+ phone: z.string().optional(),
31
+ email: z.email().optional(),
32
+ company: z.string().optional(),
33
+ province: z.string().optional(),
34
+ isPoBox: z.boolean().optional(),
35
+ });
36
+ /**
37
+ * Home delivery schema
38
+ */
39
+ const HomeDeliverySchema = z.object({
40
+ method: z.literal('HOME'),
41
+ address: AddressSchema,
42
+ instructions: z.string().optional(),
43
+ });
44
+ /**
45
+ * Pickup point delivery schema
46
+ */
47
+ const PickupPointDeliverySchema = z.object({
48
+ method: z.literal('PICKUP_POINT'),
49
+ pickupPoint: z.object({
50
+ id: z.string().min(1, 'Pickup point ID is required'),
51
+ provider: z.string().optional(),
52
+ name: z.string().optional(),
53
+ address: AddressSchema.optional(),
54
+ type: z.enum(['LOCKER', 'SHOP', 'POST_OFFICE', 'OTHER']).optional(),
55
+ }),
56
+ instructions: z.string().optional(),
57
+ });
58
+ /**
59
+ * Delivery discriminated union
60
+ */
61
+ const DeliverySchema = z.discriminatedUnion('method', [
62
+ HomeDeliverySchema,
63
+ PickupPointDeliverySchema,
64
+ ]);
65
+ /**
66
+ * Parcel schema (canonical domain type)
67
+ */
68
+ const ParcelSchema = z.object({
69
+ id: z.string().min(1),
70
+ shipper: z.object({
71
+ contact: ContactSchema,
72
+ address: AddressSchema,
73
+ }),
74
+ recipient: z.object({
75
+ contact: ContactSchema,
76
+ delivery: DeliverySchema,
77
+ }),
78
+ carrierIds: z.record(z.string(), z.string()).optional(),
79
+ service: z.enum(['standard', 'express', 'economy', 'overnight']),
80
+ carrierServiceCode: z.string().optional(),
81
+ package: z.object({
82
+ weightGrams: z.number().gt(0),
83
+ dimensionsCm: z.object({
84
+ length: z.number().gt(0),
85
+ width: z.number().gt(0),
86
+ height: z.number().gt(0),
87
+ }).optional(),
88
+ }),
89
+ handling: z.object({
90
+ fragile: z.boolean().optional(),
91
+ perishables: z.boolean().optional(),
92
+ batteries: z.enum(['NONE', 'LITHIUM_ION', 'LITHIUM_METAL']).optional(),
93
+ }).optional(),
94
+ cod: z.object({
95
+ amount: MoneySchema,
96
+ reference: z.string().optional(),
97
+ }).optional(),
98
+ declaredValue: MoneySchema.optional(),
99
+ insurance: z.object({
100
+ amount: MoneySchema,
101
+ }).optional(),
102
+ references: z.object({
103
+ orderId: z.string().optional(),
104
+ customerReference: z.string().optional(),
105
+ }).optional(),
106
+ items: z.array(z.object({
107
+ sku: z.string().optional(),
108
+ quantity: z.number().gt(0),
109
+ description: z.string().optional(),
110
+ weight: z.number().optional(),
111
+ metadata: z.record(z.string(), z.unknown()).optional(),
112
+ })).optional(),
113
+ status: z.enum(['draft', 'created', 'closed', 'label_generated', 'shipped', 'delivered', 'exception']).optional(),
114
+ metadata: z.record(z.string(), z.unknown()).optional(),
115
+ createdAt: z.date().optional(),
116
+ updatedAt: z.date().optional(),
117
+ }).strict();
118
+ /**
119
+ * Foxpost package size schema (used in both HD and APM)
120
+ */
121
+ const FoxpostPackageSizeSchema = z.enum(['XS', 'S', 'M', 'L', 'XL', '1', '2', '3', '4', '5']);
122
+ /**
123
+ * Hungarian phone number schema (mobile only, +36 or 36 prefix)
124
+ */
125
+ const HungarianPhoneSchema = z.string().regex(/^(\+36|36)(20|30|31|70|50|51)\d{7}$/, 'Phone must be Hungarian mobile');
126
+ /**
127
+ * Credentials schema - requires apiKey, basicUsername, and basicPassword all together
128
+ */
129
+ const FoxpostCredentialsSchema = z.object({
130
+ apiKey: z.string().min(1, 'API key is required'),
131
+ basicUsername: z.string().min(1, 'Basic auth username is required'),
132
+ basicPassword: z.string().min(1, 'Basic auth password is required'),
133
+ }).strict();
134
+ /**
135
+ * Home Delivery parcel schema with discriminator
136
+ */
137
+ const FoxpostParcelHDSchema = z.object({
138
+ type: z.literal('HD'),
139
+ cod: z.number().int().min(0).max(1000000).optional().default(0),
140
+ comment: z.string().max(50).optional(),
141
+ deliveryNote: z.string().optional(),
142
+ fragile: z.boolean().optional().default(false),
143
+ label: z.boolean().optional(),
144
+ recipientAddress: z.string().min(1).max(150),
145
+ recipientCity: z.string().min(1).max(50),
146
+ recipientCountry: z.string().optional(),
147
+ recipientEmail: z.email(),
148
+ recipientName: z.string().min(1).max(150),
149
+ recipientPhone: HungarianPhoneSchema,
150
+ recipientZip: z.string().min(1).max(4),
151
+ refCode: z.string().max(30).optional(),
152
+ size: FoxpostPackageSizeSchema,
153
+ }).strict();
154
+ /**
155
+ * APM (Automated Parcel Machine) parcel schema with discriminator
156
+ */
157
+ const FoxpostParcelAPMSchema = z.object({
158
+ type: z.literal('APM'),
159
+ cod: z.number().int().min(0).max(1000000).optional().default(0),
160
+ comment: z.string().max(50).optional(),
161
+ destination: z.string().min(1),
162
+ label: z.boolean().optional(),
163
+ recipientEmail: z.email(),
164
+ recipientName: z.string().min(1).max(150),
165
+ recipientPhone: HungarianPhoneSchema,
166
+ refCode: z.string().max(30).optional(),
167
+ size: FoxpostPackageSizeSchema,
168
+ uniqueBarcode: z.string().min(4).max(20).regex(/^(?=.*[a-zA-Z].*[a-zA-Z].*[a-zA-Z].*[a-zA-Z])(?=.*\d.*\d.*\d.*\d)/).optional(),
169
+ }).strict();
170
+ /**
171
+ * Discriminated union of HD and APM parcel types
172
+ */
173
+ export const FoxpostParcelSchema = z.discriminatedUnion('type', [
174
+ FoxpostParcelHDSchema,
175
+ FoxpostParcelAPMSchema
176
+ ]);
177
+ /**
178
+ * CreateParcelRequest Zod schema
179
+ */
180
+ export const CreateParcelRequestFoxpostSchema = z.object({
181
+ parcel: ParcelSchema,
182
+ credentials: FoxpostCredentialsSchema,
183
+ options: z.object({
184
+ useTestApi: z.boolean().optional()
185
+ }).optional()
186
+ });
187
+ export const CreateParcelsRequestFoxpostSchema = z.object({
188
+ parcels: z.array(ParcelSchema).min(1),
189
+ credentials: FoxpostCredentialsSchema,
190
+ options: z.object({
191
+ useTestApi: z.boolean().optional()
192
+ }).optional()
193
+ });
194
+ export const CreateLabelRequestFoxpostSchema = z.object({
195
+ parcelCarrierId: z.string().min(1, 'Parcel carrier ID is required'),
196
+ credentials: FoxpostCredentialsSchema,
197
+ options: z.object({
198
+ useTestApi: z.boolean().optional(),
199
+ size: z.enum(['A6', 'A7', '_85X85']).default('A7'),
200
+ foxpost: z.object({
201
+ startPos: z.number().int().min(0).max(7).optional(),
202
+ isPortrait: z.boolean().optional().default(false),
203
+ }).optional(),
204
+ }).optional()
205
+ });
206
+ /**
207
+ * Batch label request schema
208
+ * Similar to CreateParcelsRequest but for labels
209
+ */
210
+ export const CreateLabelsRequestFoxpostSchema = z.object({
211
+ parcelCarrierIds: z.array(z.string().min(1)).min(1, 'At least one parcel ID is required'),
212
+ credentials: FoxpostCredentialsSchema,
213
+ options: z.object({
214
+ useTestApi: z.boolean().optional(),
215
+ size: z.enum(['A6', 'A7', '_85X85']).default('A7'),
216
+ foxpost: z.object({
217
+ startPos: z.number().int().min(0).max(7).optional(),
218
+ isPortrait: z.boolean().optional().default(false),
219
+ }).optional(),
220
+ }).optional()
221
+ });
222
+ /**
223
+ * TrackingRequest Zod schema
224
+ */
225
+ export const TrackingRequestFoxpostSchema = z.object({
226
+ trackingNumber: z.string().min(1, 'Tracking number is required'),
227
+ credentials: FoxpostCredentialsSchema,
228
+ options: z.object({
229
+ useTestApi: z.boolean().optional()
230
+ }).optional()
231
+ });
232
+ /**
233
+ * Helper to validate and extract credentials from a request
234
+ * Throws ZodError if validation fails
235
+ */
236
+ export function validateFoxpostCredentials(credentials) {
237
+ return FoxpostCredentialsSchema.parse(credentials);
238
+ }
239
+ /**
240
+ * Helper to safely validate credentials without throwing
241
+ * Returns { success: true, data } or { success: false, error }
242
+ */
243
+ export function safeValidateFoxpostCredentials(credentials) {
244
+ return FoxpostCredentialsSchema.safeParse(credentials);
245
+ }
246
+ /**
247
+ * Helper to validate a CreateParcelRequest
248
+ * Returns parsed and validated request or throws ZodError
249
+ */
250
+ export function validateCreateParcelRequest(req) {
251
+ return CreateParcelRequestFoxpostSchema.parse(req);
252
+ }
253
+ /**
254
+ * Helper to validate a CreateParcelsRequest
255
+ * Returns parsed and validated request or throws ZodError
256
+ */
257
+ export function validateCreateParcelsRequest(req) {
258
+ return CreateParcelsRequestFoxpostSchema.parse(req);
259
+ }
260
+ /**
261
+ * Helper to safely validate without throwing
262
+ * Returns { success: true, data } or { success: false, error }
263
+ */
264
+ export function safeValidateCreateParcelRequest(req) {
265
+ return CreateParcelRequestFoxpostSchema.safeParse(req);
266
+ }
267
+ export function safeValidateCreateParcelsRequest(req) {
268
+ return CreateParcelsRequestFoxpostSchema.safeParse(req);
269
+ }
270
+ /**
271
+ * Helper to safely validate a CreateLabelRequest without throwing
272
+ * Returns { success: true, data } or { success: false, error }
273
+ */
274
+ export function safeValidateCreateLabelRequest(req) {
275
+ return CreateLabelRequestFoxpostSchema.safeParse(req);
276
+ }
277
+ /**
278
+ * Helper to safely validate a CreateLabelsRequest without throwing
279
+ * Returns { success: true, data } or { success: false, error }
280
+ */
281
+ export function safeValidateCreateLabelsRequest(req) {
282
+ return CreateLabelsRequestFoxpostSchema.safeParse(req);
283
+ }
284
+ /**
285
+ * Helper to safely validate a TrackingRequest without throwing
286
+ * Returns { success: true, data } or { success: false, error }
287
+ */
288
+ export function safeValidateTrackingRequest(req) {
289
+ return TrackingRequestFoxpostSchema.safeParse(req);
290
+ }
291
+ /**
292
+ * Validate a Foxpost parcel payload (after mapping from canonical Parcel)
293
+ * Used to catch mapping errors before sending to the carrier
294
+ */
295
+ export function validateFoxpostParcel(parcel) {
296
+ return FoxpostParcelSchema.parse(parcel);
297
+ }
298
+ /**
299
+ * Safely validate a Foxpost parcel payload without throwing
300
+ * Returns { success: true, data } or { success: false, error }
301
+ */
302
+ export function safeValidateFoxpostParcel(parcel) {
303
+ return FoxpostParcelSchema.safeParse(parcel);
304
+ }
305
+ /**
306
+ * ============================================================================
307
+ * Foxpost Tracking Schemas (from OpenAPI /api/tracking/{barcode})
308
+ * ============================================================================
309
+ */
310
+ /**
311
+ * Trace enum from OpenAPI Trace.status
312
+ */
313
+ const TraceStatusEnum = z.enum([
314
+ 'CREATE',
315
+ 'OPERIN',
316
+ 'OPEROUT',
317
+ 'RECEIVE',
318
+ 'RETURN',
319
+ 'REDIRECT',
320
+ 'OVERTIMEOUT',
321
+ 'SORTIN',
322
+ 'SORTOUT',
323
+ 'SLOTCHANGE',
324
+ 'OVERTIMED',
325
+ 'MPSIN',
326
+ 'C2CIN',
327
+ 'HDSENT',
328
+ 'HDDEPO',
329
+ 'HDINTRANSIT',
330
+ 'HDRETURN',
331
+ 'HDRECEIVE',
332
+ 'WBXREDIRECT',
333
+ 'BACKTOSENDER',
334
+ 'HDHUBIN',
335
+ 'HDHUBOUT',
336
+ 'HDCOURIER',
337
+ 'HDUNDELIVERABLE',
338
+ 'PREPAREDFORPD',
339
+ 'INWAREHOUSE',
340
+ 'COLLECTSENT',
341
+ 'C2BIN',
342
+ 'RETURNED',
343
+ 'COLLECTED',
344
+ 'BACKLOGINFULL',
345
+ 'BACKLOGINFAIL',
346
+ 'MISSORT',
347
+ 'EMPTYSLOT',
348
+ 'RESENT',
349
+ 'PREREDIRECT',
350
+ ]);
351
+ /**
352
+ * Trace schema (from OpenAPI components/schemas/Trace)
353
+ *
354
+ * Uses lenient validation with coercion to handle real API responses:
355
+ * - statusDate: coerces various date string formats to Date objects
356
+ * - statusStationId: coerces numbers to strings (API inconsistency)
357
+ * - Other fields: optional, pass through extra fields
358
+ */
359
+ const TraceSchema = z.object({
360
+ statusDate: z.string()
361
+ .refine((s) => {
362
+ // Accept any string that looks like a date
363
+ // Covers: ISO with timezone, ISO without timezone, other formats
364
+ const date = new Date(s);
365
+ return !isNaN(date.getTime());
366
+ }, "Invalid date format")
367
+ .transform(s => new Date(s)),
368
+ statusStationId: z.union([
369
+ z.string(),
370
+ z.number().transform(n => String(n)),
371
+ ]).optional(),
372
+ shortName: z.string().optional(),
373
+ longName: z.string().optional(),
374
+ status: TraceStatusEnum.optional(),
375
+ }).loose(); // Allow extra fields from API
376
+ /**
377
+ * TrackDTO schema (from OpenAPI components/schemas/TrackDTO)
378
+ * Note: TrackDTO mirrors Trace but with trackId and slightly different names
379
+ */
380
+ const TrackDTOSchema = z.object({
381
+ trackId: z.number().int().optional(),
382
+ status: z.string().optional(),
383
+ statusDate: z.string().datetime().or(z.string()).transform(s => new Date(s)),
384
+ }).strict();
385
+ /**
386
+ * Parcel type enum from OpenAPI Tracking.parcelType
387
+ */
388
+ const FoxpostParcelTypeEnum = z.enum(['NORMAL', 'RE', 'XRE', 'IRE', 'C2B']);
389
+ /**
390
+ * Send type enum from OpenAPI Tracking.sendType
391
+ */
392
+ const FoxpostSendTypeEnum = z.enum(['APM', 'HD', 'COLLECT']);
393
+ /**
394
+ * Tracking schema (from OpenAPI components/schemas/Tracking)
395
+ * This is the response from GET /api/tracking/{barcode}
396
+ *
397
+ * Uses lenient validation to handle API quirks:
398
+ * - All fields optional
399
+ * - estimatedDelivery can be null or string
400
+ * - Passes through extra fields from API
401
+ */
402
+ const FoxpostTrackingSchema = z.object({
403
+ clFox: z.string().optional(),
404
+ parcelType: FoxpostParcelTypeEnum.optional(),
405
+ sendType: FoxpostSendTypeEnum.optional(),
406
+ traces: z.array(TraceSchema).optional(),
407
+ relatedParcel: z.string().nullable().optional(),
408
+ estimatedDelivery: z.string().nullable().optional(),
409
+ }).loose(); // Allow extra fields from API
410
+ /**
411
+ * Helper to validate a Foxpost tracking response
412
+ * Throws ZodError if validation fails
413
+ */
414
+ export function validateFoxpostTracking(res) {
415
+ return FoxpostTrackingSchema.parse(res);
416
+ }
417
+ /**
418
+ * Helper to safely validate a Foxpost tracking response without throwing
419
+ * Returns { success: true, data } or { success: false, error }
420
+ */
421
+ export function safeValidateFoxpostTracking(res) {
422
+ return FoxpostTrackingSchema.safeParse(res);
423
+ }
424
+ /**
425
+ * ============================================================================
426
+ * Foxpost Parcel Creation Request/Response Schemas (from OpenAPI /api/parcel)
427
+ * ============================================================================
428
+ *
429
+ * Schemas for request body and response shapes, modeled after Foxpost OpenAPI
430
+ * but kept lenient to handle real-world API quirks and future changes.
431
+ */
432
+ /**
433
+ * FieldError schema (from OpenAPI components/schemas/FieldError)
434
+ * Represents a validation error on a specific field
435
+ * Uses lenient validation to accept any field name or error code
436
+ */
437
+ const FieldErrorSchema = z.object({
438
+ field: z.string().optional(),
439
+ message: z.string().optional(),
440
+ }).loose(); // Allow extra fields (e.g., error codes)
441
+ /**
442
+ * Package schema (from OpenAPI components/schemas/Package)
443
+ * Represents a single parcel result in the CreateResponse
444
+ *
445
+ * Uses lenient validation:
446
+ * - All fields optional to handle partial responses
447
+ * - Passes through extra fields from carrier
448
+ * - Lenient with field types (clFoxId may be string or number fallback)
449
+ */
450
+ const PackageSchema = z.object({
451
+ clFoxId: z.union([
452
+ z.string(),
453
+ z.number().transform(n => String(n)),
454
+ ]).optional(),
455
+ barcode: z.string().nullable().optional(), // Fallback name for barcode
456
+ newBarcode: z.string().nullable().optional(), // Alternative barcode field
457
+ refCode: z.string().nullable().optional(),
458
+ uniqueBarcode: z.string().nullable().optional(),
459
+ errors: z.array(FieldErrorSchema).nullable().optional(),
460
+ }).loose(); // Allow extra fields from API
461
+ /**
462
+ * CreateResponse schema (from OpenAPI components/schemas/CreateResponse)
463
+ * Response from POST /api/parcel
464
+ *
465
+ * Uses lenient validation:
466
+ * - valid: boolean indicating if all parcels succeeded or not
467
+ * - parcels: array of Package results
468
+ * - errors: top-level validation errors (optional)
469
+ * - Passes through extra fields for extensibility
470
+ */
471
+ const CreateResponseSchema = z.object({
472
+ valid: z.boolean().optional(),
473
+ parcels: z.array(PackageSchema).optional(),
474
+ errors: z.array(FieldErrorSchema).optional(),
475
+ }).loose(); // Allow extra fields from API
476
+ /**
477
+ * CreateParcelRequest schema (from OpenAPI components/schemas/CreateParcelRequest)
478
+ * Request body for POST /api/parcel (single or batch)
479
+ *
480
+ * Uses lenient validation:
481
+ * - Required: recipientName, recipientEmail, recipientPhone (for all parcels)
482
+ * - HD parcels: also require recipientCity, recipientZip, recipientAddress
483
+ * - APM parcels: also require destination
484
+ * - Optional fields accept any string/value, not strict enums
485
+ * - size defaults to 's' if missing
486
+ * - cod: coerced to number, defaults to 0
487
+ * - Passes through extra fields for future extensibility
488
+ */
489
+ const CreateParcelRequestItemSchema = z.object({
490
+ // Required recipient contact fields
491
+ recipientName: z.string().max(150).optional(),
492
+ recipientEmail: z.string().optional(),
493
+ recipientPhone: z.string().optional(),
494
+ // Optional contact fields
495
+ recipientCountry: z.string().max(3).optional(),
496
+ // HD (Home Delivery) address fields - optional at schema level
497
+ // (validation logic in parcels.ts checks that either all or none are present)
498
+ recipientCity: z.string().max(50).optional(),
499
+ recipientZip: z.string().optional(),
500
+ recipientAddress: z.string().max(150).optional(),
501
+ // APM delivery field
502
+ destination: z.string().optional(),
503
+ // Parcel details
504
+ size: z.union([
505
+ z.enum(['XS', 'S', 'M', 'L', 'XL']),
506
+ z.enum(['xs', 's', 'm', 'l', 'xl']).transform(s => s.toUpperCase()),
507
+ z.string(), // Lenient: accept any string size
508
+ ]).optional().default('S'),
509
+ cod: z.union([
510
+ z.number().int().min(0).max(1000000),
511
+ z.string().transform(s => parseInt(s, 10)).refine(n => !Number.isNaN(n) && n >= 0 && n <= 1000000),
512
+ ]).optional().default(0),
513
+ // Optional fields - lenient, any string accepted
514
+ comment: z.string().max(50).optional(),
515
+ deliveryNote: z.string().optional(),
516
+ label: z.boolean().optional(),
517
+ fragile: z.boolean().optional(),
518
+ refCode: z.string().max(30).optional(),
519
+ uniqueBarcode: z.string().max(20).optional(),
520
+ // Extra fields for future extensibility
521
+ }).loose();
522
+ /**
523
+ * Helper to validate a Foxpost CreateResponse
524
+ * Throws ZodError if validation fails
525
+ */
526
+ export function validateFoxpostCreateResponse(res) {
527
+ return CreateResponseSchema.parse(res);
528
+ }
529
+ /**
530
+ * Helper to safely validate a Foxpost CreateResponse without throwing
531
+ * Returns { success: true, data } or { success: false, error }
532
+ */
533
+ export function safeValidateFoxpostCreateResponse(res) {
534
+ return CreateResponseSchema.safeParse(res);
535
+ }
536
+ /**
537
+ * Helper to validate a Foxpost Package entry
538
+ * Throws ZodError if validation fails
539
+ */
540
+ export function validateFoxpostPackage(pkg) {
541
+ return PackageSchema.parse(pkg);
542
+ }
543
+ /**
544
+ * Helper to safely validate a Foxpost Package entry without throwing
545
+ * Returns { success: true, data } or { success: false, error }
546
+ */
547
+ export function safeValidateFoxpostPackage(pkg) {
548
+ return PackageSchema.safeParse(pkg);
549
+ }
550
+ /**
551
+ * Helper to validate a single Foxpost CreateParcelRequest item
552
+ * Throws ZodError if validation fails
553
+ */
554
+ export function validateFoxpostCreateParcelRequestItem(item) {
555
+ return CreateParcelRequestItemSchema.parse(item);
556
+ }
557
+ /**
558
+ * Helper to safely validate a Foxpost CreateParcelRequest item without throwing
559
+ * Returns { success: true, data } or { success: false, error }
560
+ */
561
+ export function safeValidateFoxpostCreateParcelRequestItem(item) {
562
+ return CreateParcelRequestItemSchema.safeParse(item);
563
+ }
564
+ /**
565
+ * ============================================================================
566
+ * Foxpost Label Generation Schemas (from OpenAPI /api/label/{pageSize})
567
+ * ============================================================================
568
+ *
569
+ * Schemas for label generation request parameters, response metadata,
570
+ * error handling, and label info (from /api/label/info/{barcode})
571
+ */
572
+ /**
573
+ * ApiError schema (from OpenAPI components/schemas/ApiError)
574
+ * Carrier returns this on error responses (400, 401, etc.)
575
+ * Uses lenient validation to accept any structure
576
+ */
577
+ const ApiErrorSchema = z.object({
578
+ timestamp: z.string().optional(),
579
+ error: z.string().optional(),
580
+ status: z.number().int().optional(),
581
+ }).loose(); // Allow extra fields from API
582
+ /**
583
+ * Helper to safely validate a Foxpost API error response without throwing
584
+ * Returns { success: true, data } or { success: false, error }
585
+ */
586
+ export function safeValidateFoxpostApiError(res) {
587
+ return ApiErrorSchema.safeParse(res);
588
+ }
589
+ /**
590
+ * PDF binary raw response validation
591
+ * Ensures the PDF buffer is non-empty
592
+ * Accepts Node Buffer or any object with numeric byteLength > 0
593
+ */
594
+ const FoxpostLabelPdfRawSchema = z.any().refine((v) => {
595
+ return v != null && typeof v.byteLength === 'number' && v.byteLength > 0;
596
+ }, { message: 'Expected non-empty PDF buffer' });
597
+ /**
598
+ * Helper to safely validate PDF raw response without throwing
599
+ * Returns { success: true, data } or { success: false, error }
600
+ */
601
+ export function safeValidateFoxpostLabelPdfRaw(raw) {
602
+ return FoxpostLabelPdfRawSchema.safeParse(raw);
603
+ }
604
+ /**
605
+ * PDF metadata schema for label generation
606
+ * Describes the PDF file properties and generation options
607
+ */
608
+ const FoxpostLabelPdfMetadataSchema = z.object({
609
+ size: z.enum(['A6', 'A7', '_85X85']).optional(),
610
+ barcodesCount: z.number().int().min(1).optional(),
611
+ startPos: z.number().int().min(0).max(7).optional(),
612
+ isPortrait: z.boolean().optional(),
613
+ }).loose();
614
+ /**
615
+ * Helper to validate PDF metadata
616
+ * Throws ZodError if validation fails
617
+ */
618
+ export function validateFoxpostLabelPdfMetadata(metadata) {
619
+ return FoxpostLabelPdfMetadataSchema.parse(metadata);
620
+ }
621
+ /**
622
+ * Helper to safely validate PDF metadata without throwing
623
+ * Returns { success: true, data } or { success: false, error }
624
+ */
625
+ export function safeValidateFoxpostLabelPdfMetadata(metadata) {
626
+ return FoxpostLabelPdfMetadataSchema.safeParse(metadata);
627
+ }
628
+ /**
629
+ * LabelInfo schema (from OpenAPI components/schemas/LabelInfo)
630
+ * Response from GET /api/label/info/{barcode}
631
+ * Contains pre-label metadata about the parcel
632
+ */
633
+ const LabelInfoSchema = z.object({
634
+ senderName: z.string().optional(),
635
+ senderZip: z.string().optional(),
636
+ senderCity: z.string().optional(),
637
+ senderAddress: z.string().optional(),
638
+ recipientName: z.string().optional(),
639
+ recipientEmail: z.string().optional(),
640
+ recipientPhone: z.string().optional(),
641
+ recipientZip: z.string().optional(),
642
+ recipientCity: z.string().optional(),
643
+ recipientAddress: z.string().optional(),
644
+ apm: z.string().optional(),
645
+ cod: z.number().int().optional(),
646
+ isFragile: z.boolean().optional(),
647
+ barcode: z.string().optional(),
648
+ refCode: z.string().optional(),
649
+ depoCode: z.string().optional(),
650
+ courierCode: z.string().optional(),
651
+ sendType: z.enum(['APM', 'HD', 'COLLECT']).optional(),
652
+ }).loose(); // Allow extra fields from API
653
+ /**
654
+ * Helper to validate a Foxpost LabelInfo response
655
+ * Throws ZodError if validation fails
656
+ */
657
+ export function validateFoxpostLabelInfo(res) {
658
+ return LabelInfoSchema.parse(res);
659
+ }
660
+ /**
661
+ * Helper to safely validate a Foxpost LabelInfo response without throwing
662
+ * Returns { success: true, data } or { success: false, error }
663
+ */
664
+ export function safeValidateFoxpostLabelInfo(res) {
665
+ return LabelInfoSchema.safeParse(res);
666
+ }
667
+ /**
668
+ * ============================================================================
669
+ * Foxpost APM/Pickup-Points Schemas (from foxplus.json feed)
670
+ * ============================================================================
671
+ */
672
+ /**
673
+ * Opening hours schema - Hungarian day names with optional string values
674
+ * Uses passthrough to allow extra fields and lenient validation
675
+ */
676
+ const OpeningHoursSchema = z.object({
677
+ hetfo: z.string().optional().nullable(),
678
+ kedd: z.string().optional().nullable(),
679
+ szerda: z.string().optional().nullable(),
680
+ csutortok: z.string().optional().nullable(),
681
+ pentek: z.string().optional().nullable(),
682
+ szombat: z.string().optional().nullable(),
683
+ vasarnap: z.string().optional().nullable(),
684
+ }).loose(); // Allow extra fields
685
+ /**
686
+ * Fill/empty schedule entry
687
+ */
688
+ const FillEmptyEntrySchema = z.object({
689
+ emptying: z.string().optional(),
690
+ filling: z.string().optional(),
691
+ }).loose();
692
+ /**
693
+ * Foxpost APM substitute entry
694
+ * When a pickup point is closed or unavailable, these are alternative locations
695
+ * where the customer can pick up/drop off their parcel.
696
+ */
697
+ const FoxpostSubstituteSchema = z.object({
698
+ place_id: z.union([
699
+ z.string(),
700
+ z.number().transform(n => String(n)),
701
+ ]),
702
+ operator_id: z.string().optional().nullable(),
703
+ }).loose(); // Allow extra fields for future extensibility
704
+ /**
705
+ * Foxpost APM metadata schema - carrier-specific fields
706
+ * Uses lenient validation with passthrough for future extensibility
707
+ */
708
+ const FoxpostApmMetadataSchema = z.object({
709
+ depot: z.string().optional(),
710
+ load: z.string().optional(), // Lenient: accept any string, not just predefined enum values
711
+ apmType: z.string().optional(), // Lenient: accept any string (Cleveron, Keba, Rollkon, Rotte, Z-BOX, Z-Pont, etc.)
712
+ substitutes: z.array(FoxpostSubstituteSchema).optional(), // Array of substitute APM objects
713
+ variant: z.string().optional(), // Lenient: accept any string
714
+ fillEmptyList: z.array(FillEmptyEntrySchema).optional(),
715
+ ssapt: z.string().optional(),
716
+ sdapt: z.string().optional(),
717
+ }).loose();
718
+ const FoxpostApmEntrySchema = z.object({
719
+ place_id: z.union([
720
+ z.string(),
721
+ z.number().transform(n => String(n)),
722
+ ]),
723
+ operator_id: z.string().optional().nullable(),
724
+ name: z.string().optional(),
725
+ ssapt: z.string().optional(),
726
+ sdapt: z.string().optional(),
727
+ country: z.string().optional(),
728
+ address: z.string().optional(),
729
+ zip: z.string().optional(),
730
+ city: z.string().optional(),
731
+ street: z.string().optional(),
732
+ findme: z.string().optional(),
733
+ geolat: z.union([
734
+ z.number(),
735
+ z.string().transform(s => parseFloat(s)),
736
+ ]).optional().refine(n => n === undefined || !Number.isNaN(n), 'Latitude must be a valid number'),
737
+ geolng: z.union([
738
+ z.number(),
739
+ z.string().transform(s => parseFloat(s)),
740
+ ]).optional().refine(n => n === undefined || !Number.isNaN(n), 'Longitude must be a valid number'),
741
+ allowed2: z.enum(['ALL', 'C2C', 'B2C']).optional(),
742
+ depot: z.string().optional(),
743
+ load: z.string().optional(), // Lenient: accept any string (e.g., "normal loaded", "custom value")
744
+ isOutdoor: z.boolean().optional(),
745
+ apmType: z.string().optional(), // Lenient: accept any string (Foxpost types: Cleveron/Keba/Rollkon/Rotte, Packeta types: Z-BOX/Z-Pont, etc.)
746
+ substitutes: z.array(FoxpostSubstituteSchema).optional(), // Array of substitute APM objects
747
+ open: OpeningHoursSchema.optional(),
748
+ fillEmptyList: z.array(FillEmptyEntrySchema).optional(),
749
+ cardPayment: z.boolean().optional(),
750
+ cashPayment: z.boolean().optional(),
751
+ iconUrl: z.string().optional(),
752
+ variant: z.string().optional(), // Lenient: accept any string (e.g., FOXPOST A-BOX, Packeta Z-BOX, custom types)
753
+ paymentOptions: z.array(z.string()).optional(), // Lenient: accept any payment method string
754
+ paymentOptionsString: z.string().optional(),
755
+ service: z.array(z.string()).optional(), // Lenient: accept any service string (pickup, dispatch, or others)
756
+ serviceString: z.string().optional(),
757
+ }).loose(); // Allow extra fields from API
758
+ /**
759
+ * Foxpost-specific options for fetchPickupPoints.
760
+ *
761
+ * Carrier-specific filters are namespaced under `options.foxpost`.
762
+ */
763
+ const FoxpostFetchPickupPointsCarrierOptionsSchema = z.object({
764
+ country: z.string().length(2).optional(),
765
+ bbox: z.object({
766
+ north: z.number(),
767
+ south: z.number(),
768
+ east: z.number(),
769
+ west: z.number(),
770
+ }).optional(),
771
+ updatedSince: z.union([z.string(), z.date()]).optional(),
772
+ }).catchall(z.unknown());
773
+ const FoxpostFetchPickupPointsOptionsSchema = z.object({
774
+ foxpost: FoxpostFetchPickupPointsCarrierOptionsSchema.optional(),
775
+ }).catchall(z.unknown());
776
+ const FoxpostFetchPickupPointsRequestSchema = z.object({
777
+ credentials: z.record(z.string(), z.unknown()).optional(),
778
+ options: FoxpostFetchPickupPointsOptionsSchema.optional(),
779
+ });
780
+ /**
781
+ * Helper to safely validate a Foxpost APM feed (array of entries)
782
+ * Returns { success: true, data } or { success: false, error }
783
+ */
784
+ export function safeValidateFoxpostApmFeed(feed) {
785
+ return z.array(FoxpostApmEntrySchema).safeParse(feed);
786
+ }
787
+ /**
788
+ * Helper to safely validate a single Foxpost APM entry
789
+ * Returns { success: true, data } or { success: false, error }
790
+ */
791
+ export function safeValidateFoxpostApmEntry(entry) {
792
+ return FoxpostApmEntrySchema.safeParse(entry);
793
+ }
794
+ /**
795
+ * Helper to validate Foxpost fetchPickupPoints request envelope.
796
+ */
797
+ export function safeValidateFetchPickupPointsRequest(input) {
798
+ return FoxpostFetchPickupPointsRequestSchema.safeParse(input);
799
+ }