other
This commit is contained in:
521
backend/controllers/chat.controller.js
Normal file
521
backend/controllers/chat.controller.js
Normal file
@@ -0,0 +1,521 @@
|
||||
import Chat from "../models/Chat.model.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import Vendor from "../models/Vendor.model.js";
|
||||
import { encryptWithPGP } from "../utils/encryptPgp.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import { sendTelegramMessage } from "../utils/telegramUtils.js";
|
||||
import axios from "axios";
|
||||
|
||||
// Get all chats for a vendor
|
||||
export const getVendorChats = async (req, res) => {
|
||||
try {
|
||||
const { vendorId } = req.params;
|
||||
|
||||
// Check if vendor exists and requester has access
|
||||
if (req.user._id.toString() !== vendorId) {
|
||||
return res.status(403).json({ error: "Not authorized to access these chats" });
|
||||
}
|
||||
|
||||
const chats = await Chat.find({ vendorId })
|
||||
.sort({ lastUpdated: -1 })
|
||||
.select("-messages") // Don't include messages in the list view
|
||||
.lean();
|
||||
|
||||
return res.status(200).json(chats);
|
||||
} catch (error) {
|
||||
logger.error("Error getting vendor chats", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error getting chats" });
|
||||
}
|
||||
};
|
||||
|
||||
// Get all messages in a specific chat
|
||||
export const getChatMessages = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { markAsRead = "true" } = req.query; // Default to true for backward compatibility
|
||||
const shouldMarkAsRead = markAsRead === "true";
|
||||
|
||||
const chat = await Chat.findById(chatId).lean();
|
||||
|
||||
if (!chat) {
|
||||
return res.status(404).json({ error: "Chat not found" });
|
||||
}
|
||||
|
||||
// Check if user has access to this chat
|
||||
if (req.user && chat.vendorId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({ error: "Not authorized to access this chat" });
|
||||
}
|
||||
|
||||
// Only mark messages as read if markAsRead parameter is true
|
||||
if (shouldMarkAsRead) {
|
||||
// Mark all vendor messages as read if the request is from the buyer
|
||||
if (req.telegramUser && chat.buyerId === req.telegramUser.telegramId) {
|
||||
await Chat.updateMany(
|
||||
{ _id: chatId, "messages.sender": "vendor", "messages.read": false },
|
||||
{ $set: { "messages.$[elem].read": true } },
|
||||
{ arrayFilters: [{ "elem.sender": "vendor", "elem.read": false }] }
|
||||
);
|
||||
}
|
||||
|
||||
// Mark all buyer messages as read if the request is from the vendor
|
||||
if (req.user && chat.vendorId.toString() === req.user._id.toString()) {
|
||||
await Chat.updateMany(
|
||||
{ _id: chatId, "messages.sender": "buyer", "messages.read": false },
|
||||
{ $set: { "messages.$[elem].read": true } },
|
||||
{ arrayFilters: [{ "elem.sender": "buyer", "elem.read": false }] }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(chat);
|
||||
} catch (error) {
|
||||
logger.error("Error getting chat messages", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error getting chat messages" });
|
||||
}
|
||||
};
|
||||
|
||||
// Explicitly mark messages as read
|
||||
export const markMessagesAsRead = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
|
||||
if (!chat) {
|
||||
return res.status(404).json({ error: "Chat not found" });
|
||||
}
|
||||
|
||||
// Check if user has access to this chat
|
||||
if (req.user && chat.vendorId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({ error: "Not authorized to access this chat" });
|
||||
}
|
||||
|
||||
// Mark all buyer messages as read if the request is from the vendor
|
||||
if (req.user && chat.vendorId.toString() === req.user._id.toString()) {
|
||||
await Chat.updateMany(
|
||||
{ _id: chatId, "messages.sender": "buyer", "messages.read": false },
|
||||
{ $set: { "messages.$[elem].read": true } },
|
||||
{ arrayFilters: [{ "elem.sender": "buyer", "elem.read": false }] }
|
||||
);
|
||||
|
||||
logger.info("Marked all buyer messages as read", { chatId });
|
||||
}
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error marking messages as read", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error marking messages as read" });
|
||||
}
|
||||
};
|
||||
|
||||
// Send a message from vendor to buyer
|
||||
export const sendVendorMessage = async (req, res) => {
|
||||
try {
|
||||
const { chatId } = req.params;
|
||||
const { content, attachments } = req.body;
|
||||
|
||||
if (!content && (!attachments || attachments.length === 0)) {
|
||||
return res.status(400).json({ error: "Message content or attachments required" });
|
||||
}
|
||||
|
||||
const chat = await Chat.findById(chatId);
|
||||
|
||||
if (!chat) {
|
||||
return res.status(404).json({ error: "Chat not found" });
|
||||
}
|
||||
|
||||
// Check vendor authorization
|
||||
if (chat.vendorId.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({ error: "Not authorized to send messages in this chat" });
|
||||
}
|
||||
|
||||
// Create the new message
|
||||
const newMessage = {
|
||||
sender: "vendor",
|
||||
buyerId: chat.buyerId,
|
||||
vendorId: chat.vendorId,
|
||||
content: content || "",
|
||||
attachments: attachments || [],
|
||||
read: false,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// Add message to chat
|
||||
chat.messages.push(newMessage);
|
||||
chat.lastUpdated = new Date();
|
||||
await chat.save();
|
||||
|
||||
// Send notification to Telegram client if configured
|
||||
try {
|
||||
const store = await Store.findById(chat.storeId);
|
||||
if (!store) {
|
||||
logger.warn("Store not found for chat notification", { chatId, storeId: chat.storeId });
|
||||
} else {
|
||||
// Get store name for the notification
|
||||
const storeName = store.storeName || "Vendor";
|
||||
|
||||
// Check if store has telegram token
|
||||
if (store.telegramToken) {
|
||||
logger.info("Sending Telegram notification", { buyerId: chat.buyerId, storeId: store._id });
|
||||
|
||||
// Format the message with emoji and preview
|
||||
const notificationMessage = `📬 New message from ${storeName}: ${content.substring(0, 50)}${content.length > 50 ? '...' : ''}`;
|
||||
|
||||
// Use the sendTelegramMessage utility instead of axios
|
||||
const success = await sendTelegramMessage(
|
||||
store.telegramToken,
|
||||
chat.buyerId,
|
||||
notificationMessage
|
||||
);
|
||||
|
||||
if (success) {
|
||||
logger.info("Telegram notification sent successfully", { chatId, buyerId: chat.buyerId });
|
||||
} else {
|
||||
logger.error("Failed to send Telegram notification", {
|
||||
chatId,
|
||||
buyerId: chat.buyerId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.warn("Store missing telegramToken", { storeId: store._id });
|
||||
}
|
||||
}
|
||||
} catch (notifyError) {
|
||||
// Log but don't fail if notification fails
|
||||
logger.error("Notification system error", {
|
||||
error: notifyError.message,
|
||||
stack: notifyError.stack,
|
||||
chatId
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json(newMessage);
|
||||
} catch (error) {
|
||||
logger.error("Error sending vendor message", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error sending message" });
|
||||
}
|
||||
};
|
||||
|
||||
// Process an incoming message from the Telegram client
|
||||
export const processTelegramMessage = async (req, res) => {
|
||||
try {
|
||||
const { buyerId, storeId, content, attachments } = req.body;
|
||||
|
||||
if (!buyerId || !storeId) {
|
||||
return res.status(400).json({ error: "Buyer ID and Store ID are required" });
|
||||
}
|
||||
|
||||
if (!content && (!attachments || attachments.length === 0)) {
|
||||
return res.status(400).json({ error: "Message content or attachments required" });
|
||||
}
|
||||
|
||||
// Find the store
|
||||
const store = await Store.findById(storeId);
|
||||
|
||||
if (!store) {
|
||||
logger.error("Store not found for Telegram message", { storeId });
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
// Check if vendorId field exists in store
|
||||
if (!store.vendorId) {
|
||||
logger.error("Store missing vendorId field", { storeId, store });
|
||||
return res.status(500).json({ error: "Store data is invalid (missing vendorId)" });
|
||||
}
|
||||
|
||||
// Find or create a chat
|
||||
let chat = await Chat.findOne({ buyerId, storeId });
|
||||
|
||||
if (!chat) {
|
||||
// Create a new chat
|
||||
chat = new Chat({
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
storeId,
|
||||
messages: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Create the new message
|
||||
const newMessage = {
|
||||
sender: "buyer",
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
content: content || "",
|
||||
attachments: attachments || [],
|
||||
read: false,
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
// Add message to chat
|
||||
chat.messages.push(newMessage);
|
||||
chat.lastUpdated = new Date();
|
||||
await chat.save();
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
chatId: chat._id,
|
||||
message: newMessage
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error processing Telegram message", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error processing message" });
|
||||
}
|
||||
};
|
||||
|
||||
// Get unread message counts for a vendor
|
||||
export const getVendorUnreadCounts = async (req, res) => {
|
||||
try {
|
||||
const { vendorId } = req.params;
|
||||
|
||||
// Check if vendor is authorized
|
||||
if (req.user._id.toString() !== vendorId) {
|
||||
return res.status(403).json({ error: "Not authorized" });
|
||||
}
|
||||
|
||||
// Find all chats for this vendor
|
||||
const chats = await Chat.find({ vendorId });
|
||||
|
||||
// Calculate unread counts
|
||||
const result = {
|
||||
totalUnread: 0,
|
||||
chatCounts: {}
|
||||
};
|
||||
|
||||
chats.forEach(chat => {
|
||||
const unreadCount = chat.messages.filter(
|
||||
msg => msg.sender === "buyer" && !msg.read
|
||||
).length;
|
||||
|
||||
if (unreadCount > 0) {
|
||||
result.totalUnread += unreadCount;
|
||||
result.chatCounts[chat._id] = unreadCount;
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error("Error getting unread counts", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error getting unread counts" });
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new chat directly (for customer service purposes)
|
||||
export const createChat = async (req, res) => {
|
||||
try {
|
||||
const { buyerId, storeId, initialMessage } = req.body;
|
||||
|
||||
if (!buyerId || !storeId) {
|
||||
return res.status(400).json({ error: "Buyer ID and Store ID are required" });
|
||||
}
|
||||
|
||||
// Find the store and vendor
|
||||
const store = await Store.findById(storeId);
|
||||
console.log("Store data:", JSON.stringify(store, null, 2));
|
||||
console.log("Authenticated user:", JSON.stringify(req.user, null, 2));
|
||||
|
||||
if (!store) {
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
// Check if vendorId field exists in store
|
||||
if (!store.vendorId) {
|
||||
logger.error("Store missing vendorId field", { storeId, store });
|
||||
return res.status(500).json({ error: "Store data is invalid (missing vendorId)" });
|
||||
}
|
||||
|
||||
// Check if requester is authorized for this store
|
||||
if (req.user._id.toString() !== store.vendorId.toString()) {
|
||||
return res.status(403).json({ error: "Not authorized to create chats for this store" });
|
||||
}
|
||||
|
||||
// Check if chat already exists
|
||||
const existingChat = await Chat.findOne({ buyerId, storeId });
|
||||
if (existingChat) {
|
||||
return res.status(409).json({
|
||||
error: "Chat already exists",
|
||||
chatId: existingChat._id
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new chat
|
||||
const chat = new Chat({
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
storeId,
|
||||
messages: [],
|
||||
});
|
||||
|
||||
// Add initial message if provided
|
||||
if (initialMessage) {
|
||||
chat.messages.push({
|
||||
sender: "vendor",
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
content: initialMessage,
|
||||
read: false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
chat.lastUpdated = new Date();
|
||||
await chat.save();
|
||||
|
||||
// Send notification to Telegram if configured
|
||||
try {
|
||||
if (initialMessage) {
|
||||
// Get store name for the notification
|
||||
const storeName = store.storeName || "Vendor";
|
||||
|
||||
// Check if store has telegram token
|
||||
if (store.telegramToken) {
|
||||
logger.info("Sending Telegram notification for new chat", { buyerId, storeId: store._id });
|
||||
|
||||
// Format the message with emoji and preview
|
||||
const notificationMessage = `📬 New chat created by ${storeName}: ${initialMessage.substring(0, 50)}${initialMessage.length > 50 ? '...' : ''}`;
|
||||
|
||||
// Use the sendTelegramMessage utility instead of axios
|
||||
const success = await sendTelegramMessage(
|
||||
store.telegramToken,
|
||||
buyerId,
|
||||
notificationMessage
|
||||
);
|
||||
|
||||
if (success) {
|
||||
logger.info("Telegram notification for new chat sent successfully", { chatId: chat._id, buyerId });
|
||||
} else {
|
||||
logger.error("Failed to send Telegram notification for new chat", {
|
||||
chatId: chat._id,
|
||||
buyerId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.warn("Store missing telegramToken for new chat notification", { storeId: store._id });
|
||||
}
|
||||
}
|
||||
} catch (notifyError) {
|
||||
logger.error("Notification system error for new chat", {
|
||||
error: notifyError.message,
|
||||
stack: notifyError.stack,
|
||||
chatId: chat._id,
|
||||
buyerId,
|
||||
storeId
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
chatId: chat._id
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error creating chat", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error creating chat" });
|
||||
}
|
||||
};
|
||||
|
||||
// Create a chat from Telegram client
|
||||
export const createTelegramChat = async (req, res) => {
|
||||
try {
|
||||
const { buyerId, storeId, initialMessage } = req.body;
|
||||
|
||||
if (!buyerId || !storeId) {
|
||||
return res.status(400).json({ error: "Buyer ID and Store ID are required" });
|
||||
}
|
||||
|
||||
// Find the store
|
||||
const store = await Store.findById(storeId);
|
||||
|
||||
if (!store) {
|
||||
logger.error("Store not found for Telegram chat creation", { storeId });
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
// Check if vendorId field exists in store
|
||||
if (!store.vendorId) {
|
||||
logger.error("Store missing vendorId field", { storeId, store });
|
||||
return res.status(500).json({ error: "Store data is invalid (missing vendorId)" });
|
||||
}
|
||||
|
||||
// Check if chat already exists
|
||||
const existingChat = await Chat.findOne({ buyerId, storeId });
|
||||
|
||||
// If chat exists, just return it
|
||||
if (existingChat) {
|
||||
logger.info("Chat already exists, returning existing chat", {
|
||||
chatId: existingChat._id,
|
||||
buyerId,
|
||||
storeId
|
||||
});
|
||||
|
||||
// Add initial message to existing chat if provided
|
||||
if (initialMessage) {
|
||||
existingChat.messages.push({
|
||||
sender: "buyer",
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
content: initialMessage,
|
||||
read: false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
existingChat.lastUpdated = new Date();
|
||||
await existingChat.save();
|
||||
|
||||
// Notify vendor about the new message
|
||||
// This would typically be done through a notification system
|
||||
logger.info("Added message to existing chat", {
|
||||
chatId: existingChat._id,
|
||||
buyerId,
|
||||
message: initialMessage.substring(0, 50)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Using existing chat",
|
||||
chatId: existingChat._id
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new chat
|
||||
const chat = new Chat({
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
storeId,
|
||||
messages: [],
|
||||
});
|
||||
|
||||
// Add initial message if provided
|
||||
if (initialMessage) {
|
||||
chat.messages.push({
|
||||
sender: "buyer",
|
||||
buyerId,
|
||||
vendorId: store.vendorId,
|
||||
content: initialMessage,
|
||||
read: false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
chat.lastUpdated = new Date();
|
||||
await chat.save();
|
||||
|
||||
logger.info("New chat created from Telegram", {
|
||||
chatId: chat._id,
|
||||
buyerId,
|
||||
storeId
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
message: "Chat created successfully",
|
||||
chatId: chat._id
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error creating chat from Telegram", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
buyerId: req.body.buyerId,
|
||||
storeId: req.body.storeId
|
||||
});
|
||||
return res.status(500).json({ error: "Server error creating chat" });
|
||||
}
|
||||
};
|
||||
69
backend/controllers/cryptoController.js
Normal file
69
backend/controllers/cryptoController.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import ky from "ky";
|
||||
import logger from "../utils/logger.js"
|
||||
|
||||
// Global object to store the latest crypto prices
|
||||
const cryptoPrices = {
|
||||
btc: null,
|
||||
ltc: null,
|
||||
xmr: null,
|
||||
lastUpdated: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch crypto prices from the CoinGecko API and update the global `cryptoPrices` object.
|
||||
*/
|
||||
const fetchCryptoPrices = async () => {
|
||||
try {
|
||||
const url =
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,litecoin,monero&vs_currencies=gbp";
|
||||
|
||||
// Fetch using Ky with automatic JSON parsing
|
||||
const data = await ky.get(url).json();
|
||||
|
||||
// Update the stored crypto prices
|
||||
cryptoPrices.btc = data.bitcoin?.gbp ?? null;
|
||||
cryptoPrices.ltc = data.litecoin?.gbp ?? null;
|
||||
cryptoPrices.xmr = data.monero?.gbp ?? null;
|
||||
cryptoPrices.lastUpdated = new Date().toISOString();
|
||||
|
||||
logger.info("✅ Crypto prices updated", { cryptoPrices });
|
||||
} catch (error) {
|
||||
logger.error("❌ Error fetching crypto prices", {
|
||||
message: error.message || "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the automatic crypto price updater.
|
||||
* @param {number} interval - Update interval in seconds.
|
||||
*/
|
||||
const startCryptoPriceUpdater = (interval) => {
|
||||
logger.info(`🚀 Starting crypto price updater (every ${interval} seconds)`);
|
||||
fetchCryptoPrices(); // Fetch immediately
|
||||
setInterval(fetchCryptoPrices, interval * 1000); // Fetch periodically
|
||||
};
|
||||
|
||||
/**
|
||||
* API Route: Get the latest crypto prices.
|
||||
* @route GET /api/crypto
|
||||
*/
|
||||
const getCryptoPrices = async (req, res) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
prices: cryptoPrices,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("❌ Error getting crypto prices", {
|
||||
message: error.message || "Unknown error",
|
||||
});
|
||||
res.status(500).json({ success: false, error: "Internal Server Error" });
|
||||
}
|
||||
};
|
||||
|
||||
const returnCryptoPrices = () => {
|
||||
return cryptoPrices;
|
||||
};
|
||||
|
||||
export { startCryptoPriceUpdater, getCryptoPrices, returnCryptoPrices };
|
||||
271
backend/controllers/promotion.controller.js
Normal file
271
backend/controllers/promotion.controller.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import Promotion from "../models/Promotion.model.js";
|
||||
import logger from "../utils/logger.js";
|
||||
|
||||
/**
|
||||
* Get all promotions for a store
|
||||
*/
|
||||
export const getPromotions = async (req, res) => {
|
||||
try {
|
||||
const { query } = req;
|
||||
const filter = { storeId: req.user.storeId };
|
||||
|
||||
// Apply filters if provided
|
||||
if (query.isActive) {
|
||||
filter.isActive = query.isActive === 'true';
|
||||
}
|
||||
|
||||
if (query.search) {
|
||||
filter.code = { $regex: query.search, $options: 'i' };
|
||||
}
|
||||
|
||||
// Handle date filters
|
||||
if (query.active === 'true') {
|
||||
const now = new Date();
|
||||
filter.startDate = { $lte: now };
|
||||
filter.$or = [
|
||||
{ endDate: null },
|
||||
{ endDate: { $gte: now } }
|
||||
];
|
||||
filter.isActive = true;
|
||||
}
|
||||
|
||||
const promotions = await Promotion.find(filter).sort({ createdAt: -1 });
|
||||
|
||||
return res.status(200).json(promotions);
|
||||
} catch (error) {
|
||||
logger.error("Error fetching promotions", { error });
|
||||
return res.status(500).json({ message: "Failed to fetch promotions", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single promotion by ID
|
||||
*/
|
||||
export const getPromotionById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const promotion = await Promotion.findOne({
|
||||
_id: id,
|
||||
storeId: req.user.storeId
|
||||
});
|
||||
|
||||
if (!promotion) {
|
||||
return res.status(404).json({ message: "Promotion not found" });
|
||||
}
|
||||
|
||||
return res.status(200).json(promotion);
|
||||
} catch (error) {
|
||||
logger.error("Error fetching promotion", { error });
|
||||
return res.status(500).json({ message: "Failed to fetch promotion", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new promotion
|
||||
*/
|
||||
export const createPromotion = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
code,
|
||||
discountType,
|
||||
discountValue,
|
||||
minOrderAmount = 0,
|
||||
maxUsage = null,
|
||||
isActive = true,
|
||||
startDate = new Date(),
|
||||
endDate = null,
|
||||
description = ""
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!code || !discountType || discountValue === undefined) {
|
||||
return res.status(400).json({ message: "Code, discount type, and discount value are required" });
|
||||
}
|
||||
|
||||
// Check if code already exists for this store
|
||||
const existingPromo = await Promotion.findOne({
|
||||
storeId: req.user.storeId,
|
||||
code: code.toUpperCase()
|
||||
});
|
||||
|
||||
if (existingPromo) {
|
||||
return res.status(400).json({ message: "A promotion with this code already exists" });
|
||||
}
|
||||
|
||||
// Create new promotion
|
||||
const newPromotion = new Promotion({
|
||||
storeId: req.user.storeId,
|
||||
code: code.toUpperCase(),
|
||||
discountType,
|
||||
discountValue,
|
||||
minOrderAmount,
|
||||
maxUsage,
|
||||
isActive,
|
||||
startDate,
|
||||
endDate,
|
||||
description
|
||||
});
|
||||
|
||||
await newPromotion.save();
|
||||
|
||||
return res.status(201).json(newPromotion);
|
||||
} catch (error) {
|
||||
logger.error("Error creating promotion", { error });
|
||||
|
||||
if (error.name === 'ValidationError') {
|
||||
return res.status(400).json({
|
||||
message: "Invalid promotion data",
|
||||
details: Object.values(error.errors).map(err => err.message)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({ message: "Failed to create promotion", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing promotion
|
||||
*/
|
||||
export const updatePromotion = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// Ensure the promotion exists and belongs to the vendor's store
|
||||
const promotion = await Promotion.findOne({
|
||||
_id: id,
|
||||
storeId: req.user.storeId
|
||||
});
|
||||
|
||||
if (!promotion) {
|
||||
return res.status(404).json({ message: "Promotion not found" });
|
||||
}
|
||||
|
||||
// Remove storeId from updates if present (can't change store)
|
||||
if (updates.storeId) {
|
||||
delete updates.storeId;
|
||||
}
|
||||
|
||||
// Ensure code is uppercase if present
|
||||
if (updates.code) {
|
||||
updates.code = updates.code.toUpperCase();
|
||||
|
||||
// Check if updated code conflicts with existing promo
|
||||
const codeExists = await Promotion.findOne({
|
||||
storeId: req.user.storeId,
|
||||
code: updates.code,
|
||||
_id: { $ne: id } // Exclude current promotion
|
||||
});
|
||||
|
||||
if (codeExists) {
|
||||
return res.status(400).json({ message: "A promotion with this code already exists" });
|
||||
}
|
||||
}
|
||||
|
||||
// Update the promotion
|
||||
const updatedPromotion = await Promotion.findByIdAndUpdate(
|
||||
id,
|
||||
{ ...updates, updatedAt: new Date() },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
return res.status(200).json(updatedPromotion);
|
||||
} catch (error) {
|
||||
logger.error("Error updating promotion", { error });
|
||||
|
||||
if (error.name === 'ValidationError') {
|
||||
return res.status(400).json({
|
||||
message: "Invalid promotion data",
|
||||
details: Object.values(error.errors).map(err => err.message)
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({ message: "Failed to update promotion", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a promotion
|
||||
*/
|
||||
export const deletePromotion = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Ensure the promotion exists and belongs to the vendor's store
|
||||
const promotion = await Promotion.findOne({
|
||||
_id: id,
|
||||
storeId: req.user.storeId
|
||||
});
|
||||
|
||||
if (!promotion) {
|
||||
return res.status(404).json({ message: "Promotion not found" });
|
||||
}
|
||||
|
||||
await Promotion.findByIdAndDelete(id);
|
||||
|
||||
return res.status(200).json({ message: "Promotion deleted successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Error deleting promotion", { error });
|
||||
return res.status(500).json({ message: "Failed to delete promotion", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a promotion code for a store
|
||||
*/
|
||||
export const validatePromotion = async (req, res) => {
|
||||
try {
|
||||
const { code, orderTotal } = req.body;
|
||||
const { storeId } = req.params;
|
||||
|
||||
if (!code || !storeId) {
|
||||
return res.status(400).json({ message: "Promotion code and store ID are required" });
|
||||
}
|
||||
|
||||
// Find the promotion
|
||||
const promotion = await Promotion.findOne({
|
||||
storeId,
|
||||
code: code.toUpperCase(),
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
if (!promotion) {
|
||||
return res.status(404).json({ message: "Promotion not found or inactive" });
|
||||
}
|
||||
|
||||
// Check if the promotion is valid
|
||||
if (!promotion.isValid()) {
|
||||
return res.status(400).json({ message: "Promotion is no longer valid" });
|
||||
}
|
||||
|
||||
// Check minimum order amount
|
||||
if (orderTotal && orderTotal < promotion.minOrderAmount) {
|
||||
return res.status(400).json({
|
||||
message: `Order total must be at least £${promotion.minOrderAmount} to use this promotion`,
|
||||
minOrderAmount: promotion.minOrderAmount
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate discount if order total provided
|
||||
let discountAmount = null;
|
||||
if (orderTotal) {
|
||||
discountAmount = promotion.calculateDiscount(orderTotal);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
promotion: {
|
||||
_id: promotion._id,
|
||||
code: promotion.code,
|
||||
discountType: promotion.discountType,
|
||||
discountValue: promotion.discountValue,
|
||||
minOrderAmount: promotion.minOrderAmount,
|
||||
},
|
||||
discountAmount,
|
||||
message: "Promotion is valid"
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error validating promotion", { error });
|
||||
return res.status(500).json({ message: "Failed to validate promotion", error: error.message });
|
||||
}
|
||||
};
|
||||
151
backend/controllers/stock.controller.js
Normal file
151
backend/controllers/stock.controller.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import Product from "../models/Product.model.js";
|
||||
import Order from "../models/Order.model.js";
|
||||
import Store from "../models/Store.model.js";
|
||||
import mongoose from "mongoose";
|
||||
import logger from "../utils/logger.js";
|
||||
|
||||
/**
|
||||
* Updates a product's stock quantity
|
||||
*/
|
||||
export const updateStock = async (req, res) => {
|
||||
try {
|
||||
const { productId } = req.params;
|
||||
const { currentStock, stockTracking, lowStockThreshold } = req.body;
|
||||
|
||||
if (currentStock === undefined) {
|
||||
return res.status(400).json({ message: "Stock quantity is required" });
|
||||
}
|
||||
|
||||
// Validate product exists and belongs to the vendor's store
|
||||
const product = await Product.findOne({
|
||||
_id: productId,
|
||||
storeId: req.user.storeId
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
|
||||
// Update stock values
|
||||
let stockStatus = "in_stock";
|
||||
if (currentStock <= 0) {
|
||||
stockStatus = "out_of_stock";
|
||||
} else if (currentStock <= (lowStockThreshold || product.lowStockThreshold)) {
|
||||
stockStatus = "low_stock";
|
||||
}
|
||||
|
||||
const updatedProduct = await Product.findByIdAndUpdate(
|
||||
productId,
|
||||
{
|
||||
currentStock: currentStock,
|
||||
stockTracking: stockTracking !== undefined ? stockTracking : product.stockTracking,
|
||||
lowStockThreshold: lowStockThreshold !== undefined ? lowStockThreshold : product.lowStockThreshold,
|
||||
stockStatus: stockStatus
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
return res.status(200).json(updatedProduct);
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Error updating stock", { error });
|
||||
return res.status(500).json({ message: "Failed to update stock", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets stock information for all products in a store
|
||||
*/
|
||||
export const getStoreStock = async (req, res) => {
|
||||
try {
|
||||
const products = await Product.find({ storeId: req.user.storeId })
|
||||
.select('_id name currentStock stockTracking stockStatus lowStockThreshold')
|
||||
.sort({ stockStatus: 1, name: 1 });
|
||||
|
||||
return res.status(200).json(products);
|
||||
} catch (error) {
|
||||
logger.error("Error fetching store stock", { error });
|
||||
return res.status(500).json({ message: "Failed to fetch stock information", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates stock levels when an order is placed
|
||||
* This should be called from the order creation logic
|
||||
*/
|
||||
export const decreaseStockOnOrder = async (order) => {
|
||||
try {
|
||||
// Process each ordered product
|
||||
for (const item of order.products) {
|
||||
const product = await Product.findById(item.productId);
|
||||
|
||||
// Skip if product doesn't exist or stock tracking is disabled
|
||||
if (!product || !product.stockTracking) continue;
|
||||
|
||||
// Calculate new stock level
|
||||
let newStock = Math.max(0, product.currentStock - item.quantity);
|
||||
let stockStatus = "in_stock";
|
||||
|
||||
if (newStock <= 0) {
|
||||
stockStatus = "out_of_stock";
|
||||
} else if (newStock <= product.lowStockThreshold) {
|
||||
stockStatus = "low_stock";
|
||||
}
|
||||
|
||||
// Update the product stock
|
||||
await Product.findByIdAndUpdate(
|
||||
item.productId,
|
||||
{
|
||||
currentStock: newStock,
|
||||
stockStatus: stockStatus
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Error decreasing stock on order", { orderId: order._id, error });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores stock when an order is cancelled
|
||||
*/
|
||||
export const restoreStockOnCancel = async (order) => {
|
||||
try {
|
||||
// Process each ordered product
|
||||
for (const item of order.products) {
|
||||
const product = await Product.findById(item.productId);
|
||||
|
||||
// Skip if product doesn't exist or stock tracking is disabled
|
||||
if (!product || !product.stockTracking) continue;
|
||||
|
||||
// Calculate new stock level
|
||||
let newStock = product.currentStock + item.quantity;
|
||||
let stockStatus = "in_stock";
|
||||
|
||||
if (newStock <= 0) {
|
||||
stockStatus = "out_of_stock";
|
||||
} else if (newStock <= product.lowStockThreshold) {
|
||||
stockStatus = "low_stock";
|
||||
}
|
||||
|
||||
// Update the product stock
|
||||
await Product.findByIdAndUpdate(
|
||||
item.productId,
|
||||
{
|
||||
currentStock: newStock,
|
||||
stockStatus: stockStatus
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Error restoring stock on cancel", { orderId: order._id, error });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
39
backend/controllers/vendor.controller.js
Normal file
39
backend/controllers/vendor.controller.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Handle notifications from Telegram to vendors
|
||||
export const notifyVendor = async (req, res) => {
|
||||
try {
|
||||
const { buyerId, storeId, message, source } = req.body;
|
||||
|
||||
if (!buyerId || !storeId || !message) {
|
||||
return res.status(400).json({ error: "Missing required parameters" });
|
||||
}
|
||||
|
||||
// Find the store and check if it exists
|
||||
const store = await Store.findById(storeId);
|
||||
if (!store) {
|
||||
logger.warn("Store not found for vendor notification", { storeId });
|
||||
return res.status(404).json({ error: "Store not found" });
|
||||
}
|
||||
|
||||
// Get the vendor
|
||||
const vendor = await Vendor.findById(store.vendorId);
|
||||
if (!vendor) {
|
||||
logger.warn("Vendor not found for notification", { storeId, vendorId: store.vendorId });
|
||||
return res.status(404).json({ error: "Vendor not found" });
|
||||
}
|
||||
|
||||
// Future enhancement: could implement WebSocket/Server-Sent Events for real-time notifications
|
||||
// For now, we'll just track that the notification was processed
|
||||
|
||||
logger.info("Processed vendor notification", {
|
||||
buyerId,
|
||||
storeId,
|
||||
vendorId: vendor._id,
|
||||
source: source || "unknown"
|
||||
});
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error("Error processing vendor notification", { error: error.message, stack: error.stack });
|
||||
return res.status(500).json({ error: "Server error processing notification" });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user