Files
ember-market-frontend/backend/routes/orders.routes.js
2025-03-10 17:39:37 +00:00

598 lines
19 KiB
JavaScript

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;