@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,955 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+ import {
3
+ boolean,
4
+ decimal,
5
+ index,
6
+ integer,
7
+ jsonb,
8
+ pgEnum,
9
+ pgTable,
10
+ text,
11
+ timestamp,
12
+ unique,
13
+ varchar,
14
+ } from "drizzle-orm/pg-core";
15
+
16
+ // =====================================================
17
+ // ENUMS
18
+ // =====================================================
19
+
20
+ export const productStatusEnum = pgEnum("product_status", [
21
+ "DRAFT",
22
+ "ACTIVE",
23
+ "INACTIVE",
24
+ "ARCHIVED",
25
+ "OUT_OF_STOCK",
26
+ ]);
27
+
28
+ export const productTypeEnum = pgEnum("product_type", [
29
+ "PHYSICAL",
30
+ "DIGITAL",
31
+ "SERVICE",
32
+ "SUBSCRIPTION",
33
+ "BUNDLE",
34
+ "WHOLESALE",
35
+ ]);
36
+
37
+ export const inventoryPolicyEnum = pgEnum("inventory_policy", [
38
+ "TRACK", // Track inventory
39
+ "CONTINUE", // Continue selling when out of stock
40
+ "DENY", // Stop selling when out of stock
41
+ ]);
42
+
43
+ export const fulfillmentServiceEnum = pgEnum("fulfillment_service", [
44
+ "MANUAL",
45
+ "AUTOMATED",
46
+ "THIRD_PARTY",
47
+ "DROPSHIPPING",
48
+ ]);
49
+
50
+ export const collectionTypeEnum = pgEnum("collection_type", [
51
+ "MANUAL", // Manually curated
52
+ "AUTOMATIC", // Rule-based automatic
53
+ "SMART", // AI-powered smart collections
54
+ ]);
55
+
56
+ export const discountTypeEnum = pgEnum("discount_type", [
57
+ "PERCENTAGE",
58
+ "FIXED_AMOUNT",
59
+ "FREE_SHIPPING",
60
+ "BOGO", // Buy one get one
61
+ ]);
62
+
63
+ export const variantOptionTypeEnum = pgEnum("variant_option_type", [
64
+ "COLOR",
65
+ "SIZE",
66
+ "MATERIAL",
67
+ "STYLE",
68
+ "WEIGHT",
69
+ "VOLUME",
70
+ "CUSTOM",
71
+ ]);
72
+
73
+ export const salesChannelEnum = pgEnum("sales_channel", [
74
+ "POS",
75
+ "MARKETPLACE",
76
+ "STOREFRONT",
77
+ ]);
78
+
79
+ // =====================================================
80
+ // CORE PRODUCT TABLE
81
+ // =====================================================
82
+
83
+ export const products = pgTable(
84
+ "products",
85
+ {
86
+ id: text("id")
87
+ .primaryKey()
88
+ .$defaultFn(() => createId()),
89
+ storeId: text("store_id").notNull(),
90
+
91
+ // Basic Information
92
+ title: varchar("title", { length: 255 }).notNull(),
93
+ description: text("description"),
94
+ shortDescription: varchar("short_description", { length: 500 }),
95
+ handle: varchar("handle", { length: 255 }).notNull(), // URL-friendly slug
96
+
97
+ // Product Classification
98
+ productType: productTypeEnum("product_type").notNull().default("PHYSICAL"),
99
+ vendor: varchar("vendor", { length: 255 }),
100
+ supplierId: text("supplier_id"),
101
+ category: varchar("category", { length: 255 }),
102
+ subcategory: varchar("subcategory", { length: 255 }),
103
+ tags: jsonb("tags").$type<string[]>().default([]),
104
+
105
+ // Product Specifics
106
+ features: jsonb("features").$type<string[]>().default([]),
107
+ specifications: jsonb("specifications")
108
+ .$type<Array<{ name: string; value: string }>>()
109
+ .default([]),
110
+
111
+ // Additional Product Fields
112
+ unitOfMeasure: varchar("unit_of_measure", { length: 50 }), // e.g., "piece", "kg", "liter", "box"
113
+ countryOfOrigin: varchar("country_of_origin", { length: 100 }), // Country where product is manufactured
114
+ hsCode: varchar("hs_code", { length: 20 }), // Harmonized System code for customs
115
+ returnable: boolean("returnable").notNull().default(true), // Whether product can be returned
116
+ weightUnit: varchar("weight_unit", { length: 10 }).default("kg"), // kg, lb, oz, g
117
+
118
+ // Status and Visibility
119
+ status: productStatusEnum("status").notNull().default("DRAFT"),
120
+ isPublished: boolean("is_published").notNull().default(false),
121
+ publishedAt: timestamp("published_at", { withTimezone: true }),
122
+ isFeatured: boolean("is_featured").notNull().default(false),
123
+
124
+ // Sales Channels - Control where product is available
125
+ salesChannels: jsonb("sales_channels")
126
+ .$type<("POS" | "MARKETPLACE" | "STOREFRONT")[]>()
127
+ .notNull()
128
+ .default(["POS", "MARKETPLACE", "STOREFRONT"]),
129
+
130
+ // Advanced Sales Channel Configuration
131
+ salesChannelConfig: jsonb("sales_channel_config")
132
+ .$type<{
133
+ POS?: {
134
+ enabled: boolean;
135
+ pricing?: { type: "default" | "custom"; customPrice?: number };
136
+ availability?: { allLocations: boolean; specificLocations?: string[] };
137
+ };
138
+ MARKETPLACE?: {
139
+ enabled: boolean;
140
+ platforms?: string[]; // e.g., ["amazon", "ebay", "etsy"]
141
+ pricing?: { type: "default" | "custom"; customPrice?: number };
142
+ syncInventory?: boolean;
143
+ };
144
+ STOREFRONT?: {
145
+ enabled: boolean;
146
+ featured?: boolean;
147
+ pricing?: { type: "default" | "custom"; customPrice?: number };
148
+ visibility?: "public" | "private" | "members-only";
149
+ };
150
+ }>()
151
+ .default({}),
152
+
153
+ // Pricing (in cents for precision)
154
+ basePrice: decimal("base_price", { precision: 12, scale: 2 }).notNull(),
155
+ compareAtPrice: decimal("compare_at_price", { precision: 12, scale: 2 }),
156
+ costPerItem: decimal("cost_per_item", { precision: 12, scale: 2 }),
157
+
158
+ // Wholesale Pricing
159
+ wholesalePrice: decimal("wholesale_price", { precision: 12, scale: 2 }),
160
+ minWholesaleQuantity: integer("min_wholesale_quantity").default(1),
161
+ maxWholesaleQuantity: integer("max_wholesale_quantity"),
162
+ wholesaleEnabled: boolean("wholesale_enabled").notNull().default(false),
163
+
164
+ // Inventory Management
165
+ inventoryPolicy: inventoryPolicyEnum("inventory_policy")
166
+ .notNull()
167
+ .default("TRACK"),
168
+ inventoryQuantity: integer("inventory_quantity").notNull().default(0),
169
+ inventoryTrackingEnabled: boolean("inventory_tracking_enabled")
170
+ .notNull()
171
+ .default(true),
172
+ trackStock: boolean("track_stock").notNull().default(false),
173
+ stockLocationId: text("stock_location_id"), // POS location or warehouse ID
174
+ stockLocationType: varchar("stock_location_type", { length: 20 }), // "POS" or "WAREHOUSE"
175
+ lowStockThreshold: integer("low_stock_threshold").default(10),
176
+ allowBackorders: boolean("allow_backorders").notNull().default(false),
177
+
178
+ // Advanced Inventory Tracking Features
179
+ autoReorder: boolean("auto_reorder").notNull().default(false), // Automatic reorder when stock is low
180
+ batchTracking: boolean("batch_tracking").notNull().default(false), // Track products by batch/lot number
181
+ serialTracking: boolean("serial_tracking").notNull().default(false), // Track individual serial numbers
182
+ expiryTracking: boolean("expiry_tracking").notNull().default(false), // Track expiry dates
183
+
184
+ // Physical Properties
185
+ weight: decimal("weight", { precision: 8, scale: 3 }), // in kg
186
+ dimensions: jsonb("dimensions").$type<{
187
+ length?: number;
188
+ width?: number;
189
+ height?: number;
190
+ unit?: "cm" | "in";
191
+ }>(),
192
+
193
+ // Shipping and Fulfillment
194
+ requiresShipping: boolean("requires_shipping").notNull().default(true),
195
+ fulfillmentService: fulfillmentServiceEnum("fulfillment_service")
196
+ .notNull()
197
+ .default("MANUAL"),
198
+ shippingClass: varchar("shipping_class", { length: 100 }),
199
+
200
+ // SEO and Marketing
201
+ seoTitle: varchar("seo_title", { length: 255 }),
202
+ seoDescription: varchar("seo_description", { length: 500 }),
203
+ seoKeywords: jsonb("seo_keywords").$type<string[]>().default([]),
204
+ metaFields: jsonb("meta_fields")
205
+ .$type<Record<string, unknown>>()
206
+ .default({}),
207
+
208
+ // Advanced Features
209
+ customFields: jsonb("custom_fields")
210
+ .$type<Record<string, unknown>>()
211
+ .default({}),
212
+ variantOptions: jsonb("variant_options")
213
+ .$type<
214
+ Array<{
215
+ name: string;
216
+ type: string;
217
+ values: string[];
218
+ displayType: "dropdown" | "color" | "button" | "swatch";
219
+ }>
220
+ >()
221
+ .default([]),
222
+
223
+ // Analytics and Performance
224
+ totalViews: integer("total_views").notNull().default(0),
225
+ totalSales: integer("total_sales").notNull().default(0),
226
+ totalRevenue: decimal("total_revenue", { precision: 12, scale: 2 })
227
+ .notNull()
228
+ .default("0"),
229
+ conversionRate: decimal("conversion_rate", { precision: 5, scale: 4 }),
230
+ averageRating: decimal("average_rating", { precision: 3, scale: 2 }),
231
+ reviewCount: integer("review_count").notNull().default(0),
232
+
233
+ // Timestamps
234
+ createdAt: timestamp("created_at", { withTimezone: true })
235
+ .defaultNow()
236
+ .notNull(),
237
+ updatedAt: timestamp("updated_at", { withTimezone: true })
238
+ .defaultNow()
239
+ .notNull(),
240
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
241
+ },
242
+ (table) => ({
243
+ // Performance indexes
244
+ storeIdIndex: index("idx_products_store_id").on(table.storeId),
245
+ handleUniquePerStore: unique("idx_products_handle_store_unique").on(
246
+ table.storeId,
247
+ table.handle,
248
+ ),
249
+ statusIndex: index("idx_products_status").on(table.status),
250
+ publishedIndex: index("idx_products_published").on(
251
+ table.isPublished,
252
+ table.publishedAt,
253
+ ),
254
+ featuredIndex: index("idx_products_featured").on(table.isFeatured),
255
+ categoryIndex: index("idx_products_category").on(table.category),
256
+ vendorIndex: index("idx_products_vendor").on(table.vendor),
257
+
258
+ // Analytics indexes
259
+ salesIndex: index("idx_products_sales").on(table.totalSales),
260
+ revenueIndex: index("idx_products_revenue").on(table.totalRevenue),
261
+ ratingIndex: index("idx_products_rating").on(table.averageRating),
262
+
263
+ // Search indexes
264
+ titleIndex: index("idx_products_title").on(table.title),
265
+ fullTextSearch: index("idx_products_search").on(
266
+ table.title,
267
+ table.description,
268
+ table.tags,
269
+ ),
270
+
271
+ // Inventory indexes
272
+ inventoryIndex: index("idx_products_inventory").on(table.inventoryQuantity),
273
+ lowStockIndex: index("idx_products_low_stock").on(
274
+ table.inventoryQuantity,
275
+ table.lowStockThreshold,
276
+ ),
277
+ trackStockIndex: index("idx_products_track_stock").on(table.trackStock),
278
+ stockLocationIndex: index("idx_products_stock_location").on(
279
+ table.stockLocationId,
280
+ table.stockLocationType,
281
+ ),
282
+
283
+ // Sales channel index
284
+ salesChannelsIndex: index("idx_products_sales_channels").on(
285
+ table.salesChannels,
286
+ ),
287
+ }),
288
+ );
289
+
290
+ // =====================================================
291
+ // PRODUCT VARIANTS TABLE
292
+ // =====================================================
293
+
294
+ export const productVariants = pgTable(
295
+ "product_variants",
296
+ {
297
+ id: text("id")
298
+ .primaryKey()
299
+ .$defaultFn(() => createId()),
300
+ productId: text("product_id")
301
+ .notNull()
302
+ .references(() => products.id, { onDelete: "cascade" }),
303
+ storeId: text("store_id").notNull(),
304
+
305
+ // Variant Identification
306
+ title: varchar("title", { length: 255 }).notNull(),
307
+ compareAtPrice: decimal("compare_at_price", { precision: 12, scale: 2 }),
308
+
309
+ // Pricing (in cents)
310
+ price: decimal("price", { precision: 12, scale: 2 }).notNull(),
311
+ costPerItem: decimal("cost_per_item", { precision: 12, scale: 2 }),
312
+
313
+ // Inventory
314
+ inventoryQuantity: integer("inventory_quantity").notNull().default(0),
315
+ inventoryItemId: text("inventory_item_id"), // Reference to inventory system
316
+ fulfillmentService: fulfillmentServiceEnum("fulfillment_service")
317
+ .notNull()
318
+ .default("MANUAL"),
319
+
320
+ // Physical Properties
321
+ weight: decimal("weight", { precision: 8, scale: 3 }),
322
+ dimensions: jsonb("dimensions").$type<{
323
+ length?: number;
324
+ width?: number;
325
+ height?: number;
326
+ unit?: "cm" | "in";
327
+ }>(),
328
+
329
+ // Variant Options (e.g., color: red, size: large)
330
+ option1: varchar("option1", { length: 255 }),
331
+ option2: varchar("option2", { length: 255 }),
332
+ option3: varchar("option3", { length: 255 }),
333
+ optionValues: jsonb("option_values")
334
+ .$type<Record<string, string>>()
335
+ .default({}),
336
+
337
+ // Status and Availability
338
+ availableForSale: boolean("available_for_sale").notNull().default(true),
339
+ requiresShipping: boolean("requires_shipping").notNull().default(true),
340
+
341
+ // Sales Channels - Control where variant is available (inherits from product if not set)
342
+ salesChannels: jsonb("sales_channels")
343
+ .$type<("POS" | "MARKETPLACE" | "STOREFRONT")[]>()
344
+ .default(["POS", "MARKETPLACE", "STOREFRONT"]),
345
+
346
+
347
+ // Media
348
+ imageId: text("image_id"), // Primary image for this variant
349
+ imageUrl: varchar("image_url", { length: 500 }),
350
+ imageAltText: varchar("image_alt_text", { length: 255 }),
351
+
352
+ // Advanced Features
353
+ customFields: jsonb("custom_fields")
354
+ .$type<Record<string, unknown>>()
355
+ .default({}),
356
+ taxCode: varchar("tax_code", { length: 50 }),
357
+
358
+ // Analytics
359
+ totalSales: integer("total_sales").notNull().default(0),
360
+ totalRevenue: decimal("total_revenue", { precision: 12, scale: 2 })
361
+ .notNull()
362
+ .default("0"),
363
+
364
+ // Position for ordering
365
+ position: integer("position").notNull().default(0),
366
+
367
+ // Timestamps
368
+ createdAt: timestamp("created_at", { withTimezone: true })
369
+ .defaultNow()
370
+ .notNull(),
371
+ updatedAt: timestamp("updated_at", { withTimezone: true })
372
+ .defaultNow()
373
+ .notNull(),
374
+ },
375
+ (table) => ({
376
+ // Performance indexes
377
+ productIdIndex: index("idx_variants_product_id").on(table.productId),
378
+ storeIdIndex: index("idx_variants_store_id").on(table.storeId),
379
+ priceIndex: index("idx_variants_price").on(table.price),
380
+ inventoryIndex: index("idx_variants_inventory").on(table.inventoryQuantity),
381
+ availableIndex: index("idx_variants_available").on(table.availableForSale),
382
+ positionIndex: index("idx_variants_position").on(
383
+ table.productId,
384
+ table.position,
385
+ ),
386
+
387
+ // Option indexes for filtering
388
+ option1Index: index("idx_variants_option1").on(table.option1),
389
+ option2Index: index("idx_variants_option2").on(table.option2),
390
+ option3Index: index("idx_variants_option3").on(table.option3),
391
+
392
+ // Sales channel index
393
+ salesChannelsIndex: index("idx_variants_sales_channels").on(
394
+ table.salesChannels,
395
+ ),
396
+ }),
397
+ );
398
+
399
+ // =====================================================
400
+ // COLLECTIONS TABLE
401
+ // =====================================================
402
+
403
+ export const collections = pgTable(
404
+ "collections",
405
+ {
406
+ id: text("id")
407
+ .primaryKey()
408
+ .$defaultFn(() => createId()),
409
+ storeId: text("store_id").notNull(),
410
+
411
+ // Basic Information
412
+ title: varchar("title", { length: 255 }).notNull(),
413
+ description: text("description"),
414
+ handle: varchar("handle", { length: 255 }).notNull(),
415
+
416
+ // Collection Type and Rules
417
+ collectionType: collectionTypeEnum("collection_type")
418
+ .notNull()
419
+ .default("MANUAL"),
420
+ rules: jsonb("rules")
421
+ .$type<
422
+ Array<{
423
+ field: string;
424
+ relation:
425
+ | "equals"
426
+ | "not_equals"
427
+ | "contains"
428
+ | "starts_with"
429
+ | "ends_with"
430
+ | "greater_than"
431
+ | "less_than";
432
+ condition: string;
433
+ }>
434
+ >()
435
+ .default([]),
436
+
437
+
438
+ // SEO and Marketing
439
+ seoTitle: varchar("seo_title", { length: 255 }),
440
+ seoDescription: varchar("seo_description", { length: 500 }),
441
+ seoKeywords: jsonb("seo_keywords").$type<string[]>().default([]),
442
+
443
+ // Media
444
+ imageUrl: varchar("image_url", { length: 500 }),
445
+ imageAltText: varchar("image_alt_text", { length: 255 }),
446
+
447
+ // Display and Sorting
448
+ sortOrder: varchar("sort_order", { length: 50 }).default("manual"), // manual, best-selling, created, price-asc, price-desc
449
+ isPublished: boolean("is_published").notNull().default(false),
450
+ publishedAt: timestamp("published_at", { withTimezone: true }),
451
+
452
+ // Advanced Features
453
+ customFields: jsonb("custom_fields")
454
+ .$type<Record<string, unknown>>()
455
+ .default({}),
456
+ templateSuffix: varchar("template_suffix", { length: 100 }),
457
+
458
+ // Analytics
459
+ totalProducts: integer("total_products").notNull().default(0),
460
+ totalViews: integer("total_views").notNull().default(0),
461
+
462
+ // Timestamps
463
+ createdAt: timestamp("created_at", { withTimezone: true })
464
+ .defaultNow()
465
+ .notNull(),
466
+ updatedAt: timestamp("updated_at", { withTimezone: true })
467
+ .defaultNow()
468
+ .notNull(),
469
+ },
470
+ (table) => ({
471
+ // Performance indexes
472
+ storeIdIndex: index("idx_collections_store_id").on(table.storeId),
473
+ handleUniquePerStore: unique("idx_collections_handle_store_unique").on(
474
+ table.storeId,
475
+ table.handle,
476
+ ),
477
+ typeIndex: index("idx_collections_type").on(table.collectionType),
478
+ publishedIndex: index("idx_collections_published").on(
479
+ table.isPublished,
480
+ table.publishedAt,
481
+ ),
482
+ titleIndex: index("idx_collections_title").on(table.title),
483
+ }),
484
+ );
485
+
486
+ // =====================================================
487
+ // PRODUCT COLLECTIONS JUNCTION TABLE
488
+ // =====================================================
489
+
490
+ export const productCollections = pgTable(
491
+ "product_collections",
492
+ {
493
+ id: text("id")
494
+ .primaryKey()
495
+ .$defaultFn(() => createId()),
496
+ productId: text("product_id")
497
+ .notNull()
498
+ .references(() => products.id, { onDelete: "cascade" }),
499
+ collectionId: text("collection_id")
500
+ .notNull()
501
+ .references(() => collections.id, { onDelete: "cascade" }),
502
+ position: integer("position").notNull().default(0),
503
+ isManuallyAdded: boolean("is_manually_added").notNull().default(false),
504
+
505
+ // Timestamps
506
+ createdAt: timestamp("created_at", { withTimezone: true })
507
+ .defaultNow()
508
+ .notNull(),
509
+ },
510
+ (table) => ({
511
+ // Unique constraint to prevent duplicates
512
+ productCollectionUnique: unique("idx_product_collection_unique").on(
513
+ table.productId,
514
+ table.collectionId,
515
+ ),
516
+ productIdIndex: index("idx_product_collections_product").on(
517
+ table.productId,
518
+ ),
519
+ collectionIdIndex: index("idx_product_collections_collection").on(
520
+ table.collectionId,
521
+ ),
522
+ positionIndex: index("idx_product_collections_position").on(
523
+ table.collectionId,
524
+ table.position,
525
+ ),
526
+ }),
527
+ );
528
+
529
+ // =====================================================
530
+ // PRODUCT IMAGES TABLE
531
+ // =====================================================
532
+
533
+ export const productImages = pgTable(
534
+ "product_images",
535
+ {
536
+ id: text("id")
537
+ .primaryKey()
538
+ .$defaultFn(() => createId()),
539
+ productId: text("product_id")
540
+ .notNull()
541
+ .references(() => products.id, { onDelete: "cascade" }),
542
+ variantId: text("variant_id").references(() => productVariants.id, {
543
+ onDelete: "set null",
544
+ }),
545
+
546
+ // Image Information
547
+ url: varchar("url", { length: 500 }).notNull(),
548
+ altText: varchar("alt_text", { length: 255 }),
549
+ width: integer("width"),
550
+ height: integer("height"),
551
+ size: integer("size"), // File size in bytes
552
+
553
+ // Image Variants (different sizes)
554
+ thumbnailUrl: varchar("thumbnail_url", { length: 500 }),
555
+ mediumUrl: varchar("medium_url", { length: 500 }),
556
+ largeUrl: varchar("large_url", { length: 500 }),
557
+
558
+ // Metadata
559
+ filename: varchar("filename", { length: 255 }),
560
+ contentType: varchar("content_type", { length: 100 }),
561
+ position: integer("position").notNull().default(0),
562
+
563
+ // Advanced Features
564
+ isMain: boolean("is_main").notNull().default(false),
565
+ customFields: jsonb("custom_fields")
566
+ .$type<Record<string, unknown>>()
567
+ .default({}),
568
+
569
+ // Timestamps
570
+ createdAt: timestamp("created_at", { withTimezone: true })
571
+ .defaultNow()
572
+ .notNull(),
573
+ updatedAt: timestamp("updated_at", { withTimezone: true })
574
+ .defaultNow()
575
+ .notNull(),
576
+ },
577
+ (table) => ({
578
+ // Performance indexes
579
+ productIdIndex: index("idx_product_images_product").on(table.productId),
580
+ variantIdIndex: index("idx_product_images_variant").on(table.variantId),
581
+ positionIndex: index("idx_product_images_position").on(
582
+ table.productId,
583
+ table.position,
584
+ ),
585
+ mainImageIndex: index("idx_product_images_main").on(
586
+ table.productId,
587
+ table.isMain,
588
+ ),
589
+ }),
590
+ );
591
+
592
+ // =====================================================
593
+ // PRODUCT CATEGORIES TABLE
594
+ // =====================================================
595
+
596
+ export const productCategories = pgTable(
597
+ "product_categories",
598
+ {
599
+ id: text("id")
600
+ .primaryKey()
601
+ .$defaultFn(() => createId()),
602
+ storeId: text("store_id").notNull(),
603
+
604
+ // Category Information
605
+ name: varchar("name", { length: 255 }).notNull(),
606
+ description: text("description"),
607
+ handle: varchar("handle", { length: 255 }).notNull(),
608
+
609
+ // Hierarchy
610
+ parentId: text("parent_id"),
611
+ level: integer("level").notNull().default(0),
612
+ path: varchar("path", { length: 1000 }), // e.g., /electronics/phones/smartphones
613
+
614
+ // Display
615
+ imageUrl: varchar("image_url", { length: 500 }),
616
+ iconUrl: varchar("icon_url", { length: 500 }),
617
+ position: integer("position").notNull().default(0),
618
+ isVisible: boolean("is_visible").notNull().default(true),
619
+
620
+ // SEO
621
+ seoTitle: varchar("seo_title", { length: 255 }),
622
+ seoDescription: varchar("seo_description", { length: 500 }),
623
+
624
+ // Analytics
625
+ productCount: integer("product_count").notNull().default(0),
626
+
627
+ // Timestamps
628
+ createdAt: timestamp("created_at", { withTimezone: true })
629
+ .defaultNow()
630
+ .notNull(),
631
+ updatedAt: timestamp("updated_at", { withTimezone: true })
632
+ .defaultNow()
633
+ .notNull(),
634
+ },
635
+ (table) => ({
636
+ // Performance indexes
637
+ storeIdIndex: index("idx_categories_store_id").on(table.storeId),
638
+ handleUniquePerStore: unique("idx_categories_handle_store_unique").on(
639
+ table.storeId,
640
+ table.handle,
641
+ ),
642
+ parentIdIndex: index("idx_categories_parent").on(table.parentId),
643
+ levelIndex: index("idx_categories_level").on(table.level),
644
+ visibleIndex: index("idx_categories_visible").on(table.isVisible),
645
+ positionIndex: index("idx_categories_position").on(
646
+ table.parentId,
647
+ table.position,
648
+ ),
649
+ }),
650
+ );
651
+
652
+ // =====================================================
653
+ // PRODUCT REVIEWS TABLE
654
+ // =====================================================
655
+
656
+ export const productReviews = pgTable(
657
+ "product_reviews",
658
+ {
659
+ id: text("id")
660
+ .primaryKey()
661
+ .$defaultFn(() => createId()),
662
+ productId: text("product_id")
663
+ .notNull()
664
+ .references(() => products.id, { onDelete: "cascade" }),
665
+ variantId: text("variant_id").references(() => productVariants.id, {
666
+ onDelete: "set null",
667
+ }),
668
+ storeId: text("store_id").notNull(),
669
+
670
+ // Review Information
671
+ customerId: text("customer_id"), // Customer who wrote the review
672
+ customerName: varchar("customer_name", { length: 255 }),
673
+ customerEmail: varchar("customer_email", { length: 255 }),
674
+
675
+ // Review Content
676
+ title: varchar("title", { length: 255 }),
677
+ content: text("content").notNull(),
678
+ rating: integer("rating").notNull(), // 1-5 scale
679
+
680
+ // Status and Moderation
681
+ status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, approved, rejected
682
+ isVerifiedPurchase: boolean("is_verified_purchase")
683
+ .notNull()
684
+ .default(false),
685
+ isRecommended: boolean("is_recommended"),
686
+
687
+ // Helpfulness
688
+ helpfulCount: integer("helpful_count").notNull().default(0),
689
+ unhelpfulCount: integer("unhelpful_count").notNull().default(0),
690
+
691
+ // Metadata
692
+ reviewSource: varchar("review_source", { length: 100 }), // web, mobile, import
693
+ ipAddress: varchar("ip_address", { length: 45 }),
694
+ userAgent: varchar("user_agent", { length: 500 }),
695
+
696
+ // Timestamps
697
+ createdAt: timestamp("created_at", { withTimezone: true })
698
+ .defaultNow()
699
+ .notNull(),
700
+ updatedAt: timestamp("updated_at", { withTimezone: true })
701
+ .defaultNow()
702
+ .notNull(),
703
+ moderatedAt: timestamp("moderated_at", { withTimezone: true }),
704
+ },
705
+ (table) => ({
706
+ // Performance indexes
707
+ productIdIndex: index("idx_reviews_product").on(table.productId),
708
+ storeIdIndex: index("idx_reviews_store").on(table.storeId),
709
+ customerIdIndex: index("idx_reviews_customer").on(table.customerId),
710
+ statusIndex: index("idx_reviews_status").on(table.status),
711
+ ratingIndex: index("idx_reviews_rating").on(table.rating),
712
+ verifiedIndex: index("idx_reviews_verified").on(table.isVerifiedPurchase),
713
+ createdAtIndex: index("idx_reviews_created").on(table.createdAt),
714
+ }),
715
+ );
716
+
717
+ // =====================================================
718
+ // PRODUCT ANALYTICS TABLE
719
+ // =====================================================
720
+
721
+ export const productAnalytics = pgTable(
722
+ "product_analytics",
723
+ {
724
+ id: text("id")
725
+ .primaryKey()
726
+ .$defaultFn(() => createId()),
727
+ productId: text("product_id")
728
+ .notNull()
729
+ .references(() => products.id, { onDelete: "cascade" }),
730
+ storeId: text("store_id").notNull(),
731
+
732
+ // Time-based metrics
733
+ date: timestamp("date", { withTimezone: true }).notNull(),
734
+ hour: integer("hour"), // 0-23 for hourly analytics
735
+
736
+ // Traffic Metrics
737
+ views: integer("views").notNull().default(0),
738
+ uniqueViews: integer("unique_views").notNull().default(0),
739
+ sessions: integer("sessions").notNull().default(0),
740
+ bounceRate: decimal("bounce_rate", { precision: 5, scale: 4 }),
741
+
742
+ // Conversion Metrics
743
+ addToCarts: integer("add_to_carts").notNull().default(0),
744
+ purchases: integer("purchases").notNull().default(0),
745
+ revenue: decimal("revenue", { precision: 12, scale: 2 })
746
+ .notNull()
747
+ .default("0"),
748
+ conversionRate: decimal("conversion_rate", { precision: 5, scale: 4 }),
749
+
750
+ // Engagement Metrics
751
+ averageTimeOnPage: integer("average_time_on_page"), // in seconds
752
+ clickThroughRate: decimal("click_through_rate", { precision: 5, scale: 4 }),
753
+ shareCount: integer("share_count").notNull().default(0),
754
+ wishlistAdds: integer("wishlist_adds").notNull().default(0),
755
+
756
+ // Search and Discovery
757
+ searchImpressions: integer("search_impressions").notNull().default(0),
758
+ searchClicks: integer("search_clicks").notNull().default(0),
759
+ searchRanking: decimal("search_ranking", { precision: 8, scale: 4 }),
760
+
761
+ // Geographic and Device Data
762
+ topCountries: jsonb("top_countries")
763
+ .$type<Array<{ country: string; views: number }>>()
764
+ .default([]),
765
+ topDevices: jsonb("top_devices")
766
+ .$type<Array<{ device: string; views: number }>>()
767
+ .default([]),
768
+
769
+ // Timestamps
770
+ createdAt: timestamp("created_at", { withTimezone: true })
771
+ .defaultNow()
772
+ .notNull(),
773
+ updatedAt: timestamp("updated_at", { withTimezone: true })
774
+ .defaultNow()
775
+ .notNull(),
776
+ },
777
+ (table) => ({
778
+ // Performance indexes for analytics queries
779
+ productDateIndex: index("idx_analytics_product_date").on(
780
+ table.productId,
781
+ table.date,
782
+ ),
783
+ storeIdIndex: index("idx_analytics_store").on(table.storeId),
784
+ dateIndex: index("idx_analytics_date").on(table.date),
785
+ hourlyIndex: index("idx_analytics_hourly").on(
786
+ table.productId,
787
+ table.date,
788
+ table.hour,
789
+ ),
790
+ viewsIndex: index("idx_analytics_views").on(table.views),
791
+ revenueIndex: index("idx_analytics_revenue").on(table.revenue),
792
+ conversionIndex: index("idx_analytics_conversion").on(table.conversionRate),
793
+ }),
794
+ );
795
+
796
+ // =====================================================
797
+ // WHOLESALE PRICING TIERS TABLE
798
+ // =====================================================
799
+
800
+ export const wholesalePricingTiers = pgTable(
801
+ "wholesale_pricing_tiers",
802
+ {
803
+ id: text("id")
804
+ .primaryKey()
805
+ .$defaultFn(() => createId()),
806
+ productId: text("product_id")
807
+ .notNull()
808
+ .references(() => products.id, { onDelete: "cascade" }),
809
+ variantId: text("variant_id").references(() => productVariants.id, {
810
+ onDelete: "cascade",
811
+ }),
812
+ storeId: text("store_id").notNull(),
813
+
814
+ // Tier Information
815
+ tierName: varchar("tier_name", { length: 100 }).notNull(),
816
+ tierDescription: text("tier_description"),
817
+ tierPriority: integer("tier_priority").notNull().default(0), // Higher priority = applied first
818
+ minQuantity: integer("min_quantity").notNull(),
819
+ maxQuantity: integer("max_quantity"),
820
+ price: decimal("price", { precision: 12, scale: 2 }).notNull(),
821
+ discountPercentage: decimal("discount_percentage", {
822
+ precision: 5,
823
+ scale: 2,
824
+ }),
825
+
826
+ // Enhanced Tier Conditions
827
+ customerType: varchar("customer_type", { length: 50 }), // "RETAILER", "DISTRIBUTOR", "RESELLER", "VIP"
828
+ customerTags: jsonb("customer_tags").$type<string[]>().default([]), // Additional customer segmentation
829
+ region: varchar("region", { length: 100 }),
830
+ country: varchar("country", { length: 50 }),
831
+ currency: varchar("currency", { length: 3 }).default("USD"),
832
+ minimumOrderValue: decimal("minimum_order_value", {
833
+ precision: 12,
834
+ scale: 2,
835
+ }),
836
+
837
+ // Advanced Pricing Features
838
+ bulkDiscountType: varchar("bulk_discount_type", { length: 20 }).default(
839
+ "PERCENTAGE",
840
+ ), // PERCENTAGE, FIXED_AMOUNT, TIERED
841
+ compoundDiscount: boolean("compound_discount").notNull().default(false), // Can be combined with other discounts
842
+ exclusiveDiscount: boolean("exclusive_discount").notNull().default(false), // Cannot be combined
843
+ maxDiscountAmount: decimal("max_discount_amount", {
844
+ precision: 12,
845
+ scale: 2,
846
+ }),
847
+
848
+ // Validity and Scheduling
849
+ validFrom: timestamp("valid_from", { withTimezone: true }),
850
+ validTo: timestamp("valid_to", { withTimezone: true }),
851
+ isActive: boolean("is_active").notNull().default(true),
852
+ isScheduled: boolean("is_scheduled").notNull().default(false),
853
+ scheduleStart: timestamp("schedule_start", { withTimezone: true }),
854
+ scheduleEnd: timestamp("schedule_end", { withTimezone: true }),
855
+
856
+ // Additional Features
857
+ freeShipping: boolean("free_shipping").notNull().default(false),
858
+ freeShippingThreshold: decimal("free_shipping_threshold", {
859
+ precision: 12,
860
+ scale: 2,
861
+ }),
862
+ expeditedShipping: boolean("expedited_shipping").notNull().default(false),
863
+ dropShippingEnabled: boolean("drop_shipping_enabled")
864
+ .notNull()
865
+ .default(false),
866
+
867
+ // Payment Terms
868
+ paymentTerms: varchar("payment_terms", { length: 50 }), // "NET_30", "NET_60", "COD", "PREPAID"
869
+ earlyPaymentDiscount: decimal("early_payment_discount", {
870
+ precision: 5,
871
+ scale: 2,
872
+ }),
873
+ latePaymentPenalty: decimal("late_payment_penalty", {
874
+ precision: 5,
875
+ scale: 2,
876
+ }),
877
+
878
+ // Inventory and Fulfillment
879
+ reservedInventory: integer("reserved_inventory").default(0),
880
+ autoReplenishment: boolean("auto_replenishment").notNull().default(false),
881
+ replenishmentThreshold: integer("replenishment_threshold"),
882
+ leadTimedays: integer("lead_time_days").default(0),
883
+
884
+ // Analytics and Tracking
885
+ totalOrdersCount: integer("total_orders_count").notNull().default(0),
886
+ totalRevenue: decimal("total_revenue", { precision: 12, scale: 2 })
887
+ .notNull()
888
+ .default("0"),
889
+ averageOrderValue: decimal("average_order_value", {
890
+ precision: 12,
891
+ scale: 2,
892
+ }),
893
+ lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
894
+
895
+ // Custom Fields and Metadata
896
+ customFields: jsonb("custom_fields")
897
+ .$type<Record<string, unknown>>()
898
+ .default({}),
899
+ internalNotes: text("internal_notes"),
900
+ customerVisibleNotes: text("customer_visible_notes"),
901
+
902
+ // Timestamps
903
+ createdAt: timestamp("created_at", { withTimezone: true })
904
+ .defaultNow()
905
+ .notNull(),
906
+ updatedAt: timestamp("updated_at", { withTimezone: true })
907
+ .defaultNow()
908
+ .notNull(),
909
+ },
910
+ (table) => ({
911
+ // Performance indexes
912
+ productIdIndex: index("idx_wholesale_tiers_product").on(table.productId),
913
+ variantIdIndex: index("idx_wholesale_tiers_variant").on(table.variantId),
914
+ storeIdIndex: index("idx_wholesale_tiers_store").on(table.storeId),
915
+ quantityIndex: index("idx_wholesale_tiers_quantity").on(
916
+ table.minQuantity,
917
+ table.maxQuantity,
918
+ ),
919
+ customerTypeIndex: index("idx_wholesale_tiers_customer_type").on(
920
+ table.customerType,
921
+ ),
922
+ activeIndex: index("idx_wholesale_tiers_active").on(table.isActive),
923
+ validityIndex: index("idx_wholesale_tiers_validity").on(
924
+ table.validFrom,
925
+ table.validTo,
926
+ ),
927
+
928
+ // Enhanced indexes for new features
929
+ priorityIndex: index("idx_wholesale_tiers_priority").on(table.tierPriority),
930
+ scheduledIndex: index("idx_wholesale_tiers_scheduled").on(
931
+ table.isScheduled,
932
+ table.scheduleStart,
933
+ table.scheduleEnd,
934
+ ),
935
+ regionIndex: index("idx_wholesale_tiers_region").on(table.region),
936
+ countryIndex: index("idx_wholesale_tiers_country").on(table.country),
937
+ paymentTermsIndex: index("idx_wholesale_tiers_payment_terms").on(
938
+ table.paymentTerms,
939
+ ),
940
+ revenueIndex: index("idx_wholesale_tiers_revenue").on(table.totalRevenue),
941
+ lastUsedIndex: index("idx_wholesale_tiers_last_used").on(table.lastUsedAt),
942
+
943
+ // Composite indexes for complex queries
944
+ activeScheduleIndex: index("idx_wholesale_tiers_active_schedule").on(
945
+ table.isActive,
946
+ table.isScheduled,
947
+ table.scheduleStart,
948
+ ),
949
+ customerLocationIndex: index("idx_wholesale_tiers_customer_location").on(
950
+ table.customerType,
951
+ table.region,
952
+ table.country,
953
+ ),
954
+ }),
955
+ );