Muhannad Alrisi
Home THAWANI FAKE API

Why I did make a fake API to complete the testing with Thawani

I created this fake API app to simulate the callback as the payment gateway when I was working to do the integration with Thawani Gateway API.

It's totally not related to Thawani at all , it was a big help tp understand the callback in WP WooCommerce.

Note This is not Thawani API , Not related to Thawani 😸

Let's start building this

I wrote 2 version with the same logic 1 with Deno oak & PHP Klein.php

In this post I'll go with the PHP version,

What you need to know is

  1. (Klein.php)[https://github.com/klein/klein.php]
  2. using Composer PHP Package Manager

Let's get Tech

First you need to install Klein install it with Composer by using

php composer.phar require klein/klein

our folder structure will be similar to this

├── composer.json
├── composer.lock
├── index.nginx-debian.html
├── index.php
├── src
├── vendor
└── views

create the src, views folders and the index.php

Step 1

We need to teask the composer.json file and use PSR-4 auto-load to load our class with the composer autoload.php

More info about PSR-4

your composer.json should look like

{
    "require": {
        "klein/klein": "^2.1"
    },
    "autoload": {
        "psr-4": {
            "App\\":"src/"
        }
    }
}

then run composer dump-autoload

Step 2

let's create our DB table something like

CREATE TABLE payments(
    id int PRIMARY KEY AUTO_INCREMENT,
    payment varchar(255),
    amount float ,
    `status` varchar(30), # `in quote because of key-word`
    callback_uri varchar(255)
)

Step 3 Rockin 🤞

in let's create our class in src/Config.php

<?php
namespace App;

use Klein\Klein;

class Config
{

    protected $route;
    private $db;
    const ON_HOLD = 'on-hold';
    const COMPLETE = 'complete';
    const SUB_DIR = ''; // if you test it in the root folder then make this empty
    /**
     * Start setup the class initial values
     */
    public function __construct()
    {

        $this->route = new Klein();

        $this->db = new \Mysqli('localhost', 'your_db_user', 'your_db_user_password', 'your_db_name');

        if (!$this->db) {

            $this->route->respond('/', function () {
        return $this->db->error;
        exit;
            });
        }
    }
    /**
     * loading the HTTP routes
     */
    public function load()
    {
        $this->route->respond('GET', '/', function ($request, $response, $service) {
            $service->render('views/welcome.php');
        });

        $this->route->respond('POST', '/getPayment', function ($request, $response) {
            $body = json_decode($request->body(), true);
            if (!$body || !is_array($body)) {
                $response->json([
                    'error' => 'request is wrong',
                ]);
                exit;
            }
            //get the required information
            $data = [
                'payment' => $body['remark'],
                'amount' => $body['amount'],
                'start' => self::ON_HOLD,
                'callback_url' => $body['callback_url'],
            ];
            $generateID = $this->save($data);
        //this is good
        $return_url ='http://'.$_SERVER['HTTP_HOST'].'/get/'.$generateID;
            $response->json([
                'success' => 'true',
                'returnurl' => $return_url
            ]); //resend this again
        });

        $this->route->respond('POST', '/verify',
            function ($request, $response) {
                $response_body = json_decode($request->body(), true);
                $result = $this->get_on_hold_payment($response_body['id'])->fetch_assoc();
                $query = $this->update_state($response_body['id']);
                if (!$query) {
                    $payload = [
                        'type' => 'error',
                        'data' => [
                            'Error' => false,
                            'Error_message' => 'Could not verify the transaction',
                        ],
                    ];
                    $response->json($payload);
                    exit;
                }

                $payload = $this->prepare_payload($response_body['id'], $result);
                $response->json($payload);
            });
        $this->route->respond('GET', '/get/[i:id]',
            function ($request, $response, $service) {
                //    return 'Request'. $request->id;
                $data = $this->get_on_hold_payment($request->id);
                $result = $data->fetch_assoc();
                $data->free();
                if ($result === null || count($result) == 0) {
                    $response->redirect(dirname($_SERVER['PHP_SELF']) . '/not-valid');
                }
                if (self::SUB_DIR) {
                    $service->_app = self::SUB_DIR;
                }

                $service->result = $result;
                $service->render('views/app.php');
            });


        $this->route->respond('/not-valid', function ($request, $response, $service) {
            $service->render('views/not-valid.php');
        });

        $this->route->onHttpError(function ($code, $router) {
            if ($code == 404) {
                $router->response()->sendHeaders(true, true);
                $service = $router->service();
                $service->render('views/not-valid.php');
            }
        });
        $this->route->dispatch();
    }

    /**
     * Save the info and return
     * the link for the end-user
     *
     * @return int $id
     */
    private function save($payload)
    {

        $query = $this->db->prepare("INSERT INTO
        payments (payment ,amount ,`status`, callback_uri) VALUES
        (?,?,?,?);");
        $query->bind_param('sdss', $payment, $amount, $status, $uri);

        $payment = $payload['payment'];
        $amount = (float) $payload['amount'];
        $status = $payload['start'];
        $uri = $payload['callback_url'];
        $query->execute();
        return $query->insert_id;
    }
    /**
     * update the payment status to be
     * processed
     *
     * @param int $id payment id
     *
     * @return boolean success | fail query
     */
    private function update_state($id)
    {
        $query = $this->db->prepare(" UPDATE payments SET `status` = ? WHERE id=? ");
        $query->bind_param('si', $_status, $_id);
        $_status = self::COMPLETE;
        $_id = $id;
        return $query->execute();
    }
    /**
     * get payment record form DB
     *
     * @return Object mysqli_result
     */
    private function get_on_hold_payment($id)
    {
        $query = $this->db->prepare("SELECT * FROM payments
        WHERE id = ? AND `status` = ? ");
        $query->bind_param('is', $id, $_status);
        $_id = (int) $id;
        $_status = self::ON_HOLD;
        $is_inserted = $query->execute();

        if (!$is_inserted) {
            die($query->error);
        }
        //
        $query_result = $query->get_result();

        //close the query
        $query->close();
        return $query_result;
    }
    /**
     * prepare payload for return back data
     *
     * @return array $payload
     */
    protected function prepare_payload($id, $data)
    {
        return [
        ...payload
        ];

    }

    public function __distruct()
    {
        $this->db->close();
    }
}

in our index.php

<?php
//always put it to development
error_reporting(E_ALL);
require_once 'vendor/autoload.php';
use App\Config;
$app = new Config();
$app->load();

Our API is good to go 😸

Wait customize this

The payload function

/**
     * prepare payload for return back data
     *
     * @return array $payload
     */
    protected function prepare_payload($id, $data)
    {
        return [
        ...payload
        ];

    }

you need to customize the function depeneding on the smaple of the response that you need.

Make sure to update the state of the transaction get_on_hold_payment($id)

most of the route above is simple but the one that you need to focus on is this one

$this->route->respond('POST', '/verify',
function ($request, $response) {
    $response_body = json_decode($request->body(), true); //JSON from your app
    $result = $this->get_on_hold_payment($response_body['id'])->fetch_assoc(); //update and get the last id
    $query = $this->update_state($response_body['id']);// update the state
    if (!$query) { //check if the query is working
        $payload = [
            'type' => 'error',
            'data' => [
                'Error' => false,
                'Error_message' => 'Could not verify the transaction',
            ],
        ];
        $response->json($payload);
        exit;
    }
    //prepare the payload / sump response
    $payload = $this->prepare_payload($response_body['id'], $result);
    // and send it back
    $response->json($payload);
});

so this route actually being called within the api once the user is clicking a <a>

and the markup to show the action is

<a href="javascript:void(0);" id="callback_link" class="btn btn-primary">verify the payment</a>

once the user is clicking on the link AJax request is sent to the server fetching the response / payload and insert it to input and submit the form

    <script type="text/javascript">
        <?php
        if($this->_app)
            echo("const __app__ = \"/{$this->_app}\";");
        else
             echo 'const __app__ = "";';
        ?>
        const  remote = () =>  window.location.origin+__app__;

        let id  = document.querySelector('#id');
        let redirect_data = document.querySelector("#data");
        let form  = document.querySelector("#queryForm");

        let callback_link = document.querySelector("#callback_link");

        function verify(){
            //send the client back to the link
            //forget to put the link

            let state = fetch(remote()+"/verify", {
                method :"POST",
                headers: {
                    'Content-Type': 'application/json'
                },
                body:JSON.stringify({
                    "id": id.value
                })
            })
            .then(response => response.json())
            .then((data) => {
                //rename the text
                console.log(data);
                if( data.code ) {
                    let _callback_uri = data.callback_url;
                    redirect_data.value = JSON.stringify(data);
                    form.action = _callback_uri;
                    callback_link.innerHTML = 'Wait to be redirected';
                    setTimeout(() => {
                        form.submit();
                    }, 3000);
                }
            })
            .catch(err => console.error(err))

        }
        callback_link.addEventListener('click' , verify);

    </script>

the form is

<form  id="queryForm" method="post">
<input 
type="hidden" 
name="id" 
id="id"
value="<?php echo $this->result['id'];?>"
/>

<input type="hidden" name="data" id="data" value="__nothing__">
</form>

and you can do more !