/** * Value Chain Controller * Handles all marketplace operations for oilseed by-products */ import Listing from "../models/listing.model.js"; import Offer from "../models/offer.model.js"; import Processor from "../models/processor.model.js"; import TransformRequest from "../models/transformRequest.model.js"; import PriceHistory from "../models/priceHistory.model.js"; /** * Create a new listing * POST /valuechain/listings */ export const createListing = async (req, res) => { try { const { productType, productName, description, quantityKg, grade, harvestDate, expiryDate, location, reservePrice, priceUnit, photos, certifications, tags, metadata, } = req.body; // Validate required fields if (!productType || !quantityKg || !harvestDate || !location || !reservePrice) { return res.status(400).json({ success: false, error: "Missing required fields: productType, quantityKg, harvestDate, location, reservePrice", }); } // Validate location format if (!location.coordinates || location.coordinates.length !== 2) { return res.status(400).json({ success: false, error: "Location must include valid coordinates [longitude, latitude]", }); } const listing = new Listing({ sellerId: req.user._id, productType, productName: productName || productType.replace(/_/g, " ").toUpperCase(), description, quantityKg, availableQuantityKg: quantityKg, grade: grade || "standard", harvestDate: new Date(harvestDate), expiryDate: expiryDate ? new Date(expiryDate) : null, location: { type: "Point", coordinates: location.coordinates, address: location.address, district: location.district, state: location.state, pincode: location.pincode, }, reservePrice, currentPrice: reservePrice, priceUnit: priceUnit || "per_kg", photos: photos || [], certifications: certifications || [], tags: tags || [], metadata: metadata || {}, status: "active", }); await listing.save(); // Emit socket event for real-time updates const io = req.app.get("socketio"); if (io && typeof io.to === 'function') { io.to("valuechain").emit("new_listing", { listingId: listing._id, productType: listing.productType, location: listing.location, price: listing.reservePrice, }); } res.status(201).json({ success: true, data: listing, message: "Listing created successfully", }); } catch (error) { console.error("Error creating listing:", error); res.status(500).json({ success: false, error: "Failed to create listing", details: error.message, }); } }; /** * Get listings with search and filtering * GET /valuechain/listings */ export const getListings = async (req, res) => { try { const { productType, lat, lng, radius = 50, // km minPrice, maxPrice, grade, status = "active", sort = "-createdAt", page = 1, limit = 20, } = req.query; const query = { status }; // Product type filter if (productType) { query.productType = productType; } // Geospatial filter if (lat && lng) { const radiusInMeters = parseFloat(radius) * 1000; query.location = { $near: { $geometry: { type: "Point", coordinates: [parseFloat(lng), parseFloat(lat)], }, $maxDistance: radiusInMeters, }, }; } // Price range filter if (minPrice || maxPrice) { query.reservePrice = {}; if (minPrice) query.reservePrice.$gte = parseFloat(minPrice); if (maxPrice) query.reservePrice.$lte = parseFloat(maxPrice); } // Grade filter if (grade) { query.grade = grade; } const skip = (parseInt(page) - 1) * parseInt(limit); const [listings, total] = await Promise.all([ Listing.find(query) .populate("sellerId", "name email phone") .sort(sort) .skip(skip) .limit(parseInt(limit)) .lean(), Listing.countDocuments(query), ]); res.json({ success: true, data: listings, pagination: { total, page: parseInt(page), limit: parseInt(limit), pages: Math.ceil(total / parseInt(limit)), }, }); } catch (error) { console.error("Error fetching listings:", error); res.status(500).json({ success: false, error: "Failed to fetch listings", details: error.message, }); } }; /** * Get a single listing by ID * GET /valuechain/listings/:id */ export const getListingById = async (req, res) => { try { const { id } = req.params; const listing = await Listing.findById(id) .populate("sellerId", "name email phone") .lean(); if (!listing) { return res.status(404).json({ success: false, error: "Listing not found", }); } // Increment view count await Listing.findByIdAndUpdate(id, { $inc: { viewCount: 1 } }); res.json({ success: true, data: listing, }); } catch (error) { console.error("Error fetching listing:", error); res.status(500).json({ success: false, error: "Failed to fetch listing", details: error.message, }); } }; /** * Update a listing * PUT /valuechain/listings/:id */ export const updateListing = async (req, res) => { try { const { id } = req.params; const updates = req.body; const listing = await Listing.findById(id); if (!listing) { return res.status(404).json({ success: false, error: "Listing not found", }); } // Check ownership if (listing.sellerId.toString() !== req.user._id.toString()) { return res.status(403).json({ success: false, error: "Not authorized to update this listing", }); } // Prevent updating certain fields delete updates.sellerId; delete updates.offerCount; delete updates.viewCount; const updatedListing = await Listing.findByIdAndUpdate( id, { $set: updates }, { new: true, runValidators: true } ); res.json({ success: true, data: updatedListing, message: "Listing updated successfully", }); } catch (error) { console.error("Error updating listing:", error); res.status(500).json({ success: false, error: "Failed to update listing", details: error.message, }); } }; /** * Create an offer on a listing * POST /valuechain/offer */ export const createOffer = async (req, res) => { try { const { listingId, offeredPrice, quantityKg, message, deliveryTerms, paymentTerms, expiresInHours = 48, } = req.body; // Validate required fields if (!listingId || !offeredPrice || !quantityKg) { return res.status(400).json({ success: false, error: "Missing required fields: listingId, offeredPrice, quantityKg", }); } // Get the listing const listing = await Listing.findById(listingId); if (!listing) { return res.status(404).json({ success: false, error: "Listing not found", }); } if (listing.status !== "active") { return res.status(400).json({ success: false, error: "Listing is not available for offers", }); } if (quantityKg > listing.availableQuantityKg) { return res.status(400).json({ success: false, error: `Requested quantity exceeds available quantity (${listing.availableQuantityKg} kg)`, }); } // Can't make offer on own listing if (listing.sellerId.toString() === req.user._id.toString()) { return res.status(400).json({ success: false, error: "Cannot make offer on your own listing", }); } const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000); const offer = new Offer({ listingId, buyerId: req.user._id, sellerId: listing.sellerId, offeredPrice, priceUnit: listing.priceUnit, quantityKg, message, deliveryTerms, paymentTerms: paymentTerms || { method: "escrow" }, expiresAt, }); await offer.save(); // Update listing offer count await Listing.findByIdAndUpdate(listingId, { $inc: { offerCount: 1 } }); // Emit socket event const io = req.app.get("socketio"); if (io && typeof io.to === 'function') { io.to("valuechain").emit("new_offer", { offerId: offer._id, listingId, sellerId: listing.sellerId, }); } res.status(201).json({ success: true, data: offer, message: "Offer submitted successfully", }); } catch (error) { console.error("Error creating offer:", error); res.status(500).json({ success: false, error: "Failed to create offer", details: error.message, }); } }; /** * Get offers for a user (as buyer or seller) * GET /valuechain/offers */ export const getOffers = async (req, res) => { try { const { role = "buyer", status, page = 1, limit = 20 } = req.query; const query = {}; if (role === "buyer") { query.buyerId = req.user._id; } else { query.sellerId = req.user._id; } if (status) { query.status = status; } const skip = (parseInt(page) - 1) * parseInt(limit); const [offers, total] = await Promise.all([ Offer.find(query) .populate("listingId", "productType productName reservePrice photos") .populate("buyerId", "name email") .populate("sellerId", "name email") .sort("-createdAt") .skip(skip) .limit(parseInt(limit)) .lean(), Offer.countDocuments(query), ]); res.json({ success: true, data: offers, pagination: { total, page: parseInt(page), limit: parseInt(limit), pages: Math.ceil(total / parseInt(limit)), }, }); } catch (error) { console.error("Error fetching offers:", error); res.status(500).json({ success: false, error: "Failed to fetch offers", details: error.message, }); } }; /** * Respond to an offer (accept/reject/counter) * PUT /valuechain/offer/:id/respond */ export const respondToOffer = async (req, res) => { try { const { id } = req.params; const { action, counterPrice, counterMessage } = req.body; const offer = await Offer.findById(id); if (!offer) { return res.status(404).json({ success: false, error: "Offer not found", }); } // Check if user is the seller if (offer.sellerId.toString() !== req.user._id.toString()) { return res.status(403).json({ success: false, error: "Not authorized to respond to this offer", }); } if (offer.status !== "pending") { return res.status(400).json({ success: false, error: "Can only respond to pending offers", }); } switch (action) { case "accept": offer.status = "accepted"; offer.acceptedAt = new Date(); // Update listing available quantity await Listing.findByIdAndUpdate(offer.listingId, { $inc: { availableQuantityKg: -offer.quantityKg }, }); break; case "reject": offer.status = "rejected"; break; case "counter": if (!counterPrice) { return res.status(400).json({ success: false, error: "Counter price is required for counter offers", }); } offer.status = "countered"; offer.counterOffer = { price: counterPrice, message: counterMessage, createdAt: new Date(), }; break; default: return res.status(400).json({ success: false, error: "Invalid action. Use: accept, reject, or counter", }); } await offer.save(); // Emit socket event const io = req.app.get("socketio"); if (io && typeof io.to === 'function') { io.to("valuechain").emit("offer_response", { offerId: offer._id, buyerId: offer.buyerId, action, }); } res.json({ success: true, data: offer, message: `Offer ${action}ed successfully`, }); } catch (error) { console.error("Error responding to offer:", error); res.status(500).json({ success: false, error: "Failed to respond to offer", details: error.message, }); } }; /** * Create a transform request (processor buying raw materials) * POST /valuechain/transformRequest */ export const createTransformRequest = async (req, res) => { try { const { listingId, sellerId, requestType, rawMaterial, expectedOutput, processingFee, timeline, terms, } = req.body; // Find processor profile for current user const processor = await Processor.findOne({ userId: req.user._id }); if (!processor) { return res.status(400).json({ success: false, error: "Processor profile not found. Please create a processor profile first.", }); } if (!rawMaterial || !rawMaterial.productType || !rawMaterial.quantityKg) { return res.status(400).json({ success: false, error: "Raw material details are required", }); } const transformRequest = new TransformRequest({ processorId: processor._id, listingId, sellerId, requestType: requestType || "spot_purchase", rawMaterial, expectedOutput: expectedOutput || [], processingFee, timeline, terms, }); await transformRequest.save(); res.status(201).json({ success: true, data: transformRequest, message: "Transform request created successfully", }); } catch (error) { console.error("Error creating transform request:", error); res.status(500).json({ success: false, error: "Failed to create transform request", details: error.message, }); } }; /** * Get market summary with aggregated supply-demand data * GET /valuechain/market-summary */ export const getMarketSummary = async (req, res) => { try { const { productType, state, days = 30 } = req.query; const dateFilter = new Date(); dateFilter.setDate(dateFilter.getDate() - parseInt(days)); // Aggregate supply data const supplyPipeline = [ { $match: { status: "active", createdAt: { $gte: dateFilter }, ...(productType && { productType }), ...(state && { "location.state": state }), }, }, { $group: { _id: { productType: "$productType", state: "$location.state", }, totalQuantity: { $sum: "$availableQuantityKg" }, averagePrice: { $avg: "$reservePrice" }, listingCount: { $sum: 1 }, minPrice: { $min: "$reservePrice" }, maxPrice: { $max: "$reservePrice" }, }, }, { $sort: { totalQuantity: -1 }, }, ]; // Aggregate demand data (from offers) const demandPipeline = [ { $match: { status: { $in: ["pending", "accepted"] }, createdAt: { $gte: dateFilter }, }, }, { $lookup: { from: "listings", localField: "listingId", foreignField: "_id", as: "listing", }, }, { $unwind: "$listing" }, { $match: { ...(productType && { "listing.productType": productType }), ...(state && { "listing.location.state": state }), }, }, { $group: { _id: { productType: "$listing.productType", state: "$listing.location.state", }, totalDemand: { $sum: "$quantityKg" }, averageOfferPrice: { $avg: "$offeredPrice" }, offerCount: { $sum: 1 }, }, }, ]; // Get price trends const priceTrendPipeline = [ { $match: { date: { $gte: dateFilter }, ...(productType && { commodity: productType }), }, }, { $group: { _id: { date: { $dateToString: { format: "%Y-%m-%d", date: "$date" } }, commodity: "$commodity", }, averagePrice: { $avg: "$prices.modal" }, volume: { $sum: "$volume.arrivals" }, }, }, { $sort: { "_id.date": 1 }, }, ]; const [supplyData, demandData, priceTrends] = await Promise.all([ Listing.aggregate(supplyPipeline), Offer.aggregate(demandPipeline), PriceHistory.aggregate(priceTrendPipeline), ]); // Calculate heatmap data (state-wise supply-demand ratio) const heatmapData = {}; supplyData.forEach((item) => { const key = `${item._id.state}_${item._id.productType}`; if (!heatmapData[key]) { heatmapData[key] = { state: item._id.state, productType: item._id.productType, supply: 0, demand: 0, }; } heatmapData[key].supply = item.totalQuantity; heatmapData[key].averagePrice = item.averagePrice; }); demandData.forEach((item) => { const key = `${item._id.state}_${item._id.productType}`; if (heatmapData[key]) { heatmapData[key].demand = item.totalDemand; } }); // Calculate price indices const priceIndices = supplyData.reduce((acc, item) => { if (!acc[item._id.productType]) { acc[item._id.productType] = { average: 0, min: Infinity, max: 0, count: 0, }; } acc[item._id.productType].average += item.averagePrice; acc[item._id.productType].min = Math.min(acc[item._id.productType].min, item.minPrice); acc[item._id.productType].max = Math.max(acc[item._id.productType].max, item.maxPrice); acc[item._id.productType].count += 1; return acc; }, {}); Object.keys(priceIndices).forEach((key) => { priceIndices[key].average = priceIndices[key].average / priceIndices[key].count; }); res.json({ success: true, data: { supply: supplyData, demand: demandData, heatmap: Object.values(heatmapData), priceTrends, priceIndices, generatedAt: new Date(), periodDays: parseInt(days), }, }); } catch (error) { console.error("Error fetching market summary:", error); res.status(500).json({ success: false, error: "Failed to fetch market summary", details: error.message, }); } }; export default { createListing, getListings, getListingById, updateListing, createOffer, getOffers, respondToOffer, createTransformRequest, getMarketSummary, };