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