@instockng/api-client 1.0.8 → 1.0.10
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/fetchers/brands.d.ts +1 -1
- package/dist/fetchers/carts.d.ts +85 -47
- package/dist/fetchers/delivery-zones.d.ts +2 -0
- package/dist/fetchers/orders.d.ts +26 -14
- package/dist/fetchers/products.d.ts +14 -6
- package/dist/hooks/admin/abandoned-carts.d.ts +18 -10
- package/dist/hooks/admin/brands.d.ts +4 -4
- package/dist/hooks/admin/customers.d.ts +13 -7
- package/dist/hooks/admin/delivery-zones.d.ts +23 -15
- package/dist/hooks/admin/discount-codes.d.ts +10 -10
- package/dist/hooks/admin/inventory.d.ts +4 -0
- package/dist/hooks/admin/orders.d.ts +78 -42
- package/dist/hooks/admin/products.d.ts +14 -6
- package/dist/hooks/admin/stats.d.ts +13 -7
- package/dist/hooks/admin/variants.d.ts +5 -5
- package/dist/hooks/admin/warehouses.d.ts +11 -7
- package/dist/hooks/public/brands.d.ts +1 -1
- package/dist/hooks/public/carts.d.ts +85 -47
- package/dist/hooks/public/delivery-zones.d.ts +2 -0
- package/dist/hooks/public/orders.d.ts +26 -14
- package/dist/hooks/public/products.d.ts +14 -6
- package/dist/rpc-client.d.ts +335 -187
- 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 -134
- 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 -282
- package/dist/apps/backend/src/lib/cart-helpers.js +0 -121
- 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 -190
- package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +0 -64
- 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 -53
- package/dist/apps/backend/src/lib/meta-capi.js +0 -151
- 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 -459
- package/dist/apps/backend/src/lib/order-recovery.js +0 -378
- package/dist/apps/backend/src/lib/order-response.d.ts +0 -138
- 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/sentry.d.ts +0 -48
- package/dist/apps/backend/src/lib/sentry.js +0 -180
- 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 -62
- package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +0 -180
- 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 -308
- package/dist/apps/backend/src/routes/admin/customers.js +0 -39
- package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +0 -446
- 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 -199
- package/dist/apps/backend/src/routes/admin/orders.d.ts +0 -1780
- package/dist/apps/backend/src/routes/admin/orders.js +0 -552
- package/dist/apps/backend/src/routes/admin/products.d.ts +0 -860
- package/dist/apps/backend/src/routes/admin/products.js +0 -126
- package/dist/apps/backend/src/routes/admin/stats.d.ts +0 -290
- 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 -197
- 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 -2657
- package/dist/apps/backend/src/routes/public/carts.js +0 -778
- package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +0 -37
- package/dist/apps/backend/src/routes/public/delivery-zones.js +0 -64
- package/dist/apps/backend/src/routes/public/orders.d.ts +0 -609
- package/dist/apps/backend/src/routes/public/orders.js +0 -184
- 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 -43
- 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 -35
- package/dist/apps/backend/src/validators/delivery-zone.js +0 -55
- 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 -58
- package/dist/apps/backend/src/validators/order.js +0 -62
- 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/enum-types.d.ts +0 -8
- package/dist/packages/api-client/src/enum-types.js +0 -5
- 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 -2337
- package/dist/packages/api-client/src/fetchers/carts.js +0 -174
- package/dist/packages/api-client/src/fetchers/delivery-zones.d.ts +0 -30
- 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 -544
- 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 -83
- package/dist/packages/api-client/src/hooks/admin/brands.d.ts +0 -79
- package/dist/packages/api-client/src/hooks/admin/brands.js +0 -108
- package/dist/packages/api-client/src/hooks/admin/customers.d.ts +0 -280
- package/dist/packages/api-client/src/hooks/admin/customers.js +0 -26
- package/dist/packages/api-client/src/hooks/admin/delivery-zones.d.ts +0 -278
- package/dist/packages/api-client/src/hooks/admin/delivery-zones.js +0 -176
- 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 -165
- 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 -107
- package/dist/packages/api-client/src/hooks/admin/orders.d.ts +0 -1674
- package/dist/packages/api-client/src/hooks/admin/orders.js +0 -178
- package/dist/packages/api-client/src/hooks/admin/products.d.ts +0 -374
- package/dist/packages/api-client/src/hooks/admin/products.js +0 -89
- package/dist/packages/api-client/src/hooks/admin/stats.d.ts +0 -279
- package/dist/packages/api-client/src/hooks/admin/stats.js +0 -25
- package/dist/packages/api-client/src/hooks/admin/variants.d.ts +0 -115
- package/dist/packages/api-client/src/hooks/admin/variants.js +0 -127
- package/dist/packages/api-client/src/hooks/admin/warehouses.d.ts +0 -277
- package/dist/packages/api-client/src/hooks/admin/warehouses.js +0 -108
- 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 -2407
- 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 -36
- 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 -563
- 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 -12
- 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 -36
- package/dist/packages/api-client/src/provider.js +0 -54
- package/dist/packages/api-client/src/rpc-client.d.ts +0 -9639
- 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 -34
- 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,552 +0,0 @@
|
|
|
1
|
-
import { Hono } from 'hono';
|
|
2
|
-
import { zValidator } from '@hono/zod-validator';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { createOrderSchema, updateOrderSchema, updateOrderStatusSchema } from '../../validators/order';
|
|
5
|
-
import { getPrismaClient } from '../../lib/prisma';
|
|
6
|
-
import { getPaginationParams, createPaginatedResponse } from '../../lib/utils';
|
|
7
|
-
import { adjustInventory } from '../../lib/inventory';
|
|
8
|
-
import { OrderStatus } from '@prisma/client';
|
|
9
|
-
import { formatOrderResponse } from '../../lib/order-response';
|
|
10
|
-
import { markOrderAsProspect } from '../../lib/order-recovery';
|
|
11
|
-
import { ORDER_INCLUDE_FULL } from '../../lib/cart-helpers';
|
|
12
|
-
import { captureException } from '../../lib/sentry';
|
|
13
|
-
const app = new Hono()
|
|
14
|
-
.post('/', zValidator('json', createOrderSchema), async (c) => {
|
|
15
|
-
try {
|
|
16
|
-
const input = c.req.valid('json');
|
|
17
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
18
|
-
// Validate brand exists and is active
|
|
19
|
-
const brand = await prisma.brand.findFirst({
|
|
20
|
-
where: { slug: input.brandSlug, deletedAt: null },
|
|
21
|
-
});
|
|
22
|
-
if (!brand) {
|
|
23
|
-
return c.json({ error: { code: 'INVALID_BRAND', message: 'Brand not found or inactive' } }, 404);
|
|
24
|
-
}
|
|
25
|
-
// Validate all variants exist and are active
|
|
26
|
-
const variants = await prisma.productVariant.findMany({
|
|
27
|
-
where: {
|
|
28
|
-
sku: { in: input.items.map((item) => item.sku) },
|
|
29
|
-
isActive: true,
|
|
30
|
-
deletedAt: null,
|
|
31
|
-
},
|
|
32
|
-
include: {
|
|
33
|
-
product: true,
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
if (variants.length !== input.items.length) {
|
|
37
|
-
return c.json({ error: { code: 'INVALID_VARIANT', message: 'One or more SKUs not found or inactive' } }, 404);
|
|
38
|
-
}
|
|
39
|
-
// Group items by product to calculate quantity discounts at product level
|
|
40
|
-
const itemsByProduct = new Map();
|
|
41
|
-
for (const item of input.items) {
|
|
42
|
-
const variant = variants.find((v) => v.sku === item.sku);
|
|
43
|
-
if (!variant)
|
|
44
|
-
throw new Error('Variant not found');
|
|
45
|
-
const productId = variant.product.id;
|
|
46
|
-
if (!itemsByProduct.has(productId)) {
|
|
47
|
-
itemsByProduct.set(productId, []);
|
|
48
|
-
}
|
|
49
|
-
itemsByProduct.get(productId).push(item);
|
|
50
|
-
}
|
|
51
|
-
// Calculate total price with product-level quantity discounts
|
|
52
|
-
let subtotal = 0;
|
|
53
|
-
const orderItems = input.items.map((item) => {
|
|
54
|
-
const variant = variants.find((v) => v.sku === item.sku);
|
|
55
|
-
if (!variant)
|
|
56
|
-
throw new Error('Variant not found');
|
|
57
|
-
const basePrice = Number(variant.price);
|
|
58
|
-
let discountPercent = 0;
|
|
59
|
-
// Calculate total quantity for this product across all variants
|
|
60
|
-
const productItems = itemsByProduct.get(variant.product.id) || [];
|
|
61
|
-
const totalProductQuantity = productItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
62
|
-
// Check if product has quantity discounts and apply based on total product quantity
|
|
63
|
-
if (variant.product.quantityDiscounts && typeof variant.product.quantityDiscounts === 'object') {
|
|
64
|
-
const discounts = variant.product.quantityDiscounts;
|
|
65
|
-
// Find the highest quantity tier that applies to total product quantity
|
|
66
|
-
const applicableDiscounts = Object.entries(discounts)
|
|
67
|
-
.filter(([qty]) => totalProductQuantity >= parseInt(qty))
|
|
68
|
-
.sort(([a], [b]) => parseInt(b) - parseInt(a)); // Sort descending
|
|
69
|
-
if (applicableDiscounts.length > 0) {
|
|
70
|
-
discountPercent = applicableDiscounts[0][1];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Apply discount to this variant's price
|
|
74
|
-
const discountedPrice = Math.round(basePrice * (1 - discountPercent / 100));
|
|
75
|
-
const itemTotal = discountedPrice * item.quantity;
|
|
76
|
-
subtotal += itemTotal;
|
|
77
|
-
return {
|
|
78
|
-
variantId: variant.id,
|
|
79
|
-
quantity: item.quantity,
|
|
80
|
-
priceAtPurchase: discountedPrice,
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
// Apply discount if provided
|
|
84
|
-
const discount = input.discount || 0;
|
|
85
|
-
const totalPrice = Math.max(0, subtotal + input.deliveryCharge - discount);
|
|
86
|
-
// Create order
|
|
87
|
-
const order = await prisma.order.create({
|
|
88
|
-
data: {
|
|
89
|
-
brandId: brand.id,
|
|
90
|
-
firstName: input.firstName,
|
|
91
|
-
lastName: input.lastName,
|
|
92
|
-
email: input.email,
|
|
93
|
-
phone: input.phone,
|
|
94
|
-
address: input.address,
|
|
95
|
-
city: input.city,
|
|
96
|
-
deliveryZoneId: input.deliveryZoneId,
|
|
97
|
-
deliveryCharge: input.deliveryCharge,
|
|
98
|
-
totalPrice,
|
|
99
|
-
paymentMethod: input.paymentMethod,
|
|
100
|
-
paystackReference: input.paystackReference,
|
|
101
|
-
status: 'pending',
|
|
102
|
-
items: {
|
|
103
|
-
create: orderItems,
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
include: ORDER_INCLUDE_FULL
|
|
107
|
-
});
|
|
108
|
-
// Queue confirmation email
|
|
109
|
-
try {
|
|
110
|
-
const { enqueueOrderNotification } = await import('../../notifications/producers/order-notification');
|
|
111
|
-
await enqueueOrderNotification(c.env, 'order_confirmation', order.id);
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
console.error('Failed to queue WhatsApp confirmation:', error);
|
|
115
|
-
// Don't fail the order creation if queuing fails
|
|
116
|
-
}
|
|
117
|
-
return c.json(formatOrderResponse(order), 201);
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
console.error('Error creating order:', error);
|
|
121
|
-
// CRITICAL: Admin order creation failures
|
|
122
|
-
captureException(error instanceof Error ? error : new Error(String(error)), {
|
|
123
|
-
level: 'fatal',
|
|
124
|
-
tags: {
|
|
125
|
-
error_code: 'ADMIN_ORDER_CREATE_ERROR',
|
|
126
|
-
endpoint: 'POST /admin/orders',
|
|
127
|
-
},
|
|
128
|
-
honoContext: c,
|
|
129
|
-
});
|
|
130
|
-
return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
|
|
131
|
-
}
|
|
132
|
-
}).get('/', async (c) => {
|
|
133
|
-
try {
|
|
134
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
135
|
-
const query = c.req.query();
|
|
136
|
-
const { page, limit, skip } = getPaginationParams({
|
|
137
|
-
page: query.page ? parseInt(query.page) : undefined,
|
|
138
|
-
limit: query.limit ? parseInt(query.limit) : undefined,
|
|
139
|
-
});
|
|
140
|
-
const where = { deletedAt: null };
|
|
141
|
-
if (query.brandId)
|
|
142
|
-
where.brandId = query.brandId;
|
|
143
|
-
if (query.status)
|
|
144
|
-
where.status = query.status;
|
|
145
|
-
if (query.paymentMethod)
|
|
146
|
-
where.paymentMethod = query.paymentMethod;
|
|
147
|
-
if (query.search) {
|
|
148
|
-
where.OR = [
|
|
149
|
-
{ orderNumber: isNaN(parseInt(query.search)) ? undefined : parseInt(query.search) },
|
|
150
|
-
{ phone: { contains: query.search } },
|
|
151
|
-
{ firstName: { contains: query.search, mode: 'insensitive' } },
|
|
152
|
-
{ lastName: { contains: query.search, mode: 'insensitive' } },
|
|
153
|
-
{ address: { contains: query.search, mode: 'insensitive' } },
|
|
154
|
-
].filter((item) => Object.values(item).some((v) => v !== undefined));
|
|
155
|
-
}
|
|
156
|
-
if (query.startDate)
|
|
157
|
-
where.createdAt = { ...where.createdAt, gte: new Date(query.startDate) };
|
|
158
|
-
if (query.endDate)
|
|
159
|
-
where.createdAt = { ...where.createdAt, lte: new Date(query.endDate) };
|
|
160
|
-
const [orders, total] = await Promise.all([
|
|
161
|
-
prisma.order.findMany({
|
|
162
|
-
where,
|
|
163
|
-
include: ORDER_INCLUDE_FULL,
|
|
164
|
-
skip,
|
|
165
|
-
take: limit,
|
|
166
|
-
orderBy: { createdAt: 'desc' },
|
|
167
|
-
}),
|
|
168
|
-
prisma.order.count({ where }),
|
|
169
|
-
]);
|
|
170
|
-
return c.json(createPaginatedResponse(orders.map(formatOrderResponse), total, page, limit));
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
console.error('List orders error:', error);
|
|
174
|
-
return c.json({
|
|
175
|
-
error: { code: 'INTERNAL_ERROR', message: 'Failed to retrieve orders' },
|
|
176
|
-
}, 500);
|
|
177
|
-
}
|
|
178
|
-
}).get('/:id', zValidator('param', z.object({ id: z.string().uuid() })), async (c) => {
|
|
179
|
-
try {
|
|
180
|
-
const { id } = c.req.valid('param');
|
|
181
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
182
|
-
const order = await prisma.order.findFirst({
|
|
183
|
-
where: { id, deletedAt: null },
|
|
184
|
-
include: ORDER_INCLUDE_FULL,
|
|
185
|
-
});
|
|
186
|
-
if (!order) {
|
|
187
|
-
return c.json({
|
|
188
|
-
error: { code: 'ORDER_NOT_FOUND', message: 'Order not found' },
|
|
189
|
-
}, 404);
|
|
190
|
-
}
|
|
191
|
-
return c.json(formatOrderResponse(order));
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
return c.json({
|
|
195
|
-
error: { code: 'INTERNAL_ERROR', message: 'Failed to retrieve order' },
|
|
196
|
-
}, 500);
|
|
197
|
-
}
|
|
198
|
-
}).patch('/:id', zValidator('param', z.object({ id: z.string().uuid() })), zValidator('json', updateOrderSchema), async (c) => {
|
|
199
|
-
try {
|
|
200
|
-
const { id } = c.req.valid('param');
|
|
201
|
-
const input = c.req.valid('json');
|
|
202
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
203
|
-
const order = await prisma.order.findFirst({
|
|
204
|
-
where: { id, deletedAt: null },
|
|
205
|
-
include: { brand: true },
|
|
206
|
-
});
|
|
207
|
-
if (!order) {
|
|
208
|
-
return c.json({
|
|
209
|
-
error: { code: 'ORDER_NOT_FOUND', message: 'Order not found' },
|
|
210
|
-
}, 404);
|
|
211
|
-
}
|
|
212
|
-
// Handle items update if provided
|
|
213
|
-
if (input.items) {
|
|
214
|
-
// Delete existing items and create new ones
|
|
215
|
-
await prisma.orderItem.deleteMany({
|
|
216
|
-
where: { orderId: id },
|
|
217
|
-
});
|
|
218
|
-
// Fetch variants by SKU
|
|
219
|
-
const variants = await prisma.productVariant.findMany({
|
|
220
|
-
where: {
|
|
221
|
-
sku: { in: input.items.map((item) => item.sku) },
|
|
222
|
-
isActive: true,
|
|
223
|
-
deletedAt: null,
|
|
224
|
-
},
|
|
225
|
-
include: {
|
|
226
|
-
product: {
|
|
227
|
-
include: {
|
|
228
|
-
brand: true,
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
if (variants.length !== input.items.length) {
|
|
234
|
-
return c.json({
|
|
235
|
-
error: { code: 'INVALID_VARIANT', message: 'One or more SKUs not found or inactive' },
|
|
236
|
-
}, 404);
|
|
237
|
-
}
|
|
238
|
-
// Create order items with quantity discount calculation
|
|
239
|
-
const orderItemsData = input.items.map((item) => {
|
|
240
|
-
const variant = variants.find((v) => v.sku === item.sku);
|
|
241
|
-
const product = variant.product;
|
|
242
|
-
// Apply quantity discount if available
|
|
243
|
-
let finalPrice = Number(variant.price);
|
|
244
|
-
if (product.quantityDiscounts && typeof product.quantityDiscounts === 'object') {
|
|
245
|
-
const discounts = product.quantityDiscounts;
|
|
246
|
-
const applicableQuantities = Object.keys(discounts)
|
|
247
|
-
.map(Number)
|
|
248
|
-
.filter((qty) => item.quantity >= qty)
|
|
249
|
-
.sort((a, b) => b - a);
|
|
250
|
-
if (applicableQuantities.length > 0) {
|
|
251
|
-
const discountPercent = discounts[applicableQuantities[0].toString()];
|
|
252
|
-
finalPrice = Math.round(Number(variant.price) * (1 - discountPercent / 100));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
orderId: id,
|
|
257
|
-
variantId: variant.id,
|
|
258
|
-
quantity: item.quantity,
|
|
259
|
-
priceAtPurchase: finalPrice,
|
|
260
|
-
};
|
|
261
|
-
});
|
|
262
|
-
await prisma.orderItem.createMany({
|
|
263
|
-
data: orderItemsData,
|
|
264
|
-
});
|
|
265
|
-
// Calculate subtotal with quantity discounts
|
|
266
|
-
const subtotal = orderItemsData.reduce((sum, item) => {
|
|
267
|
-
return sum + Number(item.priceAtPurchase) * item.quantity;
|
|
268
|
-
}, 0);
|
|
269
|
-
const deliveryCharge = input.deliveryCharge ?? Number(order.deliveryCharge);
|
|
270
|
-
const discount = input.discount ?? 0;
|
|
271
|
-
const totalPrice = Math.max(0, subtotal + deliveryCharge - discount);
|
|
272
|
-
// Update order with new total and other fields
|
|
273
|
-
const updatedOrder = await prisma.order.update({
|
|
274
|
-
where: { id },
|
|
275
|
-
data: {
|
|
276
|
-
firstName: input.firstName ?? order.firstName,
|
|
277
|
-
lastName: input.lastName ?? order.lastName,
|
|
278
|
-
email: input.email ?? order.email,
|
|
279
|
-
phone: input.phone ?? order.phone,
|
|
280
|
-
address: input.address ?? order.address,
|
|
281
|
-
city: input.city ?? order.city,
|
|
282
|
-
deliveryCharge,
|
|
283
|
-
paymentMethod: (input.paymentMethod ?? order.paymentMethod),
|
|
284
|
-
paystackReference: input.paystackReference ?? order.paystackReference,
|
|
285
|
-
totalPrice,
|
|
286
|
-
},
|
|
287
|
-
include: ORDER_INCLUDE_FULL
|
|
288
|
-
});
|
|
289
|
-
return c.json(updatedOrder);
|
|
290
|
-
}
|
|
291
|
-
// Update order without changing items (just customer info, delivery, payment method, etc.)
|
|
292
|
-
const deliveryCharge = input.deliveryCharge ?? Number(order.deliveryCharge);
|
|
293
|
-
// If discount changed, recalculate total
|
|
294
|
-
let totalPrice = Number(order.totalPrice);
|
|
295
|
-
if (input.discount !== undefined || input.deliveryCharge !== undefined) {
|
|
296
|
-
// Get current items to recalculate
|
|
297
|
-
const currentItems = await prisma.orderItem.findMany({
|
|
298
|
-
where: { orderId: id },
|
|
299
|
-
});
|
|
300
|
-
const subtotal = currentItems.reduce((sum, item) => {
|
|
301
|
-
return sum + Number(item.priceAtPurchase) * item.quantity;
|
|
302
|
-
}, 0);
|
|
303
|
-
const discount = input.discount ?? 0;
|
|
304
|
-
totalPrice = Math.max(0, subtotal + deliveryCharge - discount);
|
|
305
|
-
}
|
|
306
|
-
const updatedOrder = await prisma.order.update({
|
|
307
|
-
where: { id },
|
|
308
|
-
data: {
|
|
309
|
-
firstName: input.firstName,
|
|
310
|
-
lastName: input.lastName,
|
|
311
|
-
email: input.email,
|
|
312
|
-
phone: input.phone,
|
|
313
|
-
address: input.address,
|
|
314
|
-
city: input.city,
|
|
315
|
-
deliveryCharge: input.deliveryCharge,
|
|
316
|
-
paymentMethod: input.paymentMethod,
|
|
317
|
-
paystackReference: input.paystackReference,
|
|
318
|
-
totalPrice,
|
|
319
|
-
},
|
|
320
|
-
include: ORDER_INCLUDE_FULL
|
|
321
|
-
});
|
|
322
|
-
return c.json(formatOrderResponse(updatedOrder));
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
return c.json({
|
|
326
|
-
error: { code: 'INTERNAL_ERROR', message: error?.message || 'Failed to update order' },
|
|
327
|
-
}, 500);
|
|
328
|
-
}
|
|
329
|
-
}).patch('/:id/status', zValidator('param', z.object({ id: z.string().uuid() })), zValidator('json', updateOrderStatusSchema), async (c) => {
|
|
330
|
-
try {
|
|
331
|
-
const { id } = c.req.valid('param');
|
|
332
|
-
const input = c.req.valid('json');
|
|
333
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
334
|
-
const order = await prisma.order.findFirst({
|
|
335
|
-
where: { id, deletedAt: null },
|
|
336
|
-
include: {
|
|
337
|
-
items: true,
|
|
338
|
-
brand: true,
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
if (!order) {
|
|
342
|
-
return c.json({
|
|
343
|
-
error: { code: 'ORDER_NOT_FOUND', message: 'Order not found' },
|
|
344
|
-
}, 404);
|
|
345
|
-
}
|
|
346
|
-
// Track inventory adjustment transitions to prevent duplicate adjustments
|
|
347
|
-
const previousStatus = order.status;
|
|
348
|
-
const newStatus = input.status;
|
|
349
|
-
// Statuses where inventory has been deducted
|
|
350
|
-
const inventoryDeductedStatuses = ['shipped', 'delivered'];
|
|
351
|
-
const inventoryRestoredStatuses = ['pending', 'cancelled', 'returned'];
|
|
352
|
-
const wasInventoryDeducted = inventoryDeductedStatuses.includes(previousStatus);
|
|
353
|
-
const shouldRestoreInventory = wasInventoryDeducted && inventoryRestoredStatuses.includes(newStatus);
|
|
354
|
-
const shouldDeductInventory = newStatus === 'shipped' && !inventoryDeductedStatuses.includes(previousStatus);
|
|
355
|
-
// Restore inventory if transitioning from shipped/delivered to pending/cancelled/returned
|
|
356
|
-
if (shouldRestoreInventory) {
|
|
357
|
-
await prisma.$transaction(async (tx) => {
|
|
358
|
-
// Get all order items with warehouses
|
|
359
|
-
const orderItemsWithWarehouses = await tx.orderItem.findMany({
|
|
360
|
-
where: {
|
|
361
|
-
orderId: id,
|
|
362
|
-
warehouseId: { not: null }
|
|
363
|
-
},
|
|
364
|
-
include: {
|
|
365
|
-
variant: true,
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
for (const orderItem of orderItemsWithWarehouses) {
|
|
369
|
-
if (orderItem.variant && orderItem.variant.trackInventory && orderItem.warehouseId) {
|
|
370
|
-
// Credit inventory back (positive quantity)
|
|
371
|
-
await adjustInventory(tx, {
|
|
372
|
-
variantId: orderItem.variantId,
|
|
373
|
-
warehouseId: orderItem.warehouseId,
|
|
374
|
-
quantity: orderItem.quantity, // Positive to add back
|
|
375
|
-
type: 'adjustment',
|
|
376
|
-
reason: `Order #${order.orderNumber} status changed from ${previousStatus} to ${newStatus} - inventory restored`,
|
|
377
|
-
orderId: order.id,
|
|
378
|
-
userId: c.get('dbUser')?.id,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
// If status is "shipped", require warehouse selection and decrement inventory
|
|
385
|
-
if (newStatus === 'shipped' && previousStatus !== 'shipped') {
|
|
386
|
-
if (!input.warehouses || input.warehouses.length === 0) {
|
|
387
|
-
return c.json({
|
|
388
|
-
error: { code: 'WAREHOUSE_REQUIRED', message: 'Warehouse selection required when shipping order' },
|
|
389
|
-
}, 400);
|
|
390
|
-
}
|
|
391
|
-
// Validate all order items have warehouse assigned
|
|
392
|
-
const orderItemIds = order.items.map((item) => item.id);
|
|
393
|
-
const providedItemIds = input.warehouses.map((w) => w.orderItemId);
|
|
394
|
-
if (orderItemIds.some((id) => !providedItemIds.includes(id))) {
|
|
395
|
-
return c.json({
|
|
396
|
-
error: { code: 'INCOMPLETE_WAREHOUSE_ASSIGNMENT', message: 'All order items must have warehouse assigned' },
|
|
397
|
-
}, 400);
|
|
398
|
-
}
|
|
399
|
-
// Update warehouse assignments and decrement inventory (only if transitioning TO shipped)
|
|
400
|
-
await prisma.$transaction(async (tx) => {
|
|
401
|
-
for (const warehouseAssignment of input.warehouses) {
|
|
402
|
-
const orderItem = order.items.find((item) => item.id === warehouseAssignment.orderItemId);
|
|
403
|
-
if (!orderItem)
|
|
404
|
-
continue;
|
|
405
|
-
// Update order item with warehouse
|
|
406
|
-
await tx.orderItem.update({
|
|
407
|
-
where: { id: orderItem.id },
|
|
408
|
-
data: { warehouseId: warehouseAssignment.warehouseId },
|
|
409
|
-
});
|
|
410
|
-
// Only adjust inventory if this is a NEW transition to shipped status
|
|
411
|
-
if (shouldDeductInventory) {
|
|
412
|
-
// Check if variant tracks inventory
|
|
413
|
-
const variant = await tx.productVariant.findUnique({
|
|
414
|
-
where: { id: orderItem.variantId },
|
|
415
|
-
});
|
|
416
|
-
if (variant && variant.trackInventory) {
|
|
417
|
-
// Decrement inventory
|
|
418
|
-
await adjustInventory(tx, {
|
|
419
|
-
variantId: orderItem.variantId,
|
|
420
|
-
warehouseId: warehouseAssignment.warehouseId,
|
|
421
|
-
quantity: -orderItem.quantity,
|
|
422
|
-
type: 'sale',
|
|
423
|
-
reason: `Order #${order.orderNumber} shipped - inventory deducted`,
|
|
424
|
-
orderId: order.id,
|
|
425
|
-
userId: c.get('dbUser')?.id,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// Update order status
|
|
431
|
-
await tx.order.update({
|
|
432
|
-
where: { id },
|
|
433
|
-
data: { status: input.status },
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
try {
|
|
437
|
-
const { enqueueOrderNotification } = await import('../../notifications/producers/order-notification');
|
|
438
|
-
await enqueueOrderNotification(c.env, 'order_shipped', order.id);
|
|
439
|
-
}
|
|
440
|
-
catch (error) {
|
|
441
|
-
console.error('Failed to queue order shipped notification:', error);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
else if (newStatus === OrderStatus.prospect && previousStatus !== OrderStatus.prospect) {
|
|
445
|
-
// Handle prospect status with reason
|
|
446
|
-
if (!input.prospectReason) {
|
|
447
|
-
return c.json({
|
|
448
|
-
error: { code: 'PROSPECT_REASON_REQUIRED', message: 'Prospect reason is required when setting status to prospect' },
|
|
449
|
-
}, 400);
|
|
450
|
-
}
|
|
451
|
-
// Use the markOrderAsProspect function to handle all the logic
|
|
452
|
-
await markOrderAsProspect(prisma, id, input.prospectReason, input.prospectNote || '', c.get('dbUser')?.id);
|
|
453
|
-
// Queue the first prospect recovery email immediately
|
|
454
|
-
// (manual contact already attempted, so no delay needed)
|
|
455
|
-
try {
|
|
456
|
-
const { enqueueProspectRecoveryNotification } = await import('../../notifications/producers/prospect-recovery-notification');
|
|
457
|
-
await enqueueProspectRecoveryNotification(c.env, 'prospect_recovery_reminder', id, 1);
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
console.error('Failed to queue prospect recovery email:', error);
|
|
461
|
-
// Don't fail the status update if queuing fails
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
// For other status updates, just update the status
|
|
466
|
-
await prisma.order.update({
|
|
467
|
-
where: { id },
|
|
468
|
-
data: {
|
|
469
|
-
status: input.status,
|
|
470
|
-
cancellationReason: input.cancellationReason,
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
try {
|
|
474
|
-
let messageType = null;
|
|
475
|
-
if (newStatus === 'delivered' && previousStatus !== 'delivered') {
|
|
476
|
-
messageType = 'order_delivered';
|
|
477
|
-
}
|
|
478
|
-
else if ((newStatus === 'cancelled' && previousStatus !== 'cancelled') || (newStatus === 'returned' && previousStatus !== 'returned')) {
|
|
479
|
-
messageType = 'order_cancelled';
|
|
480
|
-
}
|
|
481
|
-
if (messageType) {
|
|
482
|
-
try {
|
|
483
|
-
const { enqueueOrderNotification } = await import('../../notifications/producers/order-notification');
|
|
484
|
-
await enqueueOrderNotification(c.env, messageType, order.id);
|
|
485
|
-
}
|
|
486
|
-
catch (error) {
|
|
487
|
-
console.error('Failed to queue order status update notification:', error);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
console.error('Failed to queue order status update notification:', error);
|
|
493
|
-
}
|
|
494
|
-
// Queue Meta CAPI ConfirmedPurchase event when order is delivered & paid
|
|
495
|
-
if (newStatus === 'delivered' && previousStatus !== 'delivered') {
|
|
496
|
-
try {
|
|
497
|
-
const { enqueueConfirmedPurchaseEvent } = await import('../../notifications/producers/meta-capi-producer');
|
|
498
|
-
// Fetch full order details with items for the CAPI event
|
|
499
|
-
const fullOrder = await prisma.order.findFirst({
|
|
500
|
-
where: { id, deletedAt: null },
|
|
501
|
-
include: ORDER_INCLUDE_FULL,
|
|
502
|
-
});
|
|
503
|
-
if (fullOrder) {
|
|
504
|
-
await enqueueConfirmedPurchaseEvent(c.env, fullOrder);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
console.error('Failed to queue Meta CAPI ConfirmedPurchase event:', error);
|
|
509
|
-
// Don't fail the status update if CAPI queuing fails
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// Fetch updated order to return
|
|
514
|
-
const updatedOrder = await prisma.order.findFirst({
|
|
515
|
-
where: { id, deletedAt: null },
|
|
516
|
-
include: ORDER_INCLUDE_FULL,
|
|
517
|
-
});
|
|
518
|
-
return c.json(formatOrderResponse(updatedOrder));
|
|
519
|
-
}
|
|
520
|
-
catch (error) {
|
|
521
|
-
// CRITICAL: Order status updates include inventory adjustments
|
|
522
|
-
captureException(error instanceof Error ? error : new Error(String(error)), {
|
|
523
|
-
level: 'fatal',
|
|
524
|
-
tags: {
|
|
525
|
-
error_code: 'ORDER_STATUS_UPDATE_ERROR',
|
|
526
|
-
endpoint: 'PATCH /admin/orders/:id/status',
|
|
527
|
-
},
|
|
528
|
-
extra: {
|
|
529
|
-
orderId: c.req.param('id'),
|
|
530
|
-
},
|
|
531
|
-
honoContext: c,
|
|
532
|
-
});
|
|
533
|
-
return c.json({
|
|
534
|
-
error: { code: 'INTERNAL_ERROR', message: 'Failed to update order status' },
|
|
535
|
-
}, 500);
|
|
536
|
-
}
|
|
537
|
-
}).delete('/:id', zValidator('param', z.object({ id: z.string().uuid() })), async (c) => {
|
|
538
|
-
try {
|
|
539
|
-
const { id } = c.req.valid('param');
|
|
540
|
-
const prisma = getPrismaClient(c.env.DATABASE_URL);
|
|
541
|
-
await prisma.order.delete({
|
|
542
|
-
where: { id },
|
|
543
|
-
});
|
|
544
|
-
return c.status(204);
|
|
545
|
-
}
|
|
546
|
-
catch (error) {
|
|
547
|
-
return c.json({
|
|
548
|
-
error: { code: 'INTERNAL_ERROR', message: 'Failed to delete order' },
|
|
549
|
-
}, 500);
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
export default app;
|