other
This commit is contained in:
598
backend/routes/orders.routes.js
Normal file
598
backend/routes/orders.routes.js
Normal file
@@ -0,0 +1,598 @@
|
||||
import express from "express";
|
||||
import { protectVendor } from "../middleware/vendorAuthMiddleware.js";
|
||||
import { protectCrypto } from "../middleware/apiAuthMiddleware.js";
|
||||
import Order from "../models/Order.model.js";
|
||||
import Wallet from "../models/Wallet.model.js";
|
||||
|
||||
import crypto from "crypto";
|
||||
import { setupWallet } from "../utils/litecoin/index.js";
|
||||
import { returnCryptoPrices } from "../controllers/cryptoController.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import { sendTelegramMessage } from "../utils/telegramUtils.js"
|
||||
import Product from "../models/Product.model.js";
|
||||
import mongoose from "mongoose";
|
||||
import { decreaseStockOnOrder, restoreStockOnCancel } from "../controllers/stock.controller.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 📌 Get Orders for Vendor
|
||||
* @route GET /api/orders
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.get("/", protectVendor, async (req, res) => {
|
||||
try {
|
||||
// Extract query params for filtering and navigation
|
||||
const { status, page = 1, limit = 25, before, after, storeId } = req.query;
|
||||
|
||||
// Initialize query object
|
||||
let query = {};
|
||||
|
||||
// Use storeId from query parameter if provided, otherwise use the one from user object
|
||||
query.storeId = storeId || req.user.storeId;
|
||||
|
||||
// If for some reason there's no storeId in user object and none provided in query
|
||||
if (!query.storeId) {
|
||||
return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." });
|
||||
}
|
||||
// Filter by order status if provided
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
// Add orderId filters for navigation
|
||||
if (before) {
|
||||
query.orderId = { $lt: before };
|
||||
} else if (after) {
|
||||
query.orderId = { $gt: after };
|
||||
}
|
||||
|
||||
// Count total orders for pagination
|
||||
const totalOrders = await Order.countDocuments(query);
|
||||
|
||||
// Fetch orders with pagination and sorting
|
||||
const orders = await Order.find(query)
|
||||
.sort({ orderId: -1 }) // Always sort by newest first
|
||||
.limit(parseInt(limit))
|
||||
.skip((parseInt(page) - 1) * parseInt(limit));
|
||||
|
||||
res.json({
|
||||
orders,
|
||||
page: parseInt(page),
|
||||
totalPages: Math.ceil(totalOrders / limit),
|
||||
totalOrders,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching orders:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve orders" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/stats", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const vendorId = req.user._id;
|
||||
|
||||
const totalOrders = await Order.countDocuments({ vendorId });
|
||||
const pendingOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: { $in: ["unpaid", "confirming"] },
|
||||
});
|
||||
const ongoingOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: { $in: ["paid", "shipped"] },
|
||||
});
|
||||
const cancelledOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: "cancelled",
|
||||
});
|
||||
const completedOrders = await Order.countDocuments({
|
||||
vendorId,
|
||||
status: "completed",
|
||||
});
|
||||
|
||||
res.json({ totalOrders, pendingOrders, ongoingOrders, cancelledOrders, completedOrders });
|
||||
} catch (error) {
|
||||
console.error("Error fetching order stats:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve order statistics" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Get Vendor's Best Selling Products
|
||||
* @route GET /api/orders/top-products
|
||||
* @access Private (Vendors only)
|
||||
* @returns Most sold products for the logged-in vendor
|
||||
*/
|
||||
router.get("/top-products", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const vendorId = req.user._id;
|
||||
|
||||
// Find the vendor's store
|
||||
const store = await Store.findOne({ vendorId });
|
||||
if (!store) {
|
||||
return res.status(404).json({ message: "Store not found for this vendor" });
|
||||
}
|
||||
|
||||
// Aggregate orders to find the vendor's best-selling products
|
||||
const topProducts = await Order.aggregate([
|
||||
// Match only orders for this vendor
|
||||
{ $match: { vendorId: new mongoose.Types.ObjectId(vendorId) } },
|
||||
// Unwind the products array
|
||||
{ $unwind: "$products" },
|
||||
// Group by product ID and count occurrences
|
||||
{
|
||||
$group: {
|
||||
_id: "$products.productId",
|
||||
count: { $sum: 1 },
|
||||
revenue: { $sum: "$products.price" }
|
||||
}
|
||||
},
|
||||
// Sort by count in descending order
|
||||
{ $sort: { count: -1 } },
|
||||
// Limit to top 10 products
|
||||
{ $limit: 10 }
|
||||
]);
|
||||
|
||||
// Get the actual product details
|
||||
const productIds = topProducts.map(item => item._id);
|
||||
const products = await Product.find({
|
||||
_id: { $in: productIds },
|
||||
storeId: store._id
|
||||
}).select('name price image');
|
||||
|
||||
// Combine the count with product details
|
||||
const result = topProducts.map(item => {
|
||||
const productDetails = products.find(p => p._id.toString() === item._id.toString());
|
||||
return {
|
||||
id: item._id,
|
||||
name: productDetails?.name || 'Unknown Product',
|
||||
price: productDetails?.price || 0,
|
||||
image: productDetails?.image || '',
|
||||
count: item.count,
|
||||
revenue: item.revenue
|
||||
};
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error("Error fetching top products:", error);
|
||||
res.status(500).json({ error: "Failed to fetch top products data" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/create", protectCrypto, async (req, res) => {
|
||||
try {
|
||||
const { vendorId, storeId, products, totalPrice, shippingMethod, telegramChatId, telegramBuyerId, telegramUsername, cryptoCurrency, pgpAddress } = req.body;
|
||||
|
||||
console.log("Create order request:", req.body);
|
||||
|
||||
if (!vendorId || !storeId || !products || !totalPrice || !shippingMethod || !cryptoCurrency || !pgpAddress) {
|
||||
console.log("Missing required fields");
|
||||
return res.status(400).json({ error: "Missing required fields" });
|
||||
}
|
||||
|
||||
/*
|
||||
// TEMPORARILY DISABLED: Stock check disabled to give vendors time to add stock
|
||||
// Check stock levels before creating order
|
||||
const outOfStockProducts = [];
|
||||
for (const item of products) {
|
||||
const product = await Product.findOne({ _id: item.productId, storeId });
|
||||
|
||||
if (product && product.stockTracking && product.currentStock < item.quantity) {
|
||||
outOfStockProducts.push({
|
||||
productId: item.productId,
|
||||
name: product.name,
|
||||
requested: item.quantity,
|
||||
available: product.currentStock
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If any products are out of stock, return an error
|
||||
if (outOfStockProducts.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: "Some products are out of stock or have insufficient quantity",
|
||||
outOfStockProducts
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
const cryptoPrices = returnCryptoPrices();
|
||||
const cryptoRate = cryptoPrices[cryptoCurrency.toLowerCase()];
|
||||
|
||||
if (!cryptoRate) {
|
||||
console.log("Invalid or unsupported cryptocurrency");
|
||||
return res.status(400).json({ error: "Invalid or unsupported cryptocurrency" });
|
||||
}
|
||||
|
||||
const cryptoTotal = (totalPrice / cryptoRate).toFixed(8);
|
||||
|
||||
const walletName = "order_" + crypto.randomBytes(8).toString("hex");
|
||||
const walletData = await setupWallet(walletName);
|
||||
|
||||
if (!walletData || !walletData.address || !walletData.privKey) {
|
||||
return res.status(500).json({ error: "Failed to generate payment address" });
|
||||
}
|
||||
|
||||
// ✅ Create Order with `cryptoTotal`
|
||||
const newOrder = await Order.create({
|
||||
vendorId,
|
||||
storeId,
|
||||
products,
|
||||
pgpAddress,
|
||||
totalPrice,
|
||||
cryptoTotal,
|
||||
shippingMethod,
|
||||
telegramChatId,
|
||||
telegramUsername,
|
||||
telegramBuyerId,
|
||||
status: "unpaid",
|
||||
paymentAddress: walletData.address,
|
||||
});
|
||||
|
||||
// ✅ Link the Wallet to the Order
|
||||
const newWallet = await Wallet.create({
|
||||
walletName,
|
||||
orderId: newOrder._id,
|
||||
address: walletData.address,
|
||||
encryptedPrivateKey: walletData.privKey,
|
||||
});
|
||||
|
||||
// ✅ Update Order with Wallet ID
|
||||
newOrder.wallet = newWallet._id;
|
||||
await newOrder.save();
|
||||
|
||||
// TEMPORARILY DISABLED: Stock decrease disabled to give vendors time to add stock
|
||||
// Decrease stock for ordered products
|
||||
// await decreaseStockOnOrder(newOrder);
|
||||
|
||||
res.status(201).json({
|
||||
message: "Order created successfully",
|
||||
order: newOrder,
|
||||
paymentAddress: walletData.address,
|
||||
walletName: walletName,
|
||||
cryptoTotal,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error creating order:", error);
|
||||
res.status(500).json({ error: "Failed to create order", details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id/status", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!status || !["acknowledged", "paid", "shipped", "completed", "cancelled"].includes(status)) {
|
||||
return res.status(400).json({ message: "Invalid status" });
|
||||
}
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: id, storeId });
|
||||
if (!order) {
|
||||
return res.status(404).json({ message: "Order not found" });
|
||||
}
|
||||
|
||||
const previousStatus = order.status;
|
||||
|
||||
// TEMPORARILY DISABLED: Stock restoration disabled to give vendors time to add stock
|
||||
// Handle stock changes based on status transitions
|
||||
/*
|
||||
if (status === "cancelled" && previousStatus !== "cancelled") {
|
||||
// Restore stock quantities if order is cancelled
|
||||
await restoreStockOnCancel(order);
|
||||
}
|
||||
*/
|
||||
|
||||
const store = await Store.findById(order.storeId);
|
||||
if (!store) return res.status(404).json({ message: "Store not found" });
|
||||
|
||||
order.status = status;
|
||||
await order.save();
|
||||
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
let message = '';
|
||||
|
||||
switch (status) {
|
||||
case 'acknowledged':
|
||||
message = `✅ Your order ${order.orderId} has been acknowledged.`;
|
||||
break;
|
||||
case 'paid':
|
||||
message = `💰 Your order ${order.orderId} has been marked as paid.`;
|
||||
break;
|
||||
case 'shipped':
|
||||
message = `🚚 Your order ${order.orderId} has been shipped!`;
|
||||
break;
|
||||
case 'completed':
|
||||
message = `🎉 Your order ${order.orderId} has been completed.`;
|
||||
break;
|
||||
case 'cancelled':
|
||||
message = `❌ Your order ${order.orderId} has been cancelled.`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about status update to ${status}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({ message: "Order status updated successfully" });
|
||||
} catch (error) {
|
||||
console.error("❌ Error updating order status:", error);
|
||||
return res.status(500).json({ error: "Failed to update order status", details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", protectVendor, async (req, res) => {
|
||||
if (!req.params.id) return res.status(400).json({ message: "Missing order ID" });
|
||||
|
||||
try {
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: req.params.id, storeId: storeId });
|
||||
|
||||
if(!order) return res.status(404).json({ message: "Order not found for this store" });
|
||||
|
||||
return res.status(200).json(order);
|
||||
} catch (error) {
|
||||
console.error("Error fetching order:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve order" });
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Mark Multiple Orders as Shipped
|
||||
* @route POST /api/orders/mark-shipped
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.post("/mark-shipped", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { orderIds } = req.body;
|
||||
|
||||
if (!Array.isArray(orderIds) || orderIds.length === 0) {
|
||||
return res.status(400).json({ error: "Order IDs array is required" });
|
||||
}
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store associated with this user" });
|
||||
}
|
||||
|
||||
const updatedOrders = [];
|
||||
const failedOrders = [];
|
||||
|
||||
// Get store for notifications
|
||||
const store = await Store.findById(storeId);
|
||||
if (!store) {
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
for (const orderId of orderIds) {
|
||||
try {
|
||||
// Find and update order using storeId instead of vendorId
|
||||
const order = await Order.findOne({ _id: orderId, storeId: storeId });
|
||||
|
||||
if (!order) {
|
||||
failedOrders.push({ orderId, reason: "Order not found or not associated with this store" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (order.status !== "paid") {
|
||||
failedOrders.push({ orderId, reason: "Order must be in paid status to be marked as shipped" });
|
||||
continue;
|
||||
}
|
||||
|
||||
order.status = "shipped";
|
||||
await order.save();
|
||||
updatedOrders.push(order);
|
||||
|
||||
// Send notification if possible
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
const message = `🚚 Your order ${order.orderId} has been marked as shipped!`;
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about shipping update.`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating order ${orderId}:`, error);
|
||||
failedOrders.push({ orderId, reason: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Orders processed",
|
||||
success: {
|
||||
count: updatedOrders.length,
|
||||
orders: updatedOrders.map(o => ({
|
||||
id: o._id,
|
||||
orderId: o.orderId,
|
||||
status: o.status
|
||||
}))
|
||||
},
|
||||
failed: {
|
||||
count: failedOrders.length,
|
||||
orders: failedOrders
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error marking orders as shipped:", error);
|
||||
return res.status(500).json({
|
||||
error: "Failed to mark orders as shipped",
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update Order Tracking Number
|
||||
* @route PUT /api/orders/:id/tracking
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.put("/:id/tracking", protectVendor, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { trackingNumber } = req.body;
|
||||
|
||||
if (!id) return res.status(400).json({ error: "Missing order ID" });
|
||||
if (!trackingNumber) return res.status(400).json({ error: "Missing tracking number" });
|
||||
|
||||
try {
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Use storeId for lookup instead of vendorId
|
||||
const order = await Order.findOne({ _id: id, storeId: storeId });
|
||||
if (!order) return res.status(404).json({ error: "Order not found for this store" });
|
||||
|
||||
// Only allow tracking updates for paid or shipped orders
|
||||
if (order.status !== "paid" && order.status !== "shipped") {
|
||||
return res.status(400).json({ error: "Can only add tracking to paid or shipped orders" });
|
||||
}
|
||||
|
||||
// Get store details for notification
|
||||
const store = await Store.findById(order.storeId);
|
||||
if (!store) return res.status(404).json({ error: "Store not found" });
|
||||
|
||||
// Update the order with tracking number
|
||||
order.trackingNumber = trackingNumber;
|
||||
await order.save();
|
||||
|
||||
// Send tracking notification
|
||||
if (store.telegramToken && order.telegramChatId) {
|
||||
const message = `📦 Tracking added for order ${order.orderId}\!`;
|
||||
const sent = await sendTelegramMessage(store.telegramToken, order.telegramChatId, message);
|
||||
|
||||
if (!sent) {
|
||||
console.error(`Failed to notify user ${order.telegramChatId} about tracking update.`);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
message: "Tracking number updated successfully",
|
||||
order: {
|
||||
id: order._id,
|
||||
orderId: order.orderId,
|
||||
status: order.status,
|
||||
trackingNumber: order.trackingNumber
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating tracking number:", error);
|
||||
return res.status(500).json({
|
||||
error: "Failed to update tracking number",
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete Order
|
||||
* @route DELETE /api/orders/:id
|
||||
* @access Private (Vendors only)
|
||||
*/
|
||||
router.delete("/:id", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get storeId from req.user
|
||||
const storeId = req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ message: "No store associated with this user" });
|
||||
}
|
||||
|
||||
// Find order by ID and storeId
|
||||
const order = await Order.findOne({ _id: id, storeId: storeId });
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({ message: "Order not found for this store" });
|
||||
}
|
||||
|
||||
// Delete associated wallet if exists
|
||||
if (order.wallet) {
|
||||
await Wallet.findByIdAndDelete(order.wallet);
|
||||
}
|
||||
|
||||
// Delete the order
|
||||
await Order.findByIdAndDelete(id);
|
||||
|
||||
res.status(200).json({ message: "Order deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting order:", error);
|
||||
res.status(500).json({ error: "Failed to delete order" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 📌 Get Adjacent Orders (for navigation)
|
||||
* @route GET /api/orders/adjacent/:orderId
|
||||
* @access Private (Vendors only)
|
||||
* @description Returns the immediately previous and next orders by orderId
|
||||
*/
|
||||
router.get("/adjacent/:orderId", protectVendor, async (req, res) => {
|
||||
try {
|
||||
const { orderId } = req.params;
|
||||
const numericOrderId = parseInt(orderId);
|
||||
|
||||
if (isNaN(numericOrderId)) {
|
||||
return res.status(400).json({ error: "Invalid order ID format" });
|
||||
}
|
||||
|
||||
// Get the store ID from user or query parameter
|
||||
const storeId = req.query.storeId || req.user.storeId;
|
||||
if (!storeId) {
|
||||
return res.status(400).json({ error: "No store ID found. Please specify a storeId parameter." });
|
||||
}
|
||||
|
||||
// Find the next newer order (higher orderId)
|
||||
const newerOrder = await Order.findOne({
|
||||
storeId,
|
||||
orderId: { $gt: numericOrderId }
|
||||
})
|
||||
.sort({ orderId: 1 }) // Ascending to get the immediately next one
|
||||
.select('_id orderId')
|
||||
.lean();
|
||||
|
||||
// Find the next older order (lower orderId)
|
||||
const olderOrder = await Order.findOne({
|
||||
storeId,
|
||||
orderId: { $lt: numericOrderId }
|
||||
})
|
||||
.sort({ orderId: -1 }) // Descending to get the immediately previous one
|
||||
.select('_id orderId')
|
||||
.lean();
|
||||
|
||||
res.json({
|
||||
current: { orderId: numericOrderId },
|
||||
newer: newerOrder || null,
|
||||
older: olderOrder || null
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching adjacent orders:", error);
|
||||
res.status(500).json({ error: "Failed to retrieve adjacent orders" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user