Handle AllPay third party payment service with Laravel

Create Laravel Project

Laravel new AllPay

Let’s initialize git, it’s a must be!

git init

Download AllPay SDK, in this article, we will use AllPay PHP SDK

git clone https://github.com/o-pay/Payment_PHP 

Move SDK Laravel’s app folder

cp Payment_PHP/sdk/AllPay.Payment.Integration.php AllPay/app/ 

Create a testing controller

php artisan make:controller PaymentsController

Move the example in the SDK package to our Controller as follows:

/**
*
*/

//SDK address(You could manage it yourself)
include('AllPay.Payment.Integration.php');
try {

$obj = new AllInOne();

//Service parameters
$obj->ServiceURL = "https://payment-stage.opay.tw/Cashier/AioCheckOut/V5";
//Service location
$obj->HashKey = '5294y06JbISpM5x9' ;
//Testing Hashkey, in real case, please use the one provided by AllPay
$obj->HashIV = 'v77hoKGq4kWxNNIS' ;
//Testing HashIV, in real case, please use the one provided by AllPay
$obj->MerchantID = '2000132';
//Testing MerchantID, in real case, please use the one provided by AllPay
$obj->EncryptType = EncryptType::ENC_SHA256;
//CheckMacValue encrypted type, please stay 1, using SHA256


//Basic parameters(It depends on your need)
$MerchantTradeNo = "Test".time();

$obj->Send['ReturnURL'] = 'http://localhost/simple_ServerReplyPaymentStatus.php' ;
//The URL AllPay will return after the payment is paid
$obj->Send['MerchantTradeNo'] = $MerchantTradeNo;
$obj->Send['MerchantTradeDate'] = date('Y/m/d H:i:s');
$obj->Send['TotalAmount'] = 2000;
$obj->Send['TradeDesc'] = "good to drink";
$obj->Send['ChoosePayment'] = PaymentMethod::ALL;

//Items information
array_push($obj->Send['Items'], array('Name' => "歐付寶黑芝麻豆漿", 'Price' => (int)"2000",
'Currency' => "元", 'Quantity' => (int) "1", 'URL' => "dedwed"));

# E-Invoice parameters
/*
$obj->Send['InvoiceMark'] = InvoiceState::Yes;
$obj->SendExtend['RelateNumber'] = $MerchantTradeNo;
$obj->SendExtend['CustomerEmail'] = 'test@opay.tw';
$obj->SendExtend['CustomerPhone'] = '0911222333';
$obj->SendExtend['TaxType'] = TaxType::Dutiable;
$obj->SendExtend['CustomerAddr'] = '台北市南港區三重路19-2號5樓D棟';
$obj->SendExtend['InvoiceItems'] = array();
// Add item into list of e-invoice
foreach ($obj->Send['Items'] as $info)
{
array_push($obj->SendExtend['InvoiceItems'],array('Name' => $info['Name'],'Count' =>
$info['Quantity'],'Word' => '個','Price' => $info['Price'],'TaxType' => TaxType::Dutiable));
}
$obj->SendExtend['InvoiceRemark'] = '測試發票備註';
$obj->SendExtend['DelayDay'] = '0';
$obj->SendExtend['InvType'] = InvType::General;
*/


//Create order
$obj->CheckOut();

} catch (Exception $e) {
echo $e->getMessage();
}

Let’s move some sensitive information into .env file

$obj->HashKey = env('HASHKEY');                                            
$obj->HashIV = env('HASHIV');
$obj->MerchantID = env('MERCHANTID');

$obj->Send['ReturnURL'] = env('ALLPAYRETURNURL');
$obj->Send['ClientBackURL'] = $request->ClintBackURL;

  • In .env:
    ALLPAYRETURNURL=https://163be100.ngrok.io/api/paymentsResponse

    HASHKEY=5294y06JbISpM5x9
    HASHIV=v77hoKGq4kWxNNIS
    MERCHANTID=2000132

Use use to replace include

  • Delete

    include('AllPay.Payment.Integration.php');
  • Add it in AllPay.Payment.Integration.php到composer.json file

    "autoload-dev": {
    "psr-4": {
    "Tests\\": "tests/"
    },
    "files": [
    "app/Helpers.php",
    "app/AllPay.Payment.Integration.php"
    ]
  • In terminal, under AllPay project

    composer dump-autoload
  • In the PaymentsController, use all those required classes

    namespace App\Http\Controllers;

    use AllInOne;
    use EncryptType;
    use Exception;
    use Illuminate\Http\Request;
    use PaymentMethod;

Create payment order (It depends on your need.)

$totalAmount = Order::getTotalAmountForPayments($orders);
$ordersName = Order::getOrdersNameForPayments($orders);
$MerchantTradeNo = time() . Helpers::createAUniqueNumber();
$MerchantTradeDate = date('Y/m/d H:i:s');
$TradeDesc = 'BuyBuyGo';
$quantity = 1;

//Because I am going to insert data into two tables at a time, so I use
//Transaction of Laravel to prevent the possible inconsistency of two tables

//start transaction
DB::beginTransaction();

//All those scripts below should be executed without errors, otherwise the whole action rollback
try
{
$payment_service_order = new PaymentServiceOrders();

$payment_service_order->user_id = User::getUserID($request);
//Payment service ID
$payment_service_order->payment_service_id = $thirdPartyPaymentService->id;
$payment_service_order->expiry_time = (new Carbon())->now()->addDay(1)->toDateTimeString();
$payment_service_order->MerchantID = env('MERCHANTID');
$payment_service_order->MerchantTradeNo = $MerchantTradeNo;
$payment_service_order->MerchantTradeDate = $MerchantTradeDate;
$payment_service_order->TotalAmount = $totalAmount;
$payment_service_order->TradeDesc = $TradeDesc;
//Item order number
$payment_service_order->ItemName = $ordersName;
$payment_service_order->save();

foreach ($orders as $order)
{
$order_relations = new OrderRelations();
$order_relations->payment_service_id = $thirdPartyPaymentService->id;
$order_relations->payment_service_order_id = $payment_service_order->id;
$order_relations->order_id = $order->id;
$order_relations->save();
}
//Once any errors occur, the whole action stops and rollback, and provide customized error message
} catch (Exception $e)
{
DB::rollBack();

return Helpers::result('false', 'Something went wrong with DB', 400);
}
//If no errors occur, the whole action commit
DB::commit();

Create an API for PaymentsController

  • Add new route in api.php under routes folder
Route::post('pay', 'PaymentsController@pay');

Create a simplest HTML

  • You could simply revise the content of default welcome.blade page as follows:
<!DOCTYPE html>
<html>
<head>
<title>Facebook Login JavaScript Example</title>
<meta charset="UTF-8">
</head>
<body>
// 這邊需輸入PaymentsController的API
<form action="/api/pay" method="POST">
@csrf()
<input type="checkbox" value="1" name="order_id[]">
<input type="checkbox" value="2" name="order_id[]">
<input type="checkbox" value="3" name="order_id[]">
<input type="hidden" value="https://64b30ea0.ngrok.io/" name="ClintBackURL">
<button type="submit">Submit</button>
</form>
</body>
</html>

Simple test

  • Now we are on the default page of Laravel, it should change to the simple form we just created
  • Check nothing, go summit
  • Yes, we successfully arrived payment page

Create a log

  • In order to get what AllPay will send to us after the payment is made, we need to use Log to see what we will receive.
  • Is there some place that all requests and responses will have to go through where we could have full accessibility? It seems to be a perfect one for logging
  • We could make a middleware, and use Log function of Laravel therein to log whatever it goes through
  • Make a middleware, in the terminal under the AllPay project
php artisan make:middleware TestLog
  • 註冊middleware
    • In/app/Http/Kernel.php,register the middleware we just made
protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\TestLog::class,
];
  • Create Log
    • In the TestLog file that we just made, and added:
$response = $next($request);
log::info([
$request->header(),
$request->getMethod(),
$request->getRequestUri(),
$request->all(),
$response->getStatusCode(),
$response->getContent()
]);
return $response;
  • Actually, what you might log varies upon different cases

Create a public url

  • We need to get the response from third party, so we need a public url to get it
  • We could use ngrok to create the public url
  • Install ngrok, please refer to its official website
  • Change ngrok to be globally executable
    • mv ngrok /usr/local/bin
  • Get public url
    ngrok http 8000
  • In terminal under AllPay project
php artisan serve 8000
  • Copy the public url produced by ngrok

Create receive function

  • We are going to catch the response from AllPay, firstly we need to create a function, and after catching it, we could do things there.
    • In PaymentsController, we create a function receive as follows:
      public function receive()
      {

      }
  • Make an API for it
    Route::post('pay', 'PaymentsController@pay');
    Route::post('receive', 'PaymentsController@receive');

Set up client return url

  • In .env, if you’ve been following what I taught, there should be this parameter, just simply paste the public url there

    ALLPAYRETURNURL=https://163be100.ngrok.io/api/recevie
    • Your link will be different from mine, don’t paste mine.

Test payment

  • Let’s connect AllPay payment page again
  • Login the testing buyer account provided by AllPay
    • Account:stageuser001
    • Password:test1234
  • Use the testing credit card provided by AllPay
    • Number:4311-9522-2222-2222
    • Expiry Date:12 / 20 or later than current date
    • Security number:222
  • After payment, let’s check log to see if there is a response from AllPay, in terminal under AllPay project
    cat storage/logs/laravel-2019-02-08.log
  • On, we’ve got the response.
    'MerchantID' => '2000132',
    'MerchantTradeNo' => 'Test1549597724',
    'PayAmt' => '2000',
    'PaymentDate' => '2019/02/08 11:49:03',
    'PaymentType' => 'Credit_CreditCard',
    'PaymentTypeChargeFee' => '20',
    'RedeemAmt' => '0',
    'RtnCode' => '1',
    'RtnMsg' => '交易成功',
    'SimulatePaid' => '0',
    'TradeAmt' => '2000',
    'TradeDate' => '2019/02/08 11:48:44',
    'TradeNo' => '1902081148440800',
    'CheckMacValue' => '5B1EE24B0E9D600C65578DD82D3168E2ED56799453577E17E1EBEFC536BD7EAF',

    Validation

  • Think about it, if your API was accidentally leaked, and some developer knew it. He bought some items from your service, and called your API, how could you tell?
  • So, there is a specific mechanism that only applicable between you and third party payment service
  • The validation mechanism is like a formula every column goes in and out will be calculated with which is only applicable between you and third party, if you’ve been paying attention, you should notice that in the last column of the response we’ve received from AllPay.
  • If you are interested in the formula in detail, you could check it on AllPay’s website.
  • Since in this article, we use AllPay SDK, so we are going to share how to use official SDK to validate the information.
    • Firstly, let’s check a class called CheckMacValue in app/AllPay.Payment.Integration.php
    • Secondly, you could find a function called generate, and you could check it, which it the formula of the CheckMacValue
    • So, we could calculate the received information with this formula, and it should exactly the same as what we’ve got from third party
    • Let’s get all those information except for CheckMacValue from AllPay, we could use the following code.
      $parameters = $paymentResponse->except('CheckMacValue');
    • And then, we assign a variable with the CheckMacValue we’ve got from AllPay
      $receivedCheckMacValue = $paymentResponse->CheckMacValue;
      Then, we calculate the $parameters with function generate to produce correct CheckMacValue
      $calculatedCheckMacValue = CheckMacValue::generate($parameters, env('HASHKEY'), env('HASHIV'), EncryptType::ENC_SHA256);
    • Finally, we compare if two values are identical, if so, it proves the validity of its source. If not, we shouldn’t give any credibility to this information
      if($receivedCheckMacValue == $calculatedCheckMacValue)
      return true;
      return false;

      What’s next?

  • After validating the source, we could do things accordingly
  • For example, if payment is successfully made, what we are going to do, and if not, what then?
  • In this article, we mark the orders as paid and notify the buyers via email after the payment is made.
    if (PaymentServiceOrders::checkIfCheckMacValueCorrect($request) && PaymentServiceOrders::checkIfPaymentPaid($request->RtnCode))
    {
    $paymentServiceOrder = (new PaymentServiceOrders)->where('MerchantTradeNo', $request->MerchantTradeNo)->first();
    $paymentServiceOrder->update(['status' => 1, 'expiry_time' => null]);

    $orderRelations = $paymentServiceOrder->where('MerchantTradeNo', $request->MerchantTradeNo)->first()->orderRelations;
    Order::updateStatus($orderRelations);

    $payerEmail = $paymentServiceOrder->user->email;

    if ($payerEmail !== null)
    Mail::to($payerEmail)->send(new PaymentReceived($paymentServiceOrder, $orderRelations));

    return '1|OK';
    }
  • At the end, don’t forget to return ‘1|OK’ to let AllPay knows that we’ve received the message.

Those I didn’t mentioned

  • With the testing buyer account provided by AllPay, except for credit card, is also supports multiple payment method
  • With convenient store pay or bank transferring method, you could login with a testing backed account provided by AllPay, which could simulate making the payment.
    • Account:StageTest
    • Password:test1234

Refund

  • Refund example as follows:

    public static function refund($order, $paymentServiceInstance, $orderRelation)
    {
    try
    {
    $obj = new AllInOne();

    $obj->ServiceURL = "https://payment-stage.opay.tw/Cashier/AioChargeback";
    // endpoint

    $obj->HashKey = env('HASHKEY');
    // Hash key provided by AllPay

    $obj->HashIV = env('HASHIV');
    // Hash IV provided by AllPay

    $obj->MerchantID = env('MERCHANTID');
    // Merchant ID provided by AllPay

    $obj->EncryptType = EncryptType::ENC_SHA256;
    // CheckMacValue type. Stay 1 as SHA256

    $obj->ChargeBack['MerchantTradeNo'] = $paymentServiceInstance->MerchantTradeNo;
    // The trade number you provided to AllPay

    $obj->ChargeBack['TradeNo'] = $paymentServiceInstance->TradeNo;
    // The trade no provided by AllPay

    $obj->ChargeBack['ChargeBackTotalAmount'] = $order->total_amount;
    // Refunded amount
    $obj->AioChargeback();

    } catch (Exception $e)
    {
    // If something wrong, return.
    return Helpers::result(true, 'Something wrong happened', 200);

    // Debug mode, print out the error
    echo $e->getMessage();
    }

    }
  • You could refer to official document for required parameters

Get information via Facebook graph API Task Scheduling of Laravel

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×