hamoi1/laravel-fib-payment

A Laravel package for integrating with the First Iraqi Bank (FIB) payment gateway, providing seamless payment processing and management.

Maintainers

Package info

github.com/Hamoi1/laravel-fib-payment

Homepage

pkg:composer/hamoi1/laravel-fib-payment

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

v1.0.0 2026-04-01 17:51 UTC

This package is auto-updated.

Last update: 2026-04-05 10:55:11 UTC


README

Latest Version on Packagist Supported PHP Version Laravel Versions License

A modern, type-safe Laravel SDK for First Iraqi Bank (FIB) payment integration. This package provides a fluent API for payment creation, status tracking, cancellations, refunds, and webhook protection with full dependency injection support.

Table of Contents

Requirements

  • PHP 8.2+
  • Laravel 11.x, 12.x, or 13.x
  • FIB Payment Gateway credentials (client_id and client_secret)

Installation

Install via Composer:

composer require hamoi1/laravel-fib-payment

The package will auto-register via Laravel's discovery. Publish the configuration file:

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

Configuration

Add the following to your .env file:

# Required
FIB_CLIENT_ID=your-fib-client-id
FIB_CLIENT_SECRET=your-fib-client-secret
FIB_ENVIRONMENT=stage

# Optional
FIB_CALLBACK_URL=https://your-app.test/fib/webhook
FIB_CURRENCY=IQD
FIB_TIMEOUT=15
FIB_CONNECT_TIMEOUT=5
FIB_RETRY_TIMES=3
FIB_RETRY_DELAY=200
FIB_CACHE_DRIVER=
FIB_REFUNDABLE_FOR=P7D
FIB_ALLOWED_CALLBACK_IPS=

Environment Variables Reference

Variable Required Default Description
FIB_CLIENT_ID Yes - OAuth2 client ID from FIB
FIB_CLIENT_SECRET Yes - OAuth2 client secret from FIB
FIB_ENVIRONMENT Yes stage Environment: stage, or prod
FIB_CALLBACK_URL No http://127.0.0.1:8000/fib/callback Webhook endpoint for status updates
FIB_CURRENCY No IQD Default currency: IQD or USD
FIB_TIMEOUT No 15 HTTP request timeout in seconds
FIB_CONNECT_TIMEOUT No 5 Connection timeout in seconds
FIB_RETRY_TIMES No 3 Number of retry attempts for failed requests
FIB_RETRY_DELAY No 200 Delay between retries in milliseconds
FIB_CACHE_DRIVER No - Cache driver for OAuth tokens (null = default)
FIB_REFUNDABLE_FOR No P7D ISO 8601 duration for refund window
FIB_ALLOWED_CALLBACK_IPS No - Comma-separated IPs for webhook security

Environments

Environment Base URL Use Case
stage https://fib.stage.fib.iq Testing with FIB staging apps
prod https://fib.prod.fib.iq Production transactions

Architecture Overview

Design Principles

  • Dependency Injection: No static methods - everything is injectable
  • Type Safety: PHP 8.2+ features with readonly classes and strict types
  • DTO Pattern: Data Transfer Objects for all API inputs/outputs
  • Enum Safety: Enums for currencies, environments, statuses, and error reasons
  • Exception Hierarchy: Specific exceptions for different error types
  • Token Caching: Smart OAuth2 token caching with configurable drivers

Usage

1. Quick Start with Facade

The simplest way to use the package:

use Hamoi1\FibPayment\Facades\Fib;
use Hamoi1\FibPayment\Data\PaymentRequest;
use Hamoi1\FibPayment\Enums\Currency;

// Create a payment
$response = Fib::createPayment(new PaymentRequest(
    amount: 25000,
    currency: Currency::IQD,
    description: 'Order #1024',
    callbackUrl: route('fib.webhook'),
));

// Access response data
$paymentId = $response->paymentId;
$readableCode = $response->readableCode;
$qrCode = $response->qrCode;

2. Check Payment Status

use Hamoi1\FibPayment\Facades\Fib;

$status = Fib::getPaymentStatus($paymentId);

// Helper methods
if ($status->isPaid()) {
    // Payment completed successfully
    $payerName = $status->paidBy?->name;
    $payerIban = $status->paidBy?->iban;
}

if ($status->isUnpaid()) {
    // Payment still pending
    $expiresAt = $status->validUntil;
}

if ($status->isDeclined()) {
    // Payment declined
    $reason = $status->decliningReason?->value;
}

3. Cancel or Refund Payments

// Cancel an unpaid payment
$cancelled = Fib::cancelPayment($paymentId); // bool

// Refund a paid payment (within refundable window)
$refunded = Fib::refundPayment($paymentId); // bool

4. Dependency Injection (Recommended)

For better testability and cleaner architecture:

use Hamoi1\FibPayment\Contracts\FibClientInterface;
use Hamoi1\FibPayment\Data\PaymentRequest;

final class CheckoutController
{
    public function __construct(
        private readonly FibClientInterface $fib
    ) {}

    public function store(Request $request): JsonResponse
    {
        $payment = $this->fib->createPayment(new PaymentRequest(
            amount: $request->input('amount'),
            description: "Order #{$request->input('order_id')}",
        ));

        return response()->json([
            'payment_id' => $payment->paymentId,
            'code' => $payment->readableCode,
        ]);
    }
}

Error Handling

The package throws specific exceptions for different error scenarios:

Exception Hierarchy

FibException (abstract base)
├── AuthenticationException (401/403)
├── RateLimitException (429)
└── PaymentFailedException (other HTTP errors)

Handling Examples

use Hamoi1\FibPayment\Exceptions\AuthenticationException;
use Hamoi1\FibPayment\Exceptions\RateLimitException;
use Hamoi1\FibPayment\Exceptions\PaymentFailedException;
use Hamoi1\FibPayment\Exceptions\FibException;

try {
    $response = Fib::createPayment($request);
} catch (AuthenticationException $e) {
    // Invalid credentials or token expired
    // HTTP 401 or 403
    Log::error('FIB auth failed: ' . $e->getMessage());
} catch (RateLimitException $e) {
    // Too many requests
    // HTTP 429
    Log::warning('FIB rate limit hit');
    // Retry after delay
} catch (PaymentFailedException $e) {
    // Other API errors (500, 400, etc.)
    Log::error('FIB payment error: ' . $e->getMessage());
} catch (FibException $e) {
    // Catch-all for any package exception
    Log::error('FIB error: ' . $e->getMessage());
}

Webhook Protection

Using the Middleware

Protect your webhook endpoint from invalid requests:

use Hamoi1\FibPayment\Http\Middleware\VerifyFibWebhook;
use Illuminate\Support\Facades\Route;

Route::post('/fib/webhook', [PaymentController::class, 'handleWebhook'])
    ->middleware(VerifyFibWebhook::class)
    ->name('fib.webhook');

Middleware Validation

The VerifyFibWebhook middleware validates:

  1. Required Fields: Ensures paymentId and status are present
  2. Valid Status: Checks status is one of PAID, UNPAID, or DECLINED
  3. IP Allowlist: If FIB_ALLOWED_CALLBACK_IPS is set, validates request IP

Webhook Controller Example

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Hamoi1\FibPayment\Enums\PaymentStatus;

final class PaymentController extends Controller
{
    public function handleWebhook(Request $request): JsonResponse
    {
        $paymentId = $request->input('paymentId');
        $status = PaymentStatus::from($request->input('status'));

        match ($status) {
            PaymentStatus::PAID => $this->markOrderAsPaid($paymentId),
            PaymentStatus::DECLINED => $this->markOrderAsDeclined($paymentId),
            PaymentStatus::UNPAID => null, // No action needed
        };

        return response()->json(['received' => true]);
    }

    private function markOrderAsPaid(string $paymentId): void
    {
        // Update your database
        Order::where('fib_payment_id', $paymentId)
            ->update(['status' => 'paid']);
    }

    private function markOrderAsDeclined(string $paymentId): void
    {
        Order::where('fib_payment_id', $paymentId)
            ->update(['status' => 'declined']);
    }
}

IP Allowlist Configuration

Restrict webhooks to specific FIB server IPs:

FIB_ALLOWED_CALLBACK_IPS=203.0.113.10,203.0.113.11

Testing

Mocking the Service

In your application tests, mock the interface:

use Hamoi1\FibPayment\Contracts\FibClientInterface;
use Hamoi1\FibPayment\Data\PaymentResponse;
use Hamoi1\FibPayment\Data\PaymentStatusInfo;
use Hamoi1\FibPayment\Enums\PaymentStatus;
use Illuminate\Support\Facades\App;
use Mockery;

it('processes successful payment', function () {
    // Create mock
    $mock = Mockery::mock(FibClientInterface::class);

    // Define expected behavior
    $mock->shouldReceive('createPayment')
        ->once()
        ->andReturn(new PaymentResponse(
            paymentId: 'pay-test-123',
            readableCode: 'ABC123',
            qrCode: 'base64-encoded-qr-data',
            validUntil: now()->addHour()->toIso8601String(),
            personalAppLink: 'https://fib.iq/personal/pay-test-123',
            businessAppLink: 'https://fib.iq/business/pay-test-123',
            corporateAppLink: 'https://fib.iq/corporate/pay-test-123',
        ));

    // Bind mock to container
    App::instance(FibClientInterface::class, $mock);

    // Test your controller/service
    $response = $this->postJson('/api/checkout', [
        'amount' => 1000,
    ]);

    $response->assertOk()
        ->assertJsonPath('payment_id', 'pay-test-123');
});

it('handles paid webhook', function () {
    $mock = Mockery::mock(FibClientInterface::class);
    
    $mock->shouldReceive('getPaymentStatus')
        ->with('pay-test-456')
        ->andReturn(new PaymentStatusInfo(
            paymentId: 'pay-test-456',
            status: PaymentStatus::PAID,
            validUntil: now()->addHour()->toIso8601String(),
            amount: new \Hamoi1\FibPayment\Data\MonetaryValue(1000, \Hamoi1\FibPayment\Enums\Currency::IQD),
        ));

    App::instance(FibClientInterface::class, $mock);

    $response = $this->postJson('/fib/webhook', [
        'paymentId' => 'pay-test-456',
        'status' => 'PAID',
    ]);

    $response->assertOk();
});

Package Quality Commands

Run these commands to ensure code quality:

# Run all checks
composer check

# Individual commands
composer lint      # Laravel Pint code style check
composer lint:fix  # Auto-fix code style
composer analyse   # PHPStan static analysis
composer test      # Pest tests

Troubleshooting

Common Issues

Issue Solution
Jwt issuer is not configured Your client_id isn't activated by FIB. Contact FIB support.
401 Unauthorized Check client_id and client_secret in .env
403 Forbidden Credentials may be valid but lack permissions
429 Too Many Requests You're hitting rate limits. Implement exponential backoff.
Token cache issues Run php artisan cache:clear
Config not loading Run php artisan config:clear

Debug Endpoints

Add this to your routes for debugging:

Route::get('/fib/debug', function () {
    return [
        'environment' => config('fib.environment'),
        'client_id' => config('fib.client_id') ? '***' . substr(config('fib.client_id'), -4) : 'NOT SET',
        'client_secret_set' => ! empty(config('fib.client_secret')),
        'callback_url' => config('fib.callback_url'),
        'base_url' => \Hamoi1\FibPayment\Enums\FibEnvironment::from(config('fib.environment'))->baseUrl(),
    ];
});

License

The MIT License (MIT). Please see LICENSE for more information.