@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,44 @@
1
+ import { relations } from "drizzle-orm";
2
+ import {
3
+ discounts,
4
+ discountUsages,
5
+ discountRules,
6
+ customerSegments,
7
+ discountAnalytics
8
+ } from "./discount-schema";
9
+ import { products, productVariants, collections } from "./product-schema";
10
+
11
+ // =====================================================
12
+ // DISCOUNT RELATIONS
13
+ // =====================================================
14
+
15
+ export const discountsRelations = relations(discounts, ({ many }) => ({
16
+ usages: many(discountUsages),
17
+ rules: many(discountRules),
18
+ analytics: many(discountAnalytics),
19
+ }));
20
+
21
+ export const discountUsagesRelations = relations(discountUsages, ({ one }) => ({
22
+ discount: one(discounts, {
23
+ fields: [discountUsages.discountId],
24
+ references: [discounts.id],
25
+ }),
26
+ }));
27
+
28
+ export const discountRulesRelations = relations(discountRules, ({ one }) => ({
29
+ discount: one(discounts, {
30
+ fields: [discountRules.discountId],
31
+ references: [discounts.id],
32
+ }),
33
+ }));
34
+
35
+ export const discountAnalyticsRelations = relations(discountAnalytics, ({ one }) => ({
36
+ discount: one(discounts, {
37
+ fields: [discountAnalytics.discountId],
38
+ references: [discounts.id],
39
+ }),
40
+ }));
41
+
42
+ export const customerSegmentsRelations = relations(customerSegments, ({ many }) => ({
43
+ // Relations can be added here for customer segment memberships if needed
44
+ }));
@@ -0,0 +1,464 @@
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
+ // DISCOUNT ENUMS
18
+ // =====================================================
19
+
20
+ export const discountTypeEnum = pgEnum("discount_type", [
21
+ "PERCENTAGE",
22
+ "FIXED_AMOUNT",
23
+ "FREE_SHIPPING",
24
+ "BOGO", // Buy one get one
25
+ "TIERED", // Volume-based tiered discounts
26
+ "LOYALTY_POINTS", // Loyalty points redemption
27
+ "REFERRAL", // Referral rewards
28
+ "MEMBER_ONLY", // Membership-exclusive discounts
29
+ ]);
30
+
31
+ export const discountStatusEnum = pgEnum("discount_status", [
32
+ "DRAFT",
33
+ "ACTIVE",
34
+ "PAUSED",
35
+ "SCHEDULED",
36
+ "EXPIRED",
37
+ "DISABLED",
38
+ ]);
39
+
40
+ export const discountTargetTypeEnum = pgEnum("discount_target_type", [
41
+ "STORE", // Store-wide discount
42
+ "COLLECTION", // Collection-specific discount
43
+ "PRODUCT", // Product-specific discount
44
+ "VARIANT", // Variant-specific discount
45
+ "CATEGORY", // Category-specific discount
46
+ "CUSTOMER_SEGMENT", // Customer segment-specific
47
+ ]);
48
+
49
+ export const discountApplicationTypeEnum = pgEnum("discount_application_type", [
50
+ "AUTOMATIC", // Automatically applied
51
+ "CODE", // Requires coupon code
52
+ "MANUAL", // Manually applied by staff
53
+ ]);
54
+
55
+ export const customerSegmentTypeEnum = pgEnum("customer_segment_type", [
56
+ "NEW_CUSTOMER",
57
+ "RETURNING_CUSTOMER",
58
+ "VIP",
59
+ "LOYALTY_TIER_1",
60
+ "LOYALTY_TIER_2",
61
+ "LOYALTY_TIER_3",
62
+ "GEOGRAPHIC",
63
+ "PURCHASE_HISTORY",
64
+ "CUSTOM",
65
+ ]);
66
+
67
+ export const discountStackingModeEnum = pgEnum("discount_stacking_mode", [
68
+ "NONE", // Cannot stack with other discounts
69
+ "ADDITIVE", // Add discount values together
70
+ "MULTIPLICATIVE", // Apply discounts multiplicatively
71
+ "BEST_ONLY", // Apply only the best discount
72
+ "CUSTOM", // Custom stacking logic
73
+ ]);
74
+
75
+ // =====================================================
76
+ // MAIN DISCOUNTS TABLE
77
+ // =====================================================
78
+
79
+ export const discounts = pgTable(
80
+ "discounts",
81
+ {
82
+ id: text("id")
83
+ .primaryKey()
84
+ .$defaultFn(() => createId()),
85
+ storeId: text("store_id").notNull(),
86
+
87
+ // Basic Information
88
+ name: varchar("name", { length: 255 }).notNull(),
89
+ description: text("description"),
90
+ internalNotes: text("internal_notes"), // Staff notes
91
+
92
+ // Discount Configuration
93
+ type: discountTypeEnum("type").notNull(),
94
+ status: discountStatusEnum("status").notNull().default("DRAFT"),
95
+ applicationType: discountApplicationTypeEnum("application_type")
96
+ .notNull()
97
+ .default("AUTOMATIC"),
98
+
99
+ // Discount Values
100
+ value: decimal("value", { precision: 12, scale: 4 }).notNull(), // Main discount value
101
+ maxDiscountAmount: decimal("max_discount_amount", { precision: 12, scale: 2 }), // Cap for percentage discounts
102
+
103
+ // Coupon Code (for CODE application type)
104
+ couponCode: varchar("coupon_code", { length: 100 }),
105
+ codePrefix: varchar("code_prefix", { length: 20 }), // For auto-generated codes
106
+
107
+ // Targeting Configuration
108
+ targetType: discountTargetTypeEnum("target_type").notNull().default("STORE"),
109
+ targetIds: jsonb("target_ids").$type<string[]>().default([]), // IDs of targeted items
110
+
111
+ // Customer Eligibility
112
+ customerSegments: jsonb("customer_segments")
113
+ .$type<string[]>()
114
+ .default([]),
115
+ excludedCustomerIds: jsonb("excluded_customer_ids")
116
+ .$type<string[]>()
117
+ .default([]),
118
+
119
+ // Usage Limits
120
+ usageLimit: integer("usage_limit"), // Total usage limit
121
+ usageLimitPerCustomer: integer("usage_limit_per_customer"),
122
+ currentUsageCount: integer("current_usage_count").notNull().default(0),
123
+
124
+ // Minimum Requirements
125
+ minPurchaseAmount: decimal("min_purchase_amount", { precision: 12, scale: 2 }),
126
+ minQuantity: integer("min_quantity"),
127
+ minItems: integer("min_items"), // Minimum number of different items
128
+
129
+ // Time Constraints
130
+ startsAt: timestamp("starts_at", { withTimezone: true }),
131
+ endsAt: timestamp("ends_at", { withTimezone: true }),
132
+ timeZone: varchar("time_zone", { length: 50 }).default("UTC"),
133
+
134
+ // Schedule Configuration
135
+ scheduleConfig: jsonb("schedule_config").$type<{
136
+ recurringType?: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
137
+ daysOfWeek?: number[]; // 0-6 (Sunday to Saturday)
138
+ hoursOfDay?: { start: number; end: number }; // 0-23
139
+ datesOfMonth?: number[]; // 1-31
140
+ monthsOfYear?: number[]; // 1-12
141
+ }>(),
142
+
143
+ // Geographic Restrictions
144
+ geoRestrictions: jsonb("geo_restrictions").$type<{
145
+ includedCountries?: string[];
146
+ excludedCountries?: string[];
147
+ includedStates?: string[];
148
+ excludedStates?: string[];
149
+ includedCities?: string[];
150
+ excludedCities?: string[];
151
+ includedZipCodes?: string[];
152
+ excludedZipCodes?: string[];
153
+ }>(),
154
+
155
+ // Sales Channel Configuration
156
+ salesChannels: jsonb("sales_channels")
157
+ .$type<("POS" | "MARKETPLACE" | "STOREFRONT")[]>()
158
+ .default(["POS", "MARKETPLACE", "STOREFRONT"]),
159
+
160
+ // Stacking Configuration
161
+ stackingMode: discountStackingModeEnum("stacking_mode")
162
+ .notNull()
163
+ .default("NONE"),
164
+ priority: integer("priority").notNull().default(0), // Higher number = higher priority
165
+ excludeOtherDiscounts: boolean("exclude_other_discounts")
166
+ .notNull()
167
+ .default(false),
168
+
169
+ // Advanced Configuration
170
+ customRules: jsonb("custom_rules").$type<{
171
+ conditions?: Array<{
172
+ field: string;
173
+ operator: "EQUALS" | "NOT_EQUALS" | "GREATER_THAN" | "LESS_THAN" | "CONTAINS" | "NOT_CONTAINS";
174
+ value: any;
175
+ logicalOperator?: "AND" | "OR";
176
+ }>;
177
+ actions?: Array<{
178
+ type: string;
179
+ parameters: Record<string, any>;
180
+ }>;
181
+ }>(),
182
+
183
+ // BOGO Configuration
184
+ bogoConfig: jsonb("bogo_config").$type<{
185
+ buyQuantity: number;
186
+ getQuantity: number;
187
+ getDiscountPercentage?: number; // 100 = free, 50 = 50% off
188
+ applicableToSameProduct?: boolean;
189
+ applicableToCollection?: string;
190
+ }>(),
191
+
192
+ // Tiered Discount Configuration
193
+ tieredConfig: jsonb("tiered_config").$type<Array<{
194
+ minQuantity: number;
195
+ discountPercentage?: number;
196
+ discountAmount?: number;
197
+ }>>(),
198
+
199
+ // Loyalty Points Configuration
200
+ loyaltyPointsConfig: jsonb("loyalty_points_config").$type<{
201
+ pointsRequired: number;
202
+ pointsValue: number; // Value of points in currency
203
+ allowPartialRedemption?: boolean;
204
+ }>(),
205
+
206
+ // Performance Tracking
207
+ totalSavings: decimal("total_savings", { precision: 12, scale: 2 })
208
+ .notNull()
209
+ .default("0"),
210
+ totalRevenue: decimal("total_revenue", { precision: 12, scale: 2 })
211
+ .notNull()
212
+ .default("0"),
213
+ conversionRate: decimal("conversion_rate", { precision: 5, scale: 4 }),
214
+
215
+ // Metadata
216
+ createdBy: text("created_by"), // User ID who created the discount
217
+ lastModifiedBy: text("last_modified_by"),
218
+ tags: jsonb("tags").$type<string[]>().default([]),
219
+
220
+ // Timestamps
221
+ createdAt: timestamp("created_at", { withTimezone: true })
222
+ .defaultNow()
223
+ .notNull(),
224
+ updatedAt: timestamp("updated_at", { withTimezone: true })
225
+ .defaultNow()
226
+ .notNull(),
227
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
228
+ },
229
+ (table) => ({
230
+ storeIdIndex: index("idx_discounts_store_id").on(table.storeId),
231
+ statusIndex: index("idx_discounts_status").on(table.status),
232
+ typeIndex: index("idx_discounts_type").on(table.type),
233
+ couponCodeIndex: index("idx_discounts_coupon_code").on(table.couponCode),
234
+ targetTypeIndex: index("idx_discounts_target_type").on(table.targetType),
235
+ priorityIndex: index("idx_discounts_priority").on(table.priority),
236
+ timeRangeIndex: index("idx_discounts_time_range").on(table.startsAt, table.endsAt),
237
+ usageIndex: index("idx_discounts_usage").on(table.currentUsageCount, table.usageLimit),
238
+ salesChannelsIndex: index("idx_discounts_sales_channels").on(table.salesChannels),
239
+
240
+ // Unique constraints
241
+ couponCodeUniquePerStore: unique("idx_discounts_coupon_store_unique").on(
242
+ table.storeId,
243
+ table.couponCode,
244
+ ),
245
+ }),
246
+ );
247
+
248
+ // =====================================================
249
+ // DISCOUNT USAGE TRACKING
250
+ // =====================================================
251
+
252
+ export const discountUsages = pgTable(
253
+ "discount_usages",
254
+ {
255
+ id: text("id")
256
+ .primaryKey()
257
+ .$defaultFn(() => createId()),
258
+ discountId: text("discount_id").notNull(),
259
+ storeId: text("store_id").notNull(),
260
+
261
+ // Usage Context
262
+ orderId: text("order_id"), // Reference to order if applicable
263
+ customerId: text("customer_id"),
264
+ sessionId: text("session_id"), // For anonymous users
265
+
266
+ // Applied Discount Details
267
+ originalAmount: decimal("original_amount", { precision: 12, scale: 2 }).notNull(),
268
+ discountAmount: decimal("discount_amount", { precision: 12, scale: 2 }).notNull(),
269
+ finalAmount: decimal("final_amount", { precision: 12, scale: 2 }).notNull(),
270
+
271
+ // Context Information
272
+ appliedProducts: jsonb("applied_products").$type<Array<{
273
+ productId: string;
274
+ variantId?: string;
275
+ quantity: number;
276
+ originalPrice: number;
277
+ discountedPrice: number;
278
+ }>>(),
279
+
280
+ salesChannel: varchar("sales_channel", { length: 20 }), // POS, MARKETPLACE, STOREFRONT
281
+ locationId: text("location_id"), // Store location or warehouse
282
+
283
+ // Metadata
284
+ ipAddress: varchar("ip_address", { length: 45 }),
285
+ userAgent: text("user_agent"),
286
+ deviceType: varchar("device_type", { length: 20 }),
287
+
288
+ // Timestamps
289
+ appliedAt: timestamp("applied_at", { withTimezone: true })
290
+ .defaultNow()
291
+ .notNull(),
292
+ },
293
+ (table) => ({
294
+ discountIdIndex: index("idx_discount_usages_discount_id").on(table.discountId),
295
+ storeIdIndex: index("idx_discount_usages_store_id").on(table.storeId),
296
+ customerIdIndex: index("idx_discount_usages_customer_id").on(table.customerId),
297
+ orderIdIndex: index("idx_discount_usages_order_id").on(table.orderId),
298
+ appliedAtIndex: index("idx_discount_usages_applied_at").on(table.appliedAt),
299
+ salesChannelIndex: index("idx_discount_usages_sales_channel").on(table.salesChannel),
300
+ }),
301
+ );
302
+
303
+ // =====================================================
304
+ // DISCOUNT RULES ENGINE
305
+ // =====================================================
306
+
307
+ export const discountRules = pgTable(
308
+ "discount_rules",
309
+ {
310
+ id: text("id")
311
+ .primaryKey()
312
+ .$defaultFn(() => createId()),
313
+ discountId: text("discount_id").notNull(),
314
+ storeId: text("store_id").notNull(),
315
+
316
+ // Rule Configuration
317
+ name: varchar("name", { length: 255 }).notNull(),
318
+ description: text("description"),
319
+ ruleType: varchar("rule_type", { length: 50 }).notNull(), // CONDITION, ACTION, VALIDATION
320
+
321
+ // Rule Logic
322
+ conditions: jsonb("conditions").$type<Array<{
323
+ field: string; // e.g., "cart.total", "customer.orderCount", "product.category"
324
+ operator: "EQUALS" | "NOT_EQUALS" | "GREATER_THAN" | "LESS_THAN" | "IN" | "NOT_IN" | "CONTAINS" | "NOT_CONTAINS";
325
+ value: any;
326
+ logicalOperator?: "AND" | "OR";
327
+ }>>().notNull(),
328
+
329
+ actions: jsonb("actions").$type<Array<{
330
+ type: string; // e.g., "APPLY_DISCOUNT", "ADD_FREE_PRODUCT", "UPGRADE_SHIPPING"
331
+ parameters: Record<string, any>;
332
+ }>>().default([]),
333
+
334
+ // Execution Configuration
335
+ priority: integer("priority").notNull().default(0),
336
+ isActive: boolean("is_active").notNull().default(true),
337
+
338
+ // Timestamps
339
+ createdAt: timestamp("created_at", { withTimezone: true })
340
+ .defaultNow()
341
+ .notNull(),
342
+ updatedAt: timestamp("updated_at", { withTimezone: true })
343
+ .defaultNow()
344
+ .notNull(),
345
+ },
346
+ (table) => ({
347
+ discountIdIndex: index("idx_discount_rules_discount_id").on(table.discountId),
348
+ storeIdIndex: index("idx_discount_rules_store_id").on(table.storeId),
349
+ priorityIndex: index("idx_discount_rules_priority").on(table.priority),
350
+ activeIndex: index("idx_discount_rules_active").on(table.isActive),
351
+ }),
352
+ );
353
+
354
+ // =====================================================
355
+ // CUSTOMER SEGMENTS
356
+ // =====================================================
357
+
358
+ export const customerSegments = pgTable(
359
+ "customer_segments",
360
+ {
361
+ id: text("id")
362
+ .primaryKey()
363
+ .$defaultFn(() => createId()),
364
+ storeId: text("store_id").notNull(),
365
+
366
+ // Segment Information
367
+ name: varchar("name", { length: 255 }).notNull(),
368
+ description: text("description"),
369
+ segmentType: customerSegmentTypeEnum("segment_type").notNull(),
370
+
371
+ // Segment Criteria
372
+ criteria: jsonb("criteria").$type<{
373
+ orderCount?: { min?: number; max?: number };
374
+ totalSpent?: { min?: number; max?: number };
375
+ lastOrderDate?: { before?: string; after?: string };
376
+ avgOrderValue?: { min?: number; max?: number };
377
+ productCategories?: string[];
378
+ geoLocation?: {
379
+ countries?: string[];
380
+ states?: string[];
381
+ cities?: string[];
382
+ };
383
+ loyaltyTier?: string;
384
+ registrationDate?: { before?: string; after?: string };
385
+ customFields?: Record<string, any>;
386
+ }>(),
387
+
388
+ // Segment Statistics
389
+ memberCount: integer("member_count").notNull().default(0),
390
+ lastCalculatedAt: timestamp("last_calculated_at", { withTimezone: true }),
391
+
392
+ // Configuration
393
+ isActive: boolean("is_active").notNull().default(true),
394
+ autoUpdate: boolean("auto_update").notNull().default(true), // Auto-recalculate membership
395
+
396
+ // Timestamps
397
+ createdAt: timestamp("created_at", { withTimezone: true })
398
+ .defaultNow()
399
+ .notNull(),
400
+ updatedAt: timestamp("updated_at", { withTimezone: true })
401
+ .defaultNow()
402
+ .notNull(),
403
+ },
404
+ (table) => ({
405
+ storeIdIndex: index("idx_customer_segments_store_id").on(table.storeId),
406
+ segmentTypeIndex: index("idx_customer_segments_type").on(table.segmentType),
407
+ activeIndex: index("idx_customer_segments_active").on(table.isActive),
408
+ memberCountIndex: index("idx_customer_segments_member_count").on(table.memberCount),
409
+ }),
410
+ );
411
+
412
+ // =====================================================
413
+ // DISCOUNT ANALYTICS
414
+ // =====================================================
415
+
416
+ export const discountAnalytics = pgTable(
417
+ "discount_analytics",
418
+ {
419
+ id: text("id")
420
+ .primaryKey()
421
+ .$defaultFn(() => createId()),
422
+ discountId: text("discount_id").notNull(),
423
+ storeId: text("store_id").notNull(),
424
+
425
+ // Time Period
426
+ periodType: varchar("period_type", { length: 20 }).notNull(), // DAILY, WEEKLY, MONTHLY, YEARLY
427
+ periodStart: timestamp("period_start", { withTimezone: true }).notNull(),
428
+ periodEnd: timestamp("period_end", { withTimezone: true }).notNull(),
429
+
430
+ // Usage Metrics
431
+ totalUsages: integer("total_usages").notNull().default(0),
432
+ uniqueCustomers: integer("unique_customers").notNull().default(0),
433
+ totalSavings: decimal("total_savings", { precision: 12, scale: 2 }).notNull().default("0"),
434
+ totalRevenue: decimal("total_revenue", { precision: 12, scale: 2 }).notNull().default("0"),
435
+ avgOrderValue: decimal("avg_order_value", { precision: 12, scale: 2 }),
436
+
437
+ // Performance Metrics
438
+ conversionRate: decimal("conversion_rate", { precision: 5, scale: 4 }),
439
+ roi: decimal("roi", { precision: 8, scale: 4 }), // Return on Investment
440
+ customerAcquisitionCost: decimal("customer_acquisition_cost", { precision: 12, scale: 2 }),
441
+
442
+ // Channel Breakdown
443
+ channelBreakdown: jsonb("channel_breakdown").$type<Record<string, {
444
+ usages: number;
445
+ revenue: number;
446
+ savings: number;
447
+ }>>(),
448
+
449
+ // Timestamps
450
+ calculatedAt: timestamp("calculated_at", { withTimezone: true })
451
+ .defaultNow()
452
+ .notNull(),
453
+ },
454
+ (table) => ({
455
+ discountIdIndex: index("idx_discount_analytics_discount_id").on(table.discountId),
456
+ storeIdIndex: index("idx_discount_analytics_store_id").on(table.storeId),
457
+ periodIndex: index("idx_discount_analytics_period").on(
458
+ table.periodType,
459
+ table.periodStart,
460
+ table.periodEnd,
461
+ ),
462
+ calculatedAtIndex: index("idx_discount_analytics_calculated_at").on(table.calculatedAt),
463
+ }),
464
+ );
@@ -0,0 +1,187 @@
1
+ import { relations } from "drizzle-orm";
2
+ import {
3
+ collections,
4
+ productAnalytics,
5
+ productCategories,
6
+ productCollections,
7
+ productImages,
8
+ productReviews,
9
+ products,
10
+ productVariants,
11
+ wholesalePricingTiers,
12
+ } from "./product-schema";
13
+
14
+ // =====================================================
15
+ // PRODUCT RELATIONS
16
+ // =====================================================
17
+
18
+ export const productsRelations = relations(products, ({ many, one }) => ({
19
+ // One product has many variants
20
+ variants: many(productVariants),
21
+
22
+ // One product has many images
23
+ images: many(productImages),
24
+
25
+ // One product has many reviews
26
+ reviews: many(productReviews),
27
+
28
+ // One product has many analytics records
29
+ analytics: many(productAnalytics),
30
+
31
+ // One product belongs to many collections (many-to-many)
32
+ productCollections: many(productCollections),
33
+
34
+ // One product has many wholesale pricing tiers
35
+ wholesalePricingTiers: many(wholesalePricingTiers),
36
+
37
+ // One product belongs to one category (optional)
38
+ category: one(productCategories, {
39
+ fields: [products.category],
40
+ references: [productCategories.handle],
41
+ }),
42
+ }));
43
+
44
+ // =====================================================
45
+ // PRODUCT VARIANTS RELATIONS
46
+ // =====================================================
47
+
48
+ export const productVariantsRelations = relations(
49
+ productVariants,
50
+ ({ one, many }) => ({
51
+ // One variant belongs to one product
52
+ product: one(products, {
53
+ fields: [productVariants.productId],
54
+ references: [products.id],
55
+ }),
56
+
57
+ // One variant can have many images
58
+ images: many(productImages),
59
+
60
+ // One variant can have many reviews
61
+ reviews: many(productReviews),
62
+ }),
63
+ );
64
+
65
+ // =====================================================
66
+ // COLLECTIONS RELATIONS
67
+ // =====================================================
68
+
69
+ export const collectionsRelations = relations(collections, ({ many }) => ({
70
+ // One collection has many products (many-to-many)
71
+ productCollections: many(productCollections),
72
+ }));
73
+
74
+ // =====================================================
75
+ // PRODUCT COLLECTIONS JUNCTION RELATIONS
76
+ // =====================================================
77
+
78
+ export const productCollectionsRelations = relations(
79
+ productCollections,
80
+ ({ one }) => ({
81
+ // Junction table relations
82
+ product: one(products, {
83
+ fields: [productCollections.productId],
84
+ references: [products.id],
85
+ }),
86
+
87
+ collection: one(collections, {
88
+ fields: [productCollections.collectionId],
89
+ references: [collections.id],
90
+ }),
91
+ }),
92
+ );
93
+
94
+ // =====================================================
95
+ // PRODUCT IMAGES RELATIONS
96
+ // =====================================================
97
+
98
+ export const productImagesRelations = relations(productImages, ({ one }) => ({
99
+ // One image belongs to one product
100
+ product: one(products, {
101
+ fields: [productImages.productId],
102
+ references: [products.id],
103
+ }),
104
+
105
+ // One image can belong to one variant (optional)
106
+ variant: one(productVariants, {
107
+ fields: [productImages.variantId],
108
+ references: [productVariants.id],
109
+ }),
110
+ }));
111
+
112
+ // =====================================================
113
+ // PRODUCT CATEGORIES RELATIONS
114
+ // =====================================================
115
+
116
+ export const productCategoriesRelations = relations(
117
+ productCategories,
118
+ ({ one, many }) => ({
119
+ // Self-referencing relationship for hierarchy
120
+ parent: one(productCategories, {
121
+ fields: [productCategories.parentId],
122
+ references: [productCategories.id],
123
+ relationName: "CategoryParent",
124
+ }),
125
+
126
+ children: many(productCategories, {
127
+ relationName: "CategoryParent",
128
+ }),
129
+
130
+ // One category can have many products
131
+ products: many(products),
132
+ }),
133
+ );
134
+
135
+ // =====================================================
136
+ // PRODUCT REVIEWS RELATIONS
137
+ // =====================================================
138
+
139
+ export const productReviewsRelations = relations(productReviews, ({ one }) => ({
140
+ // One review belongs to one product
141
+ product: one(products, {
142
+ fields: [productReviews.productId],
143
+ references: [products.id],
144
+ }),
145
+
146
+ // One review can belong to one variant (optional)
147
+ variant: one(productVariants, {
148
+ fields: [productReviews.variantId],
149
+ references: [productVariants.id],
150
+ }),
151
+ }));
152
+
153
+ // =====================================================
154
+ // PRODUCT ANALYTICS RELATIONS
155
+ // =====================================================
156
+
157
+ export const productAnalyticsRelations = relations(
158
+ productAnalytics,
159
+ ({ one }) => ({
160
+ // One analytics record belongs to one product
161
+ product: one(products, {
162
+ fields: [productAnalytics.productId],
163
+ references: [products.id],
164
+ }),
165
+ }),
166
+ );
167
+
168
+ // =====================================================
169
+ // WHOLESALE PRICING TIERS RELATIONS
170
+ // =====================================================
171
+
172
+ export const wholesalePricingTiersRelations = relations(
173
+ wholesalePricingTiers,
174
+ ({ one }) => ({
175
+ // One tier belongs to one product
176
+ product: one(products, {
177
+ fields: [wholesalePricingTiers.productId],
178
+ references: [products.id],
179
+ }),
180
+
181
+ // One tier can belong to one variant (optional)
182
+ variant: one(productVariants, {
183
+ fields: [wholesalePricingTiers.variantId],
184
+ references: [productVariants.id],
185
+ }),
186
+ }),
187
+ );