@instockng/api-client 1.0.11 → 1.0.12

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 (169) hide show
  1. package/package.json +2 -2
  2. package/dist/apps/backend/src/http-app.d.ts +0 -40
  3. package/dist/apps/backend/src/http-app.js +0 -134
  4. package/dist/apps/backend/src/lib/brand-response.d.ts +0 -16
  5. package/dist/apps/backend/src/lib/brand-response.js +0 -8
  6. package/dist/apps/backend/src/lib/cart-helpers.d.ts +0 -286
  7. package/dist/apps/backend/src/lib/cart-helpers.js +0 -121
  8. package/dist/apps/backend/src/lib/cart-recovery.d.ts +0 -30
  9. package/dist/apps/backend/src/lib/cart-recovery.js +0 -147
  10. package/dist/apps/backend/src/lib/cart-response.d.ts +0 -123
  11. package/dist/apps/backend/src/lib/cart-response.js +0 -150
  12. package/dist/apps/backend/src/lib/clerk.d.ts +0 -18
  13. package/dist/apps/backend/src/lib/clerk.js +0 -190
  14. package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +0 -66
  15. package/dist/apps/backend/src/lib/delivery-zone-response.js +0 -24
  16. package/dist/apps/backend/src/lib/discount-code-response.d.ts +0 -44
  17. package/dist/apps/backend/src/lib/discount-code-response.js +0 -19
  18. package/dist/apps/backend/src/lib/discount.d.ts +0 -20
  19. package/dist/apps/backend/src/lib/discount.js +0 -35
  20. package/dist/apps/backend/src/lib/inventory.d.ts +0 -26
  21. package/dist/apps/backend/src/lib/inventory.js +0 -160
  22. package/dist/apps/backend/src/lib/meta-capi.d.ts +0 -53
  23. package/dist/apps/backend/src/lib/meta-capi.js +0 -151
  24. package/dist/apps/backend/src/lib/openapi.d.ts +0 -36
  25. package/dist/apps/backend/src/lib/openapi.js +0 -69
  26. package/dist/apps/backend/src/lib/order-recovery.d.ts +0 -465
  27. package/dist/apps/backend/src/lib/order-recovery.js +0 -378
  28. package/dist/apps/backend/src/lib/order-response.d.ts +0 -140
  29. package/dist/apps/backend/src/lib/order-response.js +0 -61
  30. package/dist/apps/backend/src/lib/pricing.d.ts +0 -39
  31. package/dist/apps/backend/src/lib/pricing.js +0 -62
  32. package/dist/apps/backend/src/lib/prisma.d.ts +0 -9
  33. package/dist/apps/backend/src/lib/prisma.js +0 -30
  34. package/dist/apps/backend/src/lib/product-response.d.ts +0 -84
  35. package/dist/apps/backend/src/lib/product-response.js +0 -29
  36. package/dist/apps/backend/src/lib/sentry.d.ts +0 -48
  37. package/dist/apps/backend/src/lib/sentry.js +0 -180
  38. package/dist/apps/backend/src/lib/utils.d.ts +0 -32
  39. package/dist/apps/backend/src/lib/utils.js +0 -63
  40. package/dist/apps/backend/src/middleware/clerk-auth.d.ts +0 -8
  41. package/dist/apps/backend/src/middleware/clerk-auth.js +0 -89
  42. package/dist/apps/backend/src/middleware/cors.d.ts +0 -8
  43. package/dist/apps/backend/src/middleware/cors.js +0 -11
  44. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.d.ts +0 -62
  45. package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +0 -180
  46. package/dist/apps/backend/src/notifications/producers/order-notification.d.ts +0 -9
  47. package/dist/apps/backend/src/notifications/producers/order-notification.js +0 -18
  48. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.d.ts +0 -10
  49. package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.js +0 -11
  50. package/dist/apps/backend/src/routes/admin/abandoned-carts.d.ts +0 -609
  51. package/dist/apps/backend/src/routes/admin/abandoned-carts.js +0 -194
  52. package/dist/apps/backend/src/routes/admin/brands.d.ts +0 -183
  53. package/dist/apps/backend/src/routes/admin/brands.js +0 -118
  54. package/dist/apps/backend/src/routes/admin/customers.d.ts +0 -310
  55. package/dist/apps/backend/src/routes/admin/customers.js +0 -39
  56. package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +0 -454
  57. package/dist/apps/backend/src/routes/admin/delivery-zones.js +0 -300
  58. package/dist/apps/backend/src/routes/admin/discount-codes.d.ts +0 -488
  59. package/dist/apps/backend/src/routes/admin/discount-codes.js +0 -418
  60. package/dist/apps/backend/src/routes/admin/inventory.d.ts +0 -273
  61. package/dist/apps/backend/src/routes/admin/inventory.js +0 -199
  62. package/dist/apps/backend/src/routes/admin/orders.d.ts +0 -1792
  63. package/dist/apps/backend/src/routes/admin/orders.js +0 -552
  64. package/dist/apps/backend/src/routes/admin/products.d.ts +0 -868
  65. package/dist/apps/backend/src/routes/admin/products.js +0 -126
  66. package/dist/apps/backend/src/routes/admin/stats.d.ts +0 -292
  67. package/dist/apps/backend/src/routes/admin/stats.js +0 -55
  68. package/dist/apps/backend/src/routes/admin/variants.d.ts +0 -239
  69. package/dist/apps/backend/src/routes/admin/variants.js +0 -197
  70. package/dist/apps/backend/src/routes/admin/warehouses.d.ts +0 -375
  71. package/dist/apps/backend/src/routes/admin/warehouses.js +0 -123
  72. package/dist/apps/backend/src/routes/public/brands.d.ts +0 -41
  73. package/dist/apps/backend/src/routes/public/brands.js +0 -39
  74. package/dist/apps/backend/src/routes/public/carts.d.ts +0 -2675
  75. package/dist/apps/backend/src/routes/public/carts.js +0 -778
  76. package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +0 -37
  77. package/dist/apps/backend/src/routes/public/delivery-zones.js +0 -64
  78. package/dist/apps/backend/src/routes/public/orders.d.ts +0 -613
  79. package/dist/apps/backend/src/routes/public/orders.js +0 -184
  80. package/dist/apps/backend/src/routes/public/products.d.ts +0 -453
  81. package/dist/apps/backend/src/routes/public/products.js +0 -133
  82. package/dist/apps/backend/src/types/index.d.ts +0 -43
  83. package/dist/apps/backend/src/types/index.js +0 -2
  84. package/dist/apps/backend/src/validators/brand.d.ts +0 -21
  85. package/dist/apps/backend/src/validators/brand.js +0 -19
  86. package/dist/apps/backend/src/validators/delivery-zone.d.ts +0 -35
  87. package/dist/apps/backend/src/validators/delivery-zone.js +0 -55
  88. package/dist/apps/backend/src/validators/discount-code.d.ts +0 -74
  89. package/dist/apps/backend/src/validators/discount-code.js +0 -50
  90. package/dist/apps/backend/src/validators/inventory.d.ts +0 -20
  91. package/dist/apps/backend/src/validators/inventory.js +0 -15
  92. package/dist/apps/backend/src/validators/order.d.ts +0 -58
  93. package/dist/apps/backend/src/validators/order.js +0 -62
  94. package/dist/apps/backend/src/validators/product.d.ts +0 -18
  95. package/dist/apps/backend/src/validators/product.js +0 -19
  96. package/dist/apps/backend/src/validators/variant.d.ts +0 -19
  97. package/dist/apps/backend/src/validators/variant.js +0 -19
  98. package/dist/apps/backend/src/validators/warehouse.d.ts +0 -15
  99. package/dist/apps/backend/src/validators/warehouse.js +0 -15
  100. /package/dist/{packages/api-client/src/backend-types.d.ts → backend-types.d.ts} +0 -0
  101. /package/dist/{packages/api-client/src/backend-types.js → backend-types.js} +0 -0
  102. /package/dist/{packages/api-client/src/client.d.ts → client.d.ts} +0 -0
  103. /package/dist/{packages/api-client/src/client.js → client.js} +0 -0
  104. /package/dist/{packages/api-client/src/enum-types.d.ts → enum-types.d.ts} +0 -0
  105. /package/dist/{packages/api-client/src/enum-types.js → enum-types.js} +0 -0
  106. /package/dist/{packages/api-client/src/fetchers → fetchers}/brands.d.ts +0 -0
  107. /package/dist/{packages/api-client/src/fetchers → fetchers}/brands.js +0 -0
  108. /package/dist/{packages/api-client/src/fetchers → fetchers}/carts.d.ts +0 -0
  109. /package/dist/{packages/api-client/src/fetchers → fetchers}/carts.js +0 -0
  110. /package/dist/{packages/api-client/src/fetchers → fetchers}/delivery-zones.d.ts +0 -0
  111. /package/dist/{packages/api-client/src/fetchers → fetchers}/delivery-zones.js +0 -0
  112. /package/dist/{packages/api-client/src/fetchers → fetchers}/index.d.ts +0 -0
  113. /package/dist/{packages/api-client/src/fetchers → fetchers}/index.js +0 -0
  114. /package/dist/{packages/api-client/src/fetchers → fetchers}/orders.d.ts +0 -0
  115. /package/dist/{packages/api-client/src/fetchers → fetchers}/orders.js +0 -0
  116. /package/dist/{packages/api-client/src/fetchers → fetchers}/products.d.ts +0 -0
  117. /package/dist/{packages/api-client/src/fetchers → fetchers}/products.js +0 -0
  118. /package/dist/{packages/api-client/src/hooks → hooks}/admin/abandoned-carts.d.ts +0 -0
  119. /package/dist/{packages/api-client/src/hooks → hooks}/admin/abandoned-carts.js +0 -0
  120. /package/dist/{packages/api-client/src/hooks → hooks}/admin/brands.d.ts +0 -0
  121. /package/dist/{packages/api-client/src/hooks → hooks}/admin/brands.js +0 -0
  122. /package/dist/{packages/api-client/src/hooks → hooks}/admin/customers.d.ts +0 -0
  123. /package/dist/{packages/api-client/src/hooks → hooks}/admin/customers.js +0 -0
  124. /package/dist/{packages/api-client/src/hooks → hooks}/admin/delivery-zones.d.ts +0 -0
  125. /package/dist/{packages/api-client/src/hooks → hooks}/admin/delivery-zones.js +0 -0
  126. /package/dist/{packages/api-client/src/hooks → hooks}/admin/discount-codes.d.ts +0 -0
  127. /package/dist/{packages/api-client/src/hooks → hooks}/admin/discount-codes.js +0 -0
  128. /package/dist/{packages/api-client/src/hooks → hooks}/admin/index.d.ts +0 -0
  129. /package/dist/{packages/api-client/src/hooks → hooks}/admin/index.js +0 -0
  130. /package/dist/{packages/api-client/src/hooks → hooks}/admin/inventory.d.ts +0 -0
  131. /package/dist/{packages/api-client/src/hooks → hooks}/admin/inventory.js +0 -0
  132. /package/dist/{packages/api-client/src/hooks → hooks}/admin/orders.d.ts +0 -0
  133. /package/dist/{packages/api-client/src/hooks → hooks}/admin/orders.js +0 -0
  134. /package/dist/{packages/api-client/src/hooks → hooks}/admin/products.d.ts +0 -0
  135. /package/dist/{packages/api-client/src/hooks → hooks}/admin/products.js +0 -0
  136. /package/dist/{packages/api-client/src/hooks → hooks}/admin/stats.d.ts +0 -0
  137. /package/dist/{packages/api-client/src/hooks → hooks}/admin/stats.js +0 -0
  138. /package/dist/{packages/api-client/src/hooks → hooks}/admin/variants.d.ts +0 -0
  139. /package/dist/{packages/api-client/src/hooks → hooks}/admin/variants.js +0 -0
  140. /package/dist/{packages/api-client/src/hooks → hooks}/admin/warehouses.d.ts +0 -0
  141. /package/dist/{packages/api-client/src/hooks → hooks}/admin/warehouses.js +0 -0
  142. /package/dist/{packages/api-client/src/hooks → hooks}/public/brands.d.ts +0 -0
  143. /package/dist/{packages/api-client/src/hooks → hooks}/public/brands.js +0 -0
  144. /package/dist/{packages/api-client/src/hooks → hooks}/public/carts.d.ts +0 -0
  145. /package/dist/{packages/api-client/src/hooks → hooks}/public/carts.js +0 -0
  146. /package/dist/{packages/api-client/src/hooks → hooks}/public/delivery-zones.d.ts +0 -0
  147. /package/dist/{packages/api-client/src/hooks → hooks}/public/delivery-zones.js +0 -0
  148. /package/dist/{packages/api-client/src/hooks → hooks}/public/index.d.ts +0 -0
  149. /package/dist/{packages/api-client/src/hooks → hooks}/public/index.js +0 -0
  150. /package/dist/{packages/api-client/src/hooks → hooks}/public/orders.d.ts +0 -0
  151. /package/dist/{packages/api-client/src/hooks → hooks}/public/orders.js +0 -0
  152. /package/dist/{packages/api-client/src/hooks → hooks}/public/products.d.ts +0 -0
  153. /package/dist/{packages/api-client/src/hooks → hooks}/public/products.js +0 -0
  154. /package/dist/{packages/api-client/src/hooks → hooks}/use-query-unwrapped.d.ts +0 -0
  155. /package/dist/{packages/api-client/src/hooks → hooks}/use-query-unwrapped.js +0 -0
  156. /package/dist/{packages/api-client/src/hooks → hooks}/useApiConfig.d.ts +0 -0
  157. /package/dist/{packages/api-client/src/hooks → hooks}/useApiConfig.js +0 -0
  158. /package/dist/{packages/api-client/src/index.d.ts → index.d.ts} +0 -0
  159. /package/dist/{packages/api-client/src/index.js → index.js} +0 -0
  160. /package/dist/{packages/api-client/src/provider.d.ts → provider.d.ts} +0 -0
  161. /package/dist/{packages/api-client/src/provider.js → provider.js} +0 -0
  162. /package/dist/{packages/api-client/src/rpc-client.d.ts → rpc-client.d.ts} +0 -0
  163. /package/dist/{packages/api-client/src/rpc-client.js → rpc-client.js} +0 -0
  164. /package/dist/{packages/api-client/src/rpc-types.d.ts → rpc-types.d.ts} +0 -0
  165. /package/dist/{packages/api-client/src/rpc-types.js → rpc-types.js} +0 -0
  166. /package/dist/{packages/api-client/src/types.d.ts → types.d.ts} +0 -0
  167. /package/dist/{packages/api-client/src/types.js → types.js} +0 -0
  168. /package/dist/{packages/api-client/src/utils → utils}/query-keys.d.ts +0 -0
  169. /package/dist/{packages/api-client/src/utils → utils}/query-keys.js +0 -0
@@ -1,778 +0,0 @@
1
- import { Hono } from 'hono';
2
- import { zValidator } from '@hono/zod-validator';
3
- import { z } from 'zod';
4
- import { getPrismaClient } from '../../lib/prisma';
5
- import { calculateCartCost } from '../../lib/pricing';
6
- import { buildCartResponseWithPricing } from '../../lib/cart-response';
7
- import { toPricingItems } from '../../lib/cart-response';
8
- import { validateCart, buildCartResponse, etagFrom, paymentFromZone, CART_INCLUDE_FULL, CART_ITEM_WITH_CART_INCLUDE, ORDER_INCLUDE_FULL, } from '../../lib/cart-helpers';
9
- import { round, toNumber } from '../../lib/utils';
10
- import { formatOrderResponse } from '../../lib/order-response';
11
- import { captureMessage, captureException, getSampleRate } from '../../lib/sentry';
12
- const app = new Hono()
13
- // CREATE CART
14
- .post('/', zValidator('json', z.object({
15
- brandSlug: z.string(),
16
- })), async (c) => {
17
- const input = c.req.valid('json');
18
- try {
19
- const prisma = getPrismaClient(c.env.DATABASE_URL);
20
- // Look up brand by slug
21
- const brand = await prisma.brand.findFirst({
22
- where: { slug: input.brandSlug, deletedAt: null },
23
- });
24
- if (!brand) {
25
- // Track brand not found with sampling - could indicate broken links
26
- captureMessage('Brand not found during cart creation', {
27
- level: 'info',
28
- tags: {
29
- error_code: 'BRAND_NOT_FOUND',
30
- endpoint: 'POST /carts',
31
- },
32
- extra: {
33
- brandSlug: input.brandSlug,
34
- },
35
- sampleRate: 0.1, // 10% sampling
36
- });
37
- return c.json({ error: { code: 'BRAND_NOT_FOUND', message: 'Brand not found' } }, 404);
38
- }
39
- // Create cart that expires in 7 days
40
- const cart = await prisma.cart.create({
41
- data: {
42
- brandId: brand.id,
43
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
44
- availablePaymentMethods: ['cod', 'online'],
45
- },
46
- include: CART_INCLUDE_FULL,
47
- });
48
- const responseCart = await buildCartResponseWithPricing(prisma, cart);
49
- return c.json(responseCart, 201);
50
- }
51
- catch (error) {
52
- console.error('Error creating cart:', error);
53
- // Capture cart creation errors - could be database issues
54
- captureException(error instanceof Error ? error : new Error(String(error)), {
55
- level: 'error',
56
- tags: {
57
- error_code: 'CART_CREATE_ERROR',
58
- endpoint: 'POST /carts',
59
- },
60
- extra: {
61
- brandSlug: input.brandSlug,
62
- errorMessage: error?.message,
63
- },
64
- honoContext: c,
65
- });
66
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
67
- }
68
- })
69
- // GET CART
70
- .get('/:id', zValidator('param', z.object({ id: z.string().uuid() })), async (c) => {
71
- try {
72
- const { id: cartId } = c.req.valid('param');
73
- const prisma = getPrismaClient(c.env.DATABASE_URL);
74
- // Validate cart exists and is not expired
75
- const { cart, error } = await validateCart(prisma, cartId);
76
- if (error) {
77
- return c.json({ error: { code: error.code, message: error.message } }, error.status);
78
- }
79
- const responseCart = await buildCartResponse(prisma, cart);
80
- c.header('ETag', etagFrom(cart.updatedAt));
81
- return c.json(responseCart, 200);
82
- }
83
- catch (error) {
84
- console.error('Error fetching cart:', error);
85
- captureException(error instanceof Error ? error : new Error(String(error)), {
86
- level: 'error',
87
- tags: {
88
- error_code: 'CART_FETCH_ERROR',
89
- endpoint: 'GET /carts/:id',
90
- },
91
- extra: {
92
- cartId: c.req.param('id'),
93
- },
94
- honoContext: c,
95
- });
96
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
97
- }
98
- })
99
- // UPDATE CART
100
- .patch('/:id', zValidator('param', z.object({ id: z.string().uuid() })), zValidator('json', z.object({
101
- customerPhone: z.string().optional().nullable(),
102
- customerEmail: z.string().email().optional().nullable(),
103
- customerFirstName: z.string().optional().nullable(),
104
- customerLastName: z.string().optional().nullable(),
105
- deliveryZoneId: z.string().uuid().optional().nullable(),
106
- ifUnmodifiedSince: z.string().datetime().optional(),
107
- })), async (c) => {
108
- try {
109
- const { id: cartId } = c.req.valid('param');
110
- const input = c.req.valid('json');
111
- const prisma = getPrismaClient(c.env.DATABASE_URL);
112
- // Validate cart exists and is not expired
113
- const { cart, error } = await validateCart(prisma, cartId);
114
- if (error) {
115
- return c.json({ error: { code: error.code, message: error.message } }, error.status);
116
- }
117
- // Handle concurrency check
118
- const ifMatch = c.req.header('if-match');
119
- const bodyIfUnmodified = input.ifUnmodifiedSince ? new Date(input.ifUnmodifiedSince) : undefined;
120
- if (ifMatch && ifMatch !== etagFrom(cart.updatedAt)) {
121
- return c.json({ error: { code: 'PRECONDITION_FAILED', message: 'Cart has changed' } }, 412);
122
- }
123
- if (bodyIfUnmodified && cart.updatedAt.getTime() !== bodyIfUnmodified.getTime()) {
124
- return c.json({ error: { code: 'PRECONDITION_FAILED', message: 'Cart has changed' } }, 412);
125
- }
126
- // Build update data
127
- const updateData = {
128
- customerPhone: input.customerPhone,
129
- customerEmail: input.customerEmail,
130
- customerFirstName: input.customerFirstName,
131
- customerLastName: input.customerLastName,
132
- };
133
- // Handle delivery zone change
134
- if (input.deliveryZoneId !== undefined) {
135
- if (!input.deliveryZoneId) {
136
- // Unsetting zone
137
- updateData.deliveryZoneId = null;
138
- updateData.deliveryZoneLockedAt = null;
139
- updateData.availablePaymentMethods = ['cod', 'online'];
140
- }
141
- else {
142
- // Setting zone
143
- const zone = await prisma.deliveryZone.findFirst({
144
- where: { id: input.deliveryZoneId, isActive: true, deletedAt: null },
145
- });
146
- if (!zone) {
147
- return c.json({ error: { code: 'INVALID_DELIVERY_ZONE', message: 'Zone not available' } }, 400);
148
- }
149
- updateData.deliveryZoneId = zone.id;
150
- updateData.deliveryZoneLockedAt = new Date();
151
- updateData.availablePaymentMethods = paymentFromZone(zone);
152
- }
153
- }
154
- const updatedCart = await prisma.cart.update({
155
- where: { id: cartId },
156
- data: updateData,
157
- include: CART_INCLUDE_FULL,
158
- });
159
- const responseCart = await buildCartResponse(prisma, updatedCart);
160
- c.header('ETag', etagFrom(updatedCart.updatedAt));
161
- return c.json(responseCart, 200);
162
- }
163
- catch (error) {
164
- console.error('Error updating cart:', error);
165
- captureException(error instanceof Error ? error : new Error(String(error)), {
166
- level: 'error',
167
- tags: {
168
- error_code: 'CART_UPDATE_ERROR',
169
- endpoint: 'PATCH /carts/:id',
170
- },
171
- extra: {
172
- cartId: c.req.param('id'),
173
- },
174
- honoContext: c,
175
- });
176
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
177
- }
178
- })
179
- // APPLY DISCOUNT CODE
180
- .post('/:id/apply-discount', zValidator('param', z.object({ id: z.string().uuid() })), zValidator('json', z.object({ code: z.string() })), async (c) => {
181
- try {
182
- const { id: cartId } = c.req.valid('param');
183
- const { code } = c.req.valid('json');
184
- const prisma = getPrismaClient(c.env.DATABASE_URL);
185
- // Validate cart exists and is not expired
186
- const { cart, error } = await validateCart(prisma, cartId);
187
- if (error) {
188
- return c.json({ error: { code: error.code, message: error.message } }, error.status);
189
- }
190
- const now = new Date();
191
- const discount = await prisma.discountCode.findFirst({
192
- where: {
193
- code,
194
- deletedAt: null,
195
- isActive: true,
196
- validFrom: { lte: now },
197
- AND: [
198
- { OR: [{ validUntil: null }, { validUntil: { gt: now } }] },
199
- { OR: [{ brandId: null }, { brandId: cart.brandId }] },
200
- ],
201
- },
202
- });
203
- if (!discount) {
204
- // Track invalid discount code attempts
205
- captureMessage('Invalid or expired discount code', {
206
- level: 'info',
207
- tags: {
208
- error_code: 'INVALID_DISCOUNT_CODE',
209
- brand: cart.brand?.slug || 'unknown',
210
- },
211
- extra: {
212
- code,
213
- cartId,
214
- brandId: cart.brandId,
215
- },
216
- sampleRate: getSampleRate('INVALID_DISCOUNT_CODE'), // 20% sampling
217
- });
218
- return c.json({ error: { code: 'INVALID_DISCOUNT_CODE', message: 'Invalid or expired discount code' } }, 400);
219
- }
220
- // Check minimum purchase
221
- const pricing = calculateCartCost(toPricingItems(cart));
222
- if (discount.minPurchase) {
223
- const minPurchase = toNumber(discount.minPurchase);
224
- if (pricing.subtotal < minPurchase) {
225
- // Track minimum purchase not met
226
- captureMessage('Minimum purchase not met for discount code', {
227
- level: 'info',
228
- tags: {
229
- error_code: 'MIN_PURCHASE_NOT_MET',
230
- brand: cart.brand?.slug || 'unknown',
231
- },
232
- extra: {
233
- code,
234
- cartId,
235
- minPurchase,
236
- currentSubtotal: pricing.subtotal,
237
- shortfall: minPurchase - pricing.subtotal,
238
- },
239
- sampleRate: getSampleRate('MIN_PURCHASE_NOT_MET'), // 15% sampling
240
- });
241
- return c.json({ error: { code: 'MIN_PURCHASE_NOT_MET', message: `Minimum purchase of ₦${minPurchase.toLocaleString('en-NG', { minimumFractionDigits: 2 })} required` } }, 400);
242
- }
243
- }
244
- // Attach discount to cart
245
- const updated = await prisma.cart.update({
246
- where: { id: cartId },
247
- data: { appliedDiscountCodeId: discount.id },
248
- include: CART_INCLUDE_FULL,
249
- });
250
- const responseCart = await buildCartResponse(prisma, updated);
251
- return c.json(responseCart, 200);
252
- }
253
- catch (error) {
254
- console.error('Error applying discount to cart:', error);
255
- captureException(error instanceof Error ? error : new Error(String(error)), {
256
- level: 'error',
257
- tags: {
258
- error_code: 'DISCOUNT_APPLY_ERROR',
259
- endpoint: 'POST /carts/:id/apply-discount',
260
- },
261
- extra: {
262
- cartId: c.req.param('id'),
263
- },
264
- honoContext: c,
265
- });
266
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
267
- }
268
- })
269
- // REMOVE DISCOUNT CODE
270
- .post('/:id/remove-discount', zValidator('param', z.object({ id: z.string().uuid() })), async (c) => {
271
- try {
272
- const { id: cartId } = c.req.valid('param');
273
- const prisma = getPrismaClient(c.env.DATABASE_URL);
274
- // Validate cart exists and is not expired
275
- const { error } = await validateCart(prisma, cartId);
276
- if (error) {
277
- return c.json({ error: { code: error.code, message: error.message } }, error.status);
278
- }
279
- const updated = await prisma.cart.update({
280
- where: { id: cartId },
281
- data: { appliedDiscountCodeId: null },
282
- include: CART_INCLUDE_FULL,
283
- });
284
- const responseCart = await buildCartResponse(prisma, updated);
285
- return c.json(responseCart, 200);
286
- }
287
- catch (error) {
288
- console.error('Error removing discount from cart:', error);
289
- captureException(error instanceof Error ? error : new Error(String(error)), {
290
- level: 'error',
291
- tags: {
292
- error_code: 'DISCOUNT_REMOVE_ERROR',
293
- endpoint: 'POST /carts/:id/remove-discount',
294
- },
295
- extra: {
296
- cartId: c.req.param('id'),
297
- },
298
- honoContext: c,
299
- });
300
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
301
- }
302
- })
303
- // ADD CART ITEM
304
- .post('/:id/items', zValidator('param', z.object({ id: z.string().uuid() })), zValidator('json', z.object({
305
- sku: z.string(),
306
- quantity: z.number().int().positive(),
307
- fbc: z.string().optional(), // Facebook Click ID for attribution
308
- fbp: z.string().optional(), // Facebook Browser ID for attribution
309
- })), async (c) => {
310
- try {
311
- const { id: cartId } = c.req.valid('param');
312
- const input = c.req.valid('json');
313
- const prisma = getPrismaClient(c.env.DATABASE_URL);
314
- // Validate cart exists and is not expired
315
- const { cart, error } = await validateCart(prisma, cartId);
316
- if (error) {
317
- return c.json({ error: { code: error.code, message: error.message } }, error.status);
318
- }
319
- // Find variant by SKU for this brand
320
- const variant = await prisma.productVariant.findFirst({
321
- where: {
322
- sku: input.sku,
323
- product: { brandId: cart.brandId },
324
- isActive: true,
325
- deletedAt: null,
326
- },
327
- include: {
328
- product: true,
329
- },
330
- });
331
- if (!variant) {
332
- return c.json({ error: { code: 'VARIANT_NOT_FOUND', message: 'Product variant not found' } }, 404);
333
- }
334
- // Upsert cart item (add new or update quantity)
335
- const existingItem = await prisma.cartItem.findUnique({
336
- where: {
337
- cartId_variantId: {
338
- cartId,
339
- variantId: variant.id,
340
- },
341
- },
342
- });
343
- let cartItem;
344
- if (existingItem) {
345
- // Update quantity
346
- cartItem = await prisma.cartItem.update({
347
- where: { id: existingItem.id },
348
- data: { quantity: existingItem.quantity + input.quantity },
349
- include: {
350
- variant: {
351
- include: {
352
- product: true,
353
- },
354
- },
355
- },
356
- });
357
- }
358
- else {
359
- // Create new item
360
- cartItem = await prisma.cartItem.create({
361
- data: {
362
- cartId,
363
- variantId: variant.id,
364
- quantity: input.quantity,
365
- },
366
- include: {
367
- variant: {
368
- include: {
369
- product: true,
370
- },
371
- },
372
- },
373
- });
374
- }
375
- // Fetch updated cart for pricing
376
- const updatedCart = await prisma.cart.findUnique({
377
- where: { id: cartId },
378
- include: CART_INCLUDE_FULL,
379
- });
380
- // Queue Meta CAPI AddToCart event
381
- try {
382
- const { enqueueAddToCartEvent } = await import('../../notifications/producers/meta-capi-producer');
383
- await enqueueAddToCartEvent(c.env, {
384
- cartId,
385
- itemId: cartItem.id,
386
- pixelId: updatedCart.brand.metaPixelId,
387
- brandSiteUrl: updatedCart.brand.siteUrl,
388
- productName: variant.product.name,
389
- productSlug: variant.product.slug,
390
- sku: variant.sku,
391
- price: Number(variant.price),
392
- quantity: input.quantity,
393
- customerEmail: updatedCart.customerEmail,
394
- customerPhone: updatedCart.customerPhone,
395
- clientIpAddress: c.req.header('cf-connecting-ip'),
396
- clientUserAgent: c.req.header('user-agent'),
397
- fbc: input.fbc,
398
- fbp: input.fbp,
399
- });
400
- }
401
- catch (error) {
402
- console.error('Failed to queue Meta CAPI AddToCart event:', error);
403
- // Don't fail the request if CAPI queuing fails
404
- }
405
- const responseCart = await buildCartResponse(prisma, updatedCart);
406
- return c.json(responseCart, 200);
407
- }
408
- catch (error) {
409
- console.error('Error adding cart item:', error);
410
- captureException(error instanceof Error ? error : new Error(String(error)), {
411
- level: 'error',
412
- tags: {
413
- error_code: 'CART_ITEM_ADD_ERROR',
414
- endpoint: 'POST /carts/:id/items',
415
- },
416
- extra: {
417
- cartId: c.req.param('id'),
418
- },
419
- honoContext: c,
420
- });
421
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
422
- }
423
- })
424
- // UPDATE CART ITEM
425
- .patch('/:id/items/:itemId', zValidator('param', z.object({
426
- id: z.string().uuid(),
427
- itemId: z.string().uuid(),
428
- })), zValidator('json', z.object({
429
- quantity: z.number().int().positive(),
430
- })), async (c) => {
431
- try {
432
- const { id: cartId, itemId } = c.req.valid('param');
433
- const input = c.req.valid('json');
434
- const prisma = getPrismaClient(c.env.DATABASE_URL);
435
- const cartItem = await prisma.cartItem.findUnique({
436
- where: { id: itemId },
437
- include: CART_ITEM_WITH_CART_INCLUDE,
438
- });
439
- if (!cartItem || cartItem.cartId !== cartId) {
440
- return c.json({ error: { code: 'CART_ITEM_NOT_FOUND', message: 'Cart item not found' } }, 404);
441
- }
442
- if (cartItem.cart.expiresAt < new Date()) {
443
- return c.json({ error: { code: 'CART_EXPIRED', message: 'Cart has expired' } }, 410);
444
- }
445
- // Update quantity
446
- await prisma.cartItem.update({
447
- where: { id: itemId },
448
- data: { quantity: input.quantity },
449
- });
450
- // Fetch updated cart for pricing
451
- const updatedCart = await prisma.cart.findUnique({
452
- where: { id: cartId },
453
- include: CART_INCLUDE_FULL,
454
- });
455
- const responseCart = await buildCartResponse(prisma, updatedCart);
456
- return c.json(responseCart, 200);
457
- }
458
- catch (error) {
459
- console.error('Error updating cart item:', error);
460
- captureException(error instanceof Error ? error : new Error(String(error)), {
461
- level: 'error',
462
- tags: {
463
- error_code: 'CART_ITEM_UPDATE_ERROR',
464
- endpoint: 'PATCH /carts/:id/items/:itemId',
465
- },
466
- extra: {
467
- cartId: c.req.param('id'),
468
- itemId: c.req.param('itemId'),
469
- },
470
- honoContext: c,
471
- });
472
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
473
- }
474
- })
475
- // DELETE CART ITEM
476
- .delete('/:id/items/:itemId', zValidator('param', z.object({
477
- id: z.string().uuid(),
478
- itemId: z.string().uuid(),
479
- })), async (c) => {
480
- try {
481
- const { id: cartId, itemId } = c.req.valid('param');
482
- const prisma = getPrismaClient(c.env.DATABASE_URL);
483
- const cartItem = await prisma.cartItem.findUnique({
484
- where: { id: itemId },
485
- include: CART_ITEM_WITH_CART_INCLUDE,
486
- });
487
- if (!cartItem || cartItem.cartId !== cartId) {
488
- return c.json({ error: { code: 'CART_ITEM_NOT_FOUND', message: 'Cart item not found' } }, 404);
489
- }
490
- if (cartItem.cart.expiresAt < new Date()) {
491
- return c.json({ error: { code: 'CART_EXPIRED', message: 'Cart has expired' } }, 410);
492
- }
493
- // Delete item
494
- await prisma.cartItem.delete({
495
- where: { id: itemId },
496
- });
497
- // Fetch updated cart for pricing
498
- const updatedCart = await prisma.cart.findUnique({
499
- where: { id: cartId },
500
- include: CART_INCLUDE_FULL,
501
- });
502
- if (!updatedCart) {
503
- return c.json({ error: { code: 'CART_NOT_FOUND', message: 'Cart not found' } }, 404);
504
- }
505
- const responseCart = await buildCartResponse(prisma, updatedCart);
506
- return c.json(responseCart, 200);
507
- }
508
- catch (error) {
509
- console.error('Error removing cart item:', error);
510
- captureException(error instanceof Error ? error : new Error(String(error)), {
511
- level: 'error',
512
- tags: {
513
- error_code: 'CART_ITEM_DELETE_ERROR',
514
- endpoint: 'DELETE /carts/:id/items/:itemId',
515
- },
516
- extra: {
517
- cartId: c.req.param('id'),
518
- itemId: c.req.param('itemId'),
519
- },
520
- honoContext: c,
521
- });
522
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
523
- }
524
- })
525
- // CHECKOUT CART
526
- .post('/:id/checkout', zValidator('param', z.object({ id: z.uuid() })), zValidator('json', z.object({
527
- firstName: z.string().min(1),
528
- lastName: z.string().min(1),
529
- email: z.email().optional(),
530
- phone: z.string().optional(),
531
- address: z.string().min(1),
532
- city: z.string().min(1),
533
- deliveryZoneId: z.uuid(),
534
- paymentMethod: z.enum(['cod', 'online']),
535
- paystackReference: z.string().optional(),
536
- ifUnmodifiedSince: z.string().datetime().optional(),
537
- fbc: z.string().optional(), // Facebook Click ID for attribution
538
- fbp: z.string().optional(), // Facebook Browser ID for attribution
539
- })), async (c) => {
540
- try {
541
- const { id: cartId } = c.req.valid('param');
542
- const input = c.req.valid('json');
543
- const prisma = getPrismaClient(c.env.DATABASE_URL);
544
- const cart = await prisma.cart.findUnique({
545
- where: { id: cartId },
546
- include: CART_INCLUDE_FULL,
547
- });
548
- if (!cart) {
549
- return c.json({ error: { code: 'CART_NOT_FOUND', message: 'Cart not found' } }, 404);
550
- }
551
- if (cart.expiresAt < new Date()) {
552
- return c.json({ error: { code: 'CART_EXPIRED', message: 'Cart has expired' } }, 410);
553
- }
554
- if (cart.items.length === 0) {
555
- return c.json({ error: { code: 'CART_EMPTY', message: 'Cart is empty' } }, 400);
556
- }
557
- // Queue Meta CAPI InitiateCheckout event
558
- try {
559
- const { enqueueInitiateCheckoutEvent } = await import('../../notifications/producers/meta-capi-producer');
560
- const pricing = calculateCartCost(toPricingItems(cart));
561
- const itemCount = cart.items.reduce((sum, item) => sum + item.quantity, 0);
562
- await enqueueInitiateCheckoutEvent(c.env, {
563
- cartId,
564
- pixelId: cart.brand.metaPixelId,
565
- brandSiteUrl: cart.brand.siteUrl,
566
- cartTotal: pricing.total,
567
- itemCount,
568
- customerEmail: input.email || cart.customerEmail,
569
- customerPhone: input.phone || cart.customerPhone,
570
- clientIpAddress: c.req.header('cf-connecting-ip'),
571
- clientUserAgent: c.req.header('user-agent'),
572
- fbc: input.fbc,
573
- fbp: input.fbp,
574
- });
575
- }
576
- catch (error) {
577
- console.error('Failed to queue Meta CAPI InitiateCheckout event:', error);
578
- // Don't fail the checkout if CAPI queuing fails
579
- }
580
- // Concurrency check
581
- const ifMatch = c.req.header('if-match');
582
- if (ifMatch && ifMatch !== etagFrom(cart.updatedAt)) {
583
- return c.json({ error: { code: 'PRECONDITION_FAILED', message: 'Cart has changed' } }, 412);
584
- }
585
- if (input.ifUnmodifiedSince && new Date(input.ifUnmodifiedSince).getTime() !== cart.updatedAt.getTime()) {
586
- return c.json({ error: { code: 'PRECONDITION_FAILED', message: 'Cart has changed' } }, 412);
587
- }
588
- // Validate and load delivery zone
589
- if (!input.deliveryZoneId) {
590
- return c.json({ error: { code: 'MISSING_DELIVERY_ZONE', message: 'Select a delivery zone' } }, 400);
591
- }
592
- const zone = await prisma.deliveryZone.findFirst({
593
- where: { id: input.deliveryZoneId, isActive: true, deletedAt: null },
594
- include: { state: true },
595
- });
596
- if (!zone) {
597
- return c.json({ error: { code: 'INVALID_DELIVERY_ZONE', message: 'Zone not available' } }, 400);
598
- }
599
- // Validate payment method against cart's available methods
600
- if (!cart.availablePaymentMethods.includes(input.paymentMethod)) {
601
- return c.json({ error: { code: 'PAYMENT_METHOD_NOT_AVAILABLE', message: 'Payment not available for selection' } }, 400);
602
- }
603
- // Calculate pricing with quantity discounts
604
- const pricing = calculateCartCost(toPricingItems(cart));
605
- // Compute delivery charge with free shipping threshold
606
- const threshold = zone.freeShippingThreshold ? Number(zone.freeShippingThreshold) : null;
607
- const base = Number(zone.deliveryCost);
608
- const deliveryCharge = threshold !== null && pricing.subtotal >= threshold ? 0 : base;
609
- const estimatedDays = zone.estimatedDays ?? null;
610
- // Use the discount code already applied to the cart (if any)
611
- let discountCodeRecord = null;
612
- let discountAmount = 0;
613
- let isRecoveredCart = false;
614
- if (cart.appliedDiscountCodeId) {
615
- discountCodeRecord = await prisma.discountCode.findUnique({
616
- where: { id: cart.appliedDiscountCodeId },
617
- });
618
- // Validate discount is still active and valid
619
- if (discountCodeRecord && discountCodeRecord.isActive) {
620
- const now = new Date();
621
- const isValid = (!discountCodeRecord.validFrom || discountCodeRecord.validFrom <= now) &&
622
- (!discountCodeRecord.validUntil || discountCodeRecord.validUntil > now) &&
623
- (!discountCodeRecord.brandId || discountCodeRecord.brandId === cart.brandId);
624
- if (isValid) {
625
- // Calculate discount amount
626
- if (discountCodeRecord.type === 'percentage') {
627
- discountAmount = round(pricing.subtotal * (toNumber(discountCodeRecord.value) / 100));
628
- // Apply max discount cap if exists
629
- if (discountCodeRecord.maxDiscount) {
630
- const maxDiscountNum = toNumber(discountCodeRecord.maxDiscount);
631
- if (discountAmount > maxDiscountNum) {
632
- discountAmount = maxDiscountNum;
633
- }
634
- }
635
- }
636
- else if (discountCodeRecord.type === 'fixed') {
637
- discountAmount = Math.min(toNumber(discountCodeRecord.value), pricing.subtotal);
638
- }
639
- // Check if this is a recovery code
640
- if (discountCodeRecord.category === 'recovery') {
641
- isRecoveredCart = true;
642
- }
643
- }
644
- else {
645
- // Discount is no longer valid, ignore it
646
- discountCodeRecord = null;
647
- }
648
- }
649
- else {
650
- discountCodeRecord = null;
651
- }
652
- }
653
- // Consider cart recovered if it's at least 1 hour old (when first recovery attempt is sent)
654
- const oneHourMs = 60 * 60 * 1000;
655
- if (!isRecoveredCart && Date.now() - new Date(cart.createdAt).getTime() >= oneHourMs) {
656
- isRecoveredCart = true;
657
- }
658
- // Calculate final total
659
- const finalTotal = round(Math.max(0, pricing.subtotal + deliveryCharge - discountAmount));
660
- // Build order items
661
- const orderItems = cart.items.map((item) => {
662
- const variant = item.variant;
663
- const product = variant.product;
664
- let finalPrice = toNumber(variant.price);
665
- // Apply quantity discount if available
666
- if (product.quantityDiscounts && typeof product.quantityDiscounts === 'object') {
667
- const discounts = product.quantityDiscounts;
668
- const applicableQuantities = Object.keys(discounts)
669
- .map(Number)
670
- .filter((qty) => item.quantity >= qty)
671
- .sort((a, b) => b - a);
672
- if (applicableQuantities.length > 0) {
673
- const discountPercent = discounts[applicableQuantities[0].toString()];
674
- finalPrice = round(toNumber(variant.price) * (1 - discountPercent / 100));
675
- }
676
- }
677
- return {
678
- variantId: variant.id,
679
- quantity: item.quantity,
680
- priceAtPurchase: finalPrice,
681
- };
682
- });
683
- // Create order in transaction with conversion tracking
684
- const result = await prisma.$transaction(async (tx) => {
685
- // Create order
686
- const order = await tx.order.create({
687
- data: {
688
- brandId: cart.brandId,
689
- firstName: input.firstName,
690
- lastName: input.lastName,
691
- phone: (input.phone || cart.customerPhone), // Use checkout phone or fallback to cart phone
692
- email: input.email || cart.customerEmail, // Use checkout email or fallback to cart email
693
- address: input.address,
694
- city: input.city,
695
- deliveryZoneId: zone.id,
696
- deliveryCharge,
697
- estimatedDays,
698
- totalPrice: finalTotal,
699
- discountCodeId: discountCodeRecord?.id,
700
- discountAmount: discountAmount > 0 ? discountAmount : null,
701
- paymentMethod: input.paymentMethod,
702
- paystackReference: input.paystackReference,
703
- status: 'pending',
704
- items: {
705
- create: orderItems,
706
- },
707
- },
708
- include: ORDER_INCLUDE_FULL,
709
- });
710
- // If discount code was used, create usage log and increment count
711
- if (discountCodeRecord && discountAmount > 0) {
712
- await tx.discountCodeUsage.create({
713
- data: {
714
- discountCodeId: discountCodeRecord.id,
715
- orderId: order.id,
716
- customerPhone: input.phone || cart.customerPhone || '',
717
- discountAmount,
718
- originalAmount: pricing.subtotal,
719
- },
720
- });
721
- await tx.discountCode.update({
722
- where: { id: discountCodeRecord.id },
723
- data: { usageCount: { increment: 1 } },
724
- });
725
- }
726
- // Track conversion: Update cart with order ID and recovery status
727
- await tx.cart.update({
728
- where: { id: cartId },
729
- data: {
730
- convertedToOrderId: order.id,
731
- wasRecovered: isRecoveredCart,
732
- },
733
- });
734
- return order;
735
- });
736
- // Queue order confirmation notification
737
- try {
738
- const { enqueueOrderNotification } = await import('../../notifications/producers/order-notification');
739
- await enqueueOrderNotification(c.env, 'order_confirmation', result.id);
740
- }
741
- catch (error) {
742
- console.error('Failed to queue order confirmation notification:', error);
743
- // Don't fail the checkout if notification fails
744
- }
745
- // Queue Meta CAPI Purchase event
746
- try {
747
- const { enqueuePurchaseEvent } = await import('../../notifications/producers/meta-capi-producer');
748
- await enqueuePurchaseEvent(c.env, result, {
749
- clientIpAddress: c.req.header('cf-connecting-ip'),
750
- clientUserAgent: c.req.header('user-agent'),
751
- fbc: input.fbc,
752
- fbp: input.fbp,
753
- });
754
- }
755
- catch (error) {
756
- console.error('Failed to queue Meta CAPI Purchase event:', error);
757
- // Don't fail the checkout if CAPI queuing fails
758
- }
759
- return c.json(formatOrderResponse(result), 201);
760
- }
761
- catch (error) {
762
- console.error('Error checking out cart:', error);
763
- // CRITICAL: Checkout failures directly impact revenue
764
- captureException(error instanceof Error ? error : new Error(String(error)), {
765
- level: 'fatal', // Highest severity - revenue impacting
766
- tags: {
767
- error_code: 'CHECKOUT_ERROR',
768
- endpoint: 'POST /carts/:id/checkout',
769
- },
770
- extra: {
771
- cartId: c.req.param('id'),
772
- },
773
- honoContext: c,
774
- });
775
- return c.json({ error: { code: 'INTERNAL_ERROR', message: error.message } }, 500);
776
- }
777
- });
778
- export default app;