jeffreyvanhees/laravel-online-payment-platform

Laravel connector for Online Payment Platform API using SaloonPHP

v0.0.11 2025-07-27 10:59 UTC

This package is auto-updated.

Last update: 2025-07-27 10:59:56 UTC


README

Latest Version on Packagist Tests Coverage Total Downloads

A modern Laravel package for integrating with the Online Payment Platform API. Built with SaloonPHP and Spatie Laravel Data for an excellent developer experience.

Warning

This package is not affiliated with, endorsed by, or officially connected to Online Payment Platform B.V. It is an independent, community-driven implementation for integrating with their API. Also, this package is not intended for production use yet. It is still in development and may contain breaking changes.

โœจ Features

  • ๐Ÿš€ Laravel 11 & 12 Support - Full support for the latest Laravel versions
  • ๐Ÿ›ก๏ธ Type Safety - Fully typed DTOs using Spatie Laravel Data
  • ๐Ÿ—๏ธ Service Container - Native Laravel service container integration
  • ๐ŸŽญ Facade Support - Clean, expressive API using Laravel facades
  • ๐Ÿ”ง SaloonPHP Foundation - Built on the robust SaloonPHP HTTP client
  • ๐Ÿงช Comprehensive Testing - HTTP recording/replay for reliable tests
  • ๐Ÿ“š Intuitive API - Fluent interface: Opp::merchants()->ubos()->create()
  • ๐Ÿ”„ Environment Support - Seamless sandbox/production switching
  • โšก Exception Handling - Detailed custom exceptions for all error scenarios
  • ๐Ÿ”„ Pagination Support - Built-in pagination with SaloonPHP

๐Ÿ“‹ Requirements

  • PHP 8.2 or higher
  • Laravel 11.0 or 12.0

๐Ÿš€ Installation

Install the package via Composer:

composer require jeffreyvanhees/laravel-online-payment-platform

Publish the configuration file:

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

Configure your API credentials and URLs in your .env file:

# API Configuration
OPP_API_KEY=your_production_api_key_here
OPP_SANDBOX_API_KEY=your_sandbox_api_key_here
OPP_SANDBOX=true

# URL Configuration (optional - set default URLs for webhooks/notifications)
OPP_NOTIFY_URL=https://yourapp.com/webhooks/opp
OPP_RETURN_URL=https://yourapp.com/payment/return

# Webhook Configuration (optional)
OPP_NOTIFY_SECRET=your_webhook_secret

๐ŸŽฏ Usage

The package provides multiple ways to interact with the Online Payment Platform API:

Using the Facade (Recommended)

The facade provides the cleanest and most Laravel-like API:

<?php

use JeffreyVanHees\OnlinePaymentPlatform\OnlinePaymentPlatformFacade as Opp;
use JeffreyVanHees\OnlinePaymentPlatform\Data\Requests\Merchants\CreateConsumerMerchantData;

// Create a consumer merchant using configured URLs
$response = Opp::merchants()->create([
    'type' => 'consumer',
    'country' => 'NLD',
    'emailaddress' => 'john.doe@example.com',
    'first_name' => 'John',
    'last_name' => 'Doe',
]);

if ($response->successful()) {
    $merchant = $response->dto();
    echo "Created merchant: {$merchant->uid}";
}

Using Dependency Injection

Inject the connector directly into your classes:

<?php

namespace App\Services;

use JeffreyVanHees\OnlinePaymentPlatform\OnlinePaymentPlatformConnector;
use JeffreyVanHees\OnlinePaymentPlatform\Data\Requests\Merchants\CreateConsumerMerchantData;

class PaymentService
{
    public function __construct(
        private OnlinePaymentPlatformConnector $opp
    ) {}

    public function createMerchant(array $data): string
    {
        $response = $this->opp->merchants()->create($data);
        
        if (!$response->successful()) {
            throw new \Exception('Failed to create merchant');
        }

        return $response->dto()->uid;
    }
}

Using Type-Safe DTOs

For maximum type safety and IDE support, use the provided Data Transfer Objects:

<?php

use JeffreyVanHees\OnlinePaymentPlatform\OnlinePaymentPlatformFacade as Opp;
use JeffreyVanHees\OnlinePaymentPlatform\Data\Requests\Merchants\CreateConsumerMerchantData;
use JeffreyVanHees\OnlinePaymentPlatform\Data\Requests\Transactions\CreateTransactionData;
use JeffreyVanHees\OnlinePaymentPlatform\Data\Common\ProductData;

// Create merchant using DTO
$merchantData = new CreateConsumerMerchantData(
    type: 'consumer',
    country: 'NLD',
    emailaddress: 'jane.doe@example.com',
    first_name: 'Jane',
    last_name: 'Doe',
    notify_url: 'https://yoursite.com/webhooks/opp'
);

$merchantResponse = Opp::merchants()->create($merchantData);
$merchant = $merchantResponse->dto();

// Create transaction with products
$transactionData = new CreateTransactionData(
    merchant_uid: $merchant->uid,
    total_price: 2500, // โ‚ฌ25.00 in cents
    return_url: 'https://yoursite.com/payment/return',
    notify_url: 'https://yoursite.com/webhooks/opp',
    products: ProductData::collect([
        [
            'name' => 'Premium Subscription',
            'quantity' => 1,
            'price' => 2500,
        ],
    ])
);

$transactionResponse = Opp::transactions()->create($transactionData);
$transaction = $transactionResponse->dto();

echo "Payment URL: {$transaction->redirect_url}";

๐Ÿ“š API Documentation

Merchants

// Create merchants
$consumer = Opp::merchants()->create([
    'type' => 'consumer',
    'country' => 'NLD',
    'emailaddress' => 'user@example.com',
    'first_name' => 'John',
    'last_name' => 'Doe',
    'notify_url' => 'https://yoursite.com/webhooks/opp',
]);

$business = Opp::merchants()->create([
    'type' => 'business',
    'country' => 'NLD',
    'emailaddress' => 'business@example.com',
    'coc_nr' => '12345678',
    'legal_name' => 'Example B.V.',
    'notify_url' => 'https://yoursite.com/webhooks/opp',
]);

// Retrieve and list merchants
$merchant = Opp::merchants()->get('mer_123456789');
$merchants = Opp::merchants()->list(['limit' => 50]);

// Add contacts and addresses
$contact = Opp::merchants()->contacts('mer_123456789')->add([
    'type' => 'representative',
    'gender' => 'm',
    'title' => 'mr',
    'name' => [
        'first' => 'John',
        'last' => 'Smith',
        'initials' => 'J.S.',
        'names_given' => 'John',
    ],
    'emailaddresses' => [
        ['emailaddress' => 'john@example.com']
    ],
    'phonenumbers' => [
        ['phonenumber' => '+31612345678']
    ],
]);

$address = Opp::merchants()->addresses('mer_123456789')->add([
    'type' => 'business',
    'address_line_1' => 'Main Street 123',
    'city' => 'Amsterdam',
    'zipcode' => '1000 AA',
    'country' => 'NLD',
]);

// Manage Ultimate Beneficial Owners (UBOs) for business merchants
$ubo = Opp::merchants()->ubos('mer_123456789')->create([
    'name_first' => 'John',
    'name_last' => 'Doe', 
    'date_of_birth' => '1980-01-15',
    'country_of_residence' => 'NLD',
    'is_decision_maker' => true,
    'percentage_of_shares' => 25.5,
]);

// Create merchant profiles for different configurations
$profile = Opp::merchants()->profiles('mer_123456789')->create([
    'name' => 'E-commerce Profile',
    'description' => 'Settings for online store',
    'webhook_url' => 'https://store.example.com/webhook',
    'return_url' => 'https://store.example.com/success',
    'is_default' => false,
]);

Transactions

// Create transactions
$transaction = Opp::transactions()->create([
    'merchant_uid' => 'mer_123456789',
    'total_price' => 1000, // โ‚ฌ10.00 in cents
    'products' => [
        [
            'name' => 'Product Name',
            'quantity' => 1,
            'price' => 1000,
        ],
    ],
    'return_url' => 'https://yoursite.com/payment/return',
    'notify_url' => 'https://yoursite.com/webhooks/opp',
]);

// Retrieve and list transactions
$transaction = Opp::transactions()->get('tra_987654321');
$transactions = Opp::transactions()->list(['limit' => 100]);

// Update transaction
$updated = Opp::transactions()->update('tra_987654321', [
    'description' => 'Updated description',
]);

Charges

// Create charges for balance transfers between merchants
$charge = Opp::charges()->create([
    'type' => 'balance',
    'amount' => 1500, // โ‚ฌ15.00 in cents
    'from_owner_uid' => 'mer_123456789',
    'to_owner_uid' => 'mer_987654321',
    'description' => 'Monthly platform fee',
    'metadata' => ['invoice_id' => 'INV-2024-001'],
]);

// Retrieve charge details
$charge = Opp::charges()->get('cha_123456789');

// List charges with filters
$charges = Opp::charges()->list([
    'from_owner_uid' => 'mer_123456789',
    'status' => 'completed',
    'limit' => 50,
]);

Mandates

// Create SEPA Direct Debit mandate
$mandate = Opp::mandates()->create([
    'merchant_uid' => 'mer_123456789',
    'holder_name' => 'John Doe',
    'iban' => 'NL91ABNA0417164300',
    'bic' => 'ABNANL2A',
    'description' => 'Monthly subscription mandate',
    'reference' => 'SUBSCRIPTION-2024',
]);

// Retrieve mandate
$mandate = Opp::mandates()->get('man_123456789');

// Create transaction using mandate
$transaction = Opp::mandates()->transactions('man_123456789')->create([
    'amount' => 2500, // โ‚ฌ25.00 in cents
    'description' => 'Monthly subscription payment',
]);

// Delete mandate
Opp::mandates()->delete('man_123456789');

Withdrawals

// Create withdrawal to merchant's bank account
$withdrawal = Opp::withdrawals()->create('mer_123456789', [
    'amount' => 50000, // โ‚ฌ500.00 in cents
    'currency' => 'EUR',
    'bank_account_uid' => 'ban_123456789',
    'description' => 'Weekly payout',
    'reference' => 'PAYOUT-2024-W01',
]);

// Retrieve withdrawal status
$withdrawal = Opp::withdrawals()->get('wit_123456789');

// List withdrawals for a merchant
$withdrawals = Opp::withdrawals()->list([
    'merchant_uid' => 'mer_123456789',
    'status' => 'completed',
    'limit' => 25,
]);

// Cancel pending withdrawal
Opp::withdrawals()->delete('wit_123456789');

Disputes

// Create dispute for a transaction
$dispute = Opp::disputes()->create([
    'transaction_uid' => 'tra_123456789',
    'amount' => 1000, // โ‚ฌ10.00 in cents
    'reason' => 'Product not received',
    'message' => 'Customer claims product was never delivered',
    'evidence' => [
        'tracking_number' => 'TRACK123456',
        'shipping_date' => '2024-01-15',
    ],
]);

// Retrieve dispute with transaction details
$dispute = Opp::disputes()->get('dis_123456789', [
    'include' => 'transaction',
]);

// List all disputes
$disputes = Opp::disputes()->list([
    'status' => 'pending',
    'created_after' => '2024-01-01',
]);

Files

// Create file upload token
$upload = Opp::files()->createUpload([
    'filename' => 'invoice.pdf',
    'purpose' => 'dispute_evidence',
]);

// Upload the actual file
$file = Opp::files()->upload(
    fileUid: $upload->dto()->uid,
    token: $upload->dto()->token,
    filePath: '/path/to/invoice.pdf',
    fileName: 'invoice.pdf'
);

// List uploaded files
$files = Opp::files()->list([
    'purpose' => 'dispute_evidence',
    'created_after' => '2024-01-01',
]);

Partners

// Get partner configuration
$config = Opp::partners()->getConfiguration();

// Update partner settings
$updated = Opp::partners()->updateConfiguration([
    'webhook_url' => 'https://partner.example.com/webhooks',
    'notification_email' => 'notifications@partner.com',
    'settings' => [
        'auto_approve_merchants' => false,
        'require_vat_number' => true,
    ],
]);

Pagination

The package supports automatic pagination through SaloonPHP:

// Get paginated results
$paginator = Opp::merchants()->list(['limit' => 25]);

// Iterate through all pages
foreach ($paginator->paginate() as $response) {
    $merchants = $response->dto();
    
    foreach ($merchants->data as $merchant) {
        echo "Merchant: {$merchant->uid} - {$merchant->emailaddress}\n";
    }
}

Benefits

  • Centralized Configuration: Set URLs once in .env file
  • Environment-Specific: Different URLs for development, staging, production
  • Default Values: Automatically use configured URLs across your app
  • Override Capability: Still override URLs per request when needed

โš™๏ธ Configuration

The configuration file (config/opp.php) allows you to customize various aspects:

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | API Credentials
    |--------------------------------------------------------------------------
    */
    'api_key' => env('OPP_API_KEY'),
    'sandbox_api_key' => env('OPP_SANDBOX_API_KEY'),

    /*
    |--------------------------------------------------------------------------
    | Environment
    |--------------------------------------------------------------------------
    */
    'sandbox' => env('OPP_SANDBOX', true),

    /*
    |--------------------------------------------------------------------------
    | HTTP Configuration
    |--------------------------------------------------------------------------
    */
    'timeout' => env('OPP_TIMEOUT', 30),
    'retry' => [
        'times' => env('OPP_RETRY_TIMES', 3),
        'sleep' => env('OPP_RETRY_SLEEP', 1000),
    ],
];

๐Ÿšจ Error Handling

The package provides detailed exception handling:

use JeffreyVanHees\OnlinePaymentPlatform\Exceptions\{
    OppException,
    AuthenticationException,
    ValidationException,
    RateLimitException,
    ApiException
};

try {
    $response = Opp::merchants()->create($invalidData);
} catch (ValidationException $e) {
    // Handle validation errors
    $errors = $e->getValidationErrors();
    foreach ($errors as $field => $messages) {
        echo "{$field}: " . implode(', ', $messages);
    }
} catch (AuthenticationException $e) {
    // Handle authentication issues
    echo "Authentication failed: " . $e->getMessage();
} catch (RateLimitException $e) {
    // Handle rate limiting
    echo "Rate limit exceeded. Retry after: " . $e->getRetryAfter();
} catch (OppException $e) {
    // Handle general API errors
    echo "API Error: " . $e->getMessage();
}

๐Ÿงช Testing

Run the test suite:

# Run all tests
composer test

# Run tests with coverage report
composer test-coverage

# Generate HTML coverage report
composer test-coverage-html

# Generate Clover XML coverage report  
composer test-coverage-clover

# Record new HTTP interactions (requires real API credentials)
composer record

# Run tests using recorded interactions
composer replay

Test Coverage

The package includes comprehensive tests covering all API endpoints with 70.2% code coverage:

  • โœ… 214 tests covering all major endpoints and DTOs
  • โœ… 907 assertions ensuring functionality
  • โœ… Merchant operations - CRUD, contacts, addresses, bank accounts, settlements, UBOs, profiles
  • โœ… Transaction lifecycle - create, retrieve, update, delete
  • โœ… Payment flows - charges, mandates, withdrawals, disputes
  • โœ… File operations - upload, retrieval, management
  • โœ… Partner configuration - settings management
  • โœ… Error handling - graceful sandbox environment handling

Coverage reports are generated in multiple formats:

  • Terminal: Real-time coverage during test runs
  • HTML: Detailed browsable report in coverage-report/
  • XML: Machine-readable format for CI/CD integration

๐Ÿ“– Advanced Usage

Service Provider Registration

You can bind custom configurations in your AppServiceProvider:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use JeffreyVanHees\OnlinePaymentPlatform\OnlinePaymentPlatformConnector;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(OnlinePaymentPlatformConnector::class, function ($app) {
            return new OnlinePaymentPlatformConnector(
                apiKey: config('opp.api_key'),
                sandbox: config('opp.sandbox')
            );
        });
    }
}

Custom HTTP Client Configuration

use JeffreyVanHees\OnlinePaymentPlatform\OnlinePaymentPlatformConnector;

$connector = new OnlinePaymentPlatformConnector(
    apiKey: 'your-api-key',
    sandbox: true
);

// Add custom middleware
$connector->middleware()->onRequest(function ($request) {
    $request->headers()->add('Custom-Header', 'value');
    return $request;
});

// Add retry logic
$connector->middleware()->onResponse(function ($response) {
    if ($response->status() === 429) {
        sleep(1);
        return $response->throw(); // Retry
    }
    return $response;
});

๐Ÿš€ Releases

This package uses automated versioning and releases:

  • Automatic: Patch version bump on every push to main branch
  • Manual: Use commit messages to control version bumps:
    • [major] in commit message โ†’ Major version bump (e.g., 1.0.0 โ†’ 2.0.0)
    • [minor] in commit message โ†’ Minor version bump (e.g., 1.0.0 โ†’ 1.1.0)
    • Default โ†’ Patch version bump (e.g., 1.0.0 โ†’ 1.0.1)
  • Skip Release: Add [skip release] to commit message to skip version bump

Manual Release Trigger

You can also manually trigger a release via GitHub Actions:

  1. Go to Actions โ†’ Release workflow
  2. Click "Run workflow"
  3. Select the version bump type (patch/minor/major)

๐Ÿค Contributing

Please see CONTRIBUTING.md for details on how to contribute.

๐Ÿ“ License

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

๐Ÿ™ Credits