Case Study

FarmFlow

Architected a full-stack B2B marketplace to connect farm suppliers and buyers, featuring role-based dashboards, product catalog management, order lifecycle tracking, an AI demand-forecasting engine, and Razorpay payments with Socket.IO real-time updates.

AI-Powered B2B Marketplace

FarmFlow doesn't just connect suppliers and buyers — it actively forecasts demand using 30-day sales history, identifies low-stock risks, recommends reorder quantities, and provides personalized product recommendations to buyers based on their order history.

Core Feature Overview

🥕

Supplier Analytics

Real-time revenue dashboards built with MongoDB aggregation pipelines — 6-month trend charts, VIP/Regular/New buyer segmentation, and order volume metrics.

Complete Project Structure

root
farm-flow-prod/
backend/
src/
config/
db.js— Mongoose Atlas connection
passport.js— Google OAuth 2.0 strategy
redis.js— Redis client for caching
socket.js— Socket.IO initialization export
controllers/
analytics.controller.js— Revenue, segmentation, forecasting
auth.controller.js— JWT, Google OAuth, refresh tokens
cart.controller.js— Add, remove, update cart items
notification.controller.js— Mark-read, get all notifications
order.controller.js— Order lifecycle (place → deliver)
payment.controller.js— Razorpay create + verify endpoints
product.controller.js— CRUD + image upload via Cloudinary
user.controller.js— Profile, RBAC, account management
middleware/
auth.middleware.js— Bearer JWT verify + attach user
rbac.middleware.js— Role-based route guard (supplier/buyer/admin)
rateLimiter.middleware.js— express-rate-limit per endpoint
upload.middleware.js— Multer + Cloudinary storage engine
validate.middleware.js— Joi validation pipe
error.middleware.js— Centralized error handler
models/
User.model.js— Roles: supplier, buyer, admin with business info
Product.model.js— Catalog with stock, pricing, category
Order.model.js— Order with items, pricing, status machine
Payment.model.js— Razorpay transaction records
Cart.model.js— Per-user cart with item array
Category.model.js— Product categories
Notification.model.js— User notification feed
Discount.model.js— Discount rules
Wishlist.model.js— Buyer product wishlists
services/
ai.service.js— Demand forecasting + smart recommendations
analytics.service.js— Aggregation-based revenue analytics
auth.service.js— JWT pair generation and rotation
payment.service.js— Razorpay order + signature verification
email.service.js— Nodemailer transports
pdf.service.js— Invoice PDF generation
socket/
index.js— JWT-auth middleware + room join logic
handlers/
notification.handler.js— Real-time push to user rooms
utils/— ApiError, ApiResponse, logger, helpers
server.js
frontend/
src/
features/— Redux Toolkit slices (auth, cart, orders)
store/— Redux store config with RTK Query
docker-compose.yml— Local dev orchestrator

Order Lifecycle Flow

1

Buyer Browses Catalog

Buyers browse the marketplace product catalog filtered by category, price range, and supplier. Products have stock levels with `lowStockThreshold` for supplier alerting.
2

Cart & Checkout

Items are added to a per-user `Cart` model. On checkout, an `Order` document is created with status `pending`, containing full pricing breakdown (subtotal, taxes, total in paise for Razorpay).
3

Razorpay Order Creation

The payment controller calls `paymentService.createRazorpayOrder(order)`. A fresh `Razorpay` instance is initialized per request (to handle Render environment variable injection issues). The returned `razorpayOrderId` is stored on the order.
4

HMAC Signature Verification

After the buyer completes checkout, the frontend sends `razorpayOrderId`, `razorpayPaymentId`, and `razorpaySignature`. The backend verifies by computing `HMAC-SHA256(razorpayOrderId|razorpayPaymentId)` and comparing the digest.
5

Payment Record & Order Update

On success, a `Payment` document is created with gateway metadata (method, bank, VPA, card network). The order status advances to `confirmed` and inventory stock counts decrement.
6

Real-Time Notification Push

A Socket.IO event fires to the supplier's personal room (`supplier:{id}`) notifying them of a new order. The buyer's room receives an order-confirmed event simultaneously.

Socket.IO Authentication & Room System

javascript
// backend/src/socket/index.js
import { Server } from 'socket.io';
import jwt from 'jsonwebtoken';
import User from '../models/User.model.js';

export const initializeSocket = (server) => {
  const io = new Server(server, {
    cors: { origin: process.env.CLIENT_URL, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], credentials: true },
  });

  // JWT Authentication Middleware on Socket handshake
  io.use(async (socket, next) => {
    const token = socket.handshake.auth?.token ||
                  socket.handshake.headers?.authorization?.split(' ')[1] ||
                  socket.handshake.query?.token;

    if (!token) return next(new Error('Authentication token required'));

    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    const user = await User.findById(decoded.sub).select('-password');
    if (!user || !user.isActive) return next(new Error('User not found or deactivated'));

    socket.user = user;
    next();
  });

  io.on('connection', (socket) => {
    socket.on('join:room', (room) => {
      // Security: only allow joining own personal rooms or the global marketplace
      const isAllowed =
        room === 'marketplace' ||
        room === `supplier:${socket.user._id}` ||
        room === `buyer:${socket.user._id}`;

      if (isAllowed) socket.join(room);
    });

    socket.on('disconnect', () => {
      console.log(`Socket disconnected: ${socket.id}`);
    });
  });

  return io;
};

Razorpay Payment Service

javascript
// backend/src/services/payment.service.js
import Razorpay from 'razorpay';
import crypto from 'crypto';

// Strip invisible chars from Render environment variables
const normalizeEnvValue = (value) =>
  typeof value === 'string' ? value.replace(/['"\r\n]/g, '').trim() : '';

class PaymentService {
  async createRazorpayOrder(order) {
    const keyId     = normalizeEnvValue(process.env.RAZORPAY_KEY_ID);
    const keySecret = normalizeEnvValue(process.env.RAZORPAY_KEY_SECRET);

    // Fresh instance per call — prevents Render cold-start env issues
    const razorpayInstance = new Razorpay({ key_id: keyId, key_secret: keySecret });

    const razorpayOrder = await razorpayInstance.orders.create({
      amount:   Math.round(order.pricing.total * 100), // in paise
      currency: 'INR',
      receipt:  order.orderNumber,
      notes:    { orderId: order._id.toString(), orderNumber: order.orderNumber },
    });

    order.payment.razorpayOrderId = razorpayOrder.id;
    await order.save();
    return { razorpayOrderId: razorpayOrder.id, amount: razorpayOrder.amount, keyId };
  }

  verifyPaymentSignature(razorpayOrderId, razorpayPaymentId, signature) {
    const body = `${razorpayOrderId}|${razorpayPaymentId}`;
    const keySecret = normalizeEnvValue(process.env.RAZORPAY_KEY_SECRET);

    const expectedSignature = crypto
      .createHmac('sha256', Buffer.from(keySecret, 'utf8'))
      .update(Buffer.from(body, 'utf8'))
      .digest('hex');

    return expectedSignature === signature;
  }
}

AI Demand Forecasting Engine

The AI service uses real sales history to compute reorder recommendations. No external ML library — a pure Node.js heuristic approach:

javascript
// backend/src/services/ai.service.js
class AIService {
  async predictDemand(supplierId) {
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

    // Aggregate delivered orders for the last 30 days per product
    const salesHistory = await Order.aggregate([
      { $match: { supplier: new mongoose.Types.ObjectId(supplierId), status: 'delivered', createdAt: { $gte: thirtyDaysAgo } } },
      { $unwind: '$items' },
      { $group: {
          _id: '$items.product',
          totalQuantity: { $sum: '$items.quantity' },
          orderCount: { $sum: 1 },
      }},
    ]);

    return Promise.all(salesHistory.map(async (item) => {
      const product = await Product.findById(item._id).select('name unit stock');
      const weeklyAverage      = item.totalQuantity / 4;
      const predictedNextWeek  = weeklyAverage * 1.15; // 15% safety buffer
      const daysOfStockLeft    = weeklyAverage > 0
        ? (product.stock.quantity / weeklyAverage) * 7
        : 999;

      return {
        productId: product._id,
        name: product.name,
        currentStock: product.stock.quantity,
        averageWeeklySales: Math.round(weeklyAverage * 10) / 10,
        predictedNextWeekDemand: Math.round(predictedNextWeek * 10) / 10,
        daysOfStockRemaining: Math.round(daysOfStockLeft),
        recommendation: daysOfStockLeft < 7 ? 'REORDER_NOW' : daysOfStockLeft < 14 ? 'MONITOR' : 'OK',
      };
    }));
  }

  async getSmartRecommendations(buyerId) {
    // Find buyer's past orders to extract purchased categories
    const pastOrders = await Order.find({ buyer: buyerId }).sort('-createdAt').limit(5).lean();
    const orderedProductIds = new Set(pastOrders.flatMap(o => o.items.map(i => i.product.toString())));

    const products = await Product.find({ _id: { $in: [...orderedProductIds] } });
    const orderedCategoryIds = new Set(products.map(p => p.category.toString()));

    // Recommend from same categories, excluding already purchased
    return Product.find({
      isActive: true,
      'stock.quantity': { $gt: 0 },
      category: { $in: [...orderedCategoryIds] },
      _id: { $nin: [...orderedProductIds] },
    }).sort('-salesCount').limit(6).populate('category', 'name');
  }
}

Customer Segmentation Analytics

javascript
async analyzeCustomerSpending(supplierId) {
  const orders = await Order.aggregate([
    { $match: { supplier: new mongoose.Types.ObjectId(supplierId), status: 'delivered' } },
    { $group: {
        _id: '$buyer',
        totalSpent: { $sum: '$pricing.total' },
        orderCount: { $sum: 1 },
        averageOrderValue: { $avg: '$pricing.total' },
    }},
    { $sort: { totalSpent: -1 } },
  ]);

  return Promise.all(orders.map(async (ord) => {
    const buyer = await User.findById(ord._id).select('firstName lastName email businessInfo');
    return {
      name: `${buyer.firstName} ${buyer.lastName}`,
      totalSpent: ord.totalSpent,
      orderCount: ord.orderCount,
      // Segment buyers: VIP (>50k), Regular (>15k), New/Low Value
      segment: ord.totalSpent > 50000 ? 'VIP' : ord.totalSpent > 15000 ? 'Regular' : 'New/Low value',
    };
  }));
}

Security Layers

All API routes are protected with Helmet for HTTP headers, MongoDB Sanitize against NoSQL injection, HPP protection against parameter pollution, and express-rate-limit on authentication and payment endpoints.