@instockng/api-client 1.0.6 → 1.0.8

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 (178) hide show
  1. package/dist/apps/backend/src/generated/zod/index.d.ts +1114 -0
  2. package/dist/apps/backend/src/generated/zod/index.js +670 -0
  3. package/dist/apps/backend/src/http-app.d.ts +40 -0
  4. package/dist/apps/backend/src/http-app.js +134 -0
  5. package/dist/apps/backend/src/lib/brand-response.d.ts +14 -0
  6. package/dist/apps/backend/src/lib/brand-response.js +8 -0
  7. package/dist/apps/backend/src/lib/cart-helpers.d.ts +282 -0
  8. package/dist/apps/backend/src/lib/cart-helpers.js +121 -0
  9. package/dist/apps/backend/src/lib/cart-recovery.d.ts +30 -0
  10. package/dist/apps/backend/src/lib/cart-recovery.js +147 -0
  11. package/dist/apps/backend/src/lib/cart-response.d.ts +121 -0
  12. package/dist/apps/backend/src/lib/cart-response.js +150 -0
  13. package/dist/apps/backend/src/lib/clerk.d.ts +18 -0
  14. package/dist/apps/backend/src/lib/clerk.js +190 -0
  15. package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +64 -0
  16. package/dist/apps/backend/src/lib/delivery-zone-response.js +24 -0
  17. package/dist/apps/backend/src/lib/discount-code-response.d.ts +42 -0
  18. package/dist/apps/backend/src/lib/discount-code-response.js +19 -0
  19. package/dist/apps/backend/src/lib/discount.d.ts +20 -0
  20. package/dist/apps/backend/src/lib/discount.js +35 -0
  21. package/dist/apps/backend/src/lib/inventory.d.ts +26 -0
  22. package/dist/apps/backend/src/lib/inventory.js +160 -0
  23. package/dist/apps/backend/src/lib/meta-capi.d.ts +53 -0
  24. package/dist/apps/backend/src/lib/meta-capi.js +151 -0
  25. package/dist/apps/backend/src/lib/openapi.d.ts +36 -0
  26. package/dist/apps/backend/src/lib/openapi.js +69 -0
  27. package/dist/apps/backend/src/lib/order-recovery.d.ts +459 -0
  28. package/dist/apps/backend/src/lib/order-recovery.js +378 -0
  29. package/dist/apps/backend/src/lib/order-response.d.ts +138 -0
  30. package/dist/apps/backend/src/lib/order-response.js +61 -0
  31. package/dist/apps/backend/src/lib/pricing.d.ts +39 -0
  32. package/dist/apps/backend/src/lib/pricing.js +62 -0
  33. package/dist/apps/backend/src/lib/prisma.d.ts +9 -0
  34. package/dist/apps/backend/src/lib/prisma.js +30 -0
  35. package/dist/apps/backend/src/lib/product-response.d.ts +82 -0
  36. package/dist/apps/backend/src/lib/product-response.js +29 -0
  37. package/dist/apps/backend/src/lib/sentry.d.ts +48 -0
  38. package/dist/apps/backend/src/lib/sentry.js +180 -0
  39. package/dist/apps/backend/src/lib/utils.d.ts +32 -0
  40. package/dist/apps/backend/src/lib/utils.js +63 -0
  41. package/dist/apps/backend/src/middleware/clerk-auth.d.ts +8 -0
  42. package/dist/apps/backend/src/middleware/clerk-auth.js +89 -0
  43. package/dist/apps/backend/src/middleware/cors.d.ts +8 -0
  44. package/dist/apps/backend/src/middleware/cors.js +11 -0
  45. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.d.ts +62 -0
  46. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +180 -0
  47. package/dist/apps/backend/src/notifications/producers/order-notification.d.ts +9 -0
  48. package/dist/apps/backend/src/notifications/producers/order-notification.js +18 -0
  49. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.d.ts +10 -0
  50. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.js +11 -0
  51. package/dist/apps/backend/src/routes/admin/abandoned-carts.d.ts +605 -0
  52. package/dist/apps/backend/src/routes/admin/abandoned-carts.js +194 -0
  53. package/dist/apps/backend/src/routes/admin/brands.d.ts +175 -0
  54. package/dist/apps/backend/src/routes/admin/brands.js +118 -0
  55. package/dist/apps/backend/src/routes/admin/customers.d.ts +308 -0
  56. package/dist/apps/backend/src/routes/admin/customers.js +39 -0
  57. package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +446 -0
  58. package/dist/apps/backend/src/routes/admin/delivery-zones.js +300 -0
  59. package/dist/apps/backend/src/routes/admin/discount-codes.d.ts +478 -0
  60. package/dist/apps/backend/src/routes/admin/discount-codes.js +418 -0
  61. package/dist/apps/backend/src/routes/admin/inventory.d.ts +273 -0
  62. package/dist/apps/backend/src/routes/admin/inventory.js +199 -0
  63. package/dist/apps/backend/src/routes/admin/orders.d.ts +1780 -0
  64. package/dist/apps/backend/src/routes/admin/orders.js +552 -0
  65. package/dist/apps/backend/src/routes/admin/products.d.ts +860 -0
  66. package/dist/apps/backend/src/routes/admin/products.js +126 -0
  67. package/dist/apps/backend/src/routes/admin/stats.d.ts +290 -0
  68. package/dist/apps/backend/src/routes/admin/stats.js +55 -0
  69. package/dist/apps/backend/src/routes/admin/variants.d.ts +239 -0
  70. package/dist/apps/backend/src/routes/admin/variants.js +197 -0
  71. package/dist/apps/backend/src/routes/admin/warehouses.d.ts +373 -0
  72. package/dist/apps/backend/src/routes/admin/warehouses.js +123 -0
  73. package/dist/apps/backend/src/routes/public/brands.d.ts +40 -0
  74. package/dist/apps/backend/src/routes/public/brands.js +38 -0
  75. package/dist/apps/backend/src/routes/public/carts.d.ts +2657 -0
  76. package/dist/apps/backend/src/routes/public/carts.js +778 -0
  77. package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +37 -0
  78. package/dist/apps/backend/src/routes/public/delivery-zones.js +64 -0
  79. package/dist/apps/backend/src/routes/public/orders.d.ts +609 -0
  80. package/dist/apps/backend/src/routes/public/orders.js +184 -0
  81. package/dist/apps/backend/src/routes/public/products.d.ts +449 -0
  82. package/dist/apps/backend/src/routes/public/products.js +133 -0
  83. package/dist/apps/backend/src/types/index.d.ts +43 -0
  84. package/dist/apps/backend/src/types/index.js +2 -0
  85. package/dist/apps/backend/src/validators/brand.d.ts +17 -0
  86. package/dist/apps/backend/src/validators/brand.js +15 -0
  87. package/dist/apps/backend/src/validators/delivery-zone.d.ts +35 -0
  88. package/dist/apps/backend/src/validators/delivery-zone.js +55 -0
  89. package/dist/apps/backend/src/validators/discount-code.d.ts +74 -0
  90. package/dist/apps/backend/src/validators/discount-code.js +50 -0
  91. package/dist/apps/backend/src/validators/inventory.d.ts +20 -0
  92. package/dist/apps/backend/src/validators/inventory.js +15 -0
  93. package/dist/apps/backend/src/validators/order.d.ts +58 -0
  94. package/dist/apps/backend/src/validators/order.js +62 -0
  95. package/dist/apps/backend/src/validators/product.d.ts +18 -0
  96. package/dist/apps/backend/src/validators/product.js +19 -0
  97. package/dist/apps/backend/src/validators/variant.d.ts +19 -0
  98. package/dist/apps/backend/src/validators/variant.js +19 -0
  99. package/dist/apps/backend/src/validators/warehouse.d.ts +15 -0
  100. package/dist/apps/backend/src/validators/warehouse.js +15 -0
  101. package/dist/fetchers/orders.d.ts +258 -1
  102. package/dist/hooks/admin/orders.d.ts +285 -3
  103. package/dist/hooks/admin/orders.js +7 -4
  104. package/dist/hooks/public/orders.d.ts +258 -1
  105. package/dist/packages/api-client/src/backend-types.d.ts +10 -0
  106. package/dist/packages/api-client/src/backend-types.js +10 -0
  107. package/dist/packages/api-client/src/client.d.ts +20 -0
  108. package/dist/packages/api-client/src/client.js +40 -0
  109. package/dist/packages/api-client/src/enum-types.d.ts +8 -0
  110. package/dist/packages/api-client/src/enum-types.js +5 -0
  111. package/dist/packages/api-client/src/fetchers/brands.d.ts +25 -0
  112. package/dist/packages/api-client/src/fetchers/brands.js +26 -0
  113. package/dist/packages/api-client/src/fetchers/carts.d.ts +2337 -0
  114. package/dist/packages/api-client/src/fetchers/carts.js +174 -0
  115. package/dist/packages/api-client/src/fetchers/delivery-zones.d.ts +30 -0
  116. package/dist/packages/api-client/src/fetchers/delivery-zones.js +26 -0
  117. package/dist/packages/api-client/src/fetchers/index.d.ts +22 -0
  118. package/dist/packages/api-client/src/fetchers/index.js +22 -0
  119. package/dist/packages/api-client/src/fetchers/orders.d.ts +544 -0
  120. package/dist/packages/api-client/src/fetchers/orders.js +44 -0
  121. package/dist/packages/api-client/src/fetchers/products.d.ts +386 -0
  122. package/dist/packages/api-client/src/fetchers/products.js +42 -0
  123. package/dist/packages/api-client/src/hooks/admin/abandoned-carts.d.ts +535 -0
  124. package/dist/packages/api-client/src/hooks/admin/abandoned-carts.js +83 -0
  125. package/dist/packages/api-client/src/hooks/admin/brands.d.ts +79 -0
  126. package/dist/packages/api-client/src/hooks/admin/brands.js +108 -0
  127. package/dist/packages/api-client/src/hooks/admin/customers.d.ts +280 -0
  128. package/dist/packages/api-client/src/hooks/admin/customers.js +26 -0
  129. package/dist/packages/api-client/src/hooks/admin/delivery-zones.d.ts +278 -0
  130. package/dist/packages/api-client/src/hooks/admin/delivery-zones.js +176 -0
  131. package/dist/packages/api-client/src/hooks/admin/discount-codes.d.ts +299 -0
  132. package/dist/packages/api-client/src/hooks/admin/discount-codes.js +165 -0
  133. package/dist/packages/api-client/src/hooks/admin/index.d.ts +16 -0
  134. package/dist/packages/api-client/src/hooks/admin/index.js +16 -0
  135. package/dist/packages/api-client/src/hooks/admin/inventory.d.ts +224 -0
  136. package/dist/packages/api-client/src/hooks/admin/inventory.js +107 -0
  137. package/dist/packages/api-client/src/hooks/admin/orders.d.ts +1674 -0
  138. package/dist/packages/api-client/src/hooks/admin/orders.js +178 -0
  139. package/dist/packages/api-client/src/hooks/admin/products.d.ts +374 -0
  140. package/dist/packages/api-client/src/hooks/admin/products.js +89 -0
  141. package/dist/packages/api-client/src/hooks/admin/stats.d.ts +279 -0
  142. package/dist/packages/api-client/src/hooks/admin/stats.js +25 -0
  143. package/dist/packages/api-client/src/hooks/admin/variants.d.ts +115 -0
  144. package/dist/packages/api-client/src/hooks/admin/variants.js +127 -0
  145. package/dist/packages/api-client/src/hooks/admin/warehouses.d.ts +277 -0
  146. package/dist/packages/api-client/src/hooks/admin/warehouses.js +108 -0
  147. package/dist/packages/api-client/src/hooks/public/brands.d.ts +33 -0
  148. package/dist/packages/api-client/src/hooks/public/brands.js +30 -0
  149. package/dist/packages/api-client/src/hooks/public/carts.d.ts +2407 -0
  150. package/dist/packages/api-client/src/hooks/public/carts.js +213 -0
  151. package/dist/packages/api-client/src/hooks/public/delivery-zones.d.ts +36 -0
  152. package/dist/packages/api-client/src/hooks/public/delivery-zones.js +28 -0
  153. package/dist/packages/api-client/src/hooks/public/index.d.ts +10 -0
  154. package/dist/packages/api-client/src/hooks/public/index.js +10 -0
  155. package/dist/packages/api-client/src/hooks/public/orders.d.ts +563 -0
  156. package/dist/packages/api-client/src/hooks/public/orders.js +50 -0
  157. package/dist/packages/api-client/src/hooks/public/products.d.ts +398 -0
  158. package/dist/packages/api-client/src/hooks/public/products.js +47 -0
  159. package/dist/packages/api-client/src/hooks/use-query-unwrapped.d.ts +20 -0
  160. package/dist/packages/api-client/src/hooks/use-query-unwrapped.js +22 -0
  161. package/dist/packages/api-client/src/hooks/useApiConfig.d.ts +12 -0
  162. package/dist/packages/api-client/src/hooks/useApiConfig.js +14 -0
  163. package/dist/packages/api-client/src/index.d.ts +20 -0
  164. package/dist/packages/api-client/src/index.js +25 -0
  165. package/dist/packages/api-client/src/provider.d.ts +36 -0
  166. package/dist/packages/api-client/src/provider.js +54 -0
  167. package/dist/packages/api-client/src/rpc-client.d.ts +9639 -0
  168. package/dist/packages/api-client/src/rpc-client.js +78 -0
  169. package/dist/packages/api-client/src/rpc-types.d.ts +76 -0
  170. package/dist/packages/api-client/src/rpc-types.js +7 -0
  171. package/dist/packages/api-client/src/types.d.ts +34 -0
  172. package/dist/packages/api-client/src/types.js +16 -0
  173. package/dist/packages/api-client/src/utils/query-keys.d.ts +106 -0
  174. package/dist/packages/api-client/src/utils/query-keys.js +108 -0
  175. package/dist/rpc-client.d.ts +891 -319
  176. package/dist/utils/query-keys.d.ts +1 -1
  177. package/dist/utils/query-keys.js +1 -1
  178. package/package.json +1 -1
@@ -0,0 +1,378 @@
1
+ import { OrderStatus, ProspectReason, NoteType } from '@prisma/client';
2
+ import { subHours, addHours } from 'date-fns';
3
+ import { generateDiscountCode } from './discount';
4
+ import { ORDER_INCLUDE_FULL } from './cart-helpers';
5
+ // Recovery schedule configuration
6
+ // Note: First email sent immediately after marking as prospect (since manual contact already attempted)
7
+ export const PROSPECT_RECOVERY_SCHEDULE = [
8
+ {
9
+ attempt: 1,
10
+ delayHours: 0, // Immediate - manual contact already attempted
11
+ discountPercentage: 0,
12
+ template: 'ProspectRecoveryReminder',
13
+ subject: 'We tried reaching you about your order'
14
+ },
15
+ {
16
+ attempt: 2,
17
+ delayHours: 8,
18
+ discountPercentage: 5,
19
+ template: 'ProspectRecovery',
20
+ subject: 'Your order is waiting - 5% discount inside'
21
+ },
22
+ {
23
+ attempt: 3,
24
+ delayHours: 24, // Next day
25
+ discountPercentage: 10,
26
+ template: 'ProspectRecovery',
27
+ subject: 'Still interested? Save 10% today'
28
+ },
29
+ {
30
+ attempt: 4,
31
+ delayHours: 48, // Day 2
32
+ discountPercentage: 15,
33
+ template: 'ProspectRecoveryFinal',
34
+ subject: 'Final chance - 15% off expires soon'
35
+ }
36
+ ];
37
+ // Auto-cancel after 72 hours
38
+ export const AUTO_CANCEL_HOURS = 72;
39
+ /**
40
+ * Find prospect orders ready for recovery attempt
41
+ */
42
+ export async function findProspectOrdersForRecovery(prisma, attemptNumber) {
43
+ const schedule = PROSPECT_RECOVERY_SCHEDULE[attemptNumber - 1];
44
+ if (!schedule) {
45
+ throw new Error(`Invalid recovery attempt number: ${attemptNumber}`);
46
+ }
47
+ const windowStart = subHours(new Date(), schedule.delayHours);
48
+ const windowEnd = subHours(windowStart, 1); // 1 hour window
49
+ return prisma.order.findMany({
50
+ where: {
51
+ status: OrderStatus.prospect,
52
+ recoveryAttempts: attemptNumber - 1,
53
+ prospectSince: {
54
+ gte: windowEnd,
55
+ lt: windowStart
56
+ },
57
+ deletedAt: null,
58
+ },
59
+ include: {
60
+ items: {
61
+ include: {
62
+ variant: {
63
+ include: {
64
+ product: true
65
+ }
66
+ }
67
+ }
68
+ },
69
+ brand: true,
70
+ recoveryDiscountCode: true,
71
+ deliveryZone: true
72
+ }
73
+ });
74
+ }
75
+ /**
76
+ * Prepare order for recovery attempt (atomic operation)
77
+ */
78
+ export async function prepareOrderForRecoveryAttempt(prisma, orderId, attemptNumber, brandId) {
79
+ const schedule = PROSPECT_RECOVERY_SCHEDULE[attemptNumber - 1];
80
+ if (!schedule) {
81
+ throw new Error(`Invalid recovery attempt number: ${attemptNumber}`);
82
+ }
83
+ return prisma.$transaction(async (tx) => {
84
+ // Update recovery attempts atomically
85
+ const updateResult = await tx.order.updateMany({
86
+ where: {
87
+ id: orderId,
88
+ status: OrderStatus.prospect,
89
+ recoveryAttempts: attemptNumber - 1
90
+ },
91
+ data: {
92
+ recoveryAttempts: attemptNumber,
93
+ lastRecoveryAttemptAt: new Date()
94
+ }
95
+ });
96
+ // If no rows updated, order was already claimed by another process
97
+ if (updateResult.count === 0) {
98
+ return null;
99
+ }
100
+ // Handle discount code creation/update
101
+ if (schedule.discountPercentage > 0) {
102
+ const order = await tx.order.findUnique({
103
+ where: { id: orderId },
104
+ include: { recoveryDiscountCode: true }
105
+ });
106
+ if (!order)
107
+ return null;
108
+ let discountCodeId = order.recoveryDiscountCodeId;
109
+ if (!discountCodeId) {
110
+ // Create new recovery discount code
111
+ const code = generateDiscountCode();
112
+ const discount = await tx.discountCode.create({
113
+ data: {
114
+ code,
115
+ brandId,
116
+ type: 'percentage',
117
+ value: schedule.discountPercentage,
118
+ usageLimit: 1,
119
+ category: 'recovery',
120
+ description: `Order recovery discount - ${schedule.discountPercentage}% off`,
121
+ validFrom: new Date(),
122
+ validUntil: addHours(new Date(), 72), // Valid for 72 hours
123
+ isActive: true
124
+ }
125
+ });
126
+ discountCodeId = discount.id;
127
+ // Link discount to order
128
+ await tx.order.update({
129
+ where: { id: orderId },
130
+ data: { recoveryDiscountCodeId: discountCodeId }
131
+ });
132
+ }
133
+ else if (order.recoveryDiscountCode) {
134
+ // Update existing discount if percentage increased
135
+ const currentPercentage = order.recoveryDiscountCode.value.toNumber();
136
+ if (schedule.discountPercentage > currentPercentage) {
137
+ await tx.discountCode.update({
138
+ where: { id: discountCodeId },
139
+ data: {
140
+ value: schedule.discountPercentage.toString(),
141
+ description: `Order recovery discount - ${schedule.discountPercentage}% off`,
142
+ validUntil: addHours(new Date(), 72) // Extend validity
143
+ }
144
+ });
145
+ }
146
+ }
147
+ }
148
+ // Return the prepared order
149
+ return tx.order.findUnique({
150
+ where: { id: orderId },
151
+ include: {
152
+ items: {
153
+ include: {
154
+ variant: {
155
+ include: {
156
+ product: true
157
+ }
158
+ }
159
+ }
160
+ },
161
+ brand: true,
162
+ recoveryDiscountCode: true,
163
+ deliveryZone: true,
164
+ notes: {
165
+ orderBy: { createdAt: 'desc' },
166
+ take: 5
167
+ }
168
+ }
169
+ });
170
+ });
171
+ }
172
+ /**
173
+ * Convert prospect order back to pending when customer confirms
174
+ */
175
+ export async function confirmProspectOrder(prisma, orderId, userActionToken) {
176
+ return prisma.$transaction(async (tx) => {
177
+ // Validate order and token
178
+ const order = await tx.order.findFirst({
179
+ where: {
180
+ id: orderId,
181
+ userActionToken,
182
+ status: OrderStatus.prospect
183
+ },
184
+ include: {
185
+ recoveryDiscountCode: true
186
+ }
187
+ });
188
+ if (!order) {
189
+ throw new Error('Invalid order or token');
190
+ }
191
+ // Apply recovery discount if exists and not already applied
192
+ let updateData = {
193
+ status: OrderStatus.pending,
194
+ wasRecovered: true
195
+ };
196
+ if (order.recoveryDiscountCodeId && !order.discountCodeId) {
197
+ const discountAmount = order.totalPrice
198
+ .mul(order.recoveryDiscountCode.value)
199
+ .div(100);
200
+ updateData.discountCodeId = order.recoveryDiscountCodeId;
201
+ updateData.discountAmount = discountAmount;
202
+ }
203
+ // Update order status
204
+ await tx.order.update({
205
+ where: { id: orderId },
206
+ data: updateData
207
+ });
208
+ // Add confirmation note
209
+ await tx.orderNote.create({
210
+ data: {
211
+ orderId,
212
+ type: NoteType.status_change,
213
+ note: `Order recovered by customer via email link. Status changed from prospect to pending.`,
214
+ metadata: {
215
+ recoveryAttempt: order.recoveryAttempts,
216
+ discountApplied: order.recoveryDiscountCode?.value.toNumber()
217
+ }
218
+ }
219
+ });
220
+ // Return the full order with all relations
221
+ return tx.order.findUnique({
222
+ where: { id: orderId },
223
+ include: ORDER_INCLUDE_FULL
224
+ });
225
+ });
226
+ }
227
+ /**
228
+ * Auto-cancel prospect orders that have exceeded the time limit
229
+ */
230
+ export async function autoCancelProspectOrders(prisma) {
231
+ const cutoffTime = subHours(new Date(), AUTO_CANCEL_HOURS);
232
+ const ordersToCancel = await prisma.order.findMany({
233
+ where: {
234
+ status: OrderStatus.prospect,
235
+ prospectSince: {
236
+ lte: cutoffTime
237
+ }
238
+ },
239
+ select: {
240
+ id: true,
241
+ orderNumber: true
242
+ }
243
+ });
244
+ const results = await Promise.all(ordersToCancel.map(async (order) => {
245
+ try {
246
+ await prisma.$transaction(async (tx) => {
247
+ // Update order status
248
+ await tx.order.update({
249
+ where: { id: order.id },
250
+ data: {
251
+ status: OrderStatus.cancelled,
252
+ cancellationReason: 'No response to confirmation attempts after 72 hours'
253
+ }
254
+ });
255
+ // Add cancellation note
256
+ await tx.orderNote.create({
257
+ data: {
258
+ orderId: order.id,
259
+ type: NoteType.status_change,
260
+ note: 'Order auto-cancelled after 72 hours with no customer response',
261
+ metadata: {
262
+ autoCancelled: true,
263
+ prospectDuration: AUTO_CANCEL_HOURS
264
+ }
265
+ }
266
+ });
267
+ });
268
+ return { orderId: order.id, success: true };
269
+ }
270
+ catch (error) {
271
+ console.error(`Failed to auto-cancel order ${order.id}:`, error);
272
+ return { orderId: order.id, success: false, error };
273
+ }
274
+ }));
275
+ return {
276
+ processed: results.length,
277
+ succeeded: results.filter(r => r.success).length,
278
+ failed: results.filter(r => !r.success).length
279
+ };
280
+ }
281
+ /**
282
+ * Get recovery statistics for dashboard
283
+ */
284
+ export async function getProspectRecoveryStats(prisma, brandId) {
285
+ const whereClause = brandId ? { brandId } : {};
286
+ const [totalProspects, recovered, cancelled, stillProspect] = await Promise.all([
287
+ prisma.order.count({
288
+ where: {
289
+ ...whereClause,
290
+ prospectSince: { not: null }
291
+ }
292
+ }),
293
+ prisma.order.count({
294
+ where: {
295
+ ...whereClause,
296
+ wasRecovered: true
297
+ }
298
+ }),
299
+ prisma.order.count({
300
+ where: {
301
+ ...whereClause,
302
+ prospectSince: { not: null },
303
+ status: OrderStatus.cancelled
304
+ }
305
+ }),
306
+ prisma.order.count({
307
+ where: {
308
+ ...whereClause,
309
+ status: OrderStatus.prospect
310
+ }
311
+ })
312
+ ]);
313
+ const recoveryRate = totalProspects > 0
314
+ ? ((recovered / totalProspects) * 100).toFixed(2)
315
+ : '0.00';
316
+ const revenueRecovered = await prisma.order.aggregate({
317
+ where: {
318
+ ...whereClause,
319
+ wasRecovered: true,
320
+ status: {
321
+ in: [OrderStatus.shipped, OrderStatus.delivered]
322
+ }
323
+ },
324
+ _sum: {
325
+ totalPrice: true
326
+ }
327
+ });
328
+ return {
329
+ totalProspects,
330
+ recovered,
331
+ cancelled,
332
+ stillProspect,
333
+ recoveryRate: `${recoveryRate}%`,
334
+ revenueRecovered: revenueRecovered._sum.totalPrice?.toNumber() || 0
335
+ };
336
+ }
337
+ /**
338
+ * Mark order as prospect with reason
339
+ */
340
+ export async function markOrderAsProspect(prisma, orderId, reason, notes, userId) {
341
+ return prisma.$transaction(async (tx) => {
342
+ // Update order status
343
+ const order = await tx.order.update({
344
+ where: { id: orderId },
345
+ data: {
346
+ status: OrderStatus.prospect,
347
+ prospectSince: new Date(),
348
+ prospectReason: reason,
349
+ recoveryAttempts: 1, // First email sent immediately
350
+ lastRecoveryAttemptAt: new Date()
351
+ }
352
+ });
353
+ // Create prospect reason note
354
+ const reasonLabels = {
355
+ [ProspectReason.no_answer]: 'No answer',
356
+ [ProspectReason.thinking]: 'Thinking about it',
357
+ [ProspectReason.price_concern]: 'Concerned about price',
358
+ [ProspectReason.will_call_back]: 'Will call back',
359
+ [ProspectReason.unclear_commitment]: 'Unclear commitment',
360
+ [ProspectReason.payment_issue]: 'Payment issue',
361
+ [ProspectReason.other]: 'Other'
362
+ };
363
+ await tx.orderNote.create({
364
+ data: {
365
+ orderId,
366
+ type: NoteType.prospect_reason,
367
+ note: `Order set to prospect. Reason: ${reasonLabels[reason]}${notes ? ` - ${notes}` : ''}`,
368
+ createdBy: userId,
369
+ metadata: {
370
+ previousStatus: OrderStatus.pending,
371
+ prospectReason: reason,
372
+ customNote: notes
373
+ }
374
+ }
375
+ });
376
+ return order;
377
+ });
378
+ }
@@ -0,0 +1,138 @@
1
+ import { Prisma } from '@prisma/client';
2
+ export type OrderDBResponse = Prisma.OrderGetPayload<{
3
+ include: {
4
+ brand: true;
5
+ deliveryZone: {
6
+ include: {
7
+ state: true;
8
+ };
9
+ };
10
+ items: {
11
+ include: {
12
+ variant: {
13
+ include: {
14
+ product: true;
15
+ };
16
+ };
17
+ warehouse: true;
18
+ };
19
+ };
20
+ };
21
+ }>;
22
+ export type OrderResponse = ReturnType<typeof formatOrderResponse>;
23
+ export declare function formatOrderResponse(order: OrderDBResponse): {
24
+ subtotal: number;
25
+ deliveryCharge: number;
26
+ totalPrice: number;
27
+ discountAmount: number;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ deletedAt: string;
31
+ prospectSince: string;
32
+ lastRecoveryAttemptAt: string;
33
+ brand: {
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ deletedAt: string;
37
+ id: string;
38
+ name: string;
39
+ slug: string;
40
+ logoUrl: string | null;
41
+ siteUrl: string;
42
+ domain: string;
43
+ metaPixelId: string | null;
44
+ };
45
+ deliveryZone: {
46
+ deliveryCost: number;
47
+ freeShippingThreshold: number;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ deletedAt: string;
51
+ state: {
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ deletedAt: string;
55
+ id: string;
56
+ name: string;
57
+ isActive: boolean;
58
+ };
59
+ id: string;
60
+ name: string;
61
+ brandId: string | null;
62
+ stateId: string;
63
+ allowCOD: boolean;
64
+ allowOnline: boolean;
65
+ waybillOnly: boolean;
66
+ estimatedDays: number | null;
67
+ noteTitle: string | null;
68
+ noteContent: string | null;
69
+ isActive: boolean;
70
+ };
71
+ items: {
72
+ priceAtPurchase: number;
73
+ variant: {
74
+ price: number;
75
+ createdAt: string;
76
+ updatedAt: string;
77
+ deletedAt: string;
78
+ product: {
79
+ createdAt: string;
80
+ updatedAt: string;
81
+ deletedAt: string;
82
+ id: string;
83
+ name: string;
84
+ slug: string;
85
+ brandId: string;
86
+ isActive: boolean;
87
+ description: string | null;
88
+ thumbnailUrl: string | null;
89
+ quantityDiscounts: Prisma.JsonValue | null;
90
+ };
91
+ id: string;
92
+ name: string | null;
93
+ isActive: boolean;
94
+ thumbnailUrl: string | null;
95
+ productId: string;
96
+ sku: string;
97
+ trackInventory: boolean;
98
+ lowStockThreshold: number | null;
99
+ };
100
+ warehouse: {
101
+ createdAt: string;
102
+ updatedAt: string;
103
+ deletedAt: string;
104
+ id: string;
105
+ name: string;
106
+ isActive: boolean;
107
+ address: string | null;
108
+ city: string | null;
109
+ state: string | null;
110
+ };
111
+ id: string;
112
+ orderId: string;
113
+ variantId: string;
114
+ warehouseId: string | null;
115
+ quantity: number;
116
+ }[];
117
+ email: string | null;
118
+ id: string;
119
+ brandId: string;
120
+ deliveryZoneId: string;
121
+ recoveryAttempts: number;
122
+ recoveryDiscountCodeId: string | null;
123
+ wasRecovered: boolean;
124
+ estimatedDays: number | null;
125
+ orderNumber: number;
126
+ firstName: string;
127
+ lastName: string;
128
+ phone: string;
129
+ address: string;
130
+ city: string;
131
+ discountCodeId: string | null;
132
+ paymentMethod: import("@prisma/client").$Enums.PaymentMethod;
133
+ paystackReference: string | null;
134
+ status: import("@prisma/client").$Enums.OrderStatus;
135
+ cancellationReason: string | null;
136
+ prospectReason: import("@prisma/client").$Enums.ProspectReason | null;
137
+ userActionToken: string;
138
+ };
@@ -0,0 +1,61 @@
1
+ import { toNumber } from './utils';
2
+ export function formatOrderResponse(order) {
3
+ const subtotal = order.items.reduce((sum, item) => sum + (item.quantity * parseFloat(item.priceAtPurchase.toString())), 0);
4
+ return {
5
+ ...order,
6
+ subtotal,
7
+ deliveryCharge: toNumber(order.deliveryCharge),
8
+ totalPrice: toNumber(order.totalPrice),
9
+ discountAmount: order.discountAmount ? toNumber(order.discountAmount) : null,
10
+ createdAt: order.createdAt.toISOString(),
11
+ updatedAt: order.updatedAt.toISOString(),
12
+ deletedAt: order.deletedAt ? order.deletedAt.toISOString() : null,
13
+ prospectSince: order.prospectSince ? order.prospectSince.toISOString() : null,
14
+ lastRecoveryAttemptAt: order.lastRecoveryAttemptAt ? order.lastRecoveryAttemptAt.toISOString() : null,
15
+ brand: {
16
+ ...order.brand,
17
+ createdAt: order.brand.createdAt.toISOString(),
18
+ updatedAt: order.brand.updatedAt.toISOString(),
19
+ deletedAt: order.brand.deletedAt ? order.brand.deletedAt.toISOString() : null,
20
+ },
21
+ deliveryZone: {
22
+ ...order.deliveryZone,
23
+ deliveryCost: toNumber(order.deliveryZone.deliveryCost),
24
+ freeShippingThreshold: order.deliveryZone.freeShippingThreshold
25
+ ? toNumber(order.deliveryZone.freeShippingThreshold)
26
+ : null,
27
+ createdAt: order.deliveryZone.createdAt.toISOString(),
28
+ updatedAt: order.deliveryZone.updatedAt.toISOString(),
29
+ deletedAt: order.deliveryZone.deletedAt ? order.deliveryZone.deletedAt.toISOString() : null,
30
+ state: {
31
+ ...order.deliveryZone.state,
32
+ createdAt: order.deliveryZone.state.createdAt.toISOString(),
33
+ updatedAt: order.deliveryZone.state.updatedAt.toISOString(),
34
+ deletedAt: order.deliveryZone.state.deletedAt ? order.deliveryZone.state.deletedAt.toISOString() : null,
35
+ },
36
+ },
37
+ items: order.items.map((item) => ({
38
+ ...item,
39
+ priceAtPurchase: toNumber(item.priceAtPurchase),
40
+ variant: {
41
+ ...item.variant,
42
+ price: toNumber(item.variant.price),
43
+ createdAt: item.variant.createdAt.toISOString(),
44
+ updatedAt: item.variant.updatedAt.toISOString(),
45
+ deletedAt: item.variant.deletedAt ? item.variant.deletedAt.toISOString() : null,
46
+ product: {
47
+ ...item.variant.product,
48
+ createdAt: item.variant.product.createdAt.toISOString(),
49
+ updatedAt: item.variant.product.updatedAt.toISOString(),
50
+ deletedAt: item.variant.product.deletedAt ? item.variant.product.deletedAt.toISOString() : null,
51
+ },
52
+ },
53
+ warehouse: item.warehouse ? {
54
+ ...item.warehouse,
55
+ createdAt: item.warehouse.createdAt.toISOString(),
56
+ updatedAt: item.warehouse.updatedAt.toISOString(),
57
+ deletedAt: item.warehouse.deletedAt ? item.warehouse.deletedAt.toISOString() : null,
58
+ } : null,
59
+ })),
60
+ };
61
+ }
@@ -0,0 +1,39 @@
1
+ import { Decimal } from '@prisma/client/runtime/library';
2
+ export interface PriceCalculation {
3
+ basePrice: number;
4
+ discountPercent: number;
5
+ finalPrice: number;
6
+ subtotal: number;
7
+ }
8
+ /**
9
+ * Calculate discounted price for a product based on total quantity across all variants
10
+ */
11
+ export declare function calculatePriceWithDiscount(basePrice: number | Decimal, quantity: number, totalProductQuantity: number, quantityDiscounts?: Record<string, number> | null): PriceCalculation;
12
+ /**
13
+ * Calculate total cost for a collection of cart items
14
+ */
15
+ export interface CartItemForCalculation {
16
+ variantId: string;
17
+ quantity: number;
18
+ variant: {
19
+ price: number | Decimal;
20
+ product: {
21
+ id: string;
22
+ quantityDiscounts?: Record<string, number> | null;
23
+ };
24
+ };
25
+ }
26
+ export interface CartCostCalculation {
27
+ items: Array<{
28
+ variantId: string;
29
+ quantity: number;
30
+ basePrice: number;
31
+ discountPercent: number;
32
+ finalPrice: number;
33
+ subtotal: number;
34
+ }>;
35
+ subtotal: number;
36
+ deliveryCharge: number;
37
+ total: number;
38
+ }
39
+ export declare function calculateCartCost(items: CartItemForCalculation[], deliveryCharge?: number | Decimal): CartCostCalculation;
@@ -0,0 +1,62 @@
1
+ import { round, toNumber } from './utils';
2
+ /**
3
+ * Calculate discounted price for a product based on total quantity across all variants
4
+ */
5
+ export function calculatePriceWithDiscount(basePrice, quantity, totalProductQuantity, quantityDiscounts) {
6
+ let discountPercent = 0;
7
+ const basePriceNum = toNumber(basePrice);
8
+ // Check if product has quantity discounts and apply based on total product quantity
9
+ if (quantityDiscounts && typeof quantityDiscounts === 'object') {
10
+ // Find the highest quantity tier that applies to total product quantity
11
+ const applicableDiscounts = Object.entries(quantityDiscounts)
12
+ .filter(([qty]) => totalProductQuantity >= parseInt(qty))
13
+ .sort(([a], [b]) => parseInt(b) - parseInt(a)); // Sort descending
14
+ if (applicableDiscounts.length > 0) {
15
+ discountPercent = applicableDiscounts[0][1];
16
+ }
17
+ }
18
+ // Apply discount to this variant's price
19
+ const finalPrice = round(basePriceNum * (1 - discountPercent / 100));
20
+ const subtotal = round(finalPrice * quantity);
21
+ return {
22
+ basePrice: basePriceNum,
23
+ discountPercent,
24
+ finalPrice,
25
+ subtotal,
26
+ };
27
+ }
28
+ export function calculateCartCost(items, deliveryCharge = 0) {
29
+ // Group items by product to calculate quantity discounts at product level
30
+ const itemsByProduct = new Map();
31
+ for (const item of items) {
32
+ const productId = item.variant.product.id;
33
+ if (!itemsByProduct.has(productId)) {
34
+ itemsByProduct.set(productId, []);
35
+ }
36
+ itemsByProduct.get(productId).push(item);
37
+ }
38
+ // Calculate prices for each item
39
+ let subtotal = 0;
40
+ const calculatedItems = items.map((item) => {
41
+ const basePrice = item.variant.price;
42
+ const productId = item.variant.product.id;
43
+ // Calculate total quantity for this product across all variants
44
+ const productItems = itemsByProduct.get(productId) || [];
45
+ const totalProductQuantity = productItems.reduce((sum, i) => sum + i.quantity, 0);
46
+ // Calculate price with discount
47
+ const calculation = calculatePriceWithDiscount(basePrice, item.quantity, totalProductQuantity, item.variant.product.quantityDiscounts);
48
+ subtotal += calculation.subtotal;
49
+ return {
50
+ variantId: item.variantId,
51
+ quantity: item.quantity,
52
+ ...calculation,
53
+ };
54
+ });
55
+ const deliveryChargeNum = toNumber(deliveryCharge);
56
+ return {
57
+ items: calculatedItems,
58
+ subtotal: round(subtotal),
59
+ deliveryCharge: deliveryChargeNum,
60
+ total: round(subtotal + deliveryChargeNum),
61
+ };
62
+ }
@@ -0,0 +1,9 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ /**
3
+ * Creates a new Prisma client for each request.
4
+ *
5
+ * IMPORTANT: In Cloudflare Workers, we CANNOT use a singleton pattern because
6
+ * I/O objects (like database connections) cannot be shared across different requests.
7
+ * Each request must create its own Prisma instance.
8
+ */
9
+ export declare function getPrismaClient(databaseUrl: string): PrismaClient;