Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 91 additions & 24 deletions BACKEND/controllers/checkout.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ function productImage(product) {
}

async function restoreStock(deductions) {
for (const { productId, quantity } of deductions) {
await Product.findByIdAndUpdate(productId, { $inc: { baseStock: quantity } });
for (const { productId, variantId, quantity } of deductions) {
if (variantId) {
await Product.findOneAndUpdate(
{ _id: productId, "variants._id": variantId },
{ $inc: { "variants.$.stock": quantity } }
);
} else {
await Product.findByIdAndUpdate(productId, { $inc: { baseStock: quantity } });
}
}
}

Expand Down Expand Up @@ -68,21 +75,42 @@ export const createCheckoutSession = async (req, res) => {
return res.status(404).json({ success: false, message: `Product not found: ${item.name}` });
}

if (item.quantity > product.baseStock) {
let price = product.basePrice;
let stock = product.baseStock;
let name = product.name;
let img = productImage(product);

if (product.hasVariants) {
if (!item.variantId) {
return res.status(400).json({ success: false, message: `Variant ID required for ${product.name}` });
}
const variant = product.variants.id(item.variantId);
if (!variant) {
return res.status(404).json({ success: false, message: `Variant not found for ${product.name}` });
}
price = variant.price;
stock = variant.stock;
name = `${product.name} - ${variant.size} ${variant.color}`;
if (variant.images && variant.images.length > 0) {
img = [variant.images[0]];
}
}

if (item.quantity > stock) {
return res.status(400).json({
success: false,
message: `Insufficient stock for ${product.name}. Available: ${product.baseStock}, requested: ${item.quantity}`
message: `Insufficient stock for ${name}. Available: ${stock}, requested: ${item.quantity}`
});
}

lineItems.push({
price_data: {
currency: 'usd',
product_data: {
name: product.name,
images: productImage(product),
name: name,
images: img,
},
unit_amount: Math.round(product.basePrice * 100),
unit_amount: Math.round(price * 100),
},
quantity: item.quantity,
});
Expand Down Expand Up @@ -127,7 +155,7 @@ export const createCheckoutSession = async (req, res) => {
cancel_url: `${FRONTEND_URL}/cancel`,
metadata: {
items: JSON.stringify(
items.map((item) => ({ _id: item._id, quantity: item.quantity }))
items.map((item) => ({ _id: item._id, variantId: item.variantId || null, quantity: item.quantity }))
),
userId: req.user?._id?.toString() || '',
couponCode: couponDoc ? couponDoc.code : '',
Expand Down Expand Up @@ -184,45 +212,84 @@ export const stripeWebhook = async (req, res) => {
console.error(`Checkout webhook: product not found or deleted: ${item._id}`);
return res.json({ received: true });
}
if (item.quantity > product.baseStock) {
console.error(`Checkout webhook: insufficient stock for ${product.name}`);

let price = product.basePrice;
let stock = product.baseStock;
let name = product.name;
let img = productImage(product)[0] || "";

if (product.hasVariants && item.variantId) {
const variant = product.variants.id(item.variantId);
if (variant) {
price = variant.price;
stock = variant.stock;
name = `${product.name} - ${variant.size} ${variant.color}`;
if (variant.images && variant.images.length > 0) img = variant.images[0];
}
}

if (item.quantity > stock) {
console.error(`Checkout webhook: insufficient stock for ${name}`);
return res.json({ received: true });
}

orderItems.push({
product: product._id,
name: product.name,
price: product.basePrice,
name: name,
price: price,
quantity: item.quantity,
image: productImage(product)[0] || "",
image: img,
});
}

const deductions = [];
for (const item of cartItems) {
const updated = await Product.findOneAndUpdate(
{
_id: item._id,
isDeleted: { $ne: true },
baseStock: { $gte: item.quantity },
},
{ $inc: { baseStock: -item.quantity } },
{ new: true }
);
let updated;
if (item.variantId) {
updated = await Product.findOneAndUpdate(
{
_id: item._id,
isDeleted: { $ne: true },
"variants._id": item.variantId,
"variants.stock": { $gte: item.quantity }
},
{ $inc: { "variants.$.stock": -item.quantity } },
{ new: true }
);
} else {
updated = await Product.findOneAndUpdate(
{
_id: item._id,
isDeleted: { $ne: true },
baseStock: { $gte: item.quantity },
},
{ $inc: { baseStock: -item.quantity } },
{ new: true }
);
}

if (!updated) {
console.error(`Checkout webhook: stock changed during fulfillment for ${item._id}`);
await restoreStock(deductions);
return res.json({ received: true });
}

let newStock = 0;
if (item.variantId) {
const variant = updated.variants.id(item.variantId);
newStock = variant ? variant.stock : 0;
} else {
newStock = updated.baseStock;
}

// Emit real-time stock update
getIO()?.emit("stockUpdate", {
productId: item._id,
newStock: updated.baseStock
variantId: item.variantId || null,
newStock: newStock
});

deductions.push({ productId: item._id, quantity: item.quantity });
deductions.push({ productId: item._id, variantId: item.variantId || null, quantity: item.quantity });
}

const order = await Order.create({
Expand Down
Binary file added scratch_checkout.txt
Binary file not shown.
Binary file added scratch_lines.txt
Binary file not shown.
Loading