mrroot/orange-money-bundle

Symfony bundle for Orange Money payment integration with complete webhook, callback and error handling support

v1.1.0 2025-09-04 16:05 UTC

This package is auto-updated.

Last update: 2025-09-04 16:52:54 UTC


README

License: MIT Symfony PHP

Symfony bundle for Orange Money payment integration with complete webhook support, callbacks, and advanced error handling.

Features

  • Orange Money payment initiation with comprehensive validation
  • Transaction status verification with enhanced parameters
  • Advanced webhook and notification handling with token validation
  • Robust error handling with detailed logging
  • Flexible configuration via environment variables
  • OAuth2 support for secure API authentication
  • Symfony compatibility 5.4, 6.x and 7.x
  • Automatic data validation and sanitization
  • Configurable callback URLs with security features
  • Webhook spoofing protection via notification token validation
  • Enhanced payment response handling with comprehensive data access

Bundle Architecture

File Structure

src/OrangeMoneyBundle/
├── DependencyInjection/
│   ├── Configuration.php          # Bundle configuration
│   └── OrangeMoneyExtension.php    # Symfony extension
├── Exception/
│   └── OrangeMoneyException.php    # Custom exceptions
├── Model/
│   ├── PaymentRequest.php          # Payment request model
│   └── PaymentResponse.php         # Payment response model
├── Service/
│   └── OrangeMoneyService.php      # Main service
└── OrangeMoneyBundle.php           # Main bundle class

Flow Diagram

Application → PaymentRequest → OrangeMoneyService → Orange Money API
     ↓              ↓                ↓                    ↓
Controller ← PaymentResponse ← Token Management ← OAuth2 Response

Installation

1. Installation via Composer

composer require mrroot/orange-money-bundle

2. Bundle Registration

Add the bundle to config/bundles.php:

<?php

return [
    Mrroot\OrangeMoneyBundle\OrangeMoneyBundle::class => ['all' => true],
];

3. Configuration

Create the file config/packages/orange_money.yaml:

orange_money:
    client_id: '%env(OM_CLIENT_ID)%'
    client_secret: '%env(OM_CLIENT_SECRET)%'
    api_base_url: '%env(OM_API_BASE_URL)%'
    token_endpoint: '%env(OM_TOKEN_ENDPOINT)%'
    payment_endpoint: '%env(OM_PAYMENT_ENDPOINT)%'
    transaction_status_endpoint: '%env(OM_TRANSACTIONSTATUS_ENDPOINT)%'

parameters:
    OM_RETURN_URL: '%env(OM_RETURN_URL)%'
    OM_CANCEL_URL: '%env(OM_CANCEL_URL)%'
    OM_NOTIF_URL: '%env(OM_NOTIF_URL)%'
    OM_MERCHANT_KEY: '%env(OM_MERCHANT_KEY)%'

4. Environment Variables

Add these variables to your .env file:

# Orange Money API Configuration
OM_CLIENT_ID=your_client_id
OM_CLIENT_SECRET=your_client_secret
OM_MERCHANT_KEY=your_merchant_key
OM_API_BASE_URL=https://api.orange.com

# API Endpoints
OM_TOKEN_ENDPOINT=/oauth/v3/token
OM_PAYMENT_ENDPOINT=/orange-money-webpay/dev/v1/webpayment
OM_TRANSACTIONSTATUS_ENDPOINT=/orange-money-webpay/dev/v1/transactionstatus

# Callback URLs
OM_RETURN_URL=https://your-site.com/payment/success
OM_CANCEL_URL=https://your-site.com/payment/cancel
OM_NOTIF_URL=https://your-site.com/payment/webhook

Usage

Complete Controller Example

For a complete and detailed implementation example, see: Controller Example

Payment Initiation

<?php

namespace App\Controller;

use Mrroot\OrangeMoneyBundle\Model\PaymentRequest;
use Mrroot\OrangeMoneyBundle\Service\OrangeMoneyService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class PaymentController extends AbstractController
{
    #[Route('/payment/initiate', name: 'payment_initiate', methods: ['POST'])]
    public function initiatePayment(
        Request $request,
        OrangeMoneyService $orangeMoneyService
    ): JsonResponse {
        try {
            // Create payment request
            $paymentRequest = new PaymentRequest(
                orderId: 'ORDER_' . uniqid(),
                amount: 10000, 
                currency: 'GNF', 
                reference: 'REF_' . time(),
                merchantKey: $this->getParameter('OM_MERCHANT_KEY'),
                returnUrl: $this->getParameter('OM_RETURN_URL'),
                cancelUrl: $this->getParameter('OM_CANCEL_URL'),
                notifUrl: $this->getParameter('OM_NOTIF_URL')
            );

            // Initiate payment
            $paymentResponse = $orangeMoneyService->initiatePayment($paymentRequest);

            // IMPORTANT: Save these values for webhook validation
            // - $paymentResponse->getPayToken() (payToken)
            // - $paymentRequest->getOrderId() (orderId)
            // - $paymentResponse->getNotifToken() (notifToken)

            return new JsonResponse([
                'success' => true,
                'payment_url' => $paymentResponse->getPaymentUrl(),
                'pay_token' => $paymentResponse->getPayToken(),
                'notif_token' => $paymentResponse->getNotifToken(),
                'order_id' => $paymentRequest->getOrderId()
            ]);

        } catch (\Exception $e) {
            return new JsonResponse([
                'success' => false,
                'error' => $e->getMessage()
            ], 400);
        }
    }
}

Payment Status Verification

#[Route('/payment/status/{payToken}', name: 'payment_status')]
public function checkPaymentStatus(
    string $payToken,
    string $orderId,
    float $amount,
    OrangeMoneyService $orangeMoneyService
): JsonResponse {
    try {
        // Enhanced status check with required parameters
        $response = $orangeMoneyService->checkTransactionStatus($payToken, $orderId, $amount);

        return new JsonResponse([
            'pay_token' => $payToken,
            'order_id' => $response->getOrderId(),
            'status' => $response->getStatus(),
            'amount' => $response->getAmount(),
            'currency' => $response->getCurrency(),
            'txn_id' => $response->getTxnId()
        ]);

    } catch (\Exception $e) {
        return new JsonResponse([
            'error' => $e->getMessage()
        ], 400);
    }
}

Webhook Handling with Security Validation

#[Route('/payment/webhook', name: 'payment_webhook', methods: ['POST'])]
public function handleWebhook(
    Request $request,
    OrangeMoneyService $orangeMoneyService
): JsonResponse {
    try {
        // Retrieve stored values from your database/session
        // These values should have been saved during payment initiation:
        $payToken = "your-stored-pay-token";     // Get from database
        $orderId = "your-stored-order-id";       // Get from database  
        $notifToken = "your-stored-notif-token"; // Get from database
        $amount = 50.0;                          // Get from database

        // Optional: Check transaction status separately if needed
        // $checkResponse = $orangeMoneyService->checkTransactionStatus(
        //     $payToken,
        //     $orderId, 
        //     $amount
        // );
        
        $webhookData = json_decode($request->getContent(), true);
        
        // Automatic notif_token verification
        $paymentStatus = $orangeMoneyService->processNotification($webhookData, $notifToken);
        
        // Your business logic here
        if ($paymentStatus->getStatus() === 'SUCCESS') {
            // Confirm order
            $this->confirmOrder($paymentStatus->getOrderId());
        }

        return new JsonResponse([
            'status' => 'success',
            'message' => 'Notification processed',
            'txn_id' => $paymentStatus->getTxnId()
        ]);

    } catch (\Exception $e) {
        return new JsonResponse([
            'status' => 'error',
            'message' => $e->getMessage()
        ], 500);
    }

    private function getStoredNotifToken(?string $orderId): ?string
    {
        // Retrieve the notification token stored during payment initiation
        // This could be from database, cache, session, etc.
        return $orderId ? $this->paymentRepository->getNotifTokenByOrderId($orderId) : null;
    }

    private function confirmOrder(string $orderId): void
    {
        // Your order confirmation logic
    }
}

Transaction Status Verification (Optional)

public function checkStatus(OrangeMoneyService $orangeMoneyService): JsonResponse
{
    try {
        // Use values saved during payment initiation
        $response = $orangeMoneyService->checkTransactionStatus(
            'pay-token-from-initiation',  // Saved payment token
            'ORDER-123',                  // Saved order ID
            10000                         // Saved amount
        );
        
        return new JsonResponse([
            'status' => $response->getStatus(),
            'transaction_id' => $response->getTxnId(),
            'order_id' => $response->getOrderId()
        ]);
        
    } catch (OrangeMoneyException $e) {
        return new JsonResponse([
            'error' => $e->getMessage()
        ], 400);
    }
}

Data Models

PaymentRequest

$paymentRequest = new PaymentRequest(
    orderId: 'ORDER_123',        // Unique order ID
    amount: 10000,               // Amount 
    currency: 'OUV',             // Currency (OUV for Ouguiya)
    reference: 'REF_123',        // Optional reference
    merchantKey: 'merchant_key', // Merchant key
    returnUrl: 'https://...',    // Success return URL
    cancelUrl: 'https://...',    // Cancellation URL
    notifUrl: 'https://...'      // Notification URL
);

PaymentResponse

// Available methods
$response->getPaymentUrl();    // Payment URL
$response->getPayToken();      // Payment token
$response->getNotifToken();    // Notification token (for webhook validation)
$response->getTxnId();         // Transaction ID
$response->getStatus();        // Payment status
$response->getOrderId();       // Order ID
$response->getAmount();        // Amount
$response->getCurrency();      // Currency
$response->isSuccessful();     // Success verification
$response->hasError();         // Error verification
$response->getErrorMessage();  // Error message
$response->getErrorCode();     // Error code
$response->hasPaymentUrl();    // Payment URL validation
$response->getRawData();       // Raw data

Security

Enhanced Webhook Validation

The bundle provides comprehensive webhook security:

// Required fields validated automatically
$requiredFields = ['status', 'notif_token', 'txnid'];


// Process notification with token validation
$paymentStatus = $orangeMoneyService->processNotification($data, $expectedNotifToken);

Webhook Spoofing Protection

To prevent webhook spoofing attacks:

  1. Store the notification token during payment initiation:
$paymentResponse = $orangeMoneyService->initiatePayment($paymentRequest);
$notifToken = $paymentResponse->getNotifToken();
// Store this token securely (database, cache, etc.)
$this->storeNotifToken($paymentRequest->getOrderId(), $notifToken);
  1. Validate the token in webhook processing:
$expectedToken = $this->getStoredNotifToken($orderId);
$paymentStatus = $orangeMoneyService->processNotification($webhookData, $expectedToken);

Error Handling

All errors are encapsulated in OrangeMoneyException:

try {
    $paymentResponse = $orangeMoneyService->initiatePayment($paymentRequest);
} catch (OrangeMoneyException $e) {
    // Orange Money specific error
    $errorMessage = $e->getMessage();
    $errorData = $e->getData();
}

Logging

The bundle uses Symfony's logging system:

# config/packages/monolog.yaml
monolog:
    channels: ['orange_money']
    handlers:
        orange_money:
            type: rotating_file
            path: '%kernel.logs_dir%/orange_money.log'
            level: info
            channels: ['orange_money']

Advanced Configuration

Endpoint Customization for Development or Production Environment

# config/packages/orange_money.yaml
orange_money:
    api_base_url: 'https://api.orange.com'
    payment_endpoint: '/orange-money-webpay/dev/v1/webpayment'
    transaction_status_endpoint: '/orange-money-webpay/dev/v1/transactionstatus'

Complete Examples

Implementation examples are integrated directly into this documentation and include:

  • Complete payment controller
  • Callback handling
  • Transaction status verification
  • Webhook handling

Contributing

Contributions are welcome! Please:

  1. Fork the project
  2. Create a branch for your feature
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

This project is licensed under the MIT License. See the LICENSE file for more details.

Support

Changelog

v1.1.0

  • Enhanced webhook security with notification token validation
  • Improved transaction status checking with required parameters (payToken, orderId, amount)
  • Extended PaymentResponse model with notifToken and txnId properties
  • Code quality improvements and consistency fixes
  • Better error handling and validation
  • Webhook spoofing protection implementation

v1.0.0

  • First stable version
  • Complete Orange Money API support
  • Webhook handling
  • Complete documentation

Developed with ❤️ by Abdoulaye CAMARA alias MrROOT