@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,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;