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, PaymentController creates/updates the Client from client_data (the booking contact person), then 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. Persists flight selection to booking.flight_selection (enriched with city names and duration) for both economy and business
  5. Creates BookingUpsell records from session selections (hotels, activities, transfers, business class flights)
  6. Stores payment_method_code and payment_gateway_code on the booking for auditability
  7. Routes booking status via PaymentService::updateBookingPaymentStatus():
    • Flights selected (ECONOMY / BUSINESS) → pending_flight_booking
    • Land-only booking (no flight selection) → pending_land_confirmation
  8. Clears the checkout session via CheckoutSessionService::clear() to prevent reuse

Source: backend/app/Services/Booking/BookingFinalizationService.php, backend/app/Http/Controllers/Api/PaymentController.php

// Called automatically in PaymentController::confirm() after successful payment
$this->bookingFinalizationService->finalizeFromCheckoutSession($booking, $session);
// Session cleared after all finalization steps complete
$this->checkoutSessionService->clear();

The system supports split payments where customers pay a deposit upfront and the balance before departure. The split is based on the cost structure of the trip, not a fixed percentage.

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

  • Deposit (upfront): flight_base_price × (1 + margin / 100) + business_class_extras — covers flights that need immediate ticketing
  • Balance (deferred): total - deposit — everything else (land, hotel/activity/transfer extras, future insurance)
  • Safety cap: deposit cannot exceed total
  • Deposit available: when departure_date - balance_days_before is in the future
  • Non-standard pax: flight_base_price = (offer.flight_base_price / 2) × actual_pax_count (offers always store 2-pax pricing)

The flight_base_price is always the economy flight cost, even when business class is selected. Business class adds business_extra_price_per_person × pax_count on top (this value already includes margin and marketing rounding from BusinessFlightSearchService).

Both CheckoutSessionResource and PaymentController compute the deposit independently using the same formula and a shared calculateBusinessClassExtras helper pattern.

Source: backend/app/Services/Checkout/CheckoutSessionService.php, backend/app/Http/Resources/CheckoutSessionResource.php, backend/app/Http/Controllers/Api/PaymentController.php

Config: config/payment.php

'deposit' => [
'balance_days_before' => 7, // Balance due 7 days before departure
],

Env var: PAYMENT_BALANCE_DAYS_BEFORE (default: 7)

TypeWhen Used
depositUpfront portion (flights + margin + business class)
balanceDeferred portion (land, extras, insurance), charged before departure
fullWhen deposit not available (departure within balance_days_before)

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_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/Feature/Services/ and backend/tests/Feature/Models/

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