mosamy/payment

Simple payment gateway integration for Laravel

Maintainers

Package info

bitbucket.org/mohamedsamy_10/payment

pkg:composer/mosamy/payment

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

1.3.2 2026-04-03 20:11 UTC

This package is auto-updated.

Last update: 2026-04-03 20:11:27 UTC


README

A multi-gateway payment integration package for Laravel. Provides a unified API to initiate payments, store transactions, handle gateway callbacks, and query payment state on any Eloquent model — without coupling your application to a specific provider.

Table of Contents

Requirements

DependencyVersion
PHP>= 8.2
Laravel>= 10
myfatoorah/laravel-package^2.1
stripe/stripe-php (Cashier)^16.5

All other gateways (Tabby, Tamara, PayTabs, Paymob, PayPal, 2Checkout) use Laravel's built-in HTTP client — no additional Composer dependencies required.

Installation

Install via Composer:

composer require mosamy/payment

Laravel's auto-discovery will register Mosamy\Payment\PaymentServiceProvider automatically. No manual provider registration is needed.

Configuration

Publishing Config

Publish the configuration file to your application:

php artisan vendor:publish --tag=payment-config

This creates config/payment.php in your application which you can freely modify.

Environment Variables

Add the following to your .env file according to which gateways you use:

# Active default gateway
PAYMENT_DEFAULT=myfatoorah

# Comma-separated list of enabled gateways (defaults to PAYMENT_DEFAULT if omitted)
ACTIVATED_GATEWAYS=myfatoorah,stripe,tabby,tamara,paytabs,paymob,paypal,2checkout

# Default currency (ISO 4217 — defaults to SAR if omitted)
PAYMENT_CURRENCY=SAR

# ── MyFatoorah ────────────────────────────────────────────────────────────────
MYFATOORAH_API_KEY=your-api-key-here
MYFATOORAH_TEST_MODE=false
MYFATOORAH_COUNTRY_ISO=SAU          # e.g. SAU, KWT, ARE, QAT, BHR, OMN, EGY

# ── Stripe ────────────────────────────────────────────────────────────────────
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...

# ── Tabby ─────────────────────────────────────────────────────────────────────
TABBY_PUBLIC_KEY=pk_test_...
TABBY_SECRET_KEY=sk_test_...
TABBY_MERCHANT_CODE=your-merchant-code
TABBY_TEST_MODE=false

# ── Tamara ────────────────────────────────────────────────────────────────────
TAMARA_API_TOKEN=your-api-token
TAMARA_NOTIFICATION_TOKEN=your-notification-token
TAMARA_TEST_MODE=false

# ── PayTabs ───────────────────────────────────────────────────────────────────
PAYTABS_PROFILE_ID=your-profile-id
PAYTABS_SERVER_KEY=your-server-key
PAYTABS_REGION=saudi                 # saudi, egypt, oman, jordan, uae, global

# ── Paymob ────────────────────────────────────────────────────────────────────
PAYMOB_API_KEY=your-api-key
PAYMOB_INTEGRATION_ID=123456
PAYMOB_IFRAME_ID=654321
PAYMOB_HMAC_SECRET=your-hmac-secret

# ── PayPal ────────────────────────────────────────────────────────────────────
PAYPAL_CLIENT_ID=your-client-id
PAYPAL_CLIENT_SECRET=your-client-secret
PAYPAL_TEST_MODE=false

# ── 2Checkout ─────────────────────────────────────────────────────────────────
TWOCHECKOUT_SELLER_ID=your-seller-id
TWOCHECKOUT_SECRET_KEY=your-secret-key
TWOCHECKOUT_SECRET_WORD=your-secret-word
TWOCHECKOUT_TEST_MODE=false

Full Config File

// config/payment.php
return [

    /*
    |--------------------------------------------------------------------------
    | Default Gateway
    |--------------------------------------------------------------------------
    | The gateway used when no gateway is explicitly specified.
    | Must match a key under "gateways".
    */
    'default' => env('PAYMENT_DEFAULT'),

    /*
    |--------------------------------------------------------------------------
    | Active Gateways
    |--------------------------------------------------------------------------
    | Comma-separated list of enabled gateway keys. Only listed gateways are
    | registered as valid callback route parameters.
    */
    'active' => explode(',', env('ACTIVATED_GATEWAYS', env('PAYMENT_DEFAULT'))),

    /*
    |--------------------------------------------------------------------------
    | Default Currency
    |--------------------------------------------------------------------------
    | Used as the fallback currency when PaymentPayload is constructed without
    | an explicit currency value.
    */
    'currency' => env('PAYMENT_CURRENCY', 'SAR'),

    /*
    |--------------------------------------------------------------------------
    | Transaction Statuses
    |--------------------------------------------------------------------------
    | The allowed values for the transaction status column and PaymentResponse.
    */
    'statuses' => ['pending', 'paid', 'failed', 'refunded', 'cancelled'],

    /*
    |--------------------------------------------------------------------------
    | Gateways
    |--------------------------------------------------------------------------
    | Each entry maps a friendly name to its gateway class and its own config
    | block. You may add custom gateways here.
    */
    'gateways' => [

        'myfatoorah' => [
            'class'  => \Mosamy\Payment\Gateways\MyFatoorah::class,
            'config' => [
                'api_key'     => env('MYFATOORAH_API_KEY'),
                'test_mode'   => env('MYFATOORAH_TEST_MODE', false),
                'country_iso' => env('MYFATOORAH_COUNTRY_ISO', 'SAU'),
            ],
        ],

        'stripe' => [
            'class'  => \Mosamy\Payment\Gateways\Stripe::class,
            'config' => [
                'key'    => env('STRIPE_KEY'),
                'secret' => env('STRIPE_SECRET'),
            ],
        ],

        'tabby' => [
            'class'  => \Mosamy\Payment\Gateways\Tabby::class,
            'config' => [
                'public_key'    => env('TABBY_PUBLIC_KEY'),
                'secret_key'    => env('TABBY_SECRET_KEY'),
                'merchant_code' => env('TABBY_MERCHANT_CODE'),
                'test_mode'     => env('TABBY_TEST_MODE', false),
            ],
        ],

        'tamara' => [
            'class'  => \Mosamy\Payment\Gateways\Tamara::class,
            'config' => [
                'api_token'          => env('TAMARA_API_TOKEN'),
                'notification_token' => env('TAMARA_NOTIFICATION_TOKEN'),
                'test_mode'          => env('TAMARA_TEST_MODE', false),
            ],
        ],

        'paytabs' => [
            'class'  => \Mosamy\Payment\Gateways\PayTabs::class,
            'config' => [
                'profile_id' => env('PAYTABS_PROFILE_ID'),
                'server_key' => env('PAYTABS_SERVER_KEY'),
                'region'     => env('PAYTABS_REGION', 'saudi'),
            ],
        ],

        'paymob' => [
            'class'  => \Mosamy\Payment\Gateways\Paymob::class,
            'config' => [
                'api_key'        => env('PAYMOB_API_KEY'),
                'integration_id' => env('PAYMOB_INTEGRATION_ID'),
                'iframe_id'      => env('PAYMOB_IFRAME_ID'),
                'hmac_secret'    => env('PAYMOB_HMAC_SECRET'),
            ],
        ],

        'paypal' => [
            'class'  => \Mosamy\Payment\Gateways\PayPal::class,
            'config' => [
                'client_id'     => env('PAYPAL_CLIENT_ID'),
                'client_secret' => env('PAYPAL_CLIENT_SECRET'),
                'test_mode'     => env('PAYPAL_TEST_MODE', false),
            ],
        ],

        '2checkout' => [
            'class'  => \Mosamy\Payment\Gateways\TwoCheckout::class,
            'config' => [
                'seller_id'   => env('TWOCHECKOUT_SELLER_ID'),
                'secret_key'  => env('TWOCHECKOUT_SECRET_KEY'),
                'secret_word' => env('TWOCHECKOUT_SECRET_WORD'),
                'test_mode'   => env('TWOCHECKOUT_TEST_MODE', false),
            ],
        ],

    ],
];

Database

Run the package migrations to create the transactions table:

php artisan migrate

The transactions table schema:

ColumnTypeNotes
idbigint (PK)
referencestring (unique)Invoice / order reference, indexed
gatewaystringGateway key (e.g. myfatoorah), indexed
amountdecimal(12,2)
currencystringISO 4217 code (e.g. SAR, USD)
statusenumpending, paid, failed, refunded, cancelled
metajson nullableCustomer data and extra fields
responsejson nullableRaw gateway responses stored per stage
payable_typestring nullablePolymorphic owner type
payable_idbigint nullablePolymorphic owner id
created_attimestamp
updated_attimestamp

The allowed status values are driven by config('payment.statuses'), so they stay in sync if you extend that list.

Supported Gateways

GatewayStatusKey
MyFatoorahFull supportmyfatoorah
StripeFull supportstripe
TabbyFull supporttabby
TamaraFull supporttamara
PayTabsFull supportpaytabs
PaymobFull supportpaymob
PayPalFull supportpaypal
2CheckoutFull support2checkout

Basic Usage

Creating a Payment

The main entry point is the Payment class. Call Payment::gateway() with an optional gateway key (falls back to PAYMENT_DEFAULT), then pass a PaymentPayload to create():

use Mosamy\Payment\Payment;
use Mosamy\Payment\DTO\PaymentPayload;

$payload = new PaymentPayload(
    id: (string) $order->id,           // unique identifier for this invoice
    amount: $order->total,
    currency: 'SAR',
    customer: [
        'name'  => $user->name,
        'email' => $user->email,
        'phone' => $user->phone,        // MyFatoorah requires 05XXXXXXXX format
    ],
    items: [
        ['name' => 'Product A', 'quantity' => 2, 'unit_cost' => 50.00],
    ],
    meta: [
        'payment_method_id' => 2,       // required by MyFatoorah; get list via their API
    ],
    redirectUrl: route('orders.show', $order),
);

$response = Payment::gateway('myfatoorah')  // or omit to use default
    ->payable($order)                        // optional: links transaction to any Eloquent model
    ->create($payload);

if ($response->success) {
    return redirect($response->redirectUrl);
}

// Handle failure
return back()->withErrors($response->response['message'] ?? 'Payment initiation failed.');

->payable($model) is optional. When provided, the transaction row is linked to that model via a polymorphic relation (payable_type / payable_id).

Handling the Callback

The package automatically registers a callback route:

GET /payments/callback/{gateway}/{transaction}

Named: mosamy.payment.callback

The gateway redirects the user back to this URL after payment. The controller resolves the Transaction model via route-model binding, delegates to the correct gateway's handleCallback(), and redirects the user to PaymentResponse::$redirectUrl (or / if none is set).

You do not need to define this route yourself. It is loaded automatically by the service provider.

PaymentPayload DTO

use Mosamy\Payment\DTO\PaymentPayload;

new PaymentPayload(
    id: string,            // Required. Unique invoice identifier (e.g. order id).
    amount: float,         // Required. Amount in the given currency.
    reference: ?string,    // Optional. Defaults to "REF-{id}".
    currency: ?string,     // Optional. Defaults to config('payment.currency') — e.g. 'SAR'.
    items: ?array,         // Optional. Line items for the invoice.
    customer: ?array,      // Optional (required by some gateways). See gateway docs.
    meta: ?array,          // Optional. Extra data stored on the transaction + gateway.
    redirectUrl: ?string,  // Optional. Static URL or a placeholder template — see below.
);

Redirect URL Placeholders

When redirectUrl contains placeholders, they are resolved right after the local transaction is created and before the gateway checkout is initiated. This lets you build dynamic URLs based on data that only exists once the transaction row is persisted.

PlaceholderReplaced with
{transaction_id}The auto-incremented transaction id
{transaction_reference}The transaction reference string
{payment_id}The id value from PaymentPayload

Example — redirect to a transaction detail page without knowing the id upfront:

// No model involved, just a raw Payment::gateway() call.
$payload = new PaymentPayload(
    id: 'INV-45',
    amount: 120.50,
    redirectUrl: '/transactions/{transaction_id}',
);

$response = Payment::gateway()->create($payload);
// If the created transaction id is 981, the callback will redirect to /transactions/981

Example — redirect from an Invoiceable model:

public function toPaymentPayload(?array $meta = []): PaymentPayload
{
    return new PaymentPayload(
        id: (string) $this->id,
        amount: $this->total,
        redirectUrl: "/invoices/{$this->id}",   // static — model id is known here
    );
}

The resolved URL is persisted back into response->invoice->redirectUrl on the transaction, so the callback controller always reads the correct final URL.

CheckoutResponse DTO

Returned by Payment::gateway()->create():

$response->success        // bool    — whether the checkout session was initiated
$response->transactionId  // ?string — the local transaction id
$response->redirectUrl    // ?string — gateway checkout URL to send the customer to
$response->response       // array   — raw gateway response
$response->toArray()      // array   — all fields as an array

When success is true, both redirectUrl and transactionId are guaranteed to be non-null (enforced in the constructor).

PaymentResponse DTO

Returned by Payment::gateway()->handleCallback():

$response->success      // bool    — whether the payment was confirmed
$response->status       // string  — final transaction status written to the database
$response->redirectUrl  // ?string — where to redirect the user after callback
$response->response     // array   — raw gateway callback data

status must be one of the values defined in config('payment.statuses') (pending, paid, failed, refunded, cancelled). An InvalidArgumentException is thrown if an invalid value is provided.

The Invoiceable Contract

Implement Mosamy\Payment\Contracts\Invoiceable on any model that needs to be converted to a PaymentPayload:

use Mosamy\Payment\Contracts\Invoiceable;
use Mosamy\Payment\DTO\PaymentPayload;

class Order extends Model implements Invoiceable
{
    public function toPaymentPayload(?array $meta = []): PaymentPayload
    {
        return new PaymentPayload(
            id: (string) $this->id,
            amount: $this->total,
            currency: $this->currency,
            customer: [
                'name'  => $this->user->name,
                'email' => $this->user->email,
                'phone' => $this->user->phone,
            ],
            meta: array_merge([
                'payment_method_id' => 2,
            ], $meta),
        );
    }
}

When your model also uses the HasTransactions trait, you can call $order->pay() directly (see below).

HasTransactions Trait

Add HasTransactions to any Eloquent model that initiates or owns payments:

use Mosamy\Payment\Traits\HasTransactions;
use Mosamy\Payment\Contracts\Invoiceable;

class Order extends Model implements Invoiceable
{
    use HasTransactions;

    // Must implement toPaymentPayload() when using ->pay()
    public function toPaymentPayload(?array $meta = []): PaymentPayload { ... }
}

Relations

// All transactions belonging to this model
$order->transactions;   // Collection<Transaction>

// The latest transaction
$order->transaction;    // ?Transaction

Computed Attributes

$order->is_paid;              // bool — latest transaction status is 'paid'
$order->has_pending_payment;  // bool — latest transaction status is 'pending'

// Sum of all transactions with the given status (default: 'paid')
$order->totalAmount();
$order->totalAmount('failed');

Scopes

// Eager-load both relations at once
Order::withTransactions()->get();

// Filter models that have at least one transaction with the given status
Order::whereHasTransactions()->get();
Order::whereHasTransactions('failed')->get();

// Add a transactions_sum_amount column (filtered by status)
Order::withTotalAmount()->get();
Order::withTotalAmount('refunded')->get();

Quick Pay

When your model implements Invoiceable, you can initiate a payment in one call:

// Uses default gateway
$response = $order->pay();

// Specify gateway
$response = $order->pay(gateway: 'myfatoorah');

// Pass extra meta merged into toPaymentPayload()
$response = $order->pay(gateway: 'myfatoorah', meta: ['payment_method_id' => 6]);

pay() calls Payment::gateway($gateway)->payable($this)->create($this->toPaymentPayload($meta)) internally.

Transaction Model

Mosamy\Payment\Models\Transaction

Method / AttributeDescription
$transaction->statuspending | paid | failed | refunded | cancelled
$transaction->gatewayGateway key string
$transaction->amountDecimal amount
$transaction->currencyISO 4217 currency code
$transaction->referenceInvoice reference
$transaction->metaArray of stored metadata (includes customer)
$transaction->responseArray with invoice, checkout, and callback sub-keys
$transaction->payableRelated model (polymorphic, nullable)
$transaction->callbackUrlAuto-generated route URL for this transaction

Adding a Custom Gateway

  1. Create a class that extends Mosamy\Payment\Contracts\Gateway:
namespace App\Payments;

use Mosamy\Payment\Contracts\Gateway;
use Mosamy\Payment\DTO\CheckoutResponse;
use Mosamy\Payment\DTO\PaymentPayload;
use Mosamy\Payment\DTO\PaymentResponse;
use Mosamy\Payment\Models\Transaction;

class MyCustomGateway extends Gateway
{
    public function create(PaymentPayload $payload, Transaction $transaction): CheckoutResponse
    {
        // Call external API, get redirect URL ...

        return new CheckoutResponse(
            success: true,
            transactionId: (string) $transaction->id,
            redirectUrl: 'https://pay.example.com/invoice/...',
            response: [],
        );
    }

    public function handleCallback(array $payload, Transaction $transaction): PaymentResponse
    {
        // Verify signature, check status ...

        return new PaymentResponse(
            success: true,
            status: 'paid',
            redirectUrl: $transaction->response['invoice']['redirectUrl'] ?? '/',
            response: $payload,
        );
    }
}
  1. Register the gateway in config/payment.php:
'gateways' => [
    // ...
    'myprovider' => [
        'class'  => \App\Payments\MyCustomGateway::class,
        'config' => [
            'api_key' => env('MYPROVIDER_API_KEY'),
        ],
    ],
],
  1. Set your env:
PAYMENT_DEFAULT=myprovider

That's it. The route constraint, transaction creation, and callback flow are all handled automatically.

Gateway Reference

MyFatoorah

Required customer fields:

FieldFormatExample
namestring"Ahmed Al-Zaidi"
emailvalid email"a@example.com"
phone05XXXXXXXX"0512345678"

Required meta fields:

FieldTypeDescription
payment_method_idintegerPayment method ID from MyFatoorah API

Notes:

  • country_iso controls which regional MyFatoorah endpoint is used.
  • MobileCountryCode is currently hardcoded to +966. If your application targets other regions, override by extending the MyFatoorah gateway class and overriding payload().
  • Callback uses the same URL for success and error redirects. MyFatoorah appends ?paymentId=... on success.

Stripe

Required customer fields:

FieldFormatExample
emailvalid email"a@example.com"

Optional items fields (falls back to a single line item from amount if omitted):

FieldTypeDescription
namestringProduct name
quantityintegerDefaults to 1
unit_costfloatUnit price
currencystringOverrides payload currency

Notes:

  • Stripe creates a Checkout Session; the customer is redirected to Stripe's hosted page.
  • On success, Stripe appends ?session_id=... to the callback URL.
  • On cancel, Stripe appends ?canceled=1 to the callback URL.

Tabby

Required customer fields:

FieldFormatExample
namestring"Ahmed Al-Zaidi"
emailvalid email"a@example.com"
phonestring"+966512345678"

Optional meta fields:

FieldTypeDescription
citystringShipping city
addressstringShipping address
zipstringPostal code
langstringLanguage code (default: en)

Notes:

  • Tabby is a BNPL (Buy Now Pay Later) provider popular in the MENA region.
  • Uses Tabby Checkout API v2. The customer is redirected to Tabby's hosted checkout page.
  • Callback receives ?payment_id=... from Tabby.
  • Supports installments through their available products configuration.

Tamara

Required customer fields:

FieldFormatExample
namestring"Ahmed Al-Zaidi"
emailvalid email"a@example.com"
phonestring"+966512345678"

Optional meta fields:

FieldTypeDescription
country_codestringISO country code (default: SA)
payment_typestringPAY_BY_INSTALMENTS, PAY_BY_LATER etc.
instalmentsintNumber of instalments (default: 3)
citystringShipping city
addressstringShipping address
taxfloatTax amount
langstringLocale (default: en_US)

Notes:

  • Tamara is a BNPL provider for the MENA region.
  • After checkout, the order must be authorised (handled automatically in handleCallback).
  • Callback receives ?order_id=... from Tamara.

PayTabs

Required customer fields:

FieldFormatExample
namestring"Ahmed Al-Zaidi"
emailvalid email"a@example.com"
phonestring"0512345678"

Optional meta fields:

FieldTypeDescription
addressstringBilling address
citystringBilling city
statestringBilling state
country_codestringISO country code (default: SA)
zipstringPostal code

Config region values: saudi, egypt, oman, jordan, uae, global

Notes:

  • PayTabs supports multiple MENA regions. Set PAYTABS_REGION to route to the correct endpoint.
  • Callback receives ?tranRef=... from PayTabs.
  • Response status A = Authorised (paid), H = Hold, D = Declined, E = Error, V = Voided.

Paymob

Required customer fields:

FieldFormatExample
emailvalid email"a@example.com"

Optional customer / meta fields:

FieldTypeDescription
namestringCustomer full name (split into first/last)
phonestringCustomer phone
addressstringBilling street address
citystringBilling city
statestringBilling state
zipstringPostal code
country_codestringISO country code (default: EG)

Notes:

  • Paymob uses a 3-step flow: authenticate → register order → generate payment key.
  • The customer is redirected to Paymob's iframe with the payment token.
  • Callback receives success, is_pending, and other parameters from Paymob.
  • You need to create an integration and iframe in your Paymob dashboard.

PayPal

Required customer fields:

FieldFormatExample
emailvalid email"a@example.com"

Optional meta fields:

FieldTypeDescription
localestringPayPal locale (default: en-US)

Notes:

  • Uses PayPal REST API v2 (Orders). No SDK dependency required.
  • Creates a checkout order, redirects to PayPal for approval, then captures on callback.
  • On success, PayPal redirects with ?token=...&PayerID=....
  • On cancel, the callback URL receives ?canceled=1.
  • test_mode toggles between sandbox (api-m.sandbox.paypal.com) and live (api-m.paypal.com).

2Checkout

Required customer fields:

FieldFormatExample
namestring"Ahmed Al-Zaidi"
emailvalid email"a@example.com"

Optional customer / meta fields:

FieldTypeDescription
phonestringCustomer phone
addressstringBilling address
citystringBilling city
zipstringPostal code
country_codestringISO country code (default: US)
langstringLanguage (default: en)

Notes:

  • Uses the 2Checkout (now Verifone) REST API v6.
  • Falls back to hosted Buy-Link checkout if the API order creation doesn't return a 3DS redirect.
  • Callback receives ?refno=... (reference number) from 2Checkout.
  • test_mode controls both the API test flag and the hosted checkout URL (sandbox vs secure).
  • Order statuses: COMPLETE/AUTHRECEIVED → paid, CANCELED → cancelled, REFUND → refunded.

License

MIT — see LICENSE for details.