Skip to content

Payment Gateway System

Gateway-agnostic payment system enabling multi-provider support with unified DTOs and market-specific configuration.

The payment system uses a modular architecture where:

  1. Gateway-agnostic DTOs standardize customer and payment data
  2. Gateway Factory selects the appropriate provider per market/payment method
  3. PaymentService orchestrates all payment operations
  4. Individual gateways transform DTOs to provider-specific formats
Controller/Livewire
┌──────────────────┐
│ PaymentService │ ◄── Main orchestrator
└───────┬──────────┘
┌──────────────────┐ ┌─────────────────┐
│ GatewayFactory │────►│ MarketConfig │
└───────┬──────────┘ └─────────────────┘
┌──────────────────┐
│ PaymentGateway │ ◄── Stripe/Adyen/Redsys
│ Interface │
└──────────────────┘

Standardizes customer information across all gateways.

Source: backend/app/Services/Payment/DTOs/CustomerData.php

Properties:

  • id - Internal client ID
  • name - Customer’s full name
  • email - Customer’s email address
  • phone - Customer’s phone number (optional)
  • billingAddress - AddressData object (optional)
  • preferredLocale - Locale code e.g., ‘es_ES’ (optional)
  • preferredCurrency - Currency code e.g., ‘EUR’ (optional)
  • metadata - Additional key-value pairs

Gateway transformations:

  • toStripeFormat() - Stripe customer creation format
  • toAdyenFormat() - Adyen shopper format with shopperReference
  • toRedsysFormat() - Redsys DS_MERCHANT fields

Standardizes billing addresses across gateways.

Source: backend/app/Services/Payment/DTOs/AddressData.php

Properties:

  • line1, line2 - Street address
  • city, state, postalCode
  • country - ISO 3166-1 alpha-2 code (e.g., ‘ES’)

Methods:

  • isEmpty() - Check if address has any data
  • isValid() - Check if address has minimum required data (country)
  • toStripeFormat() - Stripe address format
  • toAdyenFormat() - Adyen address with street, houseNumberOrName

The Client model provides a single source of truth for payment customer data:

Source: backend/app/Models/Client.php:156

// Get gateway-agnostic customer data
$customerData = $client->toPaymentCustomerData();
// With additional metadata
$customerData = $client->toPaymentCustomerData([
'booking_id' => $booking->id,
'source' => 'checkout',
]);
// Convert to specific gateway format
$stripeData = $customerData->toStripeFormat();
$adyenData = $customerData->toAdyenFormat();
$redsysData = $customerData->toRedsysFormat();
GatewayStatusPayment Methods
StripeActiveCard, Apple Pay, Google Pay, SEPA Debit
AdyenPlannedCard, Apple Pay, Google Pay, iDEAL, Klarna
RedsysPlannedCard (Spanish banks)

Gateway availability is configured per market in market_payment_methods table.

The main orchestrator for all payment operations.

Source: backend/app/Services/Payment/PaymentService.php

Get payment methods for market:

$methods = $paymentService->getPaymentMethodsForMarket($market);
// Returns: Collection of {code, name, icon, gateway, display_order}

Create payment intent:

$result = $paymentService->createPaymentIntent(
booking: $booking,
client: $client,
market: $market,
paymentMethodCode: 'card',
depositOnly: true,
);
// Returns: {intent_result, payment_type, amount_cents}

Record payment after frontend confirmation:

$payment = $paymentService->recordPayment(
booking: $booking,
client: $client,
gatewayCode: 'stripe',
paymentMethodCode: 'card',
gatewayPaymentId: 'pi_xxx',
paymentType: PaymentType::Deposit,
amountInCents: 15000,
status: PaymentStatus::Succeeded,
);

After successful payment, BookingFinalizationService completes the booking:

  1. Creates Passenger records from checkout session’s traveler_data
  2. Attaches passengers to booking (first passenger = lead)
  3. Attaches passengers to client
  4. Creates BookingUpsell records from session selections
  5. Updates booking with base_price, extras_price, total_amount

Source: backend/app/Services/Booking/BookingFinalizationService.php

// Called automatically in PaymentController::confirm() after successful payment
$this->bookingFinalizationService->finalizeFromCheckoutSession($booking, $session);

The system supports split payments where customers pay a deposit upfront and the balance before departure.

Source: backend/app/Services/Payment/PaymentCalculatorService.php

  • Deposit available: When balance due date > today
  • Deposit amount: total * deposit_percentage / 100 (uses session’s total_price including extras)
  • Balance due date: departure_date - balance_days_before

Config: config/payment.php

'deposit' => [
'percentage' => 50, // 50% deposit
'balance_days_before' => 7, // Balance due 7 days before departure
],
TypeWhen Used
depositInitial partial payment
balanceRemainder charged before departure
fullWhen deposit not available (close to departure)

Payments include links to view transactions in the gateway’s dashboard.

Source: backend/app/Models/Payment.php:250

Stripe:

  • Test mode: https://dashboard.stripe.com/test/payments/{id}
  • Live mode: https://dashboard.stripe.com/payments/{id}

Adyen:

  • https://ca-{environment}.adyen.com/ca/ca/accounts/showTx.shtml?pspReference={id}

The ViewPayment page includes a “View in {Gateway}” action button.

Source: backend/app/Filament/Resources/Payments/Pages/ViewPayment.php:29

All gateways implement PaymentGatewayInterface.

Source: backend/app/Contracts/Payment/PaymentGatewayInterface.php

MethodPurpose
createCustomer()Create customer in gateway
createSetupIntent()Save payment method for future use
createPaymentIntent()Create one-time or initial charge
confirmPayment()Confirm after 3DS authentication
chargeOffSession()Charge saved method (scheduled balance)
refund()Process refund
handleWebhook()Process gateway webhooks
supportsPaymentMethod()Check method support
getGatewayCode()Return gateway identifier
DTOPurpose
PaymentIntentResultPayment intent creation result with client_secret
PaymentResultCharge/confirm result with success/failure info
SetupIntentResultSetup intent for saving payment methods
RefundResultRefund transaction result
WebhookResultWebhook processing result
Terminal window
# Stripe (via Laravel Cashier)
STRIPE_KEY=pk_test_xxx
STRIPE_SECRET=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Payment settings
PAYMENT_DEFAULT_CURRENCY=EUR
PAYMENT_DEPOSIT_PERCENTAGE=50
PAYMENT_BALANCE_DAYS_BEFORE=7
  • payment_gateways - Gateway definitions (stripe, adyen, redsys)
  • payment_methods - Method types (card, apple_pay, sepa_debit)
  • market_payment_methods - Market-specific gateway/method configuration
  • payments - Payment transactions
  • client_payment_methods - Saved payment methods per client

Tests use dedicated factories for payment components.

Source: backend/tests/Unit/Services/Payment/

Terminal window
# Run payment tests
./vendor/bin/sail artisan test --filter=Payment
# Test DTO transformations
./vendor/bin/sail artisan test --filter=CustomerDataTest
./vendor/bin/sail artisan test --filter=AddressDataTest