hakito/cakephp-paypal-checkout

PayPalCheckout plugin for CakePHP

Installs: 81

Dependents: 0

Suggesters: 0

Security: 0

Type:cakephp-plugin

v1.3 2024-01-15 19:47 UTC

This package is auto-updated.

Last update: 2024-04-18 07:05:25 UTC


README

This plugin can be used for the backend part of the PayPal Checkout API.

Installation

You can install this plugin into your CakePHP application using composer.

The recommended way to install composer packages is:

composer require hakito/paypal-checkout

Load the plugin

In your plugins.php add

'PayPalCheckout' => [
    'routes' => true
],

Configuration

In your app.php you have to setup the api credentials:

'PayPalCheckout' => [
    //'Mode' => 'sandbox', // optional set the current mode
    'ClientId' => 'CLIENT ID',
    'ClientSecret' => 'CLIENT SECRET'
],

For some basic logging you can add this to the Log section:

'PayPalCheckout' => [
    'className' => FileLog::class,
    'path' => LOGS,
    'file' => 'PayPalCheckout',
    'scopes' => ['PayPalCheckout'],
    'levels' => ['warning', 'error', 'critical', 'alert', 'emergency', 'info'],
]

Usage

Implement the client side according to the api docs. Example

<?php

use Cake\Core\Configure;
?>
<script src="https://www.paypal.com/sdk/js?client-id=<?= Configure::read('PayPalCheckout.ClientId') ?>&currency=EUR&locale=de_AT"></script>
<script lang="javascript" type="text/javascript">
window.paypal
  .Buttons({
    style: {
        layout: 'horizontal',
        label: 'buynow'
    },
    async createOrder() {
      try {
        const response = await fetch("/paypal-checkout/Orders/create", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
        });

        const orderData = await response.json();

        if (orderData.abort) // example for handling your own data from the controller
        {
          if (orderData.abort == 'already paid')
          {
            window.location.reload();
          }
          else
            throw new Error(orderData.abort);
        }

        if (orderData.id) {
          return orderData.id;
        } else {
          const errorDetail = orderData?.details?.[0];
          const errorMessage = errorDetail
            ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
            : JSON.stringify(orderData);

          throw new Error(errorMessage);
        }
      } catch (error) {
        console.error(error);
        resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
      }
    },
    async onApprove(data, actions) {
      try {
        const response = await fetch(`/paypal-checkout/Orders/capture/${data.orderID}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
        });

        const orderData = await response.json();

        // Three cases to handle:
        //   (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
        //   (2) Other non-recoverable errors -> Show a failure message
        //   (3) Successful transaction -> Show confirmation or thank you message

        const errorDetail = orderData?.details?.[0];

        if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
          // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
          // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
          return actions.restart();
        } else if (errorDetail) {
          // (2) Other non-recoverable errors -> Show a failure message
          throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
        } else if (!orderData.purchase_units) {
          throw new Error(JSON.stringify(orderData));
        } else {
          // (3) Successful transaction -> Show confirmation or thank you message
          // Or go to another URL:  actions.redirect('thank_you.html');
          console.log('redirecting');
          actions.redirect(orderData.redirectSuccessUrl);
        }
      } catch (error) {
        console.error(error);
        resultMessage(
          `Sorry, your transaction could not be processed...<br><br>${error}`,
        );
      }
    },
  })
  .render("#paypal-button-container");

// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message) {
  const container = document.querySelector("#result-message");
  container.innerHTML = message;
}

</script>

Event handling

You have to handle two events in your application:

EventManager::instance()
    ->on('PayPalCheckout.CreateOrder', OrdersController::createPayPalOrder(...))
    ->on('PayPalCheckout.CaptureOrder', PaymentCallbacks::capturePayPalOrder(...));

CreateOrder events

Fired when a request for payment is built. You can setup the ordered items and amounts here.

The function has to return an array with the order data. Either build the array on your own or use the OrderBuilder.

public static function createPayPalOrder(Event $event)
{
    // Optional you can also send back custom data and handle it on the client side
    if (ORDER_ALREADY_AID)
        return ['abort' => ['already paid']];

    $purchaseUnitBuilder = new PurchaseUnitBuilder(new AmountBuilder('USD', '123.56'));
    $purchaseUnitBuilder->ReferenceId('YOUR ORDER'); // optionally set your order id here

    return (new OrderBuilder())
        ->add($purchaseUnitBuilder)
        ->Build();
}

CaptureOrder event

Fired when the payment has been completed.

public static function capturePayPalOrder(Event $event, $args)
{
    $data = $event->getData('body');
    if ($data->status != 'COMPLETED')
    {
        Log::error("Captured PayPal checkout payment is not COMPLETED but $data->status");
        return;
    }
    $purchaseUnit = $data->purchase_units[0];
    $orderId = $purchaseUnit->reference_id;
    // Capture ID for issuing refunds
    $captureId = $purchaseUnit->payments->captures[0]->id;
    // DO YOUR PAYMENT HANDLING HERE ($orderId, $captureId);
    return ['redirectSuccessUrl' => 'http://payment-success.example.com'];
}

Refund a payment

You can do a full refund of the payment in a controller.

$this->loadComponent('PayPalCheckout.Checkout', Configure::read('PayPalCheckout'));
$this->Checkout->refundPayment('CAPTURE_ID_OF_YOUR_ORDER');