fixed coupon redemptions
This commit is contained in:
parent
6e34a57bd6
commit
efdd021712
1 changed files with 189 additions and 141 deletions
|
|
@ -922,7 +922,195 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Complete checkout after successful payment
|
router.post('/checkout', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { userId, shippingAddress, shippingMethod } = req.body;
|
||||||
|
|
||||||
|
if (req.user.id !== userId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: true,
|
||||||
|
message: 'You can only checkout your own cart'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cart
|
||||||
|
const cartResult = await query(
|
||||||
|
'SELECT * FROM carts WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cartResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: true,
|
||||||
|
message: 'Cart not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartId = cartResult.rows[0].id;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Create order
|
||||||
|
const orderId = uuidv4();
|
||||||
|
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)',
|
||||||
|
[orderId, userId, 'pending', total, shippingAddress, false, shippingCost]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create order items
|
||||||
|
for (const item of cartItemsResult.rows) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO order_items (id, order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4, $5)',
|
||||||
|
[uuidv4(), orderId, item.product_id, item.quantity, item.price]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have shipping details, save them with the order
|
||||||
|
if (shippingMethod && shippingMethod.id) {
|
||||||
|
const shippingInfo = shipmentData || {
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
// Send back cart items for Stripe checkout
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Order created successfully, ready for payment',
|
||||||
|
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) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete checkout after successful payment
|
||||||
router.post('/complete-checkout', async (req, res, next) => {
|
router.post('/complete-checkout', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { userId, orderId, sessionId } = req.body;
|
const { userId, orderId, sessionId } = req.body;
|
||||||
|
|
@ -1094,145 +1282,5 @@ module.exports = (pool, query, authMiddleware) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Complete checkout after successful payment
|
|
||||||
router.post('/complete-checkout', async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { userId, orderId, sessionId } = req.body;
|
|
||||||
console.log("Complete Checkout ", `${userId} ${orderId} ${sessionId}`)
|
|
||||||
|
|
||||||
if (req.user.id !== userId) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: true,
|
|
||||||
message: 'You can only complete your own checkout'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the order exists and belongs to the user
|
|
||||||
const orderResult = await query(
|
|
||||||
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
|
|
||||||
[orderId, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orderResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({
|
|
||||||
error: true,
|
|
||||||
message: 'Order not found'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin transaction
|
|
||||||
const client = await pool.connect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.query('BEGIN');
|
|
||||||
|
|
||||||
// Update order status and payment info
|
|
||||||
await client.query(
|
|
||||||
'UPDATE orders SET status = $1, payment_completed = true, payment_id = $2 WHERE id = $3',
|
|
||||||
['processing', sessionId, orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get cart
|
|
||||||
const cartResult = await client.query(
|
|
||||||
'SELECT * FROM carts WHERE user_id = $1',
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cartResult.rows.length > 0) {
|
|
||||||
const cartId = cartResult.rows[0].id;
|
|
||||||
|
|
||||||
// Get cart items to update product stock
|
|
||||||
const cartItemsResult = await client.query(
|
|
||||||
'SELECT * FROM cart_items WHERE cart_id = $1',
|
|
||||||
[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]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.query('COMMIT');
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Order completed successfully',
|
|
||||||
orderId
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
Loading…
Reference in a new issue