Complete Guide to Integrating Fonepay Payment Gateway with Node.js

Introduction

Fonepay is one of Nepal’s leading digital payment platforms that enables seamless online transactions through QR codes and web-based payments. In this comprehensive guide, I’ll walk you through implementing Fonepay payment gateway integration using Node.js, covering both QR code-based payments and web redirect payments.

This implementation provides a robust foundation for e-commerce platforms looking to integrate local Nepalese payment solutions. We’ll explore the complete payment flow, from payment initialization to verification, with proper error handling and security measures.

Prerequisites

Before diving into the implementation, ensure you have:

  • Node.js (v14 or higher)
  • Fonepay merchant account with API credentials
  • Basic understanding of RESTful APIs
  • Familiarity with cryptographic operations (HMAC-SHA512)

Environment Configuration

First, let’s set up the required environment variables. Create a .env file in your project root:

# Fonepay Configuration
FONEPAY_PG_URL=https://clientapi.fonepay.com
FONEPAY_PG_MERCHANT_CODE=your_merchant_code
FONEPAY_PG_MERCHANT_SECRET=your_merchant_secret
FONEPAY_PG_CALLBACK_URL=http://localhost:3000/payment/provider/fonepay-pg/verify-web

# Dynamic QR Configuration
FONEPAY_DYNAMICQR_URL=https://merchantapi.fonepay.com/api/merchant/merchantDetailsForThirdParty
FONEPAY_USERNAME=your_username
FONEPAY_PASSWORD=your_password

# Redirect Configuration
CLIENT_URL=http://localhost:3000

Required Dependencies

Install the necessary packages:

npm install axios crypto xml2js dotenv

Core Service Implementation

1. Setting Up the Fonepay Service Class

const crypto = require('crypto');
const axios = require('axios');
const xml2js = require('xml2js');
require('dotenv').config();

class FonepayService {
  constructor() {
    this.merchantCode = process.env.FONEPAY_PG_MERCHANT_CODE;
    this.merchantSecret = process.env.FONEPAY_PG_MERCHANT_SECRET;
    this.baseUrl = process.env.FONEPAY_PG_URL;
    this.dynamicQrUrl = process.env.FONEPAY_DYNAMICQR_URL;
    this.username = process.env.FONEPAY_USERNAME;
    this.password = process.env.FONEPAY_PASSWORD;
  }
}

2. QR Code Payment Implementation

The QR code payment method is ideal for mobile applications where users can scan and pay directly from their mobile banking apps.

async generateQrCodePayment(orderId, amount, remarks1, remarks2) {
  try {
    const url = `${this.dynamicQrUrl}/thirdPartyDynamicQrDownload`;
    
    // Create unique payment reference number
    const prn = `BWS_${orderId}_${Date.now()}`;
    
    // Generate data validation hash
    const dataToHash = `${amount},${prn},${this.merchantCode},${remarks1},${remarks2}`;
    const dataValidation = crypto
      .createHmac('sha512', this.merchantSecret)
      .update(dataToHash)
      .digest('hex');

    const payload = {
      amount: amount,
      remarks1: remarks1,
      remarks2: remarks2,
      prn: prn,
      merchantCode: this.merchantCode,
      dataValidation: dataValidation,
      username: this.username,
      password: this.password,
    };

    const headers = {
      'Content-Type': 'application/json',
    };

    const response = await axios.post(url, payload, { headers });
    
    return {
      success: true,
      prn: prn,
      qrCodeData: response.data,
      amount: amount
    };
    
  } catch (error) {
    throw new Error(`QR Generation failed: ${error.message}`);
  }
}

Key Points about QR Code Payment:

  • PRN (Payment Reference Number): A unique identifier for each transaction
  • Data Validation: HMAC-SHA512 hash ensuring request integrity
  • Dynamic QR: Generates QR codes with embedded payment information
  • Real-time Generation: QR codes are generated for each transaction

3. Web Payment URL Generation

For web-based payments, users are redirected to Fonepay’s payment portal:

async generateWebPaymentUrl(orderId, amount, remarks1, remarks2) {
  try {
    const prn = `BWS_${orderId}_${Date.now()}`;
    
    // Format date as MM/DD/YYYY
    const today = new Date();
    const month = String(today.getMonth() + 1).padStart(2, '0');
    const day = String(today.getDate()).padStart(2, '0');
    const year = today.getFullYear();
    const date = `${month}/${day}/${year}`;

    const paymentData = {
      PID: this.merchantCode,      // Merchant ID
      MD: 'P',                     // Payment mode
      PRN: prn,                    // Payment reference number
      AMT: amount,                 // Amount
      CRN: 'NPR',                  // Currency
      DT: date,                    // Date
      R1: remarks1,                // Remarks 1
      R2: remarks2,                // Remarks 2
      RU: process.env.FONEPAY_PG_CALLBACK_URL // Return URL
    };

    // Generate digital verification hash
    const concatenatedString = `${paymentData.PID},${paymentData.MD},${paymentData.PRN},${paymentData.AMT},${paymentData.CRN},${paymentData.DT},${paymentData.R1},${paymentData.R2},${paymentData.RU}`;
    
    const DV = crypto
      .createHmac('sha512', this.merchantSecret)
      .update(concatenatedString, 'utf-8')
      .digest('hex');

    // Build payment URL
    const paymentUrl = `${this.baseUrl}/api/merchantRequest?PID=${paymentData.PID}&MD=${paymentData.MD}&PRN=${paymentData.PRN}&AMT=${paymentData.AMT}&CRN=${paymentData.CRN}&DT=${encodeURIComponent(paymentData.DT)}&R1=${encodeURIComponent(paymentData.R1)}&R2=${encodeURIComponent(paymentData.R2)}&DV=${DV}&RU=${encodeURIComponent(paymentData.RU)}`;

    return {
      success: true,
      prn: prn,
      paymentUrl: paymentUrl,
      amount: amount
    };
    
  } catch (error) {
    throw new Error(`Web payment URL generation failed: ${error.message}`);
  }
}

Web Payment Flow:

  1. User initiates payment
  2. System generates payment URL with encrypted parameters
  3. User is redirected to Fonepay portal
  4. After payment, Fonepay redirects back to callback URL
  5. System verifies and processes the response

4. QR Code Payment Verification

async verifyQrPayment(prn) {
  try {
    const url = `${this.dynamicQrUrl}/thirdPartyDynamicQrGetStatus`;
    
    const dataToHash = `${prn},${this.merchantCode}`;
    const dataValidation = crypto
      .createHmac('sha512', this.merchantSecret)
      .update(dataToHash)
      .digest('hex');

    const payload = {
      prn: prn,
      merchantCode: this.merchantCode,
      dataValidation: dataValidation,
      username: this.username,
      password: this.password,
    };

    const response = await axios.post(url, payload);
    
    return {
      success: response.data.paymentStatus === 'success',
      status: response.data.paymentStatus,
      response: response.data
    };
    
  } catch (error) {
    console.error('QR verification error:', error);
    return {
      success: false,
      error: error.message
    };
  }
}

5. Web Payment Verification

This is the most complex part involving XML parsing and response validation:

async verifyWebPayment(prn, uid, amount, pid, bankCode) {
  try {
    const PID = pid || this.merchantCode;
    const BID = bankCode || '';

    // Generate verification hash
    const dvString = `${PID},${amount},${prn},${BID},${uid}`;
    const DV = crypto
      .createHmac('sha512', this.merchantSecret)
      .update(dvString, 'utf-8')
      .digest('hex');

    const requestData = {
      PRN: prn,
      PID: PID,
      BID: BID,
      AMT: amount,
      UID: uid,
      DV: DV,
    };

    // Build verification URL
    const queryString = new URLSearchParams(requestData).toString();
    const verificationUrl = `${this.baseUrl}/api/merchantRequest/verificationMerchant?${queryString}`;

    const response = await axios.get(verificationUrl, {
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'PaymentGateway/1.0',
      },
    });

    // Parse XML response
    const jsonResponse = await this.parseXmlToJson(response.data);
    
    if (jsonResponse && jsonResponse.response) {
      return {
        amount: parseFloat(jsonResponse.response.amount) || 0,
        bankCode: jsonResponse.response.bankCode || '',
        initiator: jsonResponse.response.initiator || '',
        message: jsonResponse.response.message || '',
        response_code: jsonResponse.response.response_code || '',
        statusCode: parseInt(jsonResponse.response.statusCode) || 0,
        success: jsonResponse.response.success === 'true',
        txnAmount: parseFloat(jsonResponse.response.txnAmount) || 0,
        uniqueId: jsonResponse.response.uniqueId || '',
      };
    }
    
    return jsonResponse;
    
  } catch (error) {
    console.error('Web payment verification error:', error);
    return {
      success: false,
      message: 'Payment verification failed',
      error: error.message,
    };
  }
}

// XML to JSON parser helper
async parseXmlToJson(xmlData) {
  try {
    const parser = new xml2js.Parser({
      explicitArray: false,
      ignoreAttrs: true,
      trim: true,
    });
    return await parser.parseStringPromise(xmlData);
  } catch (error) {
    throw new Error('Failed to parse XML response');
  }
}

6. Response Validation for Web Payments

validateWebPaymentResponse(params) {
  const requiredParams = [
    'PRN', 'PID', 'PS', 'RC', 'UID', 'BC', 'INI', 'P_AMT', 'R_AMT', 'DV'
  ];
  
  // Check for missing parameters
  for (const param of requiredParams) {
    if (!params[param]) {
      throw new Error(`Missing required parameter: ${param}`);
    }
  }

  const { PRN, PID, PS, RC, UID, BC, INI, P_AMT, R_AMT, DV } = params;
  
  // Generate expected hash
  const responseString = `${PRN},${PID},${PS},${RC},${UID},${BC},${INI},${P_AMT},${R_AMT}`;
  const expectedHash = crypto
    .createHmac('sha512', this.merchantSecret)
    .update(responseString, 'utf8')
    .digest('hex')
    .toUpperCase();

  // Validate hash
  if (expectedHash !== DV.toUpperCase()) {
    throw new Error('Invalid response signature');
  }

  return true;
}

API Controller Implementation

Now let’s create the REST API endpoints

const express = require('express');
const router = express.Router();
const FonepayService = require('./fonepay.service');

const fonepayService = new FonepayService();

// Generate QR code payment
router.post('/payment/qr/:orderId', async (req, res) => {
  try {
    const { orderId } = req.params;
    const { amount, remarks1, remarks2 } = req.body;

    const result = await fonepayService.generateQrCodePayment(
      orderId, 
      amount, 
      remarks1, 
      remarks2
    );

    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: error.message
    });
  }
});

// Generate web payment URL
router.post('/payment/web/:orderId', async (req, res) => {
  try {
    const { orderId } = req.params;
    const { amount, remarks1, remarks2 } = req.body;

    const result = await fonepayService.generateWebPaymentUrl(
      orderId, 
      amount, 
      remarks1, 
      remarks2
    );

    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: error.message
    });
  }
});

// Verify QR payment
router.get('/payment/verify-qr/:prn', async (req, res) => {
  try {
    const { prn } = req.params;
    const result = await fonepayService.verifyQrPayment(prn);

    res.json(result);
  } catch (error) {
    res.status(500).json({
      success: false,
      message: error.message
    });
  }
});

// Web payment callback (redirect endpoint)
router.get('/payment/verify-web', async (req, res) => {
  try {
    // Validate response signature
    fonepayService.validateWebPaymentResponse(req.query);

    // Verify payment with Fonepay
    const verificationResult = await fonepayService.verifyWebPayment(
      req.query.PRN,
      req.query.UID,
      req.query.P_AMT,
      req.query.PID,
      req.query.BC
    );

    if (verificationResult.success && verificationResult.response_code === "successful") {
      // Payment successful - redirect to success page
      res.redirect(`${process.env.CLIENT_URL}/payment-success?orderId=${req.query.PRN}`);
    } else {
      // Payment failed - redirect to failure page
      res.redirect(`${process.env.CLIENT_URL}/payment-failed?orderId=${req.query.PRN}`);
    }
  } catch (error) {
    console.error('Payment verification error:', error);
    res.redirect(`${process.env.CLIENT_URL}/payment-error`);
  }
});

module.exports = router;

Security Best Practices

1. Hash Validation

Always validate the hash signature in responses to ensure data integrity:

function validateHash(data, receivedHash, secret) {
  const expectedHash = crypto
    .createHmac('sha512', secret)
    .update(data, 'utf8')
    .digest('hex');
  
  return expectedHash.toUpperCase() === receivedHash.toUpperCase();
}

2. Environment Variables

Never hardcode sensitive credentials. Use environment variables:

// ❌ Bad
const merchantSecret = "your_secret_here";

// ✅ Good
const merchantSecret = process.env.FONEPAY_PG_MERCHANT_SECRET;

3. Input Validation

Always validate and sanitize input parameters:

function validatePaymentAmount(amount) {
  if (!amount || amount <= 0) {
    throw new Error('Invalid payment amount');
  }
  if (amount > 1000000) {
    throw new Error('Amount exceeds maximum limit');
  }
  return true;
}

Error Handling Strategies

Implement comprehensive error handling:

class PaymentError extends Error {
  constructor(message, code, details) {
    super(message);
    this.name = 'PaymentError';
    this.code = code;
    this.details = details;
  }
}

// Usage
try {
  const result = await fonepayService.generateQrPayment(orderId, amount);
} catch (error) {
  if (error instanceof PaymentError) {
    // Handle payment-specific errors
    console.log(`Payment Error [${error.code}]: ${error.message}`);
  } else {
    // Handle general errors
    console.log(`General Error: ${error.message}`);
  }
}

Testing the Implementation

1. QR Code Payment Test

// Test QR code generation
const testQrPayment = async () => {
  try {
    const result = await fonepayService.generateQrCodePayment(
      'TEST_ORDER_001',
      100,
      'Test Payment',
      'Order #TEST_ORDER_001'
    );
    console.log('QR Payment Result:', result);
  } catch (error) {
    console.error('Test failed:', error.message);
  }
};

2. Web Payment Test

// Test web payment URL generation
const testWebPayment = async () => {
  try {
    const result = await fonepayService.generateWebPaymentUrl(
      'TEST_ORDER_002',
      250,
      'Web Test Payment',
      'Order #TEST_ORDER_002'
    );
    console.log('Web Payment URL:', result.paymentUrl);
  } catch (error) {
    console.error('Test failed:', error.message);
  }
};

Conclusion

This comprehensive Fonepay integration provides a robust foundation for processing payments in Nepal. The implementation covers both QR code and web-based payment methods, with proper security measures and error handling.

Key takeaways:

  • Security First: Always validate signatures and use HTTPS
  • Error Handling: Implement comprehensive error catching and logging
  • Testing: Thoroughly test both payment flows in sandbox environment
  • Monitoring: Log all payment activities for debugging and compliance
  • User Experience: Provide clear feedback for payment status

Remember to test thoroughly in Fonepay’s sandbox environment before going live, and ensure you comply with all regulatory requirements for payment processing in Nepal.

The complete implementation provides a scalable solution that can handle high transaction volumes while maintaining security and reliability standards expected in production environments.


This implementation has been tested and used in production environments. Always ensure you’re using the latest Fonepay API documentation and comply with their integration guidelines.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top