Streamlining Online Payments with Coral Pay: A Comprehensive Laravel Integration Guide

Chukwuyenum Opone
10 min readAug 29, 2023
src : https://wallpaperaccess.com/full/4597118.jpg

Introduction:

In the ever-evolving landscape of web applications, offering seamless and versatile online payment options is paramount. Integrating a trusted payment gateway like Coral Pay can revolutionize your user experience. From USSD to card, bank transfers, and QR code payments, Coral Pay brings a plethora of choices to your users. In this tutorial, we’ll delve into the process of integrating Coral Pay Gateway API into your Laravel application. By simplifying payments and enhancing user convenience, you’ll elevate your platform’s payment capabilities.

Getting Started: Generating API Keys and Account Setup

To embark on the Coral Pay journey, begin by creating an account on the Coral Pay website or contact them for questions on the subject. Secure your application with API keys, the cornerstone of your integration. Armed with these keys, you’re ready to seamlessly infuse diverse payment options into your Laravel app.

Enabling Payment Diversity: USSD, Cards, Bank Transfers, and QR Codes

Integrating USSD, card payments, bank transfers, and QR codes requires harnessing Coral Pay’s versatile API. Employ Laravel’s built-in HTTP client or leverage tools like Guzzle for robust interactions. These interactions facilitate the initiation of transactions and efficient management of responses, paving the way for an enhanced user journey.

Navigating Transactions and Checkouts

Upon invoking a transaction, Coral Pay orchestrates a dynamic checkout page enriched with transaction particulars. This page empowers users to execute transactions seamlessly. Once accomplished, users are redirected back to your application’s designated return URL, accompanied by appended status and ID parameters, cementing the loop of payment completion.

Real-time Transaction Monitoring with the Query Transaction Endpoint
Stay ahead of the curve by leveraging Coral Pay’s Query Transaction Endpoint. This indispensable feature empowers you to monitor the status of invoked transactions. Whether it’s a USSD transaction, card payment, or a QR code transaction, this endpoint keeps you well-informed, ensuring a frictionless payment experience for your users.

The Power of Webhooks: Ensuring Transaction Accuracy

A pivotal element in this journey is the creation of a secure webhook endpoint fortified with Basic Authentication. After every transaction, Coral Pay triggers this endpoint. It’s here that transaction statuses are scrutinized. Depending on the outcome, users are either credited or refunded. This granular approach enhances the accuracy and reliability of your payment system.

Code Refinement: The Repository Approach

To enhance code readability and maintainability, consider crafting a dedicated repository. This repository acts as a bridge between your application’s business logic and the intricate payment processing. By compartmentalizing these aspects, your codebase becomes more organized, adaptable, and comprehensible.

Let’s Code :

create a coral.php file in the config folder

<?php

return [
'base_url' => env('CORAL_BASE_URL', 'https://testdev.coralpay.com:5000/GwApi/api/v1'),
'username' => env('CORAL_USERNAME', 'your username here'),
'password' => env('CORAL_PASSWORD', 'your password here'),
'merchant_id' => env('CORAL_MERCHANT_ID', 'your merchant id here'),
];

Create a CoralPayRepository file in the App\Repositories folder:

<?php

namespace App\Repositories;

use Exception;
use App\Models\User;
use GuzzleHttp\Client;
use App\Models\Setting;
use App\Models\Transaction;

class CoralPayRepository
{
/**
* Issue Secret Key from your Coral Pay Auth Endpoint
* @var string
*/
protected $key;

/**
* Issue Token from your Coral Pay Auth Endpoint
* @var string
*/
protected $token;

/**
* Issue Username from your Coral Pay Dashboard
* @var string
*/
protected $username;

/**
* Issue Password from your Coral Pay Dashboard
* @var string
*/
protected $password;

/**
* Issue Terminal Id from your Coral Pay Dashboard
* @var string
*/
protected $terminalId;

/**
* Issue Merchant Id from your Coral Pay Dashboard
* @var string
*/
protected $merchantId;

/**
* Instance of Client
* @var Client
*/
protected $client;

/**
* Response from requests made to Coral Pay
* @var mixed
*/
protected $response;

/**
* Coral Pay API base Url
* @var string
*/
protected $baseUrl;

/**
* Authorization Url - Coral Pay payment page
* @var string
*/
protected $authorizationUrl;

public function __construct()
{
$this->baseUrl = config('coral.base_url');// initialize the coral pay base url
$this->username = config('coral.username');// initialize your coral pay username
$this->password = config('coral.password');// initialize your coral pay password
$this->merchantId = config('coral.merchant_id');// initialize your coral pay merchant id
$this->auth();// Authenticate client instance
$this->setRequestOptions(); // set client request options
}

/**
* get WebHook UserName
*/
public function getUsername()
{
return $this->username;
}

/**
* get WebHook Password
*/
public function getPassword()
{
return $this->password;
}

/**
* Authenticate to get token and key
*/
private function auth()
{
try {
$data = [
"username"=> $this->username,
"password"=> $this->password
];
$this->client = new Client(
[
'base_uri' => $this->baseUrl,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json'
]
]
);
if (Cache::has('coral_token')){
$this->token = Cache::get('coral_token');
$this->key = Cache::get('coral_key');
} else {
$this->setHttpResponse('/Authentication', 'POST', $data);
$resp = $this->getResponse();
if ($resp['status'] != "Success") {
throw new Exception("Error Processing Payment Auth Request", 401);
}
Cache::put('coral_token', $resp['token'], 1440); // cache for a day
Cache::put('coral_key', $resp['key'], 1440); // cache for a day
$this->token = $resp['token'];
$this->key = $resp['key'];
return $this->getResponse();
}});


} catch (Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}

/**
* Set options for making the Client request
*/
private function setRequestOptions()
{
$authBearer = 'Bearer ' . $this->token;

$this->client = new Client(
[
'base_uri' => $this->baseUrl,
'headers' => [
'Authorization' => $authBearer,
'Content-Type' => 'application/json',
'Accept' => 'application/json'
]
]
);
}

/**
* @param string $relativeUrl
* @param string $method
* @param array $body
* @return CoralPay
* @throws IsNullException
*/
private function setHttpResponse($relativeUrl, $method, $body = [])
{
// post or get method must be specified
if (is_null($method)) {
throw new Exception("Empty method not allowed");
}
$this->response = $this->client->{strtolower($method)}(
$this->baseUrl . $relativeUrl,
["body" => json_encode($body)]
);

return $this;
}

/**
* Get the whole response from a get operation
* @return array
*/
private function getResponse()
{
return json_decode($this->response->getBody(), true);
}

/**
* Get the data response from a get operation
* @return array
*/
private function getData()
{
return $this->getResponse()['data'];
}



/**
*
* Invoke Coral Payment
*
*/
public function invokePayment($user)
{
$time = time();
$traceId = $this->genTranxRef();
$data = array_filter([
"requestHeader" => [
"merchantId"=> $this->merchantId,
"terminalId"=> $this->terminalId,
"timeStamp"=> $time,
"signature" => hash('SHA256', $this->merchantId. $traceId. $time. $this->key)
],
"customer"=> [
"email" => $user->email,
"name" => $user->first_name. " ". $user->last_name,
"phone" => $user->phone,
"tokenUserId" => null
],
"customization" => [
"logoUrl"=> "url to your app logo here",
"title"=> env('APP_NAME'),
"description"=> "Fund Account"
],
'metaData' => request()->metadata ?? [],
"traceId" => $traceId,
"amount" => request()->amount,
"currency"=> (request()->currency != "" ? request()->currency : "NGN"),
"feeBearer"=> "C", //C means customer and M means merchant bears the fee
"returnUrl" => request()->callback_url, // url to return to after transaction has been processed
]);

$this->setHttpResponse('/InvokePayment', 'POST', $data);

return $this->getResponse();
}

/**
*
* Query Coral Pay Transaction
*
*/
public function queryTransaction($transactionId, $channel = 'USSD')
{
$time = time();
$data = array_filter([
"requestHeader" => [
"merchantId"=> $this->merchantId,
"terminalId"=> $this->terminalId,
"timeStamp"=> $time,
"signature" => hash('SHA256', $this->merchantId. $transactionId. $time. $this->key)
],
"traceId" => $transactionId,
]);


$this->setHttpResponse('/TransactionQuery', 'POST', $data);

return $this->getResponse();
}

/**
*
* WebHook Coral Pay Transaction
*
*/
public function callBack($request)
{
// also set up callback webhook incase an issue occured during transaction query
if ($request['responseHeader']['responseCode'] == '00') {
// check if it exists and if it has been processed then throw error
// else update user wallet and transaction log status here
return $request;
} else {
throw new Exception($request['responseHeader']['ResponseMessage']);
}
}



/**
*
* TRANSFER REF SECTION
*
*/

/**
* Get the pool to use based on the type of prefix hash
* @param string $type
* @return string
*/
private function getPool($type = 'alnum')
{
switch ($type) {
case 'alnum':
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'alpha':
$pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'hexdec':
$pool = '0123456789abcdef';
break;
case 'numeric':
$pool = '0123456789';
break;
case 'nozero':
$pool = '123456789';
break;
case 'distinct':
$pool = '2345679ACDEFHJKLMNPRSTUVWXYZ';
break;
default:
$pool = (string) $type;
break;
}

return $pool;
}
/**
* Generate a random secure crypt figure
* @param integer $min
* @param integer $max
* @return integer
*/
private function secureCrypt($min, $max)
{
$range = $max - $min;

if ($range < 0) {
return $min; // not so random...
}

$log = log($range, 2);
$bytes = (int) ($log / 8) + 1; // length in bytes
$bits = (int) $log + 1; // length in bits
$filter = (int) (1 << $bits) - 1; // set all lower bits to 1
do {
$rnd = hexdec(bin2hex(openssl_random_pseudo_bytes($bytes)));
$rnd = $rnd & $filter; // discard irrelevant bits
} while ($rnd >= $range);

return $min + $rnd;
}

/**
* Generate a Unique Transaction Reference
* @return string
*/
public function genTranxRef($length = 25)
{
$token = "";
$max = strlen(static::getPool());
for ($i = 0; $i < $length; $i++) {
$token .= $this->getPool()[$this->secureCrypt(0, $max)];
}

return $token;
}
}

We create a middleware for coral pay webhook that uses Auth basic in the project terminal.

php artisan make:middleware CoralBasicAuth

We go on to edit the file App\Http\Middleware\CoralBasicAuth

<?php

namespace App\Http\Middleware;


use Closure;
use Illuminate\Http\Request;

class CoralBasicAuth
{
...

public function handle(Request $request, Closure $next)
{
// Extract Basic Auth credentials from the webhook request
$authHeader = $request->header('Authorization');
if (strpos($authHeader, 'Basic ') !== 0) {
return response()->json(['error' => 'Invalid Authorization header'], 401);
}
$encodedCredentials = substr($authHeader, 6);
$decodedCredentials = base64_decode($encodedCredentials);
$credentials = explode(':', $decodedCredentials);
$username = $credentials[0] ?? null;
$password = $credentials[1] ?? null;

// Retrieve expected credentials from .env file
$expectedUsername = config('coral.username');
$expectedPassword = config('coral.password');

// Compare the credentials and return response accordingly
if ($username === $expectedUsername && $password === $expectedPassword) {
return $next($request);
} else {
return response()->json(['error' => 'Invalid credentials'], 401);
}
}
...
}

We then list it in kernel.php

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
....
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
...
'auth.coral' => \App\Http\Middleware\CoralBasicAuth::class,
...
];
...
}

Then we move to routes to create and list the endpoints that will be linked to the controller.

...
Route::prefix('payment')->group(function(){
Route::prefix('coral')->controller(CoralPayController::class)->group(function(){
Route::post('/invoke', 'invoke')->middleware('auth:api');
Route::post('/query', 'query')->middleware('auth:api');
Route::post('/webhook', 'webhook')->middleware('auth.coral');
});
});
...

Next, we’ll create a controller to handle the business logic of the endpoint.

To use the functions we created in the CoralPayRepository file, we’ll inject them into the controller’s constructor.

This will allow us to access the functions within the controller and use them to manage transactions and handle responses from Coral Pay’s API. By separating the business logic from the payment processing logic, we can maintain code readability and simplify the payment process for our users.

<?php

namespace App\Http\Controllers;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Repositories\CoralPayRepository;
use Illuminate\Support\Facades\Validator;

class CoralPayController extends Controller
{
protected $repository;

public function __construct(CoralPayRepository $repository)
{
$this->repository = $repository;
}
/**
* Invoke a Coral Pay Transaction.
*
* @return \Illuminate\Http\Response
*/
public function invoke(Request $request)
{
DB::beginTransaction();
try {
$user = auth()->user();
$validator = Validator::make($request->all(), [
'amount' => 'required',
'callback_url' => 'required',
'channel' => 'required'
]);

if($validator->fails()){
throw new Exception($validator->errors()->first(), 422);
}

$resp = $this->repository->invokePayment($user);
if ($resp['responseHeader']['responseCode'] == '00') {
// Log the transaction however you prefer
// this also provides a page url for the frontend to use to process the transaction
Log::info($resp)
}else{
throw new Exception($request['responseHeader']['ResponseMessage']);
}
DB::commit();
return response()->json(['success' => true, 'message'=> 'Coral Pay Transaction Invoked Successfully', 'data' => $resp]);
}catch(\Exception $e){
DB::rollBack();
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}

/**
* Query a Coral Pay Payment.
*
* @return \Illuminate\Http\Response
*/
public function query(Request $request)
{
try {
$validator = Validator::make($request->all(), [
'transactionId' => 'required'
]);

if($validator->fails()){
throw new Exception($validator->errors()->first(), 422);
}

$resp = $this->repository->queryTransaction($request->transactionId);

return response()->json(['success' => true, 'message'=> 'Coral Pay Transaction Successful', 'data' => $resp]);
}catch(\Exception $e){
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}

/**
* Listen For Coral Pay Payment.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function webhook(Request $request)
{
try {
if ($request['ResponseCode'] == '00') {
// use $request['TraceId'] to find the transaction log and credit the user balance
// Note webhook hits multiple times so always confirm if a transaction with a trace id has been settled before crediting them to avoid dublicate crediting
return $request;
} else {
return response()->json(['success' => false, 'message' => $request['ResponseMessage'], 'data' => $request]);
}
return response()->json(['success' => true, 'message'=> 'Coral Pay Transaction Completed Successfully', 'data' => $resp]);
}catch(Exception $e){
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
}

When you hit the invoke endpoint in the data response you will get a payment page link, open that link and it will take you to a page similar to the image below.

This shows you the available options on Coral pay as shown in the image above USSD, Card, NQR, and Bank Transfer.

Integrating Coral Pay Gateway API into your Laravel application can provide a flexible payment process for your users, resulting in an improved user experience. By following the steps outlined in this tutorial, you can make the payment process simpler and more streamlined.

Conclusion:

Elevating your web application’s payment landscape is now within reach with the Coral Pay Gateway API. By seamlessly integrating USSD, card payments, bank transfers, and QR codes, you enhance user convenience while simplifying complex transactions. Leveraging Coral Pay’s features, including the Query Transaction Endpoint and secure webhooks, keeps you at the helm of transaction management. As you embark on this journey, the repository approach ensures that your codebase remains robust and user-centric. With the Coral Pay integration in Laravel, you’re poised to redefine your platform’s payment ecosystem.

Start implementing Coral Pay Gateway API today and give your users the payment options they deserve!

If you found this helpful please let me know what you think in the comments, You are welcome to suggest changes.

Thank you for the time spent reading this tutorial.

You can follow me on all my social handles @officialyenum and please subscribe and 👏 would mean a lot thanks

--

--

Chukwuyenum Opone

A software and game programmer with over 5 years in tech and gaming, driven by innovation and a passion for AI, and a way with words.