@instockng/api-client 1.0.3 → 1.0.5

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