@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,576 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+ import {
3
+ boolean,
4
+ decimal,
5
+ index,
6
+ integer,
7
+ jsonb,
8
+ pgTable,
9
+ text,
10
+ timestamp,
11
+ varchar,
12
+ } from "drizzle-orm/pg-core";
13
+
14
+ // Customers Table - Core customer information
15
+ export const customers = pgTable(
16
+ "customers",
17
+ {
18
+ id: text("id")
19
+ .primaryKey()
20
+ .$defaultFn(() => createId()),
21
+
22
+ // Basic Info
23
+ userId: text("user_id").notNull().unique(), // Links to auth system
24
+ email: varchar("email", { length: 255 }).notNull().unique(),
25
+ firstName: varchar("first_name", { length: 100 }).notNull(),
26
+ lastName: varchar("last_name", { length: 100 }).notNull(),
27
+ phoneNumber: varchar("phone_number", { length: 20 }),
28
+ dateOfBirth: timestamp("date_of_birth", { withTimezone: true }),
29
+
30
+ // Customer Status
31
+ status: varchar("status", { length: 20 })
32
+ .notNull()
33
+ .default("ACTIVE")
34
+ .$type<"ACTIVE" | "INACTIVE" | "SUSPENDED" | "BANNED">(),
35
+
36
+ customerType: varchar("customer_type", { length: 30 })
37
+ .notNull()
38
+ .default("RETAIL")
39
+ .$type<"RETAIL" | "WHOLESALE" | "VIP" | "BUSINESS">(),
40
+
41
+ // Customer Source and Registration
42
+ registrationSource: varchar("registration_source", { length: 30 })
43
+ .notNull()
44
+ .default("ONLINE")
45
+ .$type<"ONLINE" | "WALK_IN" | "PHONE" | "REFERRAL" | "SOCIAL_MEDIA">(),
46
+
47
+ storeLocationId: text("store_location_id"), // For walk-in customers
48
+
49
+ // Profile Information
50
+ gender: varchar("gender", { length: 20 }).$type<
51
+ "MALE" | "FEMALE" | "OTHER" | "PREFER_NOT_TO_SAY"
52
+ >(),
53
+ profilePicture: text("profile_picture"),
54
+ bio: text("bio"),
55
+
56
+ // Preferences
57
+ preferredLanguage: varchar("preferred_language", { length: 10 })
58
+ .notNull()
59
+ .default("en"),
60
+ preferredCurrency: varchar("preferred_currency", { length: 3 })
61
+ .notNull()
62
+ .default("USD"),
63
+ timezone: varchar("timezone", { length: 50 }).notNull().default("UTC"),
64
+
65
+ // Communication Preferences
66
+ marketingConsent: boolean("marketing_consent").notNull().default(false),
67
+ emailNotifications: boolean("email_notifications").notNull().default(true),
68
+ smsNotifications: boolean("sms_notifications").notNull().default(false),
69
+ pushNotifications: boolean("push_notifications").notNull().default(true),
70
+
71
+ // Purchase Metrics
72
+ totalOrders: integer("total_orders").notNull().default(0),
73
+ totalSpent: decimal("total_spent", { precision: 12, scale: 2 })
74
+ .notNull()
75
+ .default("0.00"),
76
+ averageOrderValue: decimal("average_order_value", {
77
+ precision: 12,
78
+ scale: 2,
79
+ })
80
+ .notNull()
81
+ .default("0.00"),
82
+
83
+ // Interaction Metrics
84
+ totalVisits: integer("total_visits").notNull().default(0),
85
+ totalInteractions: integer("total_interactions").notNull().default(0),
86
+
87
+ // Tracking
88
+ lastActiveAt: timestamp("last_active_at", { withTimezone: true }),
89
+ lastOrderAt: timestamp("last_order_at", { withTimezone: true }),
90
+ firstOrderAt: timestamp("first_order_at", { withTimezone: true }),
91
+
92
+ // Timestamps
93
+ createdAt: timestamp("created_at", { withTimezone: true })
94
+ .defaultNow()
95
+ .notNull(),
96
+ updatedAt: timestamp("updated_at", { withTimezone: true })
97
+ .defaultNow()
98
+ .notNull(),
99
+ },
100
+ (table) => ({
101
+ userIdIndex: index("idx_customers_user_id").on(table.userId),
102
+ emailIndex: index("idx_customers_email").on(table.email),
103
+ statusIndex: index("idx_customers_status").on(table.status),
104
+ typeIndex: index("idx_customers_type").on(table.customerType),
105
+ registrationSourceIndex: index("idx_customers_registration_source").on(
106
+ table.registrationSource,
107
+ ),
108
+ storeLocationIndex: index("idx_customers_store_location").on(
109
+ table.storeLocationId,
110
+ ),
111
+ lastActiveIndex: index("idx_customers_last_active").on(table.lastActiveAt),
112
+ }),
113
+ );
114
+
115
+ // Customer Addresses Table
116
+ export const customerAddresses = pgTable(
117
+ "customer_addresses",
118
+ {
119
+ id: text("id")
120
+ .primaryKey()
121
+ .$defaultFn(() => createId()),
122
+
123
+ customerId: text("customer_id")
124
+ .notNull()
125
+ .references(() => customers.id, { onDelete: "cascade" }),
126
+
127
+ // Address Type
128
+ type: varchar("type", { length: 20 })
129
+ .notNull()
130
+ .$type<"BILLING" | "SHIPPING" | "BOTH">(),
131
+
132
+ // Address Details
133
+ label: varchar("label", { length: 50 }), // e.g., "Home", "Work", "Office"
134
+ firstName: varchar("first_name", { length: 100 }).notNull(),
135
+ lastName: varchar("last_name", { length: 100 }).notNull(),
136
+ company: varchar("company", { length: 100 }),
137
+ addressLine1: text("address_line_1").notNull(),
138
+ addressLine2: text("address_line_2"),
139
+ city: varchar("city", { length: 100 }).notNull(),
140
+ state: varchar("state", { length: 100 }).notNull(),
141
+ postalCode: varchar("postal_code", { length: 20 }).notNull(),
142
+ country: varchar("country", { length: 2 }).notNull(), // ISO country code
143
+ phoneNumber: varchar("phone_number", { length: 20 }),
144
+
145
+ // Status
146
+ isDefault: boolean("is_default").notNull().default(false),
147
+ isActive: boolean("is_active").notNull().default(true),
148
+
149
+ // Verification
150
+ isVerified: boolean("is_verified").notNull().default(false),
151
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
152
+
153
+ // Timestamps
154
+ createdAt: timestamp("created_at", { withTimezone: true })
155
+ .defaultNow()
156
+ .notNull(),
157
+ updatedAt: timestamp("updated_at", { withTimezone: true })
158
+ .defaultNow()
159
+ .notNull(),
160
+ },
161
+ (table) => ({
162
+ customerIdIndex: index("idx_addresses_customer_id").on(table.customerId),
163
+ typeIndex: index("idx_addresses_type").on(table.type),
164
+ defaultIndex: index("idx_addresses_default").on(table.isDefault),
165
+ countryIndex: index("idx_addresses_country").on(table.country),
166
+ }),
167
+ );
168
+
169
+ // Customer Support Tickets Table
170
+ export const customerSupportTickets = pgTable(
171
+ "customer_support_tickets",
172
+ {
173
+ id: text("id")
174
+ .primaryKey()
175
+ .$defaultFn(() => createId()),
176
+
177
+ customerId: text("customer_id")
178
+ .notNull()
179
+ .references(() => customers.id, { onDelete: "cascade" }),
180
+
181
+ // Ticket Details
182
+ ticketNumber: varchar("ticket_number", { length: 20 }).notNull().unique(),
183
+ subject: varchar("subject", { length: 255 }).notNull(),
184
+ description: text("description").notNull(),
185
+ category: varchar("category", { length: 50 })
186
+ .notNull()
187
+ .$type<
188
+ | "GENERAL_INQUIRY"
189
+ | "ORDER_ISSUE"
190
+ | "BILLING_ISSUE"
191
+ | "TECHNICAL_SUPPORT"
192
+ | "PRODUCT_QUESTION"
193
+ | "RETURN_REFUND"
194
+ | "ACCOUNT_ISSUE"
195
+ | "COMPLAINT"
196
+ | "FEATURE_REQUEST"
197
+ >(),
198
+
199
+ // Priority and Status
200
+ priority: varchar("priority", { length: 20 })
201
+ .notNull()
202
+ .default("MEDIUM")
203
+ .$type<"LOW" | "MEDIUM" | "HIGH" | "URGENT" | "CRITICAL">(),
204
+
205
+ status: varchar("status", { length: 20 })
206
+ .notNull()
207
+ .default("OPEN")
208
+ .$type<
209
+ "OPEN" | "IN_PROGRESS" | "PENDING_CUSTOMER" | "RESOLVED" | "CLOSED"
210
+ >(),
211
+
212
+ // Assignment
213
+ assignedTo: text("assigned_to"), // Admin/Support user ID
214
+ assignedAt: timestamp("assigned_at", { withTimezone: true }),
215
+
216
+ // Resolution
217
+ resolution: text("resolution"),
218
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
219
+ closedAt: timestamp("closed_at", { withTimezone: true }),
220
+
221
+ // Customer Satisfaction
222
+ satisfactionRating: integer("satisfaction_rating"), // 1-5
223
+ satisfactionFeedback: text("satisfaction_feedback"),
224
+
225
+ // Metadata
226
+ tags: jsonb("tags").$type<string[]>().default([]),
227
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
228
+
229
+ // Timestamps
230
+ createdAt: timestamp("created_at", { withTimezone: true })
231
+ .defaultNow()
232
+ .notNull(),
233
+ updatedAt: timestamp("updated_at", { withTimezone: true })
234
+ .defaultNow()
235
+ .notNull(),
236
+ },
237
+ (table) => ({
238
+ customerIdIndex: index("idx_tickets_customer_id").on(table.customerId),
239
+ ticketNumberIndex: index("idx_tickets_number").on(table.ticketNumber),
240
+ statusIndex: index("idx_tickets_status").on(table.status),
241
+ priorityIndex: index("idx_tickets_priority").on(table.priority),
242
+ categoryIndex: index("idx_tickets_category").on(table.category),
243
+ assignedIndex: index("idx_tickets_assigned").on(table.assignedTo),
244
+ createdAtIndex: index("idx_tickets_created_at").on(table.createdAt),
245
+ }),
246
+ );
247
+
248
+ // Customer Reviews Table
249
+ export const customerReviews = pgTable(
250
+ "customer_reviews",
251
+ {
252
+ id: text("id")
253
+ .primaryKey()
254
+ .$defaultFn(() => createId()),
255
+
256
+ customerId: text("customer_id")
257
+ .notNull()
258
+ .references(() => customers.id, { onDelete: "cascade" }),
259
+
260
+ // Review Target
261
+ targetType: varchar("target_type", { length: 30 })
262
+ .notNull()
263
+ .$type<"PRODUCT" | "STORE" | "ORDER" | "SERVICE">(),
264
+ targetId: text("target_id").notNull(),
265
+
266
+ // Review Content
267
+ rating: integer("rating").notNull(), // 1-5
268
+ title: varchar("title", { length: 255 }),
269
+ reviewText: text("review_text"),
270
+ images: jsonb("images").$type<string[]>().default([]),
271
+
272
+ // Review Status
273
+ status: varchar("status", { length: 20 })
274
+ .notNull()
275
+ .default("PENDING")
276
+ .$type<"PENDING" | "APPROVED" | "REJECTED" | "FLAGGED">(),
277
+
278
+ isVerifiedPurchase: boolean("is_verified_purchase")
279
+ .notNull()
280
+ .default(false),
281
+
282
+ // Moderation
283
+ moderatedBy: text("moderated_by"),
284
+ moderatedAt: timestamp("moderated_at", { withTimezone: true }),
285
+ moderationNotes: text("moderation_notes"),
286
+
287
+ // Helpfulness
288
+ helpfulVotes: integer("helpful_votes").notNull().default(0),
289
+ unhelpfulVotes: integer("unhelpful_votes").notNull().default(0),
290
+
291
+ // Timestamps
292
+ createdAt: timestamp("created_at", { withTimezone: true })
293
+ .defaultNow()
294
+ .notNull(),
295
+ updatedAt: timestamp("updated_at", { withTimezone: true })
296
+ .defaultNow()
297
+ .notNull(),
298
+ },
299
+ (table) => ({
300
+ customerIdIndex: index("idx_reviews_customer_id").on(table.customerId),
301
+ targetIndex: index("idx_reviews_target").on(
302
+ table.targetType,
303
+ table.targetId,
304
+ ),
305
+ ratingIndex: index("idx_reviews_rating").on(table.rating),
306
+ statusIndex: index("idx_reviews_status").on(table.status),
307
+ verifiedIndex: index("idx_reviews_verified").on(table.isVerifiedPurchase),
308
+ createdAtIndex: index("idx_reviews_created_at").on(table.createdAt),
309
+ }),
310
+ );
311
+
312
+ // Customer Visit History Table
313
+ export const customerVisitHistory = pgTable(
314
+ "customer_visit_history",
315
+ {
316
+ id: text("id")
317
+ .primaryKey()
318
+ .$defaultFn(() => createId()),
319
+
320
+ customerId: text("customer_id")
321
+ .notNull()
322
+ .references(() => customers.id, { onDelete: "cascade" }),
323
+
324
+ // Visit Details
325
+ visitType: varchar("visit_type", { length: 30 })
326
+ .notNull()
327
+ .$type<
328
+ | "WEBSITE"
329
+ | "STORE_VISIT"
330
+ | "PHONE_CALL"
331
+ | "EMAIL"
332
+ | "CHAT"
333
+ | "SOCIAL_MEDIA"
334
+ >(),
335
+
336
+ storeLocationId: text("store_location_id"), // For physical visits
337
+ sessionId: text("session_id"), // For online visits
338
+
339
+ // Visit Data
340
+ duration: integer("duration"), // in minutes
341
+ pagesViewed: integer("pages_viewed"), // for online visits
342
+ productsViewed: jsonb("products_viewed").$type<string[]>().default([]),
343
+
344
+ // Interaction Details
345
+ interactionCount: integer("interaction_count").notNull().default(0),
346
+ hasInteraction: boolean("has_interaction").notNull().default(false),
347
+
348
+ // Visit Outcome
349
+ outcome: varchar("outcome", { length: 30 }).$type<
350
+ "BROWSING" | "INQUIRY" | "PURCHASE" | "SUPPORT" | "COMPLAINT" | "RETURN"
351
+ >(),
352
+
353
+ // Notes and Context
354
+ notes: text("notes"),
355
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
356
+
357
+ // Timestamps
358
+ visitStartAt: timestamp("visit_start_at", { withTimezone: true }).notNull(),
359
+ visitEndAt: timestamp("visit_end_at", { withTimezone: true }),
360
+ createdAt: timestamp("created_at", { withTimezone: true })
361
+ .defaultNow()
362
+ .notNull(),
363
+ },
364
+ (table) => ({
365
+ customerIdIndex: index("idx_visits_customer_id").on(table.customerId),
366
+ visitTypeIndex: index("idx_visits_type").on(table.visitType),
367
+ storeLocationIndex: index("idx_visits_store_location").on(
368
+ table.storeLocationId,
369
+ ),
370
+ visitDateIndex: index("idx_visits_date").on(table.visitStartAt),
371
+ outcomeIndex: index("idx_visits_outcome").on(table.outcome),
372
+ }),
373
+ );
374
+
375
+ // Customer Interaction History Table
376
+ export const customerInteractionHistory = pgTable(
377
+ "customer_interaction_history",
378
+ {
379
+ id: text("id")
380
+ .primaryKey()
381
+ .$defaultFn(() => createId()),
382
+
383
+ customerId: text("customer_id")
384
+ .notNull()
385
+ .references(() => customers.id, { onDelete: "cascade" }),
386
+
387
+ visitId: text("visit_id").references(() => customerVisitHistory.id, {
388
+ onDelete: "cascade",
389
+ }),
390
+
391
+ // Interaction Details
392
+ interactionType: varchar("interaction_type", { length: 30 })
393
+ .notNull()
394
+ .$type<
395
+ | "PRODUCT_VIEW"
396
+ | "PRODUCT_INQUIRY"
397
+ | "ADD_TO_CART"
398
+ | "WISHLIST_ADD"
399
+ | "SEARCH"
400
+ | "FILTER_USE"
401
+ | "SUPPORT_REQUEST"
402
+ | "COMPLAINT"
403
+ | "FEEDBACK"
404
+ | "NEWSLETTER_SIGNUP"
405
+ >(),
406
+
407
+ // Target Information
408
+ targetType: varchar("target_type", { length: 30 }).$type<
409
+ "PRODUCT" | "CATEGORY" | "BRAND" | "PAGE" | "FEATURE" | "SUPPORT"
410
+ >(),
411
+ targetId: text("target_id"),
412
+
413
+ // Interaction Data
414
+ interactionData: jsonb("interaction_data")
415
+ .$type<Record<string, unknown>>()
416
+ .default({}),
417
+
418
+ // Staff/Agent Information (for in-store or assisted interactions)
419
+ staffId: text("staff_id"),
420
+ staffName: varchar("staff_name", { length: 100 }),
421
+
422
+ // Context
423
+ description: text("description"),
424
+ outcome: varchar("outcome", { length: 30 }).$type<
425
+ "POSITIVE" | "NEUTRAL" | "NEGATIVE" | "UNRESOLVED" | "COMPLETED"
426
+ >(),
427
+
428
+ // Timestamps
429
+ createdAt: timestamp("created_at", { withTimezone: true })
430
+ .defaultNow()
431
+ .notNull(),
432
+ },
433
+ (table) => ({
434
+ customerIdIndex: index("idx_interactions_customer_id").on(table.customerId),
435
+ visitIdIndex: index("idx_interactions_visit_id").on(table.visitId),
436
+ interactionTypeIndex: index("idx_interactions_type").on(
437
+ table.interactionType,
438
+ ),
439
+ targetIndex: index("idx_interactions_target").on(
440
+ table.targetType,
441
+ table.targetId,
442
+ ),
443
+ staffIndex: index("idx_interactions_staff").on(table.staffId),
444
+ createdAtIndex: index("idx_interactions_created_at").on(table.createdAt),
445
+ }),
446
+ );
447
+
448
+ // Customer Notes Table
449
+ export const customerNotes = pgTable(
450
+ "customer_notes",
451
+ {
452
+ id: text("id")
453
+ .primaryKey()
454
+ .$defaultFn(() => createId()),
455
+
456
+ customerId: text("customer_id")
457
+ .notNull()
458
+ .references(() => customers.id, { onDelete: "cascade" }),
459
+
460
+ // Note Details
461
+ noteType: varchar("note_type", { length: 30 })
462
+ .notNull()
463
+ .$type<
464
+ | "GENERAL"
465
+ | "PREFERENCE"
466
+ | "COMPLAINT"
467
+ | "COMPLIMENT"
468
+ | "SPECIAL_REQUEST"
469
+ | "VIP_NOTE"
470
+ | "ALERT"
471
+ | "FOLLOW_UP"
472
+ >(),
473
+
474
+ title: varchar("title", { length: 255 }),
475
+ content: text("content").notNull(),
476
+
477
+ // Priority and Visibility
478
+ priority: varchar("priority", { length: 20 })
479
+ .notNull()
480
+ .default("NORMAL")
481
+ .$type<"LOW" | "NORMAL" | "HIGH" | "URGENT">(),
482
+
483
+ isVisible: boolean("is_visible").notNull().default(true),
484
+ isAlert: boolean("is_alert").notNull().default(false),
485
+
486
+ // Staff Information
487
+ createdBy: text("created_by").notNull(), // Staff/user ID
488
+ createdByName: varchar("created_by_name", { length: 100 }).notNull(),
489
+
490
+ // Context
491
+ relatedOrderId: text("related_order_id"),
492
+ relatedTicketId: text("related_ticket_id"),
493
+
494
+ // Follow-up
495
+ followUpRequired: boolean("follow_up_required").notNull().default(false),
496
+ followUpDate: timestamp("follow_up_date", { withTimezone: true }),
497
+ followUpCompleted: boolean("follow_up_completed").notNull().default(false),
498
+
499
+ // Timestamps
500
+ createdAt: timestamp("created_at", { withTimezone: true })
501
+ .defaultNow()
502
+ .notNull(),
503
+ updatedAt: timestamp("updated_at", { withTimezone: true })
504
+ .defaultNow()
505
+ .notNull(),
506
+ },
507
+ (table) => ({
508
+ customerIdIndex: index("idx_notes_customer_id").on(table.customerId),
509
+ noteTypeIndex: index("idx_notes_type").on(table.noteType),
510
+ priorityIndex: index("idx_notes_priority").on(table.priority),
511
+ alertIndex: index("idx_notes_alert").on(table.isAlert),
512
+ followUpIndex: index("idx_notes_follow_up").on(
513
+ table.followUpRequired,
514
+ table.followUpDate,
515
+ ),
516
+ createdByIndex: index("idx_notes_created_by").on(table.createdBy),
517
+ }),
518
+ );
519
+
520
+ // Customer Preferences Table
521
+ export const customerPreferences = pgTable(
522
+ "customer_preferences",
523
+ {
524
+ id: text("id")
525
+ .primaryKey()
526
+ .$defaultFn(() => createId()),
527
+
528
+ customerId: text("customer_id")
529
+ .notNull()
530
+ .references(() => customers.id, { onDelete: "cascade" })
531
+ .unique(),
532
+
533
+ // Shopping Preferences
534
+ preferredCategories: jsonb("preferred_categories")
535
+ .$type<string[]>()
536
+ .default([]),
537
+ preferredBrands: jsonb("preferred_brands").$type<string[]>().default([]),
538
+ priceRange: jsonb("price_range")
539
+ .$type<{ min?: number; max?: number }>()
540
+ .default({}),
541
+
542
+ // Notification Preferences
543
+ emailMarketing: boolean("email_marketing").notNull().default(false),
544
+ smsMarketing: boolean("sms_marketing").notNull().default(false),
545
+ pushMarketing: boolean("push_marketing").notNull().default(false),
546
+ orderUpdates: boolean("order_updates").notNull().default(true),
547
+ productRecommendations: boolean("product_recommendations")
548
+ .notNull()
549
+ .default(true),
550
+ priceDropAlerts: boolean("price_drop_alerts").notNull().default(false),
551
+ backInStockAlerts: boolean("back_in_stock_alerts").notNull().default(false),
552
+
553
+ // Privacy Preferences
554
+ dataSharing: boolean("data_sharing").notNull().default(false),
555
+ analyticsTracking: boolean("analytics_tracking").notNull().default(true),
556
+ personalizedExperience: boolean("personalized_experience")
557
+ .notNull()
558
+ .default(true),
559
+
560
+ // Custom Preferences
561
+ customPreferences: jsonb("custom_preferences")
562
+ .$type<Record<string, unknown>>()
563
+ .default({}),
564
+
565
+ // Timestamps
566
+ createdAt: timestamp("created_at", { withTimezone: true })
567
+ .defaultNow()
568
+ .notNull(),
569
+ updatedAt: timestamp("updated_at", { withTimezone: true })
570
+ .defaultNow()
571
+ .notNull(),
572
+ },
573
+ (table) => ({
574
+ customerIdIndex: index("idx_preferences_customer_id").on(table.customerId),
575
+ }),
576
+ );