How do I asscociate a payment transaction with a user while consuming the MPESA Express (STKPUSH) api v1

634 Views Asked by At

I am using the MPESA-Express also called the STK push api V1 to receive payments from my clients. To get the customer paying, I am looking for the PhoneNumber value in the results body of the response if the payment is successful. This way I can associate a payment with a customer. However now that we'll be having data minimisation on the MPesa api, the PhoneNumber will not be displayed fully, and I am facing a challenge of how to associate a payment transaction with a client. I have tried setting the AcccountReference in the request as shown below, but I can't get this AccountReference back in the response results body. I was thinking of setting a unique AcccountReference for each customer.

The data I am sending to the endpoint https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest

 $postData = json_encode([
                "BusinessShortCode" => Yii::$app->params['businessShortCode'],
                "Password" => $this->createMpesaRequestsPassword($timestamp),
                "Timestamp" => $timestamp,
                "TransactionType" => $transactionType,
                "Amount" => $amount,
                "PartyA" => $phoneNumber,
                "PartyB" => Yii::$app->params['businessShortCode'],
                "PhoneNumber" => $phoneNumber,
                "CallBackURL" => $callBackUrl,
                "AccountReference" => $phoneNumber,
                "TransactionDesc" => $transactionDesc
            ]);

On my callback url I get this response:

{
    "Body": {
        "stkCallback": {
            "MerchantRequestID": "9183-42212949-1",
            "CheckoutRequestID": "ws_CO_23072022133552132714385056",
            "ResultCode": 0,
            "ResultDesc": "The service request is processed successfully.",
            "CallbackMetadata": {
                "Item": [
                    {
                        "Name": "Amount",
                        "Value": 1
                    },
                    {
                        "Name": "MpesaReceiptNumber",
                        "Value": "QGN2XSH6MQ"
                    },
                    {
                        "Name": "Balance"
                    },
                    {
                        "Name": "TransactionDate",
                        "Value": 20220723133617
                    },
                    {
                        "Name": "PhoneNumber",
                        "Value": 254711111111
                    }
                ]
            }
        }
    }
}

How do know which transaction belongs to which user?

2

There are 2 best solutions below

0
On BEST ANSWER

This is too late to respond but maybe for who ever may be looking for a similar answer.

1st, you initiate the STK Push: This can be done using a submit button.

<?php
    if(isset($_POST['mpesastk'])){
        $app_id = mysqli_real_escape_string($conx, $_POST['app_id']);// Value to be updated in a different table during the mpesa callback url process.
        $amount = '1'; //Amount to be paid
        $phone = mysqli_real_escape_string($conx, $_POST['pay_phone']); //Phone Number

        $config = array(
            "env"              => "sandbox",
            "BusinessShortCode"=> "174379",
            "key"              => "", //Enter your consumer key here
            "secret"           => "", //Enter your consumer secret here
            "username"         => "apitest",
            "TransactionType"  => "CustomerPayBillOnline",
            "passkey"          => "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919", //Enter your passkey here
            "CallBackURL"      => "", //Must have SSL When using localhost, Use Ngrok to forward the response to your Localhost
            "AccountReference" => "Name to appear.",
            "TransactionDesc"  => "Payment of X Fee for ",
        );
        
        $phone = (substr($phone, 0, 1) == "+") ? str_replace("+", "", $phone) : $phone;
        $phone = (substr($phone, 0, 1) == "0") ? preg_replace("/^0/", "254", $phone) : $phone;
        $phone = (substr($phone, 0, 1) == "7") ? "254{$phone}" : $phone;

        
        $access_token = ($config['env']  == "live") ? "https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" : "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"; 
        //$access_token = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"; 
        $credentials = base64_encode($config['key'] . ':' . $config['secret']); 
        
        $ch = curl_init($access_token);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Basic " . $credentials]);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
        $response = curl_exec($ch);
        curl_close($ch);
        $result = json_decode($response); 
        $token = isset($result->{'access_token'}) ? $result->{'access_token'} : "N/A";
    
        $timestamp = date("YmdHis");
        $password  = base64_encode($config['BusinessShortCode'] . "" . $config['passkey'] ."". $timestamp);
    
        $curl_post_data = array( 
            "BusinessShortCode" => $config['BusinessShortCode'],
            "Password" => $password,
            "Timestamp" => $timestamp,
            "TransactionType" => $config['TransactionType'],
            "Amount" => $amount,
            "PartyA" => $phone,
            "PartyB" => $config['BusinessShortCode'],
            "PhoneNumber" => $phone,
            "CallBackURL" => $config['CallBackURL'],
            "AccountReference" => $config['AccountReference'],
            "TransactionDesc" => $config['TransactionDesc'],
        ); 
    
        $data_string = json_encode($curl_post_data);
    
        //$endpoint = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"; 
        $endpoint = ($config['env'] == "live") ? "https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest" : "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"; 

    
        $ch = curl_init($endpoint );
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer '.$token,
            'Content-Type: application/json'
        ]);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $response     = curl_exec($ch);
        curl_close($ch);
    
        $result = json_decode(json_encode(json_decode($response)), true);
    
        if(!preg_match('/^[0-9]{10}+$/', $phone) && array_key_exists('errorMessage', $result)){
            $errors['phone'] = $result["errorMessage"];
        }

        if($result['ResponseCode'] === "0"){
            $MerchantRequestID = $result['MerchantRequestID'];
            $CheckoutRequestID = $result['CheckoutRequestID'];
                           
            $sql = "INSERT INTO mpesastk(mpesastk_appid,mpesastk_phone,mpesastk_amount,CheckoutRequestID,MerchantRequestID)
                VALUES('$app_id','$phone','$amount','$CheckoutRequestID','$MerchantRequestID')";
            
            if ($conx->query($sql) === TRUE){
                //Response to user
                $err_color = "success";
                $err_title = "SUCCESS!";
                $err_message = '<h4><font color="#fff">Payment of X fee was sent to your phone.</font></h4>';
                header("refresh:15;");
            }else{
                $errors['database'] = "Unable to initiate your order: ".$conx->error;;  
                foreach($errors as $error) {
                    $err_message .= $error . '<br />';
                } 
            }
        }else{
            $err_color = "error";
            $err_title = "ERROR!";
            $err_message = '<h4><font color="#fff">Failed to send Payment Request of X fee to your phone.</font></h4>';
            header("refresh:3;");
        }
    }
?>

Note that

$CheckoutRequestID = $result['CheckoutRequestID'];
$app_id = mysqli_real_escape_string($conx, $_POST['app_id']);

was inserted. This value will be used by CallBack URL to update the database accordingly.

Now the CallBack URL

<?php
    echo '<a href="../../">Home<br /></a>';

    $content = file_get_contents('php://input'); //Receives the JSON Result from safaricom
    $res = json_decode($content, true); //Convert the json to an array 
    

    $dataToLog = array(
        date("Y-m-d H:i:s"), //Date and time
        " MerchantRequestID: ".$res['Body']['stkCallback']['MerchantRequestID'],
        " CheckoutRequestID: ".$res['Body']['stkCallback']['CheckoutRequestID'],
        " ResultCode: ".$res['Body']['stkCallback']['ResultCode'],
        " ResultDesc: ".$res['Body']['stkCallback']['ResultDesc'],
        " MpesaReceiptNumber: ".$res['Body']['stkCallback']['CallbackMetadata']['Item'][1]['Value'],
    );

    $data = implode(" - ", $dataToLog);
    $data .= PHP_EOL;
    file_put_contents('mpesastk_log', $data, FILE_APPEND); //Create a txt file and log the results to our log file


    //Saves the result to the database
    //Change the values accordingly to your system setup
    $conn=new PDO("mysql:host=localhost;dbname=dbname","root","password");
    $conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

    $stmt = $conn->query("SELECT * FROM mpesastk ORDER BY mpesastk_id DESC LIMIT 1");
    $stmt->execute();
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    foreach($rows as $row){
        $mpesastk_id = $row['mpesastk_id'];
        $app_id = $row['mpesastk_appid'];//remember this, it will be
        $ResultCode = $res['Body']['stkCallback']['ResultCode'];
        $ResultDesc = $res['Body']['stkCallback']['ResultDesc'];
        $MpesaReceiptNumber = $res['Body']['stkCallback']['CallbackMetadata']['Item'][1]['Value'];

        
        if($res['Body']['stkCallback']['ResultCode'] == '1032'){//if transaction canceled
            $sql = $conn->query("UPDATE mpesastk SET mpesastk_status = '0',ResultCode = '$ResultCode',
                ResultDesc='$ResultDesc',MpesaReceiptNumber='$MpesaReceiptNumber' WHERE mpesastk_id = $mpesastk_id");
            $rs = $sql->execute();
        }else{//if transaction was paid
            $sql = $conn->query("UPDATE mpesastk SET mpesastk_status = '1',ResultCode = '$ResultCode',
                ResultDesc='$ResultDesc',MpesaReceiptNumber='$MpesaReceiptNumber' WHERE mpesastk_id = $mpesastk_id");
            $rs = $sql->execute();
            //Now update a different table in the database
            // Not the $app_id as set in the submit :)
            $asql = $conn->query("UPDATE tblX SET tblX_status = '3' WHERE tblX_id = $app_id");
            $ars = $asql->execute();
        }

        if($rs){
            file_put_contents('error_log', "Records Inserted", FILE_APPEND);;
        }else{
            file_put_contents('error_log', "Failed to insert Records", FILE_APPEND);
        }
    }
?>

Happy Coding

0
On

Mpesa sends you two responses with the same 'callbackrequestID'. The immediate one can be saved in a cache memory of your choice attaching your product/user to it and when you receive the payment completion, use it to match the cache data and update the database. Add security layers and whitelist the given ips from Safaricom to avoid getting fake callback responses from elsewhere. In development you may not have control of ips since you are using services like Ngrok or smee.io Other methods may be hashing your own values and setting them as part of the callback URL so you have unique callback URL for each transaction for matching users or products.