@instockng/api-client 1.0.4 → 1.0.6
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.
- package/dist/enum-types.d.ts +8 -0
- package/dist/enum-types.js +5 -0
- package/dist/fetchers/carts.js +5 -0
- package/dist/hooks/admin/abandoned-carts.js +12 -8
- package/dist/hooks/admin/brands.js +15 -10
- package/dist/hooks/admin/customers.js +3 -2
- package/dist/hooks/admin/delivery-zones.js +24 -16
- package/dist/hooks/admin/discount-codes.js +24 -16
- package/dist/hooks/admin/inventory.js +15 -10
- package/dist/hooks/admin/orders.js +18 -12
- package/dist/hooks/admin/products.js +15 -10
- package/dist/hooks/admin/stats.js +3 -2
- package/dist/hooks/admin/variants.js +18 -12
- package/dist/hooks/admin/warehouses.js +15 -10
- package/dist/hooks/useApiConfig.d.ts +2 -1
- package/dist/hooks/useApiConfig.js +2 -2
- package/dist/provider.d.ts +7 -4
- package/dist/provider.js +5 -3
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/dist/apps/backend/src/generated/zod/index.d.ts +0 -1114
- package/dist/apps/backend/src/generated/zod/index.js +0 -670
- package/dist/apps/backend/src/http-app.d.ts +0 -40
- package/dist/apps/backend/src/http-app.js +0 -106
- package/dist/apps/backend/src/lib/brand-response.d.ts +0 -14
- package/dist/apps/backend/src/lib/brand-response.js +0 -8
- package/dist/apps/backend/src/lib/cart-helpers.d.ts +0 -280
- package/dist/apps/backend/src/lib/cart-helpers.js +0 -93
- package/dist/apps/backend/src/lib/cart-recovery.d.ts +0 -30
- package/dist/apps/backend/src/lib/cart-recovery.js +0 -147
- package/dist/apps/backend/src/lib/cart-response.d.ts +0 -121
- package/dist/apps/backend/src/lib/cart-response.js +0 -150
- package/dist/apps/backend/src/lib/clerk.d.ts +0 -18
- package/dist/apps/backend/src/lib/clerk.js +0 -167
- package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +0 -62
- package/dist/apps/backend/src/lib/delivery-zone-response.js +0 -24
- package/dist/apps/backend/src/lib/discount-code-response.d.ts +0 -42
- package/dist/apps/backend/src/lib/discount-code-response.js +0 -19
- package/dist/apps/backend/src/lib/discount.d.ts +0 -20
- package/dist/apps/backend/src/lib/discount.js +0 -35
- package/dist/apps/backend/src/lib/inventory.d.ts +0 -26
- package/dist/apps/backend/src/lib/inventory.js +0 -160
- package/dist/apps/backend/src/lib/meta-capi.d.ts +0 -48
- package/dist/apps/backend/src/lib/meta-capi.js +0 -120
- package/dist/apps/backend/src/lib/openapi.d.ts +0 -36
- package/dist/apps/backend/src/lib/openapi.js +0 -69
- package/dist/apps/backend/src/lib/order-recovery.d.ts +0 -367
- package/dist/apps/backend/src/lib/order-recovery.js +0 -373
- package/dist/apps/backend/src/lib/order-response.d.ts +0 -136
- package/dist/apps/backend/src/lib/order-response.js +0 -61
- package/dist/apps/backend/src/lib/pricing.d.ts +0 -39
- package/dist/apps/backend/src/lib/pricing.js +0 -62
- package/dist/apps/backend/src/lib/prisma.d.ts +0 -9
- package/dist/apps/backend/src/lib/prisma.js +0 -30
- package/dist/apps/backend/src/lib/product-response.d.ts +0 -82
- package/dist/apps/backend/src/lib/product-response.js +0 -29
- package/dist/apps/backend/src/lib/utils.d.ts +0 -32
- package/dist/apps/backend/src/lib/utils.js +0 -63
- package/dist/apps/backend/src/middleware/clerk-auth.d.ts +0 -8
- package/dist/apps/backend/src/middleware/clerk-auth.js +0 -89
- package/dist/apps/backend/src/middleware/cors.d.ts +0 -8
- package/dist/apps/backend/src/middleware/cors.js +0 -11
- package/dist/apps/backend/src/notifications/producers/meta-capi-producer.d.ts +0 -55
- package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +0 -125
- package/dist/apps/backend/src/notifications/producers/order-notification.d.ts +0 -9
- package/dist/apps/backend/src/notifications/producers/order-notification.js +0 -18
- package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.d.ts +0 -10
- package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.js +0 -11
- package/dist/apps/backend/src/routes/admin/abandoned-carts.d.ts +0 -605
- package/dist/apps/backend/src/routes/admin/abandoned-carts.js +0 -194
- package/dist/apps/backend/src/routes/admin/brands.d.ts +0 -175
- package/dist/apps/backend/src/routes/admin/brands.js +0 -118
- package/dist/apps/backend/src/routes/admin/customers.d.ts +0 -306
- package/dist/apps/backend/src/routes/admin/customers.js +0 -39
- package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +0 -438
- package/dist/apps/backend/src/routes/admin/delivery-zones.js +0 -300
- package/dist/apps/backend/src/routes/admin/discount-codes.d.ts +0 -478
- package/dist/apps/backend/src/routes/admin/discount-codes.js +0 -418
- package/dist/apps/backend/src/routes/admin/inventory.d.ts +0 -273
- package/dist/apps/backend/src/routes/admin/inventory.js +0 -189
- package/dist/apps/backend/src/routes/admin/orders.d.ts +0 -1478
- package/dist/apps/backend/src/routes/admin/orders.js +0 -503
- package/dist/apps/backend/src/routes/admin/products.d.ts +0 -860
- package/dist/apps/backend/src/routes/admin/products.js +0 -107
- package/dist/apps/backend/src/routes/admin/stats.d.ts +0 -288
- package/dist/apps/backend/src/routes/admin/stats.js +0 -55
- package/dist/apps/backend/src/routes/admin/variants.d.ts +0 -239
- package/dist/apps/backend/src/routes/admin/variants.js +0 -173
- package/dist/apps/backend/src/routes/admin/warehouses.d.ts +0 -373
- package/dist/apps/backend/src/routes/admin/warehouses.js +0 -123
- package/dist/apps/backend/src/routes/public/brands.d.ts +0 -40
- package/dist/apps/backend/src/routes/public/brands.js +0 -38
- package/dist/apps/backend/src/routes/public/carts.d.ts +0 -2655
- package/dist/apps/backend/src/routes/public/carts.js +0 -631
- package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +0 -35
- package/dist/apps/backend/src/routes/public/delivery-zones.js +0 -62
- package/dist/apps/backend/src/routes/public/orders.d.ts +0 -323
- package/dist/apps/backend/src/routes/public/orders.js +0 -160
- package/dist/apps/backend/src/routes/public/products.d.ts +0 -449
- package/dist/apps/backend/src/routes/public/products.js +0 -133
- package/dist/apps/backend/src/types/index.d.ts +0 -42
- package/dist/apps/backend/src/types/index.js +0 -2
- package/dist/apps/backend/src/validators/brand.d.ts +0 -17
- package/dist/apps/backend/src/validators/brand.js +0 -15
- package/dist/apps/backend/src/validators/delivery-zone.d.ts +0 -31
- package/dist/apps/backend/src/validators/delivery-zone.js +0 -51
- package/dist/apps/backend/src/validators/discount-code.d.ts +0 -74
- package/dist/apps/backend/src/validators/discount-code.js +0 -50
- package/dist/apps/backend/src/validators/inventory.d.ts +0 -20
- package/dist/apps/backend/src/validators/inventory.js +0 -15
- package/dist/apps/backend/src/validators/order.d.ts +0 -87
- package/dist/apps/backend/src/validators/order.js +0 -61
- package/dist/apps/backend/src/validators/product.d.ts +0 -18
- package/dist/apps/backend/src/validators/product.js +0 -19
- package/dist/apps/backend/src/validators/variant.d.ts +0 -19
- package/dist/apps/backend/src/validators/variant.js +0 -19
- package/dist/apps/backend/src/validators/warehouse.d.ts +0 -15
- package/dist/apps/backend/src/validators/warehouse.js +0 -15
- package/dist/packages/api-client/src/backend-types.d.ts +0 -10
- package/dist/packages/api-client/src/backend-types.js +0 -10
- package/dist/packages/api-client/src/client.d.ts +0 -20
- package/dist/packages/api-client/src/client.js +0 -40
- package/dist/packages/api-client/src/fetchers/brands.d.ts +0 -25
- package/dist/packages/api-client/src/fetchers/brands.js +0 -26
- package/dist/packages/api-client/src/fetchers/carts.d.ts +0 -2335
- package/dist/packages/api-client/src/fetchers/carts.js +0 -169
- package/dist/packages/api-client/src/fetchers/delivery-zones.d.ts +0 -28
- package/dist/packages/api-client/src/fetchers/delivery-zones.js +0 -26
- package/dist/packages/api-client/src/fetchers/index.d.ts +0 -22
- package/dist/packages/api-client/src/fetchers/index.js +0 -22
- package/dist/packages/api-client/src/fetchers/orders.d.ts +0 -283
- package/dist/packages/api-client/src/fetchers/orders.js +0 -44
- package/dist/packages/api-client/src/fetchers/products.d.ts +0 -386
- package/dist/packages/api-client/src/fetchers/products.js +0 -42
- package/dist/packages/api-client/src/hooks/admin/abandoned-carts.d.ts +0 -535
- package/dist/packages/api-client/src/hooks/admin/abandoned-carts.js +0 -79
- package/dist/packages/api-client/src/hooks/admin/brands.d.ts +0 -79
- package/dist/packages/api-client/src/hooks/admin/brands.js +0 -103
- package/dist/packages/api-client/src/hooks/admin/customers.d.ts +0 -278
- package/dist/packages/api-client/src/hooks/admin/customers.js +0 -25
- package/dist/packages/api-client/src/hooks/admin/delivery-zones.d.ts +0 -270
- package/dist/packages/api-client/src/hooks/admin/delivery-zones.js +0 -168
- package/dist/packages/api-client/src/hooks/admin/discount-codes.d.ts +0 -299
- package/dist/packages/api-client/src/hooks/admin/discount-codes.js +0 -157
- package/dist/packages/api-client/src/hooks/admin/index.d.ts +0 -16
- package/dist/packages/api-client/src/hooks/admin/index.js +0 -16
- package/dist/packages/api-client/src/hooks/admin/inventory.d.ts +0 -224
- package/dist/packages/api-client/src/hooks/admin/inventory.js +0 -102
- package/dist/packages/api-client/src/hooks/admin/orders.d.ts +0 -1380
- package/dist/packages/api-client/src/hooks/admin/orders.js +0 -169
- package/dist/packages/api-client/src/hooks/admin/products.d.ts +0 -374
- package/dist/packages/api-client/src/hooks/admin/products.js +0 -84
- package/dist/packages/api-client/src/hooks/admin/stats.d.ts +0 -277
- package/dist/packages/api-client/src/hooks/admin/stats.js +0 -24
- package/dist/packages/api-client/src/hooks/admin/variants.d.ts +0 -115
- package/dist/packages/api-client/src/hooks/admin/variants.js +0 -121
- package/dist/packages/api-client/src/hooks/admin/warehouses.d.ts +0 -277
- package/dist/packages/api-client/src/hooks/admin/warehouses.js +0 -103
- package/dist/packages/api-client/src/hooks/public/brands.d.ts +0 -33
- package/dist/packages/api-client/src/hooks/public/brands.js +0 -30
- package/dist/packages/api-client/src/hooks/public/carts.d.ts +0 -2405
- package/dist/packages/api-client/src/hooks/public/carts.js +0 -213
- package/dist/packages/api-client/src/hooks/public/delivery-zones.d.ts +0 -34
- package/dist/packages/api-client/src/hooks/public/delivery-zones.js +0 -28
- package/dist/packages/api-client/src/hooks/public/index.d.ts +0 -10
- package/dist/packages/api-client/src/hooks/public/index.js +0 -10
- package/dist/packages/api-client/src/hooks/public/orders.d.ts +0 -302
- package/dist/packages/api-client/src/hooks/public/orders.js +0 -50
- package/dist/packages/api-client/src/hooks/public/products.d.ts +0 -398
- package/dist/packages/api-client/src/hooks/public/products.js +0 -47
- package/dist/packages/api-client/src/hooks/use-query-unwrapped.d.ts +0 -20
- package/dist/packages/api-client/src/hooks/use-query-unwrapped.js +0 -22
- package/dist/packages/api-client/src/hooks/useApiConfig.d.ts +0 -11
- package/dist/packages/api-client/src/hooks/useApiConfig.js +0 -14
- package/dist/packages/api-client/src/index.d.ts +0 -20
- package/dist/packages/api-client/src/index.js +0 -25
- package/dist/packages/api-client/src/provider.d.ts +0 -33
- package/dist/packages/api-client/src/provider.js +0 -52
- package/dist/packages/api-client/src/rpc-client.d.ts +0 -9035
- package/dist/packages/api-client/src/rpc-client.js +0 -78
- package/dist/packages/api-client/src/rpc-types.d.ts +0 -76
- package/dist/packages/api-client/src/rpc-types.js +0 -7
- package/dist/packages/api-client/src/types.d.ts +0 -33
- package/dist/packages/api-client/src/types.js +0 -16
- package/dist/packages/api-client/src/utils/query-keys.d.ts +0 -106
- package/dist/packages/api-client/src/utils/query-keys.js +0 -108
|
@@ -1,373 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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;
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
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;
|