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;