fixed coupon redemptions
This commit is contained in:
parent
b209a77be9
commit
6e34a57bd6
1 changed files with 126 additions and 142 deletions
|
|
@ -922,123 +922,33 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/checkout', async (req, res, next) => {
|
// Complete checkout after successful payment
|
||||||
|
router.post('/complete-checkout', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { userId, shippingAddress, shippingMethod } = req.body;
|
const { userId, orderId, sessionId } = req.body;
|
||||||
|
console.log("Complete Checkout ", `${userId} ${orderId} ${sessionId}`)
|
||||||
|
|
||||||
if (req.user.id !== userId) {
|
if (req.user.id !== userId) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'You can only checkout your own cart'
|
message: 'You can only complete your own checkout'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cart
|
// Verify the order exists and belongs to the user
|
||||||
const cartResult = await query(
|
const orderResult = await query(
|
||||||
'SELECT * FROM carts WHERE user_id = $1',
|
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
|
||||||
[userId]
|
[orderId, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cartResult.rows.length === 0) {
|
if (orderResult.rows.length === 0) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
error: true,
|
error: true,
|
||||||
message: 'Cart not found'
|
message: 'Order not found'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cartId = cartResult.rows[0].id;
|
const order = orderResult.rows[0];
|
||||||
|
|
||||||
// Get cart items
|
|
||||||
const cartItemsResult = await query(
|
|
||||||
`SELECT ci.*, p.price, p.name, p.description, p.weight_grams, p.length_cm, p.width_cm, p.height_cm,
|
|
||||||
(
|
|
||||||
SELECT json_build_object(
|
|
||||||
'path', pi.image_path,
|
|
||||||
'isPrimary', pi.is_primary
|
|
||||||
)
|
|
||||||
FROM product_images pi
|
|
||||||
WHERE pi.product_id = p.id AND pi.is_primary = true
|
|
||||||
LIMIT 1
|
|
||||||
) AS primary_image
|
|
||||||
FROM cart_items ci
|
|
||||||
JOIN products p ON ci.product_id = p.id
|
|
||||||
WHERE ci.cart_id = $1`,
|
|
||||||
[cartId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cartItemsResult.rows.length === 0) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: true,
|
|
||||||
message: 'Cart is empty'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate subtotal
|
|
||||||
const subtotal = cartItemsResult.rows.reduce((sum, item) => {
|
|
||||||
return sum + (parseFloat(item.price) * item.quantity);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Determine shipping cost and create shipment if needed
|
|
||||||
let shippingCost = 0;
|
|
||||||
let shipmentData = null;
|
|
||||||
|
|
||||||
if (config.shipping.enabled) {
|
|
||||||
// If a specific shipping method was selected
|
|
||||||
if (shippingMethod && shippingMethod.id) {
|
|
||||||
shippingCost = parseFloat(shippingMethod.rate) || 0;
|
|
||||||
|
|
||||||
// Check if this is a non-flat rate and we need to purchase a real shipment
|
|
||||||
if (config.shipping.easypostEnabled &&
|
|
||||||
!shippingMethod.id.includes('flat-rate') &&
|
|
||||||
!shippingMethod.id.includes('free-shipping')) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse shipping address to object if it's a string
|
|
||||||
const parsedAddress = typeof shippingAddress === 'string'
|
|
||||||
? shippingService.parseAddressString(shippingAddress)
|
|
||||||
: shippingAddress;
|
|
||||||
|
|
||||||
// Retrieve temporary shipment ID from cart metadata
|
|
||||||
const cartMetadataResult = await query(
|
|
||||||
'SELECT metadata FROM carts WHERE id = $1',
|
|
||||||
[cartId]
|
|
||||||
);
|
|
||||||
|
|
||||||
let shipmentId = null;
|
|
||||||
if (cartMetadataResult.rows.length > 0 &&
|
|
||||||
cartMetadataResult.rows[0].metadata &&
|
|
||||||
cartMetadataResult.rows[0].metadata.temp_shipment_id) {
|
|
||||||
shipmentId = cartMetadataResult.rows[0].metadata.temp_shipment_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shipmentId) {
|
|
||||||
// Purchase the shipment with the selected rate
|
|
||||||
shipmentData = await shippingService.purchaseShipment(
|
|
||||||
shipmentId,
|
|
||||||
shippingMethod.id
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Shipment purchased successfully:', shipmentData.shipment_id);
|
|
||||||
|
|
||||||
// Use the actual rate from the purchased shipment
|
|
||||||
shippingCost = shipmentData.selected_rate.rate;
|
|
||||||
} else {
|
|
||||||
console.log('No shipment ID found in cart metadata, using standard rate');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error purchasing shipment:', error);
|
|
||||||
// Continue with the rate provided
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default to flat rate if no method selected
|
|
||||||
const shippingRates = shippingService.getFlatRateShipping(subtotal);
|
|
||||||
shippingCost = shippingRates[0].rate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total with shipping
|
|
||||||
const total = subtotal + shippingCost;
|
|
||||||
|
|
||||||
// Begin transaction
|
// Begin transaction
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
|
|
@ -1046,59 +956,133 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
|
|
||||||
// Create order
|
// Update order status and payment info
|
||||||
const orderId = uuidv4();
|
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO orders (id, user_id, status, total_amount, shipping_address, payment_completed, shipping_cost) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
'UPDATE orders SET status = $1, payment_completed = true, payment_id = $2 WHERE id = $3',
|
||||||
[orderId, userId, 'pending', total, shippingAddress, false, shippingCost]
|
['processing', sessionId, orderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create order items
|
// Get cart
|
||||||
for (const item of cartItemsResult.rows) {
|
const cartResult = await client.query(
|
||||||
|
'SELECT * FROM carts WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cartResult.rows.length > 0) {
|
||||||
|
const cartId = cartResult.rows[0].id;
|
||||||
|
|
||||||
|
// Check if cart has a coupon applied
|
||||||
|
const cartMetadata = cartResult.rows[0].metadata;
|
||||||
|
if (cartMetadata && cartMetadata.coupon) {
|
||||||
|
const couponInfo = cartMetadata.coupon;
|
||||||
|
|
||||||
|
// Increment the coupon's current_redemptions
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO order_items (id, order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4, $5)',
|
'UPDATE coupons SET current_redemptions = current_redemptions + 1 WHERE id = $1',
|
||||||
[uuidv4(), orderId, item.product_id, item.quantity, item.price]
|
[couponInfo.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a coupon redemption record
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO coupon_redemptions (coupon_id, order_id, user_id, discount_amount) VALUES ($1, $2, $3, $4)',
|
||||||
|
[couponInfo.id, orderId, userId, couponInfo.discount_amount]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the order with coupon information
|
||||||
|
await client.query(
|
||||||
|
'UPDATE orders SET coupon_id = $1, discount_amount = $2 WHERE id = $3',
|
||||||
|
[couponInfo.id, couponInfo.discount_amount, orderId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have shipping details, save them with the order
|
// Get cart items to update product stock
|
||||||
if (shippingMethod && shippingMethod.id) {
|
const cartItemsResult = await client.query(
|
||||||
const shippingInfo = shipmentData || {
|
'SELECT * FROM cart_items WHERE cart_id = $1',
|
||||||
method_id: shippingMethod.id,
|
|
||||||
carrier: shippingMethod.carrier,
|
|
||||||
service: shippingMethod.service,
|
|
||||||
rate: shippingMethod.rate,
|
|
||||||
estimated_days: shippingMethod.delivery_days,
|
|
||||||
tracking_code: null
|
|
||||||
};
|
|
||||||
|
|
||||||
await client.query(
|
|
||||||
'UPDATE orders SET shipping_info = $1 WHERE id = $2',
|
|
||||||
[JSON.stringify(shippingInfo), orderId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the temporary shipment ID from cart metadata
|
|
||||||
await client.query(
|
|
||||||
`UPDATE carts SET metadata = metadata - 'temp_shipment_id' WHERE id = $1`,
|
|
||||||
[cartId]
|
[cartId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update product stock
|
||||||
|
for (const item of cartItemsResult.rows) {
|
||||||
|
await client.query(
|
||||||
|
'UPDATE products SET stock_quantity = stock_quantity - $1 WHERE id = $2',
|
||||||
|
[item.quantity, item.product_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process stock notifications after updating stock
|
||||||
|
try {
|
||||||
|
const productWithNotification = await client.query(
|
||||||
|
`SELECT id, name, stock_quantity, stock_notification
|
||||||
|
FROM products
|
||||||
|
WHERE id = $1`,
|
||||||
|
[item.product_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (productWithNotification.rows.length > 0) {
|
||||||
|
const product = productWithNotification.rows[0];
|
||||||
|
let stockNotification;
|
||||||
|
|
||||||
|
// Handle different ways the JSON could be stored
|
||||||
|
if (product.stock_notification) {
|
||||||
|
try {
|
||||||
|
// If it's a string, parse it
|
||||||
|
// why are we doing this its been an obj for months now
|
||||||
|
if (typeof product.stock_notification === 'string') {
|
||||||
|
stockNotification = JSON.parse(product.stock_notification);
|
||||||
|
} else {
|
||||||
|
// Otherwise use as is
|
||||||
|
stockNotification = product.stock_notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Stock notification for product:", product.id, stockNotification);
|
||||||
|
|
||||||
|
// Check if notification is enabled and stock is below threshold
|
||||||
|
if (stockNotification &&
|
||||||
|
stockNotification.enabled === true &&
|
||||||
|
stockNotification.email &&
|
||||||
|
stockNotification.threshold &&
|
||||||
|
product.stock_quantity <= parseInt(stockNotification.threshold)) {
|
||||||
|
|
||||||
|
// Log the notification with the order ID
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO notification_logs
|
||||||
|
(order_id, notification_type, sent_at, status)
|
||||||
|
VALUES ($1, $2, NOW(), $3)`,
|
||||||
|
[orderId, 'low_stock_alert', 'pending']
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Low stock notification queued for product ${product.id} - ${product.name}`);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Error parsing stock notification JSON:", parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (notificationError) {
|
||||||
|
console.error("Error processing stock notification:", notificationError);
|
||||||
|
// Continue with checkout even if notification processing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cart
|
||||||
|
await client.query(
|
||||||
|
'DELETE FROM cart_items WHERE cart_id = $1',
|
||||||
|
[cartId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear cart metadata (including coupon)
|
||||||
|
await client.query(
|
||||||
|
'UPDATE carts SET metadata = $1 WHERE id = $2',
|
||||||
|
['{}', cartId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
|
|
||||||
// Send back cart items for Stripe checkout
|
res.status(200).json({
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Order created successfully, ready for payment',
|
message: 'Order completed successfully',
|
||||||
orderId,
|
orderId
|
||||||
cartItems: cartItemsResult.rows,
|
|
||||||
subtotal,
|
|
||||||
shippingCost,
|
|
||||||
total,
|
|
||||||
shipmentData
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: We don't clear the cart here now - we'll do that after successful payment
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue