Multipay PHP Payment Gateway Abstraction

repository·master·Indexed 43 days ago

https://github.com/shetabit/multipay

A PHP library providing a unified API to handle multiple payment drivers, such as Zarinpal and custom gateways. It simplifies the process of purchasing, paying, and verifying invoices through a consistent interface, including support for custom driver implementation and a local driver for development simulation.

Tokens
22.4K
Snippets
72
Records
85
Agent score
47%

What's inside Multipay

  1. Run the test suite locally

    master

    If you have PHP and Composer installed, you can run the project's test suite and quality checks using the following commands:

    composer install
    
    composer test           # run the test suite
    composer test-coverage  # run the test suite and report code coverage
    composer check-style    # check the coding style
    composer fix-style      # fix the coding style where possible
    composer analyse        # run static analysis
    composer ci             # run all of the checks above
    composer test
  2. Create a Custom Payment Driver

    master

    To add a new gateway, follow these steps:

    1. Register the driver in your payment.php config file under the drivers array.
    2. Map the driver in the map section of the same config file, pointing to your class.
    3. Implement the Driver class by extending Shetabit\Multipay\Abstracts\Driver. You must implement three core methods:
      • purchase(): Request a transaction ID from the gateway and return it.
      • pay(): Return a RedirectionForm object to redirect the user.
      • verify(): Send a request to the gateway to confirm payment status. Return a Receipt on success or throw an InvalidPaymentException on failure.

    Example Driver Implementation:

    namespace App\Packages\Multipay\Driver;
    
    use Shetabit\Multipay\Abstracts\Driver;
    use Shetabit\Multipay\Exceptions\InvalidPaymentException;
    use Shetabit\Multipay\{Contracts\ReceiptInterface, Invoice, RedirectionForm, Receipt};
    
    class MyDriver extends Driver
    {
        public function __construct(Invoice $invoice, array|object $settings)
        {
            $this->invoice($invoice);
            $this->settings = (object) $settings;
        }
    
        public function purchase() : string|int|null {
            // ... logic to get transaction ID
            $transId = '123';
            $this->invoice->transactionId($transId);
            return $transId;
        }
    
        public function pay() : RedirectionForm {
            $bankUrl = $this->settings->bankApiUrl;
            $payUrl = $bankUrl . $this->invoice->getTransactionId();
            return $this->redirectWithForm($payUrl, [], 'GET');
        }
    
        public function verify(): ReceiptInterface {
            // ... logic to verify
            if (!$isSuccessful) {
                throw new InvalidPaymentException('Payment failed');
            }
            return new Receipt('my_driver', 'receipt_number');
        }
    }
  3. Request a payment purchase

    master

    To initiate a payment, use the Payment class. The purchase method requests a transaction ID from the gateway. It accepts a callback function that is executed once the transaction ID is received. This ID should be stored in your database to verify the payment later.

    You can dynamically set the callbackUrl before calling purchase.

    use Shetabit\Multipay\Invoice;
    use Shetabit\Multipay\Payment;
    
    $paymentConfig = require('path/to/payment.php');
    $payment = new Payment($paymentConfig);
    
    $invoice = (new Invoice)->amount(1000);
    
    $payment->purchase($invoice, function($driver, $transactionId) {
        // Store $transactionId in your database
    });
  4. Configure the payment gateway

    master

    Follow these steps to set up the payment configuration:

    1. Copy the config file: Copy the configuration file from vendor/shetabit/multipay/config/payment.php to your project's config/payment.php directory.
    2. Set the default driver: In config/payment.php, specify your preferred driver in the default key.
    3. Configure driver credentials: Fill in the required credentials (such as API URLs, merchant IDs, and callback URLs) for your chosen driver within the drivers array.
    4. Initialize the Payment class: Create an instance of Shetabit\Multipay\Payment by passing the loaded configuration array to its constructor.
    use Shetabit\Multipay\Payment;
    
    // Load the config file from your project
    $paymentConfig = require('path/to/payment.php');
    
    $payment = new Payment($paymentConfig);
  5. Execute a basic payment flow with Xendit

    master

    To initiate a payment, create an Invoice object, set the amount (in the smallest currency unit, e.g., cents for IDR) and a unique UUID. You can add metadata using the detail() method. Then, initialize the payment via the xendit driver using Payment::via('xendit'), call purchase($invoice), and finally pay() to get the redirect URL.

    use Shetabit//Multipay//Invoice;
    use Shetabit//Multipay//Payment;
    
    // Create an invoice
    $invoice = new Invoice;
    $invoice->amount(100000) // Amount in smallest currency unit (e.g., cents for IDR)
            ->uuid('unique-order-id-123');
    
    // Optional: Add invoice details as metadata
    $invoice->detail('order_id', 'ORD-123')
            ->detail('customer_id', 'CUST-456');
    
    // Initialize payment with Xendit driver
    $payment = Payment::via('xendit');
    
    // Purchase (create invoice)
    $transactionId = $payment->purchase($invoice);
    
    // Redirect user to payment page
    return $payment->pay();
  6. Configure multipay drivers

    master

    To configure the package, follow these steps:

    1. Copy the configuration file from vendor/shetabit/multipay/config/payment.php to a location in your project.
    2. Set your default driver in the config/payment.php file.
    3. Provide the necessary credentials for your chosen driver within the drivers array.

    Example configuration for zarinpal:

    'default' => 'zarinpal',
    
    'drivers' => [
        'zarinpal' => [
            'apiPurchaseUrl' => 'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
            'apiPaymentUrl' => 'https://www.zarinpal.com/pg/StartPay/',
            'apiVerificationUrl' => 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
            'merchantId' => '',
            'callbackUrl' => 'http://yoursite.com/path/to',
            'description' => 'payment in '.config('app.name'),
        ],
        ...
    ]
    // Eg. if you want to use zarinpal.
    'default' => 'zarinpal',
    
    'drivers' => [
        'zarinpal' => [
            // Fill in the credentials here.
            'apiPurchaseUrl' => 'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
            'apiPaymentUrl' => 'https://www.zarinpal.com/pg/StartPay/',
            'apiVerificationUrl' => 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
            'merchantId' => '',
            'callbackUrl' => 'http://yoursite.com/path/to',
            'description' => 'payment in '.config('app.name'),
        ],
        ...
    ]
  7. Use the Local driver for development

    master

    The Local driver simulates a real payment gateway flow for development purposes. It allows you to initiate a payment, render an HTML form with Accept and Cancel buttons, and verify the transaction via a callback URL.

    To use it, specify the driver using via('local'). Calling render() will output an HTML form that redirects to your configured callback URL with a transactionId in the query string.

    If a user clicks 'Cancel' in the simulated form, the verify() method will throw a PurchaseFailedException.

    $invoice = (new Invoice)->amount(10000);
    $payment->via('local')->purchase($invoice, function($driver, $transactionId) {
        // a fake transaction ID is generated and returned.
    })->pay()->render();
  8. Verify a completed payment

    master

    Once a user is redirected back to your site, you must verify that the payment was actually successful. Use the verify() method on the Payment instance, providing the amount and the transactionId you stored earlier.

    If the verification fails, an InvalidPaymentException will be thrown. You should wrap the call in a try-catch block to handle invalid payments gracefully.

    use Shetabit\Multipay\Payment;
    use Shetabit\Multipay\Exceptions\InvalidPaymentException;
    
    $paymentConfig = require('path/to/payment.php');
    $payment = new Payment($paymentConfig);
    
    try {
        // Verify using amount and the stored transaction ID
        $receipt = $payment->amount(1000)->transactionId($transaction_id)->verify();
    
        // Success: access the receipt reference ID
        echo $receipt->getReferenceId();
    } catch (InvalidPaymentException $exception) {
        // Handle invalid payment
        echo $exception->getMessage();
    }
  9. Run tests and checks locally with Composer

    master

    If you have PHP and Composer installed on your machine, you can run the project's test suite, style checks, and static analysis using the following Composer commands:

    composer install
    
    composer test           # Run the test suite
    composer test-coverage  # Run tests and report coverage
    composer check-style    # Check code style
    composer fix-style      # Attempt to fix code style
    composer analyse        # Run static analysis
    composer ci             # Run all checks
  10. Initiate a purchase using the Payment class

    master

    To start a payment process, instantiate the Payment class with your configuration and call the purchase method on an Invoice. The purchase method accepts a callback that provides the $driver and $transactionId. It is highly recommended to store the $transactionId in your database for future verification.

    You can also specify a callbackUrl before calling purchase to change the redirection target at runtime.

    use Shetabit\Multipay\Invoice;
    use Shetabit\Multipay\Payment;
    
    $paymentConfig = require('path/to/payment.php');
    $payment = new Payment($paymentConfig);
    
    $invoice = (new Invoice)->amount(1000);
    
    // Purchase with a callback to save the transaction ID
    $payment->purchase($invoice, function($driver, $transactionId) {
        // Store $transactionId in database
    });
    
    // Purchase with a custom callback URL
    $payment->callbackUrl('http://yoursite.com/verify')->purchase($invoice, function($driver, $transactionId) {
        // Store $transactionId in database
    });
  11. Purchase and redirect using Bajet

    master

    To initiate a payment, create an Invoice and use the Payment class to execute a purchase.

    Required Details:

    • mobile: The customer's mobile number.
    • orderId: A unique string for your order. If omitted, the driver uses the invoice UUID.

    Optional Details:

    • nationalId: Customer's national ID.
    • basketItems: A list of items. Each item must contain a brand (string), productType (numeric), and count (positive integer). Contact Bajet to obtain valid productType values.

    Important: The driver stores the redirection URL in the invoice detail bajetReferUrl. You must preserve this detail if you recreate the invoice before calling pay(). Do not attempt to construct the URL manually from the reference ID.

    use Shetabit\Multipay\Invoice;
    use Shetabit\Multipay\Payment;
    
    $invoice = (new Invoice)->amount(1000)->detail([
        'orderId' => 'merchant-order-123',
        'mobile' => '09120000000',
    ]);
    
    $payment = new Payment($config);
    $payment->via('bajet')->purchase($invoice, function ($driver, $referenceId) {
        // Persist $referenceId, the invoice amount and currency, and
        // $driver->getInvoice()->getDetails() against your local payment attempt.
    });
    
    echo $payment->pay()->render();
  12. Redirect users to a payment gateway

    master

    After calling purchase, you can chain methods to handle the redirection to the third-party payment provider.

    • Use pay()->render() to return a redirection response (e.g., for a web controller).
    • Use pay()->toJson() to get a JSON representation of the redirection data, which is useful if you want to handle the redirection manually via JavaScript.
    // Redirect user to the bank page
    return $payment->purchase(
        (new Invoice)->amount(1000), 
        function($driver, $transactionId) {
            // Store transactionId in database
        }
    )->pay()->render();
    
    // Get JSON format for manual redirection handling
    return $payment->purchase(
        (new Invoice)->amount(1000), 
        function($driver, $transactionId) {
            // Store transactionId in database
        }
    )->pay()->toJson();