@instockng/api-client 1.0.9 → 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.
Files changed (193) hide show
  1. package/dist/fetchers/brands.d.ts +1 -1
  2. package/dist/fetchers/carts.d.ts +85 -47
  3. package/dist/fetchers/delivery-zones.d.ts +2 -0
  4. package/dist/fetchers/orders.d.ts +26 -14
  5. package/dist/fetchers/products.d.ts +14 -6
  6. package/dist/hooks/admin/abandoned-carts.d.ts +18 -10
  7. package/dist/hooks/admin/brands.d.ts +4 -4
  8. package/dist/hooks/admin/customers.d.ts +13 -7
  9. package/dist/hooks/admin/delivery-zones.d.ts +23 -15
  10. package/dist/hooks/admin/discount-codes.d.ts +10 -10
  11. package/dist/hooks/admin/inventory.d.ts +4 -0
  12. package/dist/hooks/admin/orders.d.ts +78 -42
  13. package/dist/hooks/admin/products.d.ts +14 -6
  14. package/dist/hooks/admin/stats.d.ts +13 -7
  15. package/dist/hooks/admin/variants.d.ts +5 -5
  16. package/dist/hooks/admin/warehouses.d.ts +11 -7
  17. package/dist/hooks/public/brands.d.ts +1 -1
  18. package/dist/hooks/public/carts.d.ts +85 -47
  19. package/dist/hooks/public/delivery-zones.d.ts +2 -0
  20. package/dist/hooks/public/orders.d.ts +26 -14
  21. package/dist/hooks/public/products.d.ts +14 -6
  22. package/dist/rpc-client.d.ts +335 -187
  23. package/package.json +1 -1
  24. package/dist/apps/backend/src/generated/zod/index.d.ts +0 -1114
  25. package/dist/apps/backend/src/generated/zod/index.js +0 -670
  26. package/dist/apps/backend/src/http-app.d.ts +0 -40
  27. package/dist/apps/backend/src/http-app.js +0 -134
  28. package/dist/apps/backend/src/lib/brand-response.d.ts +0 -14
  29. package/dist/apps/backend/src/lib/brand-response.js +0 -8
  30. package/dist/apps/backend/src/lib/cart-helpers.d.ts +0 -282
  31. package/dist/apps/backend/src/lib/cart-helpers.js +0 -121
  32. package/dist/apps/backend/src/lib/cart-recovery.d.ts +0 -30
  33. package/dist/apps/backend/src/lib/cart-recovery.js +0 -147
  34. package/dist/apps/backend/src/lib/cart-response.d.ts +0 -121
  35. package/dist/apps/backend/src/lib/cart-response.js +0 -150
  36. package/dist/apps/backend/src/lib/clerk.d.ts +0 -18
  37. package/dist/apps/backend/src/lib/clerk.js +0 -190
  38. package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +0 -64
  39. package/dist/apps/backend/src/lib/delivery-zone-response.js +0 -24
  40. package/dist/apps/backend/src/lib/discount-code-response.d.ts +0 -42
  41. package/dist/apps/backend/src/lib/discount-code-response.js +0 -19
  42. package/dist/apps/backend/src/lib/discount.d.ts +0 -20
  43. package/dist/apps/backend/src/lib/discount.js +0 -35
  44. package/dist/apps/backend/src/lib/inventory.d.ts +0 -26
  45. package/dist/apps/backend/src/lib/inventory.js +0 -160
  46. package/dist/apps/backend/src/lib/meta-capi.d.ts +0 -53
  47. package/dist/apps/backend/src/lib/meta-capi.js +0 -151
  48. package/dist/apps/backend/src/lib/openapi.d.ts +0 -36
  49. package/dist/apps/backend/src/lib/openapi.js +0 -69
  50. package/dist/apps/backend/src/lib/order-recovery.d.ts +0 -459
  51. package/dist/apps/backend/src/lib/order-recovery.js +0 -378
  52. package/dist/apps/backend/src/lib/order-response.d.ts +0 -138
  53. package/dist/apps/backend/src/lib/order-response.js +0 -61
  54. package/dist/apps/backend/src/lib/pricing.d.ts +0 -39
  55. package/dist/apps/backend/src/lib/pricing.js +0 -62
  56. package/dist/apps/backend/src/lib/prisma.d.ts +0 -9
  57. package/dist/apps/backend/src/lib/prisma.js +0 -30
  58. package/dist/apps/backend/src/lib/product-response.d.ts +0 -82
  59. package/dist/apps/backend/src/lib/product-response.js +0 -29
  60. package/dist/apps/backend/src/lib/sentry.d.ts +0 -48
  61. package/dist/apps/backend/src/lib/sentry.js +0 -180
  62. package/dist/apps/backend/src/lib/utils.d.ts +0 -32
  63. package/dist/apps/backend/src/lib/utils.js +0 -63
  64. package/dist/apps/backend/src/middleware/clerk-auth.d.ts +0 -8
  65. package/dist/apps/backend/src/middleware/clerk-auth.js +0 -89
  66. package/dist/apps/backend/src/middleware/cors.d.ts +0 -8
  67. package/dist/apps/backend/src/middleware/cors.js +0 -11
  68. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.d.ts +0 -62
  69. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +0 -180
  70. package/dist/apps/backend/src/notifications/producers/order-notification.d.ts +0 -9
  71. package/dist/apps/backend/src/notifications/producers/order-notification.js +0 -18
  72. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.d.ts +0 -10
  73. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.js +0 -11
  74. package/dist/apps/backend/src/routes/admin/abandoned-carts.d.ts +0 -613
  75. package/dist/apps/backend/src/routes/admin/abandoned-carts.js +0 -194
  76. package/dist/apps/backend/src/routes/admin/brands.d.ts +0 -175
  77. package/dist/apps/backend/src/routes/admin/brands.js +0 -118
  78. package/dist/apps/backend/src/routes/admin/customers.d.ts +0 -312
  79. package/dist/apps/backend/src/routes/admin/customers.js +0 -39
  80. package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +0 -446
  81. package/dist/apps/backend/src/routes/admin/delivery-zones.js +0 -300
  82. package/dist/apps/backend/src/routes/admin/discount-codes.d.ts +0 -478
  83. package/dist/apps/backend/src/routes/admin/discount-codes.js +0 -418
  84. package/dist/apps/backend/src/routes/admin/inventory.d.ts +0 -277
  85. package/dist/apps/backend/src/routes/admin/inventory.js +0 -199
  86. package/dist/apps/backend/src/routes/admin/orders.d.ts +0 -1804
  87. package/dist/apps/backend/src/routes/admin/orders.js +0 -552
  88. package/dist/apps/backend/src/routes/admin/products.d.ts +0 -876
  89. package/dist/apps/backend/src/routes/admin/products.js +0 -126
  90. package/dist/apps/backend/src/routes/admin/stats.d.ts +0 -294
  91. package/dist/apps/backend/src/routes/admin/stats.js +0 -55
  92. package/dist/apps/backend/src/routes/admin/variants.d.ts +0 -239
  93. package/dist/apps/backend/src/routes/admin/variants.js +0 -197
  94. package/dist/apps/backend/src/routes/admin/warehouses.d.ts +0 -377
  95. package/dist/apps/backend/src/routes/admin/warehouses.js +0 -123
  96. package/dist/apps/backend/src/routes/public/brands.d.ts +0 -40
  97. package/dist/apps/backend/src/routes/public/brands.js +0 -38
  98. package/dist/apps/backend/src/routes/public/carts.d.ts +0 -2693
  99. package/dist/apps/backend/src/routes/public/carts.js +0 -778
  100. package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +0 -37
  101. package/dist/apps/backend/src/routes/public/delivery-zones.js +0 -64
  102. package/dist/apps/backend/src/routes/public/orders.d.ts +0 -617
  103. package/dist/apps/backend/src/routes/public/orders.js +0 -184
  104. package/dist/apps/backend/src/routes/public/products.d.ts +0 -457
  105. package/dist/apps/backend/src/routes/public/products.js +0 -133
  106. package/dist/apps/backend/src/types/index.d.ts +0 -43
  107. package/dist/apps/backend/src/types/index.js +0 -2
  108. package/dist/apps/backend/src/validators/brand.d.ts +0 -17
  109. package/dist/apps/backend/src/validators/brand.js +0 -15
  110. package/dist/apps/backend/src/validators/delivery-zone.d.ts +0 -35
  111. package/dist/apps/backend/src/validators/delivery-zone.js +0 -55
  112. package/dist/apps/backend/src/validators/discount-code.d.ts +0 -74
  113. package/dist/apps/backend/src/validators/discount-code.js +0 -50
  114. package/dist/apps/backend/src/validators/inventory.d.ts +0 -20
  115. package/dist/apps/backend/src/validators/inventory.js +0 -15
  116. package/dist/apps/backend/src/validators/order.d.ts +0 -58
  117. package/dist/apps/backend/src/validators/order.js +0 -62
  118. package/dist/apps/backend/src/validators/product.d.ts +0 -18
  119. package/dist/apps/backend/src/validators/product.js +0 -19
  120. package/dist/apps/backend/src/validators/variant.d.ts +0 -19
  121. package/dist/apps/backend/src/validators/variant.js +0 -19
  122. package/dist/apps/backend/src/validators/warehouse.d.ts +0 -15
  123. package/dist/apps/backend/src/validators/warehouse.js +0 -15
  124. package/dist/packages/api-client/src/backend-types.d.ts +0 -10
  125. package/dist/packages/api-client/src/backend-types.js +0 -10
  126. package/dist/packages/api-client/src/client.d.ts +0 -20
  127. package/dist/packages/api-client/src/client.js +0 -40
  128. package/dist/packages/api-client/src/enum-types.d.ts +0 -8
  129. package/dist/packages/api-client/src/enum-types.js +0 -5
  130. package/dist/packages/api-client/src/fetchers/brands.d.ts +0 -25
  131. package/dist/packages/api-client/src/fetchers/brands.js +0 -26
  132. package/dist/packages/api-client/src/fetchers/carts.d.ts +0 -2373
  133. package/dist/packages/api-client/src/fetchers/carts.js +0 -174
  134. package/dist/packages/api-client/src/fetchers/delivery-zones.d.ts +0 -30
  135. package/dist/packages/api-client/src/fetchers/delivery-zones.js +0 -26
  136. package/dist/packages/api-client/src/fetchers/index.d.ts +0 -22
  137. package/dist/packages/api-client/src/fetchers/index.js +0 -22
  138. package/dist/packages/api-client/src/fetchers/orders.d.ts +0 -552
  139. package/dist/packages/api-client/src/fetchers/orders.js +0 -44
  140. package/dist/packages/api-client/src/fetchers/products.d.ts +0 -394
  141. package/dist/packages/api-client/src/fetchers/products.js +0 -42
  142. package/dist/packages/api-client/src/hooks/admin/abandoned-carts.d.ts +0 -543
  143. package/dist/packages/api-client/src/hooks/admin/abandoned-carts.js +0 -83
  144. package/dist/packages/api-client/src/hooks/admin/brands.d.ts +0 -79
  145. package/dist/packages/api-client/src/hooks/admin/brands.js +0 -108
  146. package/dist/packages/api-client/src/hooks/admin/customers.d.ts +0 -284
  147. package/dist/packages/api-client/src/hooks/admin/customers.js +0 -26
  148. package/dist/packages/api-client/src/hooks/admin/delivery-zones.d.ts +0 -278
  149. package/dist/packages/api-client/src/hooks/admin/delivery-zones.js +0 -176
  150. package/dist/packages/api-client/src/hooks/admin/discount-codes.d.ts +0 -299
  151. package/dist/packages/api-client/src/hooks/admin/discount-codes.js +0 -165
  152. package/dist/packages/api-client/src/hooks/admin/index.d.ts +0 -16
  153. package/dist/packages/api-client/src/hooks/admin/index.js +0 -16
  154. package/dist/packages/api-client/src/hooks/admin/inventory.d.ts +0 -228
  155. package/dist/packages/api-client/src/hooks/admin/inventory.js +0 -107
  156. package/dist/packages/api-client/src/hooks/admin/orders.d.ts +0 -1698
  157. package/dist/packages/api-client/src/hooks/admin/orders.js +0 -178
  158. package/dist/packages/api-client/src/hooks/admin/products.d.ts +0 -382
  159. package/dist/packages/api-client/src/hooks/admin/products.js +0 -89
  160. package/dist/packages/api-client/src/hooks/admin/stats.d.ts +0 -283
  161. package/dist/packages/api-client/src/hooks/admin/stats.js +0 -25
  162. package/dist/packages/api-client/src/hooks/admin/variants.d.ts +0 -115
  163. package/dist/packages/api-client/src/hooks/admin/variants.js +0 -127
  164. package/dist/packages/api-client/src/hooks/admin/warehouses.d.ts +0 -281
  165. package/dist/packages/api-client/src/hooks/admin/warehouses.js +0 -108
  166. package/dist/packages/api-client/src/hooks/public/brands.d.ts +0 -33
  167. package/dist/packages/api-client/src/hooks/public/brands.js +0 -30
  168. package/dist/packages/api-client/src/hooks/public/carts.d.ts +0 -2443
  169. package/dist/packages/api-client/src/hooks/public/carts.js +0 -213
  170. package/dist/packages/api-client/src/hooks/public/delivery-zones.d.ts +0 -36
  171. package/dist/packages/api-client/src/hooks/public/delivery-zones.js +0 -28
  172. package/dist/packages/api-client/src/hooks/public/index.d.ts +0 -10
  173. package/dist/packages/api-client/src/hooks/public/index.js +0 -10
  174. package/dist/packages/api-client/src/hooks/public/orders.d.ts +0 -571
  175. package/dist/packages/api-client/src/hooks/public/orders.js +0 -50
  176. package/dist/packages/api-client/src/hooks/public/products.d.ts +0 -406
  177. package/dist/packages/api-client/src/hooks/public/products.js +0 -47
  178. package/dist/packages/api-client/src/hooks/use-query-unwrapped.d.ts +0 -20
  179. package/dist/packages/api-client/src/hooks/use-query-unwrapped.js +0 -22
  180. package/dist/packages/api-client/src/hooks/useApiConfig.d.ts +0 -12
  181. package/dist/packages/api-client/src/hooks/useApiConfig.js +0 -14
  182. package/dist/packages/api-client/src/index.d.ts +0 -20
  183. package/dist/packages/api-client/src/index.js +0 -25
  184. package/dist/packages/api-client/src/provider.d.ts +0 -36
  185. package/dist/packages/api-client/src/provider.js +0 -54
  186. package/dist/packages/api-client/src/rpc-client.d.ts +0 -9755
  187. package/dist/packages/api-client/src/rpc-client.js +0 -78
  188. package/dist/packages/api-client/src/rpc-types.d.ts +0 -76
  189. package/dist/packages/api-client/src/rpc-types.js +0 -7
  190. package/dist/packages/api-client/src/types.d.ts +0 -34
  191. package/dist/packages/api-client/src/types.js +0 -16
  192. package/dist/packages/api-client/src/utils/query-keys.d.ts +0 -106
  193. 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;