Easy Payment Gateway with PayPal REST API

Introduction

In this article, we are going to share how to do the follows with PayPal REST API

  1. Create authorization order
  2. Authorization
  3. Capture
  4. Refund
  5. Place funds on hold

Since it’s a learning technical diary, it will contain my personal project. You could selectively refer to this article.

Install PayPal REST API official SDK

In this article, we use the latest released version of the official SDK

Install

composer require paypal/paypal-checkout-sdk

Setting

Personal setting

  • After installation, you could find the example under SDK directory as photo below:

  • Configure PayPal Client.php as follows: 1. Apply for a developer account and login 2. Create an App 3. Get your Client ID and Secret
    4. Fill in with the got Client ID and Secret, Ray set them as environment variables.

public static function environment()
{
$clientId = env('PAYPAL_SANDBOX_API_ClientID');
$clientSecret = env('PAYPAL_SANDBOX_API_SECRET');
return new SandboxEnvironment($clientId, $clientSecret);
}

Let’s begin

As mentioned previously, we could find almost all of the examples for every situation in the official sample directory. You could customize it according to your need. The following will be Ray’s version.
If you have any questions, feel free to refer to the samples under sample directory, and the official document as follows:
order
payment

The difference between order and payment

Here are some main differences:

  • order: It only supports members of PayPal. You could delay your payment, and partially capture the payment upon what you need.
  • payment: You could delay the payment, but you can’t partially capture it.

For more information, you could refer to the official document

Create an order

public function createOrder($toBeSavedInfo, Recipient $recipient, $debug = false)
{
// Use SDK
$request = new OrdersCreateRequest();
$request->headers["prefer"] = "return=representation";

// We will show this part later.
$request->body = self::buildRequestBody($toBeSavedInfo, $recipient);

// We use the PayPalClient we just set up
$client = PayPalClient::client();
$response = $client->execute($request);
if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Order ID: {$response->result->id}\n";
print "Intent: {$response->result->intent}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}

// After the order is created, I only take the approve link. In default, PayPal provides several links for difference usage. However, we could achieve others except for approve with REST API.
foreach (($response->result->links) as $link)
{
if ($link->rel === 'approve')
{
$linkForApproval = $link->href;
break;
}
}

// We get some information which we are going to use later, and return.
$toBeSavedInfo['payment_id'] = $response->result->id;
$toBeSavedInfo['statusCode'] = $response->statusCode;
$toBeSavedInfo['custom_id'] = $response->result->purchase_units[0]->custom_id;
$toBeSavedInfo['PayPal_total_amount'] = $response->result->purchase_units[0]->amount->value;
$toBeSavedInfo['orderStatus'] = $response->result->status;
$toBeSavedInfo['linkForApproval'] = $linkForApproval;

return $toBeSavedInfo;
}

Please refer to the RequestBody for creating an order as follows:

public static function buildRequestBody($toBeSavedInfo, Recipient $recipient)
{
// The setting here allows us to see items in detail in PayPal payment page
$item = [];
$i = 1;
foreach ($toBeSavedInfo['orders'] as $order)
{
$item[] = [
'name' => $order->item_name,
'description' => $order->item_description,
'sku' => $i,
'unit_amount' => [
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $order->unit_price,
],
'quantity' => $order->quantity,
];
$i ++;
}

// Here we specify the intent, which I had set as a environment variable
return [
'intent' => env('PAYPAL_SANDBOX_INTENT_OF_CREATED_ORDERS'),
'application_context' =>
[
'return_url' => env('PAYPAL_SANDBOX_RETURN_URL'),
'cancel_url' => env('PAYPAL_SANDBOX_CANCEL_URL'),
'brand_name' => env('APP_NAME'),
'locale' => env('PAYPAL_SANDBOX_LOCALE'),
'landing_page' => env('PAYPAL_SANDBOX_LANDING_PAGE'),
'shipping_preferences' => env('PAYPAL_SANDBOX_SHIPPING_PREFERENCES'),
'user_action' => env('PAYPAL_SANDBOX_USER_ACTION'),
],
// Here we could set purchase_unit. We could set tax, shipping fee, etc... under purchase_unit. We will skip those here.
'purchase_units' =>
[
[
'custom_id' => $toBeSavedInfo['merchant_trade_no'],
'amount' =>
[
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $toBeSavedInfo['total_amount'],
'breakdown' =>
[
'item_total' =>
[
'currency_code' => $toBeSavedInfo['mc_currency'],
'value' => $toBeSavedInfo['total_amount'],
],
],
],

'items' => $item,
// We could specify the recipient here.
'shipping' =>
array(
'name' =>
array(
'full_name' => $recipient->name,
),
'address' =>
array(
'address_line_1' => $recipient->others,
'admin_area_2' => $recipient->district,
'admin_area_1' => $recipient->city,
'postal_code' => $recipient->postcode,
'country_code' => $recipient->country_code,
),
),
],
],
];
}

Authorization

Now, we are going to use one of the important functions in REST API, authorization.

  • After authorization, we will be able to capture the authorized amount within 29 days. However, PayPal only guarantees that the authorized amount will be available for three days right after a single authorization.
  • It means that PayPal will temporarily place authorized funds on hold within buyer’s account for only three days after authorization. Please note that it’s only for three days, and it’s called Honor Period.
  • After the first authorization, we will be able to apply for multiple authorization up to 10 times, which is called reauthorize
    If you think the number is too less, you could contact PayPal, and raise the number to up to 99 times.
  • Actually if you count the time properly and precisely, 10-time authorization should be enough. If you authorize once every three days, you could place funds on hold for a month with 10-time authorization, which is even enough for sea fright.
  • You will be allowed to change the order amount for up to 115% or not more than USD 75. You could use this feature when there are some required changes on tax or shipping fee.
  • You could refer to official document

Here is the authorization example as follows.

Please note that it’s Ray’s version. You could refer to official version and then modify it according to your need, or make one on our own with a reference on its SDK, which I think might be the best solution.

/**
* This function can be used to perform authorization on the approved order.
* Valid Approved order id should be passed as an argument.
*/
// Here we could revise the authorized amount upon your need.
public static function authorizeOrder($orderId, $amount = null, $debug = false)
{
$request = new OrdersAuthorizeRequest($orderId);
// RequestBody as mentioned above. You could refer to the official example.
$request->body = self::buildRequestBodyForAuthorizeOrder($amount);

$client = PayPalClient::client();
$response = $client->execute($request);
if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Order ID: {$response->result->id}\n";
print "Authorization ID: {$response->result->purchase_units[0]->payments->authorizations[0]->id}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
print "Authorization Links:\n";
foreach ($response->result->purchase_units[0]->payments->authorizations[0]->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}

return $response;
}

After authorization, we need to check if the authorization is successful, and therefore Ray made a validation function with the response got from the SDK

public static function checkIfAuthorizedSuccessfully($response)
{
$newPayPal = (new NewPayPal())->where('payment_id', request()->token)->first();
// Check if the authorization is completed
if (($response->result->status) !== 'COMPLETED')
return 'Authorization isn\'t completed';

// Check if the authorization has started.
if (($response->result->purchase_units[0]->payments->authorizations[0]->status) !== 'CREATED')
return 'Authorization was not created';

// Check if the currency is matched
if (($response->result->purchase_units[0]->payments->authorizations[0]->amount->currency_code) !== ($newPayPal->mc_currency))
return 'The currency is mismatched';

// Check if authorized amount is correct. Here is my own version, and you could revise the amount if you need.
if (intval($response->result->purchase_units[0]->payments->authorizations[0]->amount->value) !== ($newPayPal->total_amount))
return 'The total amount is not correct';
}

Capture

  • Just like mentioned above, we will be allowed to capture the order after a successful authorization.
  • However, PayPal only guarantees the authorized amount will be available for three days, which is also called honor period.
  • Generally speaking, a proper and precise time counting with authorization and reauthorizing will be able to temporarily place funds on hold for 30 days.

Here is the capture example

public static function captureAuthorization(NewPayPal $newPayPal, $final_capture = false, $debug = false)
{
$NewPayPal = (new NewPayPal)->where('merchant_trade_no', $newPayPal->merchant_trade_no)->first();
// Capture function requires an authorization id
$request = new AuthorizationsCaptureRequest($newPayPal->authorization_id);
// Here we need to fill in to-be-captured amount. As mentioned above, amount capture could be partial. If final_capture is set to true, this authorization will end, and reauthorizing will be required for any further capture.
$request->body = self::buildRequestBodyForCaptureAuthorization($NewPayPal->to_be_captured_amount, $final_capture, $newPayPal->mc_currency);
$client = PayPalClient::client();
$response = $client->execute($request);

if ($debug)
{
print "Status Code: {$response->statusCode}\n";
print "Status: {$response->result->status}\n";
print "Capture ID: {$response->result->id}\n";
print "Links:\n";
foreach ($response->result->links as $link)
{
print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
}
// To toggle printing the whole response body comment/uncomment below line
echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
}
return $response;
}

Here is RequestBody for capture.

public static function buildRequestBodyForCaptureAuthorization($amount = null, $final_capture = false, $currency = 'USD')
{
if ($amount != null)
{
// We specify the amount and currency of capture, which need to match the ones for authorization.
return [
"amount" => [
'currency_code' => $currency,
'value' => $amount,
],
'final_capture' => $final_capture
];
}

return "{}";
}

Here is the logic of capture as follows:

Ray’s logic is to set a to-be-captured amount to decide when the amount will be captured, which could save handing fee because each time a refund request will cost some handing fee. So Ray’s logic is to place funds on hold, and only revise to-be-captured amount when a refund request is made before to-be-captured date. Therefore, we could at the most extend save the handing fee on the seller side. The allowable refund period is 7 days in Ray’s logic, and capture the authorization with the final to-be-captured amount.
So the following function only runs once a day. If the current time is beyond the to-be-captured date, the authorization will be captured and update order state accordingly.

public static function dailyCaptureAuthorization()
{
$toBeCapturedPayments = NewPayPal::whereNotNull('authorization_id')->whereNull('capture_id')->where('to_be_captured_date', '<', Carbon::now()->toDateTimeString())->get();
foreach ($toBeCapturedPayments as $toBeCapturedPayment)
{
$response = NewPayPal::captureAuthorization($toBeCapturedPayment);
if (($response->result->status) === 'COMPLETED')
{
$toBeCapturedPayment->update(['capture_id' => $response->result->id, 'status' => 7]);
foreach ($toBeCapturedPayment->orderRelations as $orderRelation)
{
if (($orderRelation->status == 5) || ($orderRelation->status == 6))
{
$orderRelation->order->update(['status' => 7]);
$orderRelation->update(['status' => 7]);
}
}
}
}
}

Refund

  • The rule of refund is to refund an amount of money partially or one-time towards specific authorization.
  • If you specify an amount, you refund the order partially.
  • If you would like to fund it at a time, you could leave a empty RequestBody as the official example.
    public static function refundOrder($captureId, $amount, $currency, $debug = false)
    {
    $request = new CapturesRefundRequest($captureId);
    // The required to-be-refunded amount and currency should match the ones of the authorization.
    $request->body = self::buildRequestBodyForRefundOrder($amount, $currency);
    $client = PayPalClient::client();
    $response = $client->execute($request);

    if ($debug)
    {
    print "Status Code: {$response->statusCode}\n";
    print "Status: {$response->result->status}\n";
    print "Order ID: {$response->result->id}\n";
    print "Links:\n";
    foreach ($response->result->links as $link)
    {
    print "\t{$link->rel}: {$link->href}\tCall Type: {$link->method}\n";
    }
    // To toggle printing the whole response body comment/uncomment below line
    echo json_encode($response->result, JSON_PRETTY_PRINT), "\n";
    }

    return $response;
    }

The following is the RequestBody for refund.

public static function buildRequestBodyForRefundOrder($amount = null, $currency = 'USD', $final_capture = false)
{
// If the amount is specified, it will be a specified amount. If not, it will be default setting.
if ($amount != null)
{
return [
"amount" => [
'currency_code' => $currency,
'value' => $amount,
],
'final_capture' => $final_capture
];
}

return "{}";
}

The logic for refund.

  • Corresponding to the capture action, before the seller make any capture to buyers, all those refund requests from buyers are only to revise numbers in the database.
  • If 7 days have passed, but in a particular case, a refund request still raised by a buyer, the amount could still be refunded with inevitable handing fee.
  • All logic mentioned above is only for PayPal. Since this project integrate with two payment gateway, the above mentioned logic doens’t suit AllPay. However, generally speaking, it makes no difference to buyers.
    public static function refund(Order $order, NewPayPal $paymentServiceInstance, OrderRelations $orderRelation)
    {
    // When the order is authorized but not yet captured.
    if (($paymentServiceInstance->capture_id === null) && ($paymentServiceInstance->authorization_id !== null))
    {
    // As mentioned above, we only revise to-be-captured amount in the database.
    $paymentServiceInstance->update([
    'to_be_captured_amount' => $paymentServiceInstance->to_be_captured_amount - $order->total_amount,
    'total_amount' => $paymentServiceInstance->total_amount - $order->total_amount
    ]);
    $order->update(['status' => 4]);
    $orderRelation->update(['status' => 4]);
    }

    // When the order has been captured
    if ($paymentServiceInstance->capture_id !== null)
    {
    // We do implement refund API, returning the amount to buyers.
    $response = self::refundOrder($paymentServiceInstance->capture_id, $order->total_amount, $paymentServiceInstance->mc_currency);
    // If the refund is completed, the order state will be updated.
    if ($response->result->status == 'COMPLETED')
    {
    $order->update(['status' => 4]);
    $orderRelation->update(['status' => 4]);
    $paymentServiceInstance->update([
    'total_amount' => $paymentServiceInstance->total_amount - $order->total_amount
    ]);
    }
    }
    }

Cancel an authorization.

Cancelling an authorization is pretty easy with official example. You only have to provide authorization ID with required format, we are not going to explain explicitly.
The authorization ID will be returned after a successful authorization, so remember to save it.

Get authorization data.

Getting an authorization is pretty easy with official example. You only have to provide authorization ID with required format, we are not going to explain explicitly.
The authorization ID will be returned after a successful authorization, so remember to save it.

Getting capture data

Getting a capture data is pretty easy with official example. You only have to provide authorization ID with required format, we are not going to explain explicitly.
The capture ID will be returned after a successful authorization, so remember to save it.

Conclusion

According to the official document, you could use Smart Button of JavaScript SDK with PayPal REST APIO. However, Ray is responsible for backend, so this part wasn’t deeply dug.
It looks interesting. If you are interested, you could spend some time on it.
PayPal is veritably an international payment gateway. It provides various features and supports. What a pity that PayPal has taken back its service from Taiwan. However, as far as I know, it happened due to tax safeguarding in Taiwan. It’s hard to judge good or bad.
I’ve spent some time those days digging in PayPal gateway. Surely there are still some dedicate features that I haven’t tried. I will write another article for that after I give it a shot!

You are free to share this article wherever you want, but kindly cite the source. Thanks!

A flexible git flow Implement a transaction via PayPal Payment Standard and PayPal IPN Message

Comments

Your browser is out-of-date!

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

×