Build Payment Verification and Order Controllers
Controllers are server-side modules that handle HTTP requests, orchestrate business logic, and coordinate between different services (platform APIs, payment gateways, databases). In extensions, controllers manage the critical flow of order creation, payment verification, and checkout completion.
This page focuses on concepts and patterns for building payment gateway controllers. Adapt the examples to your specific platform and payment gateway requirements. The examples use generic names (PaymentGateway, Platform) that should be replaced with your actual implementations.
Controllers implement the backend logic for two main endpoints:
- Order Creation Controller: Handles the buy-now endpoint called by your injected script. This creates payment gateway orders and returns popup configuration.
- Payment Verification Controller: Handles the verify endpoint called by your payment handler. This verifies payments, completes checkout, and updates payment status.
The implementation details for these controllers are also referenced on the Script Injection and Verify Payment pages, which show how frontend code interacts with them.
The controller acts as the central orchestration layer that:
- Receives requests from frontend/routes
- Validates input data
- Calls platform APIs (like Fynd GraphQL/FDK)
- Integrates with payment gateway APIs
- Manages database operations
- Returns structured responses
Key Concepts
Controller Responsibilities
Controllers handle three main responsibilities:
- Request Handling: Receive and validate HTTP requests
- Business Logic: Orchestrate complex workflows (order creation, payment verification)
- Service Coordination: Coordinate between platform APIs, payment gateways, and databases
Platform Integration
Controllers integrate with platform systems through:
- GraphQL APIs: Query/mutate platform data (cart, checkout)
- FDK (Platform SDK): Access platform services (payment updates, coupon management)
- Database: Store orders, transactions, and metadata
How Controllers Work
HTTP Request → Route → Controller → [Platform API | Payment Gateway | Database] → Response
The controller sits between routes and services, orchestrating the flow without directly handling HTTP concerns.
Request Flow
- Route receives request and passes to controller
- Controller validates input and extracts context
- Controller calls services (platform APIs, payment gateway, database)
- Controller processes results and handles errors
- Controller returns response to route/client
Main Controller Functions
1. Order Creation Function
Creates payment gateway orders from platform cart data. Transform platform cart into payment gateway order format, create order with gateway, save to database. This function is called by your buy-now endpoint, which receives requests from the injected script when users click Buy Now. The detailed step-by-step implementation is covered in the Script Injection page.
- Extract context early (application/company IDs)
- Fetch platform data before processing
- Transform data to payment gateway format
- Save order for later reference
- Return configuration for frontend
// Example: Order creation function structure
exports.createOrder = asyncHandler(async (req, res) => {
try {
// Step 1: Extract context (application/company IDs)
const { applicationId, companyId } = extractContext(req);
// Step 2: Fetch cart data from platform
const cart = await getPlatformCartData(req, cartId);
// Step 3: Validate cart
if (!cart.is_valid || cart.items.length === 0) {
return res.status(400).json({
success: false,
message: 'Cart is empty'
});
}
// Step 4: Get payment gateway credentials
const credentials = await getPaymentGatewayCredentials(applicationId);
// Step 5: Transform cart to payment gateway format
const lineItems = transformCartToLineItems(cart);
const orderAmount = calculateOrderAmount(cart);
// Step 6: Create order with payment gateway
const gatewayOrder = await createGatewayOrder({
amount: orderAmount,
currency: cart.currency,
line_items: lineItems,
notes: { cart_id: cart.id }
}, credentials);
// Step 7: Save order to database
const order = await saveOrderToDatabase({
gateway_order_id: gatewayOrder.id,
amount: orderAmount,
cart_id: cart.id,
meta: { cart, gateway_order: gatewayOrder }
});
// Step 8: Return popup configuration
return res.json({
success: true,
popup_config: {
key: credentials.public_key,
amount: gatewayOrder.amount,
order_id: gatewayOrder.id
},
order_id: order.id
});
} catch (error) {
logger.error('Order creation failed:', error);
return res.status(500).json({
success: false,
message: error.message
});
}
});
2. Payment Verification Function
Verifies payment signatures and completes platform checkout. Verify payment authenticity, extract addresses, complete platform checkout, update payment status. This function is called by your verify endpoint, which receives requests from the payment handler after users complete payment. The complete verification flow and response structure are documented in the Verify Payment page.
- Verify signature first (security critical)
- Update transaction status immediately
- Fetch addresses from payment gateway
- Complete platform checkout with addresses
- Update payment status in platform
- Return comprehensive result
// Example: Payment verification function structure
exports.verifyPayment = asyncHandler(async (req, res) => {
try {
// Step 1: Extract payment data
const { payment_id, order_id, signature, internal_order_id } = req.body;
// Step 2: Find order in database
const order = await Order.findOne({ id: internal_order_id });
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found'
});
}
// Step 3: Get credentials for signature verification
// Use app_id from order meta (as stored during order creation)
const applicationId = order.meta?.app_id || order.app_id;
if (!applicationId) {
throw new Error(`Missing application_id in order for order ${internal_order_id}`);
}
const credentials = await getPaymentGatewayCredentials(applicationId);
// Step 4: Verify payment signature
// Signature format varies by payment gateway (e.g., "order_id|payment_id")
const isAuthentic = verifyPaymentSignature(
order_id,
payment_id,
signature,
credentials.secret_key
);
if (!isAuthentic) {
return res.status(400).json({
success: false,
message: 'Payment signature verification failed'
});
}
// Step 5: Update transaction status immediately after verification
await updateTransaction(internal_order_id, {
aggregator_payment_id: payment_id,
current_status: 'complete'
});
// Step 6: Fetch order details from payment gateway (for addresses)
// This is needed to extract shipping/billing addresses
const gatewayOrderDetails = await fetchGatewayOrderDetails(
order_id,
credentials
);
// Step 7: Extract addresses from payment gateway order details
// Address format varies by payment gateway
const addresses = extractAddresses(gatewayOrderDetails);
// Step 8: Complete platform checkout with addresses
// Get buyNow flag from order meta (saved during order creation)
const buyNow = order.meta?.buyNow || false;
const checkoutResult = await completePlatformCheckout(
order,
addresses,
req,
buyNow
);
// Step 9: Update payment status in platform via FDK
// This should be done AFTER successful checkoutCart
// Payment update uses FDK's payment.updatePaymentSession() method
if (checkoutResult.success && checkoutResult.merchant_transaction_id) {
// Skip payment update for COD (handled by platform automatically)
const paymentMethod = await getPaymentMethodFromGateway(payment_id, credentials);
if (paymentMethod !== 'cod') {
await updatePlatformPayment(
checkoutResult.merchant_transaction_id,
payment_id,
order_id,
credentials,
order
);
}
}
// Step 10: Return comprehensive verification result
return res.json({
success: true,
message: 'Payment verified successfully',
data: {
order_id: internal_order_id,
payment_id: payment_id,
status: 'verified',
gateway_order_details: gatewayOrderDetails ? {
id: gatewayOrderDetails.id,
status: gatewayOrderDetails.status,
has_customer_details: !!gatewayOrderDetails.customer_details
} : null,
platform_checkout: {
order_id: checkoutResult.order_id,
merchant_transaction_id: checkoutResult.merchant_transaction_id,
success: checkoutResult.success,
message: checkoutResult.message
},
payment_verification: {
success: true,
merchant_transaction_id: checkoutResult.merchant_transaction_id
}
}
});
} catch (error) {
logger.error('Payment verification failed:', error);
return res.status(500).json({
success: false,
message: error.message
});
}
});
3. Helper Functions
Controllers use helper functions for reusable operations. Extract tenant context from headers. Provide fallbacks for different header formats.
- Build GraphQL URL dynamically from request origin
- Forward authorization headers
- Handle GraphQL errors separately from data
- Return cart data structure
Extract Context
Extract application and company identifiers from request headers. This context is needed for all subsequent operations in the controller, including credential retrieval and order creation. The context extraction pattern is used in both Order Creation and Payment Verification functions.
// Example: Extract application/company IDs
function extractContext(req) {
// Try headers first
const appDataHeader = req.headers['x-application-data'];
const userDataHeader = req.headers['x-user-data'];
if (appDataHeader) {
const appData = JSON.parse(appDataHeader);
return {
applicationId: appData.id,
companyId: appData.company_id
};
}
// Fallback to user data header
if (userDataHeader) {
const userData = JSON.parse(userDataHeader);
return {
applicationId: userData.application_id,
companyId: userData.company_id
};
}
throw new Error('Missing application context in headers');
}
Get Platform Cart Data
Fetch cart data from the platform GraphQL API. This helper function is used in the Order Creation Function to retrieve cart details needed for payment gateway order creation. The cart data includes items, prices, breakup values, and coupon information.
// Example: Fetch cart from platform GraphQL API
async function getPlatformCartData(req, cartId = null) {
// Build GraphQL URL from request origin
const baseUrl = req.headers['origin'] ||
req.protocol + '://' + req.get('host');
const graphqlUrl = `${baseUrl}/api/service/application/graphql/`;
// Build GraphQL query
const query = `
query Cart($cartId: String) {
cart(id: $cartId) {
id
items {
quantity
product { name, uid }
price { base { effective } }
}
breakup_values {
raw {
total
subtotal
delivery_charge
}
}
}
}
`;
// Get authorization from request
const authorization = req.headers['authorization'] ||
req.headers['Authorization'];
// Call platform API
const response = await axios.post(graphqlUrl, {
query,
variables: { cartId }
}, {
headers: {
'authorization': authorization,
'content-type': 'application/json'
}
});
if (response.data.errors) {
throw new Error('GraphQL errors: ' + JSON.stringify(response.data.errors));
}
return response.data.data.cart;
}
Get Payment Gateway Credentials
- Store credentials encrypted in database
- Decrypt using extension secret
- Validate credential structure
- Return in format needed by payment gateway SDK
- Always retrieve from database (never hardcode)
// Example: Fetch and decrypt credentials
async function getPaymentGatewayCredentials(appId) {
// Find encrypted credentials in database
const secret = await Secret.findOne({ app_id: appId });
if (!secret) {
throw new Error(`No credentials found for app_id: ${appId}`);
}
// Decrypt credentials using extension encryption secret
const credentials = EncryptHelper.decrypt(
config.extension.encrypt_secret,
secret.secrets
);
// Validate structure (field names vary by payment gateway)
if (!credentials.key_id || !credentials.key_secret) {
throw new Error('Invalid credentials structure - missing key_id or key_secret');
}
// Return in format needed by payment gateway SDK
// Field names may vary: key_id/key_secret, api_key/api_secret, etc.
return {
key_id: credentials.key_id, // Public key (for frontend)
key_secret: credentials.key_secret // Secret key (for backend only)
};
}
Verify Payment Signature
- Signature format varies by payment gateway (e.g., "order_id|payment_id")
- Use HMAC SHA256 for signature generation (most common)
- Use constant-time comparison to prevent timing attacks
- Never log full signatures (log only first few characters for debugging)
- Always verify signature before processing payment
// Example: Verify payment signature
function verifyPaymentSignature(orderId, paymentId, signature, secretKey) {
// Build signature string (format depends on payment gateway)
// Common format: "order_id|payment_id" or "payment_id|order_id"
const signatureString = `${orderId}|${paymentId}`;
// Generate expected signature using HMAC SHA256
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(signatureString)
.digest('hex');
// Compare signatures (constant-time comparison prevents timing attacks)
// Note: Some gateways may use different comparison methods
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
Complete Platform Checkout
This helper function is used in the Payment Verification Function after addresses are extracted from the payment gateway. It maps addresses to platform format, uses GraphQL mutation for checkout, includes payment method configuration, forwards authorization headers, and uses the cart ID from order meta (saved during order creation). Returns platform order ID and transaction ID, which are used for payment update and redirection.
The checkoutCart mutation requires the cart ID that was saved in the order meta during order creation. Use order.meta.fynd_cart_id or order.meta.cart_id.
// Example: Complete platform checkout with addresses
async function completePlatformCheckout(order, addresses, req, buyNow = false) {
// Map payment gateway addresses to platform format
const shippingAddress = mapAddressToPlatform(
addresses.shipping_address || addresses.customer_details?.shipping_address
);
const billingAddress = mapAddressToPlatform(
addresses.billing_address || addresses.customer_details?.billing_address || shippingAddress
);
// Build GraphQL mutation
const mutation = `
mutation checkoutCart($buyNow: Boolean, $input: CartCheckoutDetailRequestInput) {
checkoutCart(buyNow: $buyNow, cartCheckoutDetailRequestInput: $input) {
success
order_id
message
data {
merchant_transaction_id
}
}
}
`;
// Build variables
// Note: checkoutCart uses the cart ID from order meta (saved during order creation)
const variables = {
buyNow: buyNow,
input: {
delivery_address: shippingAddress,
billing_address: billingAddress,
payment_methods: {
mode: 'payment_gateway',
payment: 'required'
}
}
};
// Call platform GraphQL API
const baseUrl = req.headers['origin'] || req.protocol + '://' + req.get('host');
const graphqlUrl = `${baseUrl}/api/service/application/graphql/`;
const response = await axios.post(graphqlUrl, {
query: mutation,
variables
}, {
headers: {
'authorization': req.headers['authorization'] || req.headers['Authorization'],
'cookie': req.headers['cookie'] || '',
'content-type': 'application/json',
'origin': req.headers['origin']
}
});
if (response.data.errors) {
throw new Error('Checkout failed: ' + JSON.stringify(response.data.errors));
}
const checkoutData = response.data.data.checkoutCart;
return {
success: checkoutData.success,
order_id: checkoutData.order_id,
merchant_transaction_id: checkoutData.data?.merchant_transaction_id,
message: checkoutData.message
};
}
Update Platform Payment via FDK
This helper function is called in the Payment Verification Function after successful checkoutCart (completed by Complete Platform Checkout). It updates the payment session using FDK's payment API, linking the payment gateway payment to the platform's payment session. The merchant_transaction_id from the checkout result is used here.
Important:
- This should be done AFTER successful
checkoutCart - Skip for COD payments (handled by platform automatically)
- Use the
merchant_transaction_idfrom checkoutCart response - Requires correct checksum secret for payment updates
// Example: Update payment via FDK
async function updatePlatformPayment(
merchantTransactionId,
paymentId,
gatewayOrderId,
credentials,
order
) {
// Get company_id and app_id from order
const companyId = order.company_id || order.meta?.company_id;
const applicationId = order.app_id || order.meta?.app_id;
if (!companyId || !applicationId) {
throw new Error('Missing company_id or app_id in order');
}
// Initialize FDK
const Fdkfactory = require('../fdk');
const fdkExtension = await Fdkfactory.fdkExtension();
const platformClient = await fdkExtension.getPlatformClient(companyId);
// Get checksum secret (from extension config or credentials)
const checksumSecret = config.extension.checksum_secret;
// Update payment session
// This links the payment gateway payment to the platform payment session
const paymentUpdateResponse = await platformClient
.application(applicationId)
.payment
.updatePaymentSession({
payment_session_id: merchantTransactionId,
body: {
payment_id: paymentId,
aggregator_order_id: gatewayOrderId,
aggregator_payment_id: paymentId,
status: 'success'
}
});
return {
success: true,
merchant_transaction_id: merchantTransactionId,
fynd_order_id: paymentUpdateResponse?.order_id
};
}
Extract Addresses from Payment Gateway
This helper function is used in the Payment Verification Function after fetching gateway order details. Payment gateways store customer addresses in their order details. Extract these addresses after payment verification for use in platform checkout. The addresses are then mapped to platform format before calling checkoutCart.
Address field names and structure vary by payment gateway. Common locations:
customer_details.shipping_addresscustomer_details.billing_addressnotes.shipping_address(some gateways)
// Example: Extract addresses from payment gateway order details
function extractAddresses(gatewayOrderDetails) {
if (!gatewayOrderDetails || !gatewayOrderDetails.customer_details) {
throw new Error('No customer details found in payment gateway order');
}
const customerDetails = gatewayOrderDetails.customer_details;
// Extract shipping address
const shippingAddress = customerDetails.shipping_address ||
customerDetails.address ||
{};
// Extract billing address (fallback to shipping if not provided)
const billingAddress = customerDetails.billing_address ||
customerDetails.shipping_address ||
shippingAddress;
return {
shipping_address: shippingAddress,
billing_address: billingAddress
};
}
Map Address to Platform Format
This helper function is used by Complete Platform Checkout to convert payment gateway address formats (extracted by Extract Addresses from Payment Gateway) to the platform's expected format. Payment gateway address formats differ from platform requirements. Map gateway addresses to platform format with proper field mapping and defaults.
// Example: Map payment gateway address to platform format
function mapAddressToPlatform(gatewayAddress) {
if (!gatewayAddress) {
throw new Error('Address is required');
}
return {
address: gatewayAddress.line1 || gatewayAddress.address_line1 || '',
address2: gatewayAddress.line2 || gatewayAddress.address_line2 || '',
city: gatewayAddress.city || '',
state: gatewayAddress.state || '',
country: gatewayAddress.country || 'IN',
pincode: gatewayAddress.postal_code ||
gatewayAddress.pincode ||
gatewayAddress.zipcode || '',
phone: gatewayAddress.contact ||
gatewayAddress.phone ||
gatewayAddress.phone_number || '',
name: gatewayAddress.name || '',
email: gatewayAddress.email || ''
};
}
How Controllers Connect to the Flow
Controllers implement the backend logic for the checkout extension:
- Order Creation Controller → Called by Script Injection when users click Buy Now
- Payment Verification Controller → Called by Payment Handler after payment completion
- Payment Gateway Integration Endpoints → Called by payment gateway SDK during popup interaction (optional)
The helper functions in this page are used across these controllers to maintain consistency and reduce code duplication.
Best Practices
Context Extraction
Context extraction is the process of retrieving application and company identifiers from request headers, platform state, or database records. Extract context once at the beginning of each function and reuse the values throughout to ensure consistency and avoid redundant lookups. Always validate that both application ID and company ID are present before proceeding, as missing context will cause failures in downstream operations.
// Example: Extract context at start of function
exports.createOrder = asyncHandler(async (req, res) => {
try {
// Extract context first
const { applicationId, companyId } = extractContext(req);
// Use context throughout function
const credentials = await getCredentials(applicationId);
const cart = await getCart(req, applicationId);
// ...
} catch (error) {
// Handle errors
}
});
Map addresses between formats carefully:
// Example: Map payment gateway address to platform format
function mapAddressToPlatform(gatewayAddress) {
return {
address: gatewayAddress.line1 || '',
address2: gatewayAddress.line2 || '',
city: gatewayAddress.city || '',
state: gatewayAddress.state || '',
country: gatewayAddress.country || 'IN',
pincode: gatewayAddress.postal_code || '',
phone: gatewayAddress.contact || '',
// Handle missing fields with defaults
};
}
Order Lookup
Order lookup is the process of retrieving order records from your database using various identifiers. Always use a fallback strategy that tries multiple identifiers in order of preference: start with payment gateway order ID, then fall back to internal order IDs, and finally to other identifiers like receipt numbers. Always validate that an order exists before attempting to process it, and return clear error messages when orders cannot be found.
// Example: Find order with fallbacks
let order = await Order.findOne({
gateway_order_id: gatewayOrderId
});
if (!order) {
order = await Order.findOne({
id: internalOrderId
});
}
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found'
});
}
Coupon Handling
Coupon handling involves extracting and processing discount information from multiple sources during the checkout flow: frontend requests, cart state, or payment gateway responses. When multiple sources are available, prioritize frontend data as it represents the most recent user action. Always validate coupon data before use and store coupon information in order metadata for later reference during refunds and reconciliation.
// Example: Extract coupon with priority
const frontendCoupon = req.body.coupon_data;
const cartCoupon = cart.breakup_values?.coupon;
// Prefer frontend coupon (more recent)
const couponData = frontendCoupon ||
(cartCoupon?.is_applied ? cartCoupon : null);
const couponCode = couponData?.code || null;
const couponDiscount = Math.abs(cart.breakup_values.raw.coupon || 0);
Amount Calculations
Amount calculations are critical for accurate payment processing and must account for currency units, rounding, and various charge components. Payment gateways typically require amounts in the smallest currency unit (e.g., paise for INR, cents for USD), which means you must convert from decimal currency values by multiplying by the appropriate factor (usually 100). Always calculate the amount before coupon application, use proper rounding functions (Math.round) to avoid floating-point precision errors, and validate that calculated amounts match expected totals from the cart breakup values.
// Example: Calculate order amount
const subtotal = cart.breakup_values.raw.subtotal || 0;
const deliveryCharge = cart.breakup_values.raw.delivery_charge || 0;
const codCharge = cart.breakup_values.raw.cod_charge || 0;
// Amount before coupon
const amountBeforeCoupon = subtotal + deliveryCharge + codCharge;
// Convert to smallest currency unit (paise for INR)
const amountInPaise = Math.round(amountBeforeCoupon * 100);
Payment Gateway Integration Endpoints
Some payment gateways (like Razorpay Magic Checkout) require additional endpoints to support features like address validation, coupon management, and shipping information during the payment popup interaction. These endpoints are called by the payment gateway SDK, not by your frontend code.
When these are used: These endpoints are called by the payment gateway popup after it opens (using the popup configuration returned by your Buy Now Endpoint) but before the user completes payment. They enable the payment gateway to validate addresses, fetch coupons, and apply discounts during the popup interaction, providing a seamless checkout experience.
Implementation Notes
- These endpoints are optional - only implement them if your payment gateway requires them
- All endpoints should find orders using both gateway order ID and internal order ID (fallback strategy)
- Extract
cart_idfromorder.meta.fynd_cart_idororder.meta.cart_idfor FDK method calls - Extract
company_idandapp_idfrom order meta (not headers) for multi-tenant support - Return data in payment gateway's expected format (may differ from platform format)
- Handle CORS headers if payment gateway makes cross-origin requests
These endpoints are invoked by the payment gateway during the payment popup interaction, after the popup opens but before payment completion:
- checkCartServiceability (Shipping Info Endpoint): Called when user enters or selects a shipping address in the payment gateway popup
- getAppCoupons (Coupons Endpoint): Called when the payment gateway requests available coupons for the cart
- applyCoupon (Apply Coupon Endpoint): Called when user applies a coupon code in the payment gateway popup
- checkoutCart (Checkout Cart): Called internally by your backend after payment verification (not by payment gateway)
Shipping Info Endpoint
Triggered by the payment gateway when a user provides a shipping address in the popup. Checks if the cart is serviceable to the address and returns delivery charges, COD availability, delivery estimates, and overall serviceability status.
This endpoint should:
- Accept address/pincode from payment gateway request
- Find the order using gateway order ID or internal order ID
- Extract cart ID from order meta
- Call platform's
checkCartServiceabilityFDK method - Return shipping information in payment gateway format
Get Available Coupons
Invoked by the payment gateway to retrieve coupons valid for the current cart.
Returns a list of eligible coupons (code, summary, description) in the gateway's required format.
This endpoint should:
- Accept order ID from payment gateway request
- Find the order and extract cart ID from order meta
- Call platform's
getAppCouponsFDK method with cart ID - Map platform coupon format to payment gateway format
- Return coupons array
Apply Coupon Endpoint
This endpoint is triggered when the payment gateway popup requests to apply a coupon code. It applies the coupon to the cart and responds with refreshed pricing details—including the updated cart, new delivery and COD charges, and the recalculated total—allowing the user to review accurate savings and payment amounts in real time.
This endpoint should:
- Accept coupon code and order ID from payment gateway request
- Find the order and extract cart ID from order meta
- Call platform's
applyCouponFDK method - Return updated cart pricing in payment gateway format
Checkout Cart
This step is triggered internally by your backend after verifying payment success. It creates the platform order using shipping and billing addresses from the payment gateway, and returns the platform order ID along with the merchant transaction ID.
This is not an endpoint exposed to the payment gateway, but rather an internal GraphQL mutation call made during the verification flow. It's documented in the Verify Payment section.