Skip to main content
The Crypto Shop Backend implements a multi-stage payment flow that integrates traditional e-commerce order management with TRON blockchain transactions.

Payment Flow Overview

Stage 1: Order Creation

The first stage creates an order record with pending status.

Order Creation Endpoint

// POST /api/orders
export const createOrder = async (req, res) => {
  const { products } = req.body;
  const userId = req.user.id;

  // 1. Prevent multiple pending orders per user
  const pendingOrder = await Order.findOne({ userId, status: 'pending' });
  if (pendingOrder) {
    return res.status(409).json({ 
      error: 'You have a pending order. Please complete or cancel it first.',
      code: 'PENDING_ORDER_EXISTS',
      pendingOrderId: pendingOrder._id
    });
  }

  // 2. Validate user wallet
  const user = await User.findById(userId);
  if (!user || !user.wallet.address) {
    return res.status(400).json({ error: 'User wallet not found' });
  }

  // 3. Calculate order total
  let subtotal = 0;
  const orderProducts = [];

  for (const item of products) {
    const product = await Product.findById(item.productId);
    if (!product) {
      return res.status(404).json({ error: `Product not found` });
    }
    if (product.stock < item.quantity) {
      return res.status(400).json({ error: `Insufficient stock` });
    }
    subtotal += product.price * item.quantity;
    orderProducts.push({
      productId: product._id,
      name: product.name,
      price: product.price,
      quantity: item.quantity,
      color: item.color || null
    });
  }

  const networkFee = -0.01;
  const total = subtotal + networkFee;

  // 4. Get merchant wallet address
  const admin = await User.findOne({ role: 'admin' });
  const merchantAddress = admin?.wallet?.address || 
    process.env.MERCHANT_WALLET_ADDRESS;

  // 5. Create order
  const order = new Order({
    userId,
    products: orderProducts,
    subtotal,
    networkFee,
    total,
    walletAddress: user.wallet.address,
    merchantAddress
  });
  await order.save();

  res.status(201).json({ success: true, order });
};
Reference: src/api/orders/createOrder.js:5-85
Important: Users can only have one pending order at a time. This prevents inventory reservation conflicts and simplifies payment tracking.

Order Pricing Calculation

const networkFee = -0.01;  // TRX network fee estimate
const total = subtotal + networkFee;
Reference: src/api/orders/createOrder.js:48-49
The network fee is a negative value (-0.01 TRX) representing the estimated TRON blockchain transaction cost. The actual fee is paid from the user’s wallet balance separately.

Stage 2: Payment Execution

Once an order is created, the user initiates payment by submitting the transaction to the blockchain.

Payment Endpoint

// POST /api/orders/:id/pay
export const payOrder = async (req, res) => {
  const { id } = req.params;
  const userId = req.user.id;

  // 1. Verify order ownership and status
  const order = await Order.findById(id);
  if (order.userId.toString() !== userId) {
    return res.status(403).json({ error: 'Unauthorized' });
  }
  if (order.status !== 'pending') {
    return res.status(400).json({ error: 'Order is not pending' });
  }

  // 2. Verify wallet and balance
  const user = await User.findById(userId);
  const balance = await getBalance(user.wallet.address);
  
  if (balance < Math.abs(order.total)) {
    return res.status(400).json({ 
      error: 'Insufficient balance',
      code: 'INSUFFICIENT_BALANCE',
      userBalance: balance,
      requiredAmount: Math.abs(order.total)
    });
  }

  // 3. Send TRX transaction
  try {
    const tx = await sendTRX(
      user.wallet.privateKey,
      order.merchantAddress,
      Math.abs(order.total)
    );

    const txHash = tx.txid || tx.txID || tx.transaction?.txID;

    // 4. Create transaction record
    const transaction = new Transaction({
      userId,
      orderId: order._id,
      type: 'purchase',
      amount: Math.abs(order.total),
      currency: 'TRX',
      network: 'TRC-20',
      transactionHash: txHash,
      fromAddress: order.walletAddress,
      toAddress: order.merchantAddress,
      status: 'pending',
      confirmations: 0
    });
    await transaction.save();

    // 5. Link transaction to order
    order.transactionHash = txHash;
    await order.save();

    res.json({
      success: true,
      message: 'Payment sent. Waiting for blockchain confirmation.',
      transaction: {
        hash: txHash,
        status: 'pending'
      }
    });
  } catch (txError) {
    // Mark order as failed if transaction fails
    order.status = 'failed';
    await order.save();
    throw txError;
  }
};
Reference: src/api/orders/payOrder.js:6-88

Balance Verification

const balance = await getBalance(user.wallet.address);

if (balance < Math.abs(order.total)) {
  return res.status(400).json({ 
    error: 'Insufficient balance',
    code: 'INSUFFICIENT_BALANCE',
    userBalance: balance,
    requiredAmount: Math.abs(order.total)
  });
}
Reference: src/api/orders/payOrder.js:30-39
The payment endpoint checks the user’s TRX balance before attempting the transaction to provide immediate feedback and prevent failed blockchain submissions.

Stage 3: Blockchain Confirmation

After payment submission, the transaction listener monitors the blockchain for confirmation.

Transaction Listener Service

// src/services/transactionListener.js
const CONFIRMATIONS_REQUIRED = 21;
const CHECK_INTERVAL = 15000; // 15 seconds

export const startTransactionListener = () => {
  syncPendingTransactions();
  setInterval(() => {
    syncPendingTransactions();
  }, CHECK_INTERVAL);
};
Reference: src/services/transactionListener.js:16-107

Transaction Synchronization

export const syncPendingTransactions = async () => {
  // 1. Find all pending transactions
  const pendingTransactions = await Transaction.find({
    status: 'pending',
    transactionHash: { $ne: null }
  });

  for (const transaction of pendingTransactions) {
    // 2. Check blockchain status
    const status = await getTransactionStatus(transaction.transactionHash);

    if (status && status.confirmed) {
      // 3. Update transaction
      transaction.status = 'confirmed';
      transaction.confirmations = CONFIRMATIONS_REQUIRED;
      transaction.updatedAt = Date.now();
      await transaction.save();

      // 4. Process order
      if (transaction.orderId) {
        const order = await Order.findById(transaction.orderId);
        
        if (transaction.type === 'purchase' && order.status === 'pending') {
          // 5. Complete order
          order.status = 'completed';
          order.updatedAt = Date.now();
          await order.save();

          // 6. Update inventory
          for (const item of order.products) {
            await Product.findByIdAndUpdate(
              item.productId,
              { $inc: { stock: -item.quantity } }
            );
          }

          // 7. Notify user
          emitTransactionConfirmed(
            transaction.userId.toString(),
            transaction.orderId.toString(),
            transaction.transactionHash
          );
        }
      }
    }
  }
};
Reference: src/services/transactionListener.js:36-100
Stock is only decremented after blockchain confirmation, not at order creation. This prevents inventory locking for unpaid orders.

Blockchain Status Check

export const getTransactionStatus = async (txHash) => {
  try {
    const tx = await tronWeb.trx.getTransactionInfo(txHash);
    
    if (!tx || !tx.blockNumber) {
      return null;
    }
    
    return {
      confirmed: true,
      confirmations: 21
    };
  } catch (error) {
    return null;
  }
};
Reference: src/services/transactionListener.js:19-34

Stage 4: Real-Time Notification

When confirmation occurs, the user receives an instant notification via WebSocket.

Socket Event Emission

export const emitTransactionConfirmed = (userId, orderId, txHash) => {
  const io = getIO();
  io.to(`user:${userId}`).emit('transaction:confirmed', {
    orderId,
    txHash,
    message: 'Your purchase has been confirmed. You can now place new orders.',
    timestamp: new Date()
  });
};
Reference: src/config/socket.js:37-45

Client-Side Connection

Clients must join their user room to receive notifications:
// Client-side example
socket.emit('join-user', userId);

socket.on('transaction:confirmed', (data) => {
  console.log('Order confirmed:', data.orderId);
  console.log('Transaction:', data.txHash);
});

Order Schema

The Order model tracks all payment-related information:
{
  orderId: String,        // Auto-generated: #TRX-1, #TRX-2, etc.
  userId: ObjectId,
  products: [{
    productId: ObjectId,
    name: String,
    price: Number,
    quantity: Number,
    color: String
  }],
  subtotal: Number,
  networkFee: Number,     // -0.01 TRX
  total: Number,
  status: String,         // pending, completed, failed, refunded, cancelled
  paymentMethod: String,  // TRC-20
  transactionHash: String,
  walletAddress: String,  // User's wallet
  merchantAddress: String // Admin's wallet
}
Reference: src/models/Order.js:3-74

Order ID Generation

orderSchema.pre('save', async function(next) {
  if (!this.isNew) return next();
  
  const lastOrder = await mongoose.model('Order')
    .findOne()
    .sort({ createdAt: -1 });
  const lastNumber = lastOrder ? 
    parseInt(lastOrder.orderId.replace('#TRX-', '')) : 0;
  this.orderId = `#TRX-${lastNumber + 1}`;
  next();
});
Reference: src/models/Order.js:76-87

Error Handling

Insufficient Balance

{
  "error": "Insufficient balance",
  "code": "INSUFFICIENT_BALANCE",
  "userBalance": 5.2,
  "requiredAmount": 10.5
}

Pending Order Exists

{
  "error": "You have a pending order. Please complete or cancel it first.",
  "code": "PENDING_ORDER_EXISTS",
  "pendingOrderId": "507f1f77bcf86cd799439011"
}

Transaction Failure

If the blockchain transaction fails, the order status is set to failed:
catch (txError) {
  order.status = 'failed';
  await order.save();
  throw txError;
}
Reference: src/api/orders/payOrder.js:80-84

Refund Flow

The transaction listener also handles refunds:
if (transaction.type === 'refund' && order.status !== 'refunded') {
  order.status = 'refunded';
  order.updatedAt = Date.now();
  await order.save();

  // Restore product stock
  for (const item of order.products) {
    await Product.findByIdAndUpdate(
      item.productId,
      { $inc: { stock: item.quantity } }  // Add back to inventory
    );
  }
}
Reference: src/services/transactionListener.js:79-91
Refunds restore product inventory by incrementing stock quantities. The refund transaction must also be confirmed on the blockchain before the order status changes.

Payment Timeline

StageDurationStatus
Order creationInstantpending
Payment submission3-5 secondspending
Blockchain broadcast5-10 secondspending
Block confirmation1-3 minutespending
21 confirmations1-3 minutesconfirmed
Order completionInstantcompleted
Total payment flow typically completes in 2-6 minutes on the TRON network. Nile testnet may be faster, while mainnet during high congestion could take longer.

Build docs developers (and LLMs) love