djweb/payments

Payment and invoice integration package for Stripe and iFirma

1.0.0 2025-09-19 08:23 UTC

This package is not auto-updated.

Last update: 2025-09-20 07:20:37 UTC


README

PHP Version Tests Coverage PHPStan PHPInsights PHPInsights PHPInsights License

Modern, secure and flexible payment system for PHP applications. Supports Stripe, invoice generation via IFirma and EU VAT data validation.

✨ Features

  • πŸ”’ Security - VAT validation, type checking, sensitive data protection
  • πŸ’° Stripe Payments - Full Stripe PaymentIntents support with webhooks
  • πŸ“„ IFirma Invoices - Automatic invoice generation with various VAT strategies
  • 🌍 EU Support - VAT number validation, tax rates, regional requirements
  • πŸ§ͺ High Quality - 92% test coverage, PHPStan level 8, PHPInsights A+
  • πŸš€ Modern PHP - Uses latest PHP 8.4 features (property hooks, readonly classes)

πŸ“¦ Installation

composer require djweb/payments

πŸš€ Quick Start

Stripe Payments

use DjWeb\Payments\Services\Payment\Stripe\StripePaymentGateway;
use DjWeb\Payments\DTOs\PaymentRequest;
use DjWeb\Payments\DTOs\CustomerData;
use DjWeb\Payments\DTOs\AddressData;
use DjWeb\Payments\ValueObjects\Money;
use DjWeb\Payments\ValueObjects\VatNumber;

// Gateway configuration
$stripe = new StripePaymentGateway(
    secretKey: 'sk_test_...',
    webhookSecret: 'whsec_...'
);

// Customer data
$customer = new CustomerData(
    email: 'customer@example.com',
    firstName: 'John',
    lastName: 'Doe',
    address: new AddressData(
        street: '123 Example Street',
        city: 'Warsaw',
        postalCode: '00-001',
        country: 'PL'
    ),
    companyName: 'Example Company Ltd.',
    vatNumber: new VatNumber('PL', '5260001246')
);

// Payment request
$request = new PaymentRequest(
    amount: new Money(299.99, 'PLN'),
    customer: $customer,
    description: 'Premium Product Purchase'
);

// Create PaymentIntent
$intent = $stripe->createPaymentIntent($request);

// Process payment
$result = $stripe->processPayment($intent);

if ($result->success) {
    echo "Payment successful: {$result->transactionId}";
}

IFirma Invoice Generation

use DjWeb\Payments\Services\Invoice\IFirma\IFirmaInvoiceService;
use DjWeb\Payments\DTOs\InvoiceData;

$invoiceService = new IFirmaInvoiceService(
    username: 'your_username',
    invoiceKey: 'your_invoice_key',
    apiUrl: 'https://www.ifirma.pl/iapi'
);

$invoiceData = new InvoiceData(
    customer: $customer,
    amount: new Money(299.99, 'PLN'),
    originalAmount: new Money(299.99, 'PLN'),
    productName: 'Premium Product'
);

$invoiceResult = $invoiceService->createInvoice($invoiceData);

if ($invoiceResult->success) {
    echo "Invoice created: {$invoiceResult->invoiceNumber}";
    echo "PDF: {$invoiceResult->pdfUrl}";
}

Working with Discounts

use DjWeb\Payments\DTOs\DiscountData;

$discount = new DiscountData(
    code: 'SAVE20',
    percentage: 20.0,
    maxUsages: 100,
    currentUsages: 15,
    validUntil: new DateTimeImmutable('2024-12-31')
);

if ($discount->isValid) {
    $originalAmount = 299.99;
    $discountAmount = $discount->calculateDiscountAmount($originalAmount);
    $finalAmount = $discount->calculateFinalAmount($originalAmount);
    
    echo "Discount: {$discountAmount} PLN";
    echo "Total: {$finalAmount} PLN";
}

VAT and Country Validation

use DjWeb\Payments\ValueObjects\VatNumber;
use DjWeb\Payments\ValueObjects\Country;

// Create and validate VAT number
$vatNumber = new VatNumber('PL', '526-000-12-46');
echo $vatNumber; // PL5260001246

// Check EU country
$country = new Country('PL');
echo $country->name; // Poland
echo $country->getVatRate(); // 0.23 (23%)
echo $country->isEu ? 'EU' : 'Non-EU'; // EU

// Check state/province requirements
if ($country->requiresStateProvince()) {
    // Required for US, CA, AU, BR, MX, IN, MY, AR
}

Stripe Webhook Handling

use DjWeb\Payments\DTOs\WebhookEvent;

// Webhook signature verification
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_STRIPE_SIGNATURE'];

try {
    $isValid = $stripe->verifyWebhookSignature($payload, $signature);
    
    if ($isValid) {
        $eventData = json_decode($payload, true);
        
        $event = new WebhookEvent(
            id: $eventData['id'],
            type: $eventData['type'],
            data: $eventData['data'],
            source: 'stripe',
            createdAt: new DateTimeImmutable()
        );
        
        // Handle different event types
        if ($event->isPaymentEvent) {
            // payment_intent.succeeded, payment_intent.payment_failed, etc.
            handlePaymentEvent($event);
        } elseif ($event->isInvoiceEvent) {
            // invoice.payment_succeeded, invoice.payment_failed, etc.
            handleInvoiceEvent($event);
        }
    }
} catch (Exception $e) {
    http_response_code(400);
    exit('Webhook error: ' . $e->getMessage());
}

πŸ—οΈ Architecture

Design Patterns

  • Strategy Pattern - Different invoicing strategies (domestic, EU B2B, export, OSS)
  • Factory Pattern - Automatic strategy selection based on customer data
  • Value Objects - Safe representations of money, countries, VAT numbers
  • DTOs - Data transfer with validation and transformation

Project Structure

src/
β”œβ”€β”€ Contracts/          # Interfaces
β”‚   β”œβ”€β”€ Arrayable.php
β”‚   β”œβ”€β”€ InvoiceServiceContract.php
β”‚   β”œβ”€β”€ PaymentGatewayContract.php
β”‚   └── WebhookHandlerContract.php
β”œβ”€β”€ DTOs/              # Data Transfer Objects
β”‚   β”œβ”€β”€ AddressData.php
β”‚   β”œβ”€β”€ CustomerData.php
β”‚   β”œβ”€β”€ DiscountData.php
β”‚   β”œβ”€β”€ InvoiceData.php
β”‚   β”œβ”€β”€ PaymentIntent.php
β”‚   β”œβ”€β”€ PaymentRequest.php
β”‚   β”œβ”€β”€ PaymentResult.php
β”‚   └── WebhookEvent.php
β”œβ”€β”€ Exceptions/        # Business Exceptions
β”‚   β”œβ”€β”€ InvoiceError.php
β”‚   └── PaymentError.php
β”œβ”€β”€ Services/
β”‚   β”œβ”€β”€ Invoice/       # Invoice Services
β”‚   β”‚   └── IFirma/   # IFirma Implementation
β”‚   β”œβ”€β”€ Payment/       # Payment Gateways
β”‚   β”‚   └── Stripe/   # Stripe Implementation
β”‚   └── Validators/    # VAT Validators
└── ValueObjects/      # Value Objects
    β”œβ”€β”€ Country.php
    β”œβ”€β”€ Money.php
    └── VatNumber.php

πŸ§ͺ Code Quality

The project maintains the highest code quality through:

Test Coverage

  • 92% class coverage (23/25)
  • 91% method coverage (74/81)
  • 89% line coverage (389/438)
  • 276 tests, 1007 assertions

Quality Control Tools

# All tests
vendor/bin/phpunit

# With code coverage
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html coverage

# Static analysis PHPStan (level 9)
vendor/bin/phpstan analyse

# Quality analysis PHPInsights
vendor/bin/phpinsights analyse

# Code standards check
vendor/bin/phpcs

πŸ“ Invoice Strategies

The system automatically selects the appropriate strategy based on customer data:

Strategy Conditions VAT IFirma Endpoint
Domestic Polish customer, PLN 23% /fakturakraj.json
Currency Polish customer, EUR/USD 23% /fakturawaluta.json
EU B2B EU customer with valid VAT 0% (reverse charge) /fakturaeksportuslugue.json
OSS EU consumer Customer's country VAT /fakturaoss.json
Export Non-EU customer 0% /fakturaeksportuslug.json

Custom Strategy Example

use DjWeb\Payments\Services\Invoice\IFirma\Strategies\InvoiceStrategyContract;

class CustomInvoiceStrategy implements InvoiceStrategyContract
{
    public function shouldApply(CustomerData $customer): bool
    {
        return $customer->address->country->code === 'US' 
            && $customer->companyName !== null;
    }
    
    public function getEndpoint(): string
    {
        return '/custom-us-b2b.json';
    }
    
    public function prepareInvoiceData(InvoiceData $data): array
    {
        return [
            'NabywcaNazwa' => $data->customer->companyName,
            'NabywcaAdres' => $data->customer->address->street,
            'StawkaVat' => 0, // No VAT for export
            // ... more fields
        ];
    }
}

🌍 Supported Countries and Currencies

EU Countries with Automatic VAT Rates

All 27 EU countries with automatic VAT rates:

Country Code VAT Rate Requires State/Province
πŸ‡΅πŸ‡± Poland PL 23% ❌
πŸ‡©πŸ‡ͺ Germany DE 19% ❌
πŸ‡«πŸ‡· France FR 20% ❌
πŸ‡ΊπŸ‡Έ USA US 0% βœ…
πŸ‡¨πŸ‡¦ Canada CA 0% βœ…

Currencies

  • EUR, USD, PLN, GBP - full support
  • JPY, KRW - no decimal places
  • Support for 100+ currencies via Stripe
// Automatic conversion to Stripe units
$money = new Money(99.99, 'PLN');
echo $money->toSmallestUnit(); // 9999 (grosze)

$yen = new Money(1000, 'JPY');  
echo $yen->toSmallestUnit(); // 1000 (no conversion)

πŸ”’ Security

Built-in Protections

  • Input validation - All DTOs validate data at construction
  • Sensitive data protection - #[SensitiveParameter] attribute for API keys
  • Safe Value Objects - Immutable objects with validation
  • VAT validation - Automatic checksum validation for Polish NIPs
  • Webhook verification - Stripe signature checking

Error Handling Example

use DjWeb\Payments\Exceptions\PaymentError;
use DjWeb\Payments\Exceptions\InvoiceError;

try {
    $intent = $stripe->createPaymentIntent($request);
} catch (PaymentError $e) {
    // Safe logging with context
    logger()->error('Payment failed', [
        'message' => $e->getMessage(),
        'context' => $e->context, // Additional context data
        'customer_id' => $request->customer->email
    ]);
}

// Error factories for different scenarios
throw PaymentError::invalidAmount(-100);
throw PaymentError::unsupportedCurrency('XYZ');
throw PaymentError::gatewayError('Stripe', 'Card declined');

throw InvoiceError::invalidCustomerData('vatNumber');
throw InvoiceError::unsupportedCountry('XX');
throw InvoiceError::apiError('IFirma', 'Connection timeout');

πŸ”§ Configuration

Environment Variables

# Stripe
STRIPE_SECRET_KEY=sk_test_51234567890...
STRIPE_WEBHOOK_SECRET=whsec_1234567890...

# IFirma  
IFIRMA_USERNAME=your_username
IFIRMA_INVOICE_KEY=123456789abcdef
IFIRMA_API_URL=https://www.ifirma.pl/iapi

# Optional
APP_ENV=production
LOG_LEVEL=info

Dependency Injection (Laravel/Symfony)

// Laravel Service Provider
use DjWeb\Payments\Contracts\PaymentGatewayContract;
use DjWeb\Payments\Contracts\InvoiceServiceContract;

$this->app->bind(PaymentGatewayContract::class, function () {
    return new StripePaymentGateway(
        secretKey: config('services.stripe.secret'),
        webhookSecret: config('services.stripe.webhook_secret')
    );
});

$this->app->bind(InvoiceServiceContract::class, function () {
    return new IFirmaInvoiceService(
        username: config('services.ifirma.username'),
        invoiceKey: config('services.ifirma.invoice_key'),
        apiUrl: config('services.ifirma.api_url', 'https://www.ifirma.pl/iapi')
    );
});

πŸ“Š Quality Metrics

Metric Value Status
Class Coverage 92% (23/25) βœ… Excellent
Method Coverage 91% (74/81) βœ… Excellent
Line Coverage 89% (389/438) βœ… Very Good
PHPStan Level 8/9 βœ… Perfect
PHPInsights Code 97% βœ… Excellent
PHPInsights Complexity 100% βœ… Excellent
PHPInsights Architecture 100% βœ… Excellent
PHPInsights Style 98% βœ… Excellent
Tests 276 passed βœ… All Green

πŸ“„ License

MIT License. See LICENSE for details.

πŸ†˜ Support

**Created with ❀️ by DjWeb **

Modern PHP solutions for future businesses