@axova/shared 1.0.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.
Files changed (112) hide show
  1. package/CONFIGURATION_GUIDE.md +1 -0
  2. package/README.md +384 -0
  3. package/SCHEMA_ORGANIZATION.md +209 -0
  4. package/dist/configs/index.d.ts +85 -0
  5. package/dist/configs/index.js +555 -0
  6. package/dist/events/kafka.d.ts +40 -0
  7. package/dist/events/kafka.js +311 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.js +41 -0
  10. package/dist/interfaces/customer-events.d.ts +85 -0
  11. package/dist/interfaces/customer-events.js +2 -0
  12. package/dist/interfaces/inventory-events.d.ts +453 -0
  13. package/dist/interfaces/inventory-events.js +3 -0
  14. package/dist/interfaces/inventory-types.d.ts +894 -0
  15. package/dist/interfaces/inventory-types.js +3 -0
  16. package/dist/interfaces/order-events.d.ts +320 -0
  17. package/dist/interfaces/order-events.js +3 -0
  18. package/dist/lib/auditLogger.d.ts +162 -0
  19. package/dist/lib/auditLogger.js +626 -0
  20. package/dist/lib/authOrganization.d.ts +24 -0
  21. package/dist/lib/authOrganization.js +110 -0
  22. package/dist/lib/db.d.ts +6 -0
  23. package/dist/lib/db.js +88 -0
  24. package/dist/middleware/serviceAuth.d.ts +60 -0
  25. package/dist/middleware/serviceAuth.js +272 -0
  26. package/dist/middleware/storeOwnership.d.ts +15 -0
  27. package/dist/middleware/storeOwnership.js +156 -0
  28. package/dist/middleware/storeValidationMiddleware.d.ts +44 -0
  29. package/dist/middleware/storeValidationMiddleware.js +180 -0
  30. package/dist/middleware/userAuth.d.ts +27 -0
  31. package/dist/middleware/userAuth.js +218 -0
  32. package/dist/schemas/admin/admin-schema.d.ts +741 -0
  33. package/dist/schemas/admin/admin-schema.js +111 -0
  34. package/dist/schemas/ai-moderation/ai-moderation-schema.d.ts +648 -0
  35. package/dist/schemas/ai-moderation/ai-moderation-schema.js +88 -0
  36. package/dist/schemas/common/common-schemas.d.ts +436 -0
  37. package/dist/schemas/common/common-schemas.js +94 -0
  38. package/dist/schemas/compliance/compliance-schema.d.ts +3388 -0
  39. package/dist/schemas/compliance/compliance-schema.js +472 -0
  40. package/dist/schemas/compliance/kyc-schema.d.ts +2642 -0
  41. package/dist/schemas/compliance/kyc-schema.js +361 -0
  42. package/dist/schemas/customer/customer-schema.d.ts +2727 -0
  43. package/dist/schemas/customer/customer-schema.js +399 -0
  44. package/dist/schemas/index.d.ts +27 -0
  45. package/dist/schemas/index.js +138 -0
  46. package/dist/schemas/inventory/inventory-tables.d.ts +9476 -0
  47. package/dist/schemas/inventory/inventory-tables.js +1470 -0
  48. package/dist/schemas/inventory/lot-tables.d.ts +3281 -0
  49. package/dist/schemas/inventory/lot-tables.js +608 -0
  50. package/dist/schemas/order/order-schema.d.ts +5825 -0
  51. package/dist/schemas/order/order-schema.js +954 -0
  52. package/dist/schemas/product/discount-relations.d.ts +15 -0
  53. package/dist/schemas/product/discount-relations.js +34 -0
  54. package/dist/schemas/product/discount-schema.d.ts +1975 -0
  55. package/dist/schemas/product/discount-schema.js +297 -0
  56. package/dist/schemas/product/product-relations.d.ts +41 -0
  57. package/dist/schemas/product/product-relations.js +133 -0
  58. package/dist/schemas/product/product-schema.d.ts +4544 -0
  59. package/dist/schemas/product/product-schema.js +671 -0
  60. package/dist/schemas/store/store-audit-schema.d.ts +4135 -0
  61. package/dist/schemas/store/store-audit-schema.js +556 -0
  62. package/dist/schemas/store/store-schema.d.ts +3100 -0
  63. package/dist/schemas/store/store-schema.js +381 -0
  64. package/dist/schemas/store/store-settings-schema.d.ts +665 -0
  65. package/dist/schemas/store/store-settings-schema.js +141 -0
  66. package/dist/schemas/types.d.ts +50 -0
  67. package/dist/schemas/types.js +3 -0
  68. package/dist/types/events.d.ts +2396 -0
  69. package/dist/types/events.js +505 -0
  70. package/dist/utils/errorHandler.d.ts +12 -0
  71. package/dist/utils/errorHandler.js +36 -0
  72. package/dist/utils/subdomain.d.ts +6 -0
  73. package/dist/utils/subdomain.js +20 -0
  74. package/nul +8 -0
  75. package/package.json +43 -0
  76. package/src/configs/index.ts +654 -0
  77. package/src/events/kafka.ts +429 -0
  78. package/src/index.ts +26 -0
  79. package/src/interfaces/customer-events.ts +106 -0
  80. package/src/interfaces/inventory-events.ts +545 -0
  81. package/src/interfaces/inventory-types.ts +1004 -0
  82. package/src/interfaces/order-events.ts +381 -0
  83. package/src/lib/auditLogger.ts +1117 -0
  84. package/src/lib/authOrganization.ts +153 -0
  85. package/src/lib/db.ts +64 -0
  86. package/src/middleware/serviceAuth.ts +328 -0
  87. package/src/middleware/storeOwnership.ts +199 -0
  88. package/src/middleware/storeValidationMiddleware.ts +247 -0
  89. package/src/middleware/userAuth.ts +248 -0
  90. package/src/schemas/admin/admin-schema.ts +208 -0
  91. package/src/schemas/ai-moderation/ai-moderation-schema.ts +180 -0
  92. package/src/schemas/common/common-schemas.ts +108 -0
  93. package/src/schemas/compliance/compliance-schema.ts +927 -0
  94. package/src/schemas/compliance/kyc-schema.ts +649 -0
  95. package/src/schemas/customer/customer-schema.ts +576 -0
  96. package/src/schemas/index.ts +189 -0
  97. package/src/schemas/inventory/inventory-tables.ts +1927 -0
  98. package/src/schemas/inventory/lot-tables.ts +799 -0
  99. package/src/schemas/order/order-schema.ts +1400 -0
  100. package/src/schemas/product/discount-relations.ts +44 -0
  101. package/src/schemas/product/discount-schema.ts +464 -0
  102. package/src/schemas/product/product-relations.ts +187 -0
  103. package/src/schemas/product/product-schema.ts +955 -0
  104. package/src/schemas/store/ethiopian_business_api.md.resolved +212 -0
  105. package/src/schemas/store/store-audit-schema.ts +1257 -0
  106. package/src/schemas/store/store-schema.ts +661 -0
  107. package/src/schemas/store/store-settings-schema.ts +231 -0
  108. package/src/schemas/types.ts +67 -0
  109. package/src/types/events.ts +646 -0
  110. package/src/utils/errorHandler.ts +44 -0
  111. package/src/utils/subdomain.ts +19 -0
  112. package/tsconfig.json +21 -0
@@ -0,0 +1,799 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+ import {
3
+ boolean,
4
+ date,
5
+ decimal,
6
+ index,
7
+ integer,
8
+ jsonb,
9
+ pgEnum,
10
+ pgTable,
11
+ text,
12
+ timestamp,
13
+ unique,
14
+ varchar,
15
+ } from "drizzle-orm/pg-core";
16
+
17
+ // =====================================================
18
+ // LOT & BATCH MANAGEMENT ENUMS
19
+ // =====================================================
20
+
21
+ export const lotStatusEnum = pgEnum("lot_status", [
22
+ "PO_INCOMING", // Incoming from purchase order, not yet received
23
+ "PENDING", // Just received, not yet QC'd
24
+ "ACTIVE", // Available for use
25
+ "QUARANTINED", // Held for investigation
26
+ "ALLOCATED", // Reserved for orders
27
+ "EXPIRED", // Past expiry date
28
+ "RECALLED", // Subject to recall
29
+ "DEPLETED", // Fully consumed
30
+ "RETURNED", // Returned to supplier
31
+ "DISPOSED", // Disposed/destroyed
32
+ ]);
33
+
34
+ export const qcStatusEnum = pgEnum("qc_status", [
35
+ "NOT_REQUIRED",
36
+ "PENDING",
37
+ "PASSED",
38
+ "FAILED",
39
+ ]);
40
+
41
+ // Quality status is handled via relation to quality_controls table
42
+ // We don't need a separate enum for lots
43
+
44
+ export const lotMovementTypeEnum = pgEnum("lot_movement_type", [
45
+ "RECEIPT",
46
+ "SALE",
47
+ "TRANSFER",
48
+ "ADJUSTMENT",
49
+ "ADJUSTMENT_INCREASE",
50
+ "ADJUSTMENT_DECREASE",
51
+ "RETURN_FROM_CUSTOMER",
52
+ "RETURN_TO_SUPPLIER",
53
+ "QUARANTINE",
54
+ "RELEASE",
55
+ "SPLIT",
56
+ "MERGE",
57
+ "DISPOSAL",
58
+ "RECALL",
59
+ "EXPIRY",
60
+ "PRODUCTION_USE",
61
+ "SAMPLE",
62
+ "DAMAGED",
63
+ "RESERVE",
64
+ "UNRESERVE",
65
+ "CYCLE_COUNT_ADJUSTMENT",
66
+ "PO_INCOMING",
67
+ ]);
68
+
69
+ export const alertTypeEnum = pgEnum("alert_type", [
70
+ "EXPIRY_WARNING",
71
+ "EXPIRING_SOON",
72
+ "EXPIRED",
73
+ "LOW_QUANTITY",
74
+ "QUALITY_ISSUE",
75
+ "RECALL_REQUIRED",
76
+ "TEMPERATURE_BREACH",
77
+ "STORAGE_VIOLATION",
78
+ "COMPLIANCE_DUE",
79
+ ]);
80
+
81
+ export const alertSeverityEnum = pgEnum("alert_severity", [
82
+ "INFO",
83
+ "WARNING",
84
+ "CRITICAL",
85
+ ]);
86
+
87
+ export const alertStatusEnum = pgEnum("alert_status", [
88
+ "PENDING",
89
+ "ACKNOWLEDGED",
90
+ "RESOLVED",
91
+ "DISMISSED",
92
+ ]);
93
+
94
+ // =====================================================
95
+ // INVENTORY LOTS TABLE
96
+ // =====================================================
97
+
98
+ export const inventoryLots = pgTable(
99
+ "inventory_lots",
100
+ {
101
+ // Identification
102
+ id: text("id")
103
+ .primaryKey()
104
+ .$defaultFn(() => createId()),
105
+ storeId: text("store_id").notNull(),
106
+ lotNumber: varchar("lot_number", { length: 255 }).notNull(),
107
+ batchNumber: varchar("batch_number", { length: 255 }),
108
+ internalCode: varchar("internal_code", { length: 255 }),
109
+
110
+ // Product Association
111
+ productId: text("product_id").notNull(),
112
+ variantId: text("variant_id"),
113
+ sku: varchar("sku", { length: 255 }).notNull(),
114
+
115
+ // Barcode (unique identifier for scanning and tracking)
116
+ barcode: varchar("barcode", { length: 255 }),
117
+
118
+ // Location Information (REQUIRED - Lots are location-based)
119
+ locationId: text("location_id").notNull(), // Warehouse or POS location ID
120
+ locationType: varchar("location_type", { length: 20 }).notNull(), // WAREHOUSE or POS
121
+
122
+ // Supplier Information
123
+ supplierId: text("supplier_id"),
124
+ supplierLotNumber: varchar("supplier_lot_number", { length: 255 }),
125
+ supplierBatchNumber: varchar("supplier_batch_number", { length: 255 }),
126
+ supplierInvoiceNumber: varchar("supplier_invoice_number", { length: 255 }),
127
+
128
+ // Quantities
129
+ originalQuantity: integer("original_quantity").notNull(),
130
+ currentQuantity: integer("current_quantity").notNull(),
131
+ allocatedQuantity: integer("allocated_quantity").notNull().default(0),
132
+ quarantinedQuantity: integer("quarantined_quantity").notNull().default(0),
133
+ damagedQuantity: integer("damaged_quantity").notNull().default(0),
134
+ soldQuantity: integer("sold_quantity").notNull().default(0),
135
+ returnedQuantity: integer("returned_quantity").notNull().default(0),
136
+
137
+ // Enhanced Inventory Management Fields
138
+ availableQuantity: integer("available_quantity").notNull().default(0), // currentQuantity - allocatedQuantity - quarantinedQuantity - damagedQuantity
139
+ reservedQuantity: integer("reserved_quantity").notNull().default(0), // Reserved for specific orders/customers
140
+
141
+ // Stock Level Management
142
+ reorderPoint: integer("reorder_point"), // Minimum quantity before reorder alert
143
+ maxStockLevel: integer("max_stock_level"), // Maximum allowed quantity
144
+ safetyStock: integer("safety_stock").notNull().default(0), // Buffer quantity
145
+
146
+ // Unit of Measure
147
+ unitOfMeasure: varchar("unit_of_measure", { length: 50 }).notNull().default("EACH"), // EACH, CASE, PALLET, etc.
148
+
149
+ // Serial Number Tracking
150
+ trackSerial: boolean("track_serial").notNull().default(false), // Enable serial number tracking
151
+ serialNumbers: jsonb("serial_numbers").$type<Array<{
152
+ serialNumber: string;
153
+ status: "AVAILABLE" | "SOLD" | "RESERVED" | "DAMAGED" | "RETURNED";
154
+ soldTo?: string;
155
+ soldDate?: string;
156
+ reservedFor?: string;
157
+ reservedDate?: string;
158
+ }>>(), // Individual serial numbers when trackSerial=true
159
+
160
+ // Initial Inventory Flag
161
+ isInitialInventory: boolean("is_initial_inventory").notNull().default(false), // Marks lots created during product setup
162
+
163
+
164
+ // Dates
165
+ manufactureDate: date("manufacture_date"),
166
+ expiryDate: date("expiry_date"),
167
+ bestBeforeDate: date("best_before_date"),
168
+ receivedDate: timestamp("received_date", { withTimezone: true }).notNull(),
169
+ firstUseDate: timestamp("first_use_date", { withTimezone: true }),
170
+ lastMovementDate: timestamp("last_movement_date", { withTimezone: true }),
171
+
172
+ // Status
173
+ status: lotStatusEnum("status").notNull().default("PENDING"),
174
+ qcStatus: qcStatusEnum("qc_status").notNull().default("NOT_REQUIRED"),
175
+
176
+ // Quality Control (via relation to quality_controls table)
177
+ qualityControlId: text("quality_control_id"), // FK to quality_controls
178
+ lastQcDate: timestamp("last_qc_date", { withTimezone: true }),
179
+
180
+ // Traceability
181
+ parentLotId: text("parent_lot_id"),
182
+ genealogy: jsonb("genealogy").$type<{
183
+ ancestors?: Array<{
184
+ lotId: string;
185
+ lotNumber: string;
186
+ generation: number;
187
+ splitDate: string;
188
+ }>;
189
+ descendants?: Array<{
190
+ lotId: string;
191
+ lotNumber: string;
192
+ generation: number;
193
+ splitDate: string;
194
+ }>;
195
+ }>(),
196
+ splitCount: integer("split_count").notNull().default(0),
197
+
198
+ // Quality Control metadata (detailed QC in quality_controls table)
199
+ qcCertificateNumber: varchar("qc_certificate_number", { length: 255 }),
200
+ qcNotes: text("qc_notes"),
201
+
202
+ // Compliance
203
+ regulatoryCompliance: jsonb("regulatory_compliance").$type<{
204
+ fda?: { approved: boolean; number?: string };
205
+ iso?: { certified: boolean; number?: string };
206
+ gmp?: { compliant: boolean; auditDate?: string };
207
+ }>(),
208
+ certificates: jsonb("certificates").$type<Array<{
209
+ type: string;
210
+ number: string;
211
+ issuedDate: string;
212
+ expiryDate?: string;
213
+ issuer: string;
214
+ }>>(),
215
+ requiresRecall: boolean("requires_recall").notNull().default(false),
216
+ recallDate: timestamp("recall_date", { withTimezone: true }),
217
+ recallReason: text("recall_reason"),
218
+
219
+ // Storage Conditions
220
+ storageLocationId: text("storage_location_id"),
221
+ storageZone: varchar("storage_zone", { length: 255 }),
222
+ storageTempMin: decimal("storage_temp_min", { precision: 5, scale: 2 }),
223
+ storageTempMax: decimal("storage_temp_max", { precision: 5, scale: 2 }),
224
+ storageHumidityMin: decimal("storage_humidity_min", { precision: 5, scale: 2 }),
225
+ storageHumidityMax: decimal("storage_humidity_max", { precision: 5, scale: 2 }),
226
+ specialHandling: jsonb("special_handling").$type<{
227
+ refrigerated?: boolean;
228
+ frozen?: boolean;
229
+ hazardous?: boolean;
230
+ fragile?: boolean;
231
+ instructions?: string;
232
+ }>(),
233
+
234
+ // Cost & Value
235
+ unitCost: decimal("unit_cost", { precision: 15, scale: 4 }),
236
+ landedCost: decimal("landed_cost", { precision: 15, scale: 4 }),
237
+ totalCost: decimal("total_cost", { precision: 15, scale: 4 }),
238
+ currency: varchar("currency", { length: 3 }).notNull().default("USD"),
239
+
240
+ // Alerts & Notifications
241
+ expiryAlertDays: integer("expiry_alert_days").notNull().default(30),
242
+ lowQuantityAlert: integer("low_quantity_alert"),
243
+ alertRecipients: jsonb("alert_recipients").$type<Array<{
244
+ type: "email" | "sms" | "push";
245
+ address: string;
246
+ }>>(),
247
+
248
+ // Metadata
249
+ customAttributes: jsonb("custom_attributes").$type<Record<string, any>>().default({}),
250
+ tags: text("tags").array(),
251
+ notes: text("notes"),
252
+
253
+ // Timestamps
254
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
255
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
256
+ createdBy: text("created_by"),
257
+ updatedBy: text("updated_by"),
258
+
259
+ // Soft Delete
260
+ isActive: boolean("is_active").notNull().default(true),
261
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
262
+ deletedBy: text("deleted_by"),
263
+ },
264
+ (table) => ({
265
+ storeIdIndex: index("idx_lots_store_id").on(table.storeId),
266
+ lotNumberIndex: index("idx_lots_lot_number").on(table.lotNumber),
267
+ productIdIndex: index("idx_lots_product_id").on(table.productId),
268
+ skuIndex: index("idx_lots_sku").on(table.sku),
269
+ barcodeIndex: index("idx_lots_barcode").on(table.barcode),
270
+ statusIndex: index("idx_lots_status").on(table.status),
271
+ expiryDateIndex: index("idx_lots_expiry_date").on(table.expiryDate),
272
+ qualityControlIdIndex: index("idx_lots_quality_control_id").on(table.qualityControlId),
273
+ locationIndex: index("idx_lots_location").on(table.locationId, table.locationType),
274
+
275
+ // New Inventory Management Indexes
276
+ availableQuantityIndex: index("idx_lots_available_quantity").on(table.availableQuantity),
277
+ reorderPointIndex: index("idx_lots_reorder_point").on(table.reorderPoint),
278
+ isInitialInventoryIndex: index("idx_lots_is_initial_inventory").on(table.isInitialInventory),
279
+ storeProductLocationIndex: index("idx_lots_store_product_location").on(table.storeId, table.productId, table.locationId),
280
+
281
+ // Unique Constraints
282
+ uniqueLotNumber: unique("idx_lots_lot_number_unique").on(table.storeId, table.lotNumber),
283
+ uniqueBarcode: unique("idx_lots_barcode_unique").on(table.storeId, table.barcode),
284
+ }),
285
+ );
286
+
287
+ // =====================================================
288
+ // INVENTORY LOT MOVEMENTS TABLE
289
+ // =====================================================
290
+
291
+ export const inventoryLotMovements = pgTable(
292
+ "inventory_lot_movements",
293
+ {
294
+ id: text("id")
295
+ .primaryKey()
296
+ .$defaultFn(() => createId()),
297
+ lotId: text("lot_id").notNull(),
298
+ storeId: text("store_id").notNull(),
299
+
300
+ // Movement Details
301
+ movementType: lotMovementTypeEnum("movement_type").notNull(),
302
+ quantityMoved: integer("quantity_moved").notNull(),
303
+ quantityBefore: integer("quantity_before").notNull(),
304
+ quantityAfter: integer("quantity_after").notNull(),
305
+
306
+ // Source & Destination
307
+ fromLocationId: text("from_location_id"),
308
+ toLocationId: text("to_location_id"),
309
+ fromStatus: lotStatusEnum("from_status"),
310
+ toStatus: lotStatusEnum("to_status"),
311
+
312
+ // Related Transactions
313
+ orderId: text("order_id"),
314
+ transferId: text("transfer_id"),
315
+ adjustmentId: text("adjustment_id"),
316
+ returnId: text("return_id"),
317
+
318
+ // User & Context
319
+ performedBy: text("performed_by").notNull(),
320
+ reason: text("reason"),
321
+ notes: text("notes"),
322
+
323
+ // Timestamps
324
+ movementDate: timestamp("movement_date", { withTimezone: true }).defaultNow().notNull(),
325
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
326
+ },
327
+ (table) => ({
328
+ lotIdIndex: index("idx_lot_movements_lot_id").on(table.lotId),
329
+ storeIdIndex: index("idx_lot_movements_store_id").on(table.storeId),
330
+ movementDateIndex: index("idx_lot_movements_date").on(table.movementDate),
331
+ movementTypeIndex: index("idx_lot_movements_type").on(table.movementType),
332
+ }),
333
+ );
334
+
335
+ // =====================================================
336
+ // INVENTORY LOT SPLITS TABLE
337
+ // =====================================================
338
+
339
+ export const inventoryLotSplits = pgTable(
340
+ "inventory_lot_splits",
341
+ {
342
+ id: text("id")
343
+ .primaryKey()
344
+ .$defaultFn(() => createId()),
345
+ parentLotId: text("parent_lot_id").notNull(),
346
+
347
+ // Split Details
348
+ splitDate: timestamp("split_date", { withTimezone: true }).notNull(),
349
+ splitReason: text("split_reason"),
350
+ originalQuantity: integer("original_quantity").notNull(),
351
+
352
+ // Child Lots
353
+ childLots: jsonb("child_lots").$type<Array<{
354
+ lotId: string;
355
+ lotNumber: string;
356
+ quantity: number;
357
+ }>>().notNull(),
358
+
359
+ // User
360
+ performedBy: text("performed_by").notNull(),
361
+ notes: text("notes"),
362
+
363
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
364
+ },
365
+ (table) => ({
366
+ parentLotIdIndex: index("idx_lot_splits_parent_lot_id").on(table.parentLotId),
367
+ splitDateIndex: index("idx_lot_splits_date").on(table.splitDate),
368
+ }),
369
+ );
370
+
371
+ // =====================================================
372
+ // INVENTORY LOT MERGES TABLE
373
+ // =====================================================
374
+
375
+ export const inventoryLotMerges = pgTable(
376
+ "inventory_lot_merges",
377
+ {
378
+ id: text("id")
379
+ .primaryKey()
380
+ .$defaultFn(() => createId()),
381
+ targetLotId: text("target_lot_id").notNull(),
382
+
383
+ // Merge Details
384
+ mergeDate: timestamp("merge_date", { withTimezone: true }).notNull(),
385
+ mergeReason: text("merge_reason"),
386
+
387
+ // Source Lots
388
+ sourceLots: jsonb("source_lots").$type<Array<{
389
+ lotId: string;
390
+ lotNumber: string;
391
+ quantity: number;
392
+ }>>().notNull(),
393
+ totalQuantity: integer("total_quantity").notNull(),
394
+
395
+ // Validation
396
+ compatibilityCheck: jsonb("compatibility_check").$type<{
397
+ compatible: boolean;
398
+ checks: Array<{
399
+ criterion: string;
400
+ passed: boolean;
401
+ details: string;
402
+ }>;
403
+ }>(),
404
+ approvedBy: text("approved_by"),
405
+
406
+ // User
407
+ performedBy: text("performed_by").notNull(),
408
+ notes: text("notes"),
409
+
410
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
411
+ },
412
+ (table) => ({
413
+ targetLotIdIndex: index("idx_lot_merges_target_lot_id").on(table.targetLotId),
414
+ mergeDateIndex: index("idx_lot_merges_date").on(table.mergeDate),
415
+ }),
416
+ );
417
+
418
+ // =====================================================
419
+ // INVENTORY LOT ALERTS TABLE
420
+ // =====================================================
421
+
422
+ export const inventoryLotAlerts = pgTable(
423
+ "inventory_lot_alerts",
424
+ {
425
+ id: text("id")
426
+ .primaryKey()
427
+ .$defaultFn(() => createId()),
428
+ lotId: text("lot_id").notNull(),
429
+ storeId: text("store_id").notNull(),
430
+
431
+ // Alert Details
432
+ alertType: alertTypeEnum("alert_type").notNull(),
433
+ severity: alertSeverityEnum("severity").notNull(),
434
+ message: text("message").notNull(),
435
+
436
+ // Status
437
+ status: alertStatusEnum("status").notNull().default("PENDING"),
438
+ acknowledged: boolean("acknowledged").notNull().default(false),
439
+ acknowledgedBy: text("acknowledged_by"),
440
+ acknowledgedAt: timestamp("acknowledged_at", { withTimezone: true }),
441
+
442
+ // Resolution
443
+ resolved: boolean("resolved").notNull().default(false),
444
+ resolvedBy: text("resolved_by"),
445
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
446
+ resolutionNotes: text("resolution_notes"),
447
+
448
+ // Notification
449
+ notificationSent: boolean("notification_sent").notNull().default(false),
450
+ notificationChannels: jsonb("notification_channels").$type<Array<"email" | "sms" | "push">>(),
451
+ recipients: jsonb("recipients").$type<Array<string>>(),
452
+
453
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
454
+ },
455
+ (table) => ({
456
+ lotIdIndex: index("idx_lot_alerts_lot_id").on(table.lotId),
457
+ storeIdIndex: index("idx_lot_alerts_store_id").on(table.storeId),
458
+ statusIndex: index("idx_lot_alerts_status").on(table.status),
459
+ alertTypeIndex: index("idx_lot_alerts_type").on(table.alertType),
460
+ severityIndex: index("idx_lot_alerts_severity").on(table.severity),
461
+ }),
462
+ );
463
+
464
+ // =====================================================
465
+ // ZOD VALIDATION SCHEMAS
466
+ // =====================================================
467
+
468
+ import { z } from "zod";
469
+
470
+ // Base Lot Schema
471
+ export const LotSchema = z.object({
472
+ id: z.string().cuid2(),
473
+ storeId: z.string(),
474
+ lotNumber: z.string(),
475
+ batchNumber: z.string().optional(),
476
+ internalCode: z.string().optional(),
477
+
478
+ // Product Association
479
+ productId: z.string(),
480
+ variantId: z.string().optional(),
481
+ sku: z.string(),
482
+
483
+ // Location
484
+ locationId: z.string(),
485
+ locationType: z.enum(["WAREHOUSE", "POS"]),
486
+
487
+ // Supplier
488
+ supplierId: z.string().optional(),
489
+ supplierLotNumber: z.string().optional(),
490
+ supplierBatchNumber: z.string().optional(),
491
+ supplierInvoiceNumber: z.string().optional(),
492
+
493
+ // Quantities
494
+ originalQuantity: z.number().int().nonnegative(),
495
+ currentQuantity: z.number().int().nonnegative(),
496
+ availableQuantity: z.number().int().nonnegative(),
497
+ allocatedQuantity: z.number().int().nonnegative().default(0),
498
+ reservedQuantity: z.number().int().nonnegative().default(0),
499
+ quarantinedQuantity: z.number().int().nonnegative().default(0),
500
+ damagedQuantity: z.number().int().nonnegative().default(0),
501
+ soldQuantity: z.number().int().nonnegative().default(0),
502
+ returnedQuantity: z.number().int().nonnegative().default(0),
503
+
504
+ // Stock Management
505
+ reorderPoint: z.number().int().nonnegative().optional(),
506
+ maxStockLevel: z.number().int().nonnegative().optional(),
507
+ safetyStock: z.number().int().nonnegative().default(0),
508
+ unitOfMeasure: z.string().default("EACH"),
509
+
510
+ // Serial Tracking
511
+ trackSerial: z.boolean().default(false),
512
+ serialNumbers: z.array(z.object({
513
+ serialNumber: z.string(),
514
+ status: z.enum(["AVAILABLE", "SOLD", "RESERVED", "DAMAGED", "RETURNED"]),
515
+ soldTo: z.string().optional(),
516
+ soldDate: z.string().optional(),
517
+ reservedFor: z.string().optional(),
518
+ reservedDate: z.string().optional(),
519
+ })).optional(),
520
+
521
+ // Flags
522
+ isInitialInventory: z.boolean().default(false),
523
+
524
+ // Dates
525
+ manufactureDate: z.coerce.date().optional(),
526
+ expiryDate: z.coerce.date().optional(),
527
+ bestBeforeDate: z.coerce.date().optional(),
528
+ receivedDate: z.coerce.date(),
529
+ firstUseDate: z.coerce.date().optional(),
530
+ lastMovementDate: z.coerce.date().optional(),
531
+
532
+ // Status
533
+ status: z.enum(["PENDING", "ACTIVE", "QUARANTINED", "ALLOCATED", "EXPIRED", "RECALLED", "DEPLETED", "RETURNED", "DISPOSED"]),
534
+
535
+ // Quality Control
536
+ qualityControlId: z.string().optional(),
537
+ qcCertificateNumber: z.string().optional(),
538
+ qcNotes: z.string().optional(),
539
+
540
+ // Cost
541
+ unitCost: z.number().nonnegative().optional(),
542
+ landedCost: z.number().nonnegative().optional(),
543
+ totalCost: z.number().nonnegative().optional(),
544
+ currency: z.string().length(3).default("USD"),
545
+
546
+ // Metadata
547
+ notes: z.string().optional(),
548
+ tags: z.array(z.string()).optional(),
549
+ customAttributes: z.record(z.any()).optional(),
550
+
551
+ // Timestamps
552
+ createdAt: z.coerce.date(),
553
+ updatedAt: z.coerce.date(),
554
+ createdBy: z.string().optional(),
555
+ updatedBy: z.string().optional(),
556
+ isActive: z.boolean().default(true),
557
+ });
558
+
559
+ // Initialize Product Inventory Schema (for product_svc integration)
560
+ export const InitializeProductInventorySchema = z.object({
561
+ productId: z.string().cuid2(),
562
+ variantId: z.string().cuid2().optional(),
563
+ sku: z.string().optional(), // Auto-generated if not provided
564
+ locations: z.array(z.object({
565
+ locationId: z.string(),
566
+ locationType: z.enum(["WAREHOUSE", "POS"]),
567
+ initialQuantity: z.number().int().nonnegative(),
568
+
569
+ // Barcode for lot tracking
570
+ barcode: z.string().optional(),
571
+
572
+ // Stock Management
573
+ reorderPoint: z.number().int().nonnegative().optional(),
574
+ maxStockLevel: z.number().int().nonnegative().optional(),
575
+ safetyStock: z.number().int().nonnegative().default(0),
576
+ unitOfMeasure: z.string().default("EACH"),
577
+
578
+ // Cost
579
+ unitCost: z.number().nonnegative().optional(),
580
+ landedCost: z.number().nonnegative().optional(),
581
+
582
+ // Dates
583
+ expiryDate: z.coerce.date().optional(),
584
+ manufactureDate: z.coerce.date().optional(),
585
+
586
+ // Supplier
587
+ supplierId: z.string().optional(),
588
+ supplierLotNumber: z.string().optional(),
589
+
590
+ // Serial Tracking
591
+ trackSerial: z.boolean().default(false),
592
+ serialNumbers: z.array(z.string()).optional(), // List of serial numbers to assign
593
+
594
+ // Metadata
595
+ notes: z.string().optional(),
596
+ tags: z.array(z.string()).optional(),
597
+ })).min(1),
598
+ });
599
+
600
+ // Create Lot Schema
601
+ export const CreateLotSchema = z.object({
602
+ storeId: z.string(),
603
+ lotNumber: z.string().optional(), // Auto-generated if not provided
604
+ batchNumber: z.string().optional(),
605
+
606
+ // Product
607
+ productId: z.string(),
608
+ variantId: z.string().optional(),
609
+ sku: z.string(),
610
+ barcode: z.string().optional(),
611
+
612
+ // Location
613
+ locationId: z.string(),
614
+ locationType: z.enum(["WAREHOUSE", "POS"]),
615
+
616
+ // Supplier
617
+ supplierId: z.string().optional(),
618
+ supplierLotNumber: z.string().optional(),
619
+ supplierBatchNumber: z.string().optional(),
620
+ supplierInvoiceNumber: z.string().optional(),
621
+
622
+ // Quantities
623
+ originalQuantity: z.number().int().positive(),
624
+ reorderPoint: z.number().int().nonnegative().optional(),
625
+ maxStockLevel: z.number().int().nonnegative().optional(),
626
+ safetyStock: z.number().int().nonnegative().default(0),
627
+ unitOfMeasure: z.string().default("EACH"),
628
+
629
+ // Dates
630
+ expiryDate: z.coerce.date().optional(),
631
+ manufactureDate: z.coerce.date().optional(),
632
+ receivedDate: z.coerce.date().default(() => new Date()),
633
+
634
+ // Cost
635
+ unitCost: z.number().nonnegative().optional(),
636
+ landedCost: z.number().nonnegative().optional(),
637
+
638
+ // Storage
639
+ storageLocationId: z.string().optional(),
640
+ storageZone: z.string().optional(),
641
+ storageTempMin: z.number().optional(),
642
+ storageTempMax: z.number().optional(),
643
+
644
+ // Serial Tracking
645
+ trackSerial: z.boolean().default(false),
646
+ serialNumbers: z.array(z.string()).optional(),
647
+
648
+ // Flags
649
+ isInitialInventory: z.boolean().default(false),
650
+
651
+ // Metadata
652
+ notes: z.string().optional(),
653
+ tags: z.array(z.string()).optional(),
654
+ customAttributes: z.record(z.any()).optional(),
655
+
656
+ // User
657
+ createdBy: z.string(),
658
+ });
659
+
660
+ // Reserve Lot Quantity Schema
661
+ export const ReserveLotQuantitySchema = z.object({
662
+ lotId: z.string().cuid2(),
663
+ quantity: z.number().int().positive(),
664
+ orderId: z.string().optional(),
665
+ customerId: z.string().optional(),
666
+ reason: z.string(),
667
+ expiresAt: z.coerce.date().optional(), // Reservation expiry
668
+ notes: z.string().optional(),
669
+ performedBy: z.string(),
670
+ });
671
+
672
+ // Release Lot Reservation Schema
673
+ export const ReleaseLotReservationSchema = z.object({
674
+ lotId: z.string().cuid2(),
675
+ quantity: z.number().int().positive(),
676
+ reservationId: z.string().optional(),
677
+ reason: z.string(),
678
+ performedBy: z.string(),
679
+ });
680
+
681
+ // Adjust Lot Quantity Schema
682
+ export const AdjustLotQuantitySchema = z.object({
683
+ lotId: z.string().cuid2(),
684
+ quantityChange: z.number().int(), // Can be positive or negative
685
+ adjustmentType: z.enum([
686
+ "CYCLE_COUNT",
687
+ "DAMAGE",
688
+ "FOUND",
689
+ "LOST",
690
+ "CORRECTION",
691
+ "THEFT",
692
+ "WASTE",
693
+ "SAMPLE",
694
+ "EXPIRED",
695
+ "RETURNED",
696
+ ]),
697
+ reason: z.string().min(1),
698
+ notes: z.string().optional(),
699
+ performedBy: z.string(),
700
+ });
701
+
702
+ // Transfer Lot Schema
703
+ export const TransferLotSchema = z.object({
704
+ lotId: z.string().cuid2(),
705
+ toLocationId: z.string(),
706
+ toLocationType: z.enum(["WAREHOUSE", "POS"]),
707
+ quantity: z.number().int().positive().optional(), // If not provided, transfer entire lot
708
+ splitIfPartial: z.boolean().default(true), // Create new lot for partial transfer
709
+ reason: z.string().min(1),
710
+ notes: z.string().optional(),
711
+ expectedArrivalDate: z.coerce.date().optional(),
712
+ performedBy: z.string(),
713
+ });
714
+
715
+ // Merge Lots Schema
716
+ export const MergeLotsSchema = z.object({
717
+ targetLotId: z.string().cuid2(),
718
+ sourceLotIds: z.array(z.string().cuid2()).min(1),
719
+ reason: z.string().min(1),
720
+ performedBy: z.string(),
721
+ approvedBy: z.string().optional(),
722
+ notes: z.string().optional(),
723
+ });
724
+
725
+ // Split Lot Schema
726
+ export const SplitLotSchema = z.object({
727
+ parentLotId: z.string().cuid2(),
728
+ splits: z.array(z.object({
729
+ quantity: z.number().int().positive(),
730
+ locationId: z.string().optional(), // If different from parent
731
+ locationType: z.enum(["WAREHOUSE", "POS"]).optional(),
732
+ notes: z.string().optional(),
733
+ })).min(1),
734
+ reason: z.string().min(1),
735
+ performedBy: z.string(),
736
+ });
737
+
738
+ // Get Lots Query Schema
739
+ export const GetLotsQuerySchema = z.object({
740
+ storeId: z.string(),
741
+ productId: z.string().optional(),
742
+ variantId: z.string().optional(),
743
+ sku: z.string().optional(),
744
+ status: z.enum(["PENDING", "ACTIVE", "QUARANTINED", "ALLOCATED", "EXPIRED", "RECALLED", "DEPLETED", "RETURNED", "DISPOSED"]).optional(),
745
+ locationId: z.string().optional(),
746
+ locationType: z.enum(["WAREHOUSE", "POS"]).optional(),
747
+ qualityControlId: z.string().optional(),
748
+ expiringBefore: z.coerce.date().optional(),
749
+ isInitialInventory: z.boolean().optional(),
750
+ lowStock: z.boolean().optional(), // currentQuantity < reorderPoint
751
+ page: z.coerce.number().int().positive().default(1),
752
+ limit: z.coerce.number().int().positive().max(100).default(20),
753
+ sortBy: z.enum(["lotNumber", "receivedDate", "expiryDate", "currentQuantity", "availableQuantity", "status"]).default("receivedDate"),
754
+ sortOrder: z.enum(["asc", "desc"]).default("desc"),
755
+ });
756
+
757
+ // Get Available Lots for Order (FIFO) Schema
758
+ export const GetAvailableLotsSchema = z.object({
759
+ storeId: z.string(),
760
+ productId: z.string(),
761
+ variantId: z.string().optional(),
762
+ quantity: z.number().int().positive(),
763
+ locationId: z.string().optional(),
764
+ selectionMethod: z.enum(["FIFO", "FEFO", "LIFO"]).default("FIFO"),
765
+ });
766
+
767
+ // Lot Analytics Response Schema
768
+ export const LotAnalyticsSchema = z.object({
769
+ productId: z.string(),
770
+ totalLots: z.number().int(),
771
+ totalQuantity: z.number().int(),
772
+ availableQuantity: z.number().int(),
773
+ allocatedQuantity: z.number().int(),
774
+ reservedQuantity: z.number().int(),
775
+ quarantinedQuantity: z.number().int(),
776
+ damagedQuantity: z.number().int(),
777
+ byLocation: z.array(z.object({
778
+ locationId: z.string(),
779
+ locationType: z.enum(["WAREHOUSE", "POS"]),
780
+ lotCount: z.number().int(),
781
+ totalQuantity: z.number().int(),
782
+ availableQuantity: z.number().int(),
783
+ })),
784
+ byStatus: z.record(z.string(), z.number().int()),
785
+ expiringLots: z.array(z.object({
786
+ lotId: z.string(),
787
+ lotNumber: z.string(),
788
+ expiryDate: z.coerce.date(),
789
+ daysUntilExpiry: z.number().int(),
790
+ currentQuantity: z.number().int(),
791
+ })),
792
+ lowStockLots: z.array(z.object({
793
+ lotId: z.string(),
794
+ lotNumber: z.string(),
795
+ currentQuantity: z.number().int(),
796
+ reorderPoint: z.number().int(),
797
+ shortfall: z.number().int(),
798
+ })),
799
+ });