@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.
- package/package.json +2 -2
- package/dist/apps/backend/src/http-app.d.ts +0 -40
- package/dist/apps/backend/src/http-app.js +0 -134
- package/dist/apps/backend/src/lib/brand-response.d.ts +0 -16
- package/dist/apps/backend/src/lib/brand-response.js +0 -8
- package/dist/apps/backend/src/lib/cart-helpers.d.ts +0 -286
- package/dist/apps/backend/src/lib/cart-helpers.js +0 -121
- package/dist/apps/backend/src/lib/cart-recovery.d.ts +0 -30
- package/dist/apps/backend/src/lib/cart-recovery.js +0 -147
- package/dist/apps/backend/src/lib/cart-response.d.ts +0 -123
- package/dist/apps/backend/src/lib/cart-response.js +0 -150
- package/dist/apps/backend/src/lib/clerk.d.ts +0 -18
- package/dist/apps/backend/src/lib/clerk.js +0 -190
- package/dist/apps/backend/src/lib/delivery-zone-response.d.ts +0 -66
- package/dist/apps/backend/src/lib/delivery-zone-response.js +0 -24
- package/dist/apps/backend/src/lib/discount-code-response.d.ts +0 -44
- package/dist/apps/backend/src/lib/discount-code-response.js +0 -19
- package/dist/apps/backend/src/lib/discount.d.ts +0 -20
- package/dist/apps/backend/src/lib/discount.js +0 -35
- package/dist/apps/backend/src/lib/inventory.d.ts +0 -26
- package/dist/apps/backend/src/lib/inventory.js +0 -160
- package/dist/apps/backend/src/lib/meta-capi.d.ts +0 -53
- package/dist/apps/backend/src/lib/meta-capi.js +0 -151
- package/dist/apps/backend/src/lib/openapi.d.ts +0 -36
- package/dist/apps/backend/src/lib/openapi.js +0 -69
- package/dist/apps/backend/src/lib/order-recovery.d.ts +0 -465
- package/dist/apps/backend/src/lib/order-recovery.js +0 -378
- package/dist/apps/backend/src/lib/order-response.d.ts +0 -140
- package/dist/apps/backend/src/lib/order-response.js +0 -61
- package/dist/apps/backend/src/lib/pricing.d.ts +0 -39
- package/dist/apps/backend/src/lib/pricing.js +0 -62
- package/dist/apps/backend/src/lib/prisma.d.ts +0 -9
- package/dist/apps/backend/src/lib/prisma.js +0 -30
- package/dist/apps/backend/src/lib/product-response.d.ts +0 -84
- package/dist/apps/backend/src/lib/product-response.js +0 -29
- package/dist/apps/backend/src/lib/sentry.d.ts +0 -48
- package/dist/apps/backend/src/lib/sentry.js +0 -180
- package/dist/apps/backend/src/lib/utils.d.ts +0 -32
- package/dist/apps/backend/src/lib/utils.js +0 -63
- package/dist/apps/backend/src/middleware/clerk-auth.d.ts +0 -8
- package/dist/apps/backend/src/middleware/clerk-auth.js +0 -89
- package/dist/apps/backend/src/middleware/cors.d.ts +0 -8
- package/dist/apps/backend/src/middleware/cors.js +0 -11
- package/dist/apps/backend/src/notifications/producers/meta-capi-producer.d.ts +0 -62
- package/dist/apps/backend/src/notifications/producers/meta-capi-producer.js +0 -180
- package/dist/apps/backend/src/notifications/producers/order-notification.d.ts +0 -9
- package/dist/apps/backend/src/notifications/producers/order-notification.js +0 -18
- package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.d.ts +0 -10
- package/dist/apps/backend/src/notifications/producers/prospect-recovery-notification.js +0 -11
- package/dist/apps/backend/src/routes/admin/abandoned-carts.d.ts +0 -609
- package/dist/apps/backend/src/routes/admin/abandoned-carts.js +0 -194
- package/dist/apps/backend/src/routes/admin/brands.d.ts +0 -183
- package/dist/apps/backend/src/routes/admin/brands.js +0 -118
- package/dist/apps/backend/src/routes/admin/customers.d.ts +0 -310
- package/dist/apps/backend/src/routes/admin/customers.js +0 -39
- package/dist/apps/backend/src/routes/admin/delivery-zones.d.ts +0 -454
- package/dist/apps/backend/src/routes/admin/delivery-zones.js +0 -300
- package/dist/apps/backend/src/routes/admin/discount-codes.d.ts +0 -488
- package/dist/apps/backend/src/routes/admin/discount-codes.js +0 -418
- package/dist/apps/backend/src/routes/admin/inventory.d.ts +0 -273
- package/dist/apps/backend/src/routes/admin/inventory.js +0 -199
- package/dist/apps/backend/src/routes/admin/orders.d.ts +0 -1792
- package/dist/apps/backend/src/routes/admin/orders.js +0 -552
- package/dist/apps/backend/src/routes/admin/products.d.ts +0 -868
- package/dist/apps/backend/src/routes/admin/products.js +0 -126
- package/dist/apps/backend/src/routes/admin/stats.d.ts +0 -292
- package/dist/apps/backend/src/routes/admin/stats.js +0 -55
- package/dist/apps/backend/src/routes/admin/variants.d.ts +0 -239
- package/dist/apps/backend/src/routes/admin/variants.js +0 -197
- package/dist/apps/backend/src/routes/admin/warehouses.d.ts +0 -375
- package/dist/apps/backend/src/routes/admin/warehouses.js +0 -123
- package/dist/apps/backend/src/routes/public/brands.d.ts +0 -41
- package/dist/apps/backend/src/routes/public/brands.js +0 -39
- package/dist/apps/backend/src/routes/public/carts.d.ts +0 -2675
- package/dist/apps/backend/src/routes/public/carts.js +0 -778
- package/dist/apps/backend/src/routes/public/delivery-zones.d.ts +0 -37
- package/dist/apps/backend/src/routes/public/delivery-zones.js +0 -64
- package/dist/apps/backend/src/routes/public/orders.d.ts +0 -613
- package/dist/apps/backend/src/routes/public/orders.js +0 -184
- package/dist/apps/backend/src/routes/public/products.d.ts +0 -453
- package/dist/apps/backend/src/routes/public/products.js +0 -133
- package/dist/apps/backend/src/types/index.d.ts +0 -43
- package/dist/apps/backend/src/types/index.js +0 -2
- package/dist/apps/backend/src/validators/brand.d.ts +0 -21
- package/dist/apps/backend/src/validators/brand.js +0 -19
- package/dist/apps/backend/src/validators/delivery-zone.d.ts +0 -35
- package/dist/apps/backend/src/validators/delivery-zone.js +0 -55
- package/dist/apps/backend/src/validators/discount-code.d.ts +0 -74
- package/dist/apps/backend/src/validators/discount-code.js +0 -50
- package/dist/apps/backend/src/validators/inventory.d.ts +0 -20
- package/dist/apps/backend/src/validators/inventory.js +0 -15
- package/dist/apps/backend/src/validators/order.d.ts +0 -58
- package/dist/apps/backend/src/validators/order.js +0 -62
- package/dist/apps/backend/src/validators/product.d.ts +0 -18
- package/dist/apps/backend/src/validators/product.js +0 -19
- package/dist/apps/backend/src/validators/variant.d.ts +0 -19
- package/dist/apps/backend/src/validators/variant.js +0 -19
- package/dist/apps/backend/src/validators/warehouse.d.ts +0 -15
- package/dist/apps/backend/src/validators/warehouse.js +0 -15
- /package/dist/{packages/api-client/src/backend-types.d.ts → backend-types.d.ts} +0 -0
- /package/dist/{packages/api-client/src/backend-types.js → backend-types.js} +0 -0
- /package/dist/{packages/api-client/src/client.d.ts → client.d.ts} +0 -0
- /package/dist/{packages/api-client/src/client.js → client.js} +0 -0
- /package/dist/{packages/api-client/src/enum-types.d.ts → enum-types.d.ts} +0 -0
- /package/dist/{packages/api-client/src/enum-types.js → enum-types.js} +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/brands.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/brands.js +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/carts.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/carts.js +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/delivery-zones.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/delivery-zones.js +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/index.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/index.js +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/orders.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/orders.js +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/products.d.ts +0 -0
- /package/dist/{packages/api-client/src/fetchers → fetchers}/products.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/abandoned-carts.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/abandoned-carts.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/brands.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/brands.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/customers.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/customers.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/delivery-zones.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/delivery-zones.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/discount-codes.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/discount-codes.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/index.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/index.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/inventory.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/inventory.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/orders.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/orders.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/products.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/products.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/stats.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/stats.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/variants.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/variants.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/warehouses.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/admin/warehouses.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/brands.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/brands.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/carts.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/carts.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/delivery-zones.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/delivery-zones.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/index.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/index.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/orders.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/orders.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/products.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/public/products.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/use-query-unwrapped.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/use-query-unwrapped.js +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/useApiConfig.d.ts +0 -0
- /package/dist/{packages/api-client/src/hooks → hooks}/useApiConfig.js +0 -0
- /package/dist/{packages/api-client/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{packages/api-client/src/index.js → index.js} +0 -0
- /package/dist/{packages/api-client/src/provider.d.ts → provider.d.ts} +0 -0
- /package/dist/{packages/api-client/src/provider.js → provider.js} +0 -0
- /package/dist/{packages/api-client/src/rpc-client.d.ts → rpc-client.d.ts} +0 -0
- /package/dist/{packages/api-client/src/rpc-client.js → rpc-client.js} +0 -0
- /package/dist/{packages/api-client/src/rpc-types.d.ts → rpc-types.d.ts} +0 -0
- /package/dist/{packages/api-client/src/rpc-types.js → rpc-types.js} +0 -0
- /package/dist/{packages/api-client/src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{packages/api-client/src/types.js → types.js} +0 -0
- /package/dist/{packages/api-client/src/utils → utils}/query-keys.d.ts +0 -0
- /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;
|