Payment Gateway System
Gateway-agnostic payment system enabling multi-provider support with unified DTOs and market-specific configuration.
Architecture Overview
Section titled “Architecture Overview”The payment system uses a modular architecture where:
- Gateway-agnostic DTOs standardize customer and payment data
- Gateway Factory selects the appropriate provider per market/payment method
- PaymentService orchestrates all payment operations
- Individual gateways transform DTOs to provider-specific formats
Controller/Livewire │ ▼┌──────────────────┐│ PaymentService │ ◄── Main orchestrator└───────┬──────────┘ │ ▼┌──────────────────┐ ┌─────────────────┐│ GatewayFactory │────►│ MarketConfig │└───────┬──────────┘ └─────────────────┘ │ ▼┌──────────────────┐│ PaymentGateway │ ◄── Stripe/Adyen/Redsys│ Interface │└──────────────────┘Gateway-Agnostic DTOs
Section titled “Gateway-Agnostic DTOs”CustomerData
Section titled “CustomerData”Standardizes customer information across all gateways.
Source: backend/app/Services/Payment/DTOs/CustomerData.php
Properties:
id- Internal client IDname- Customer’s full nameemail- Customer’s email addressphone- 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 formattoAdyenFormat()- Adyen shopper format with shopperReferencetoRedsysFormat()- Redsys DS_MERCHANT fields
AddressData
Section titled “AddressData”Standardizes billing addresses across gateways.
Source: backend/app/Services/Payment/DTOs/AddressData.php
Properties:
line1,line2- Street addresscity,state,postalCodecountry- ISO 3166-1 alpha-2 code (e.g., ‘ES’)
Methods:
isEmpty()- Check if address has any dataisValid()- Check if address has minimum required data (country)toStripeFormat()- Stripe address formattoAdyenFormat()- Adyen address withstreet,houseNumberOrName
Creating Customer Data from Client
Section titled “Creating Customer Data from Client”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();Supported Gateways
Section titled “Supported Gateways”| Gateway | Status | Payment Methods |
|---|---|---|
| Stripe | Active | Card, Apple Pay, Google Pay, SEPA Debit |
| Adyen | Planned | Card, Apple Pay, Google Pay, iDEAL, Klarna |
| Redsys | Planned | Card (Spanish banks) |
Gateway availability is configured per market in market_payment_methods table.
Payment Service
Section titled “Payment Service”The main orchestrator for all payment operations.
Source: backend/app/Services/Payment/PaymentService.php
Key Operations
Section titled “Key Operations”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,);Post-Payment Finalization
Section titled “Post-Payment Finalization”After successful payment, PaymentController creates/updates the Client from client_data (the booking contact person), then BookingFinalizationService completes the booking:
- Creates
Passengerrecords from checkout session’straveler_data - Attaches passengers to booking (first passenger = lead)
- Attaches passengers to client
- Persists flight selection to
booking.flight_selection(enriched with city names and duration) for both economy and business - Creates
BookingUpsellrecords from session selections (hotels, activities, transfers, business class flights) - Stores
payment_method_codeandpayment_gateway_codeon the booking for auditability - Routes booking status via
PaymentService::updateBookingPaymentStatus():- Flights selected (
ECONOMY/BUSINESS) →pending_flight_booking - Land-only booking (no flight selection) →
pending_land_confirmation
- Flights selected (
- 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();Deposit/Balance Payments
Section titled “Deposit/Balance Payments”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
Calculation Logic
Section titled “Calculation Logic”- 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_beforeis 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
Configuration
Section titled “Configuration”Config: config/payment.php
'deposit' => [ 'balance_days_before' => 7, // Balance due 7 days before departure],Env var: PAYMENT_BALANCE_DAYS_BEFORE (default: 7)
Payment Types
Section titled “Payment Types”| Type | When Used |
|---|---|
deposit | Upfront portion (flights + margin + business class) |
balance | Deferred portion (land, extras, insurance), charged before departure |
full | When deposit not available (departure within balance_days_before) |
Gateway Dashboard URLs
Section titled “Gateway Dashboard URLs”Payments include links to view transactions in the gateway’s dashboard.
Source: backend/app/Models/Payment.php:250
Gateway Dashboard Links
Section titled “Gateway Dashboard Links”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}
Admin Panel Integration
Section titled “Admin Panel Integration”The ViewPayment page includes a “View in {Gateway}” action button.
Source: backend/app/Filament/Resources/Payments/Pages/ViewPayment.php:29
Gateway Interface
Section titled “Gateway Interface”All gateways implement PaymentGatewayInterface.
Source: backend/app/Contracts/Payment/PaymentGatewayInterface.php
Required Methods
Section titled “Required Methods”| Method | Purpose |
|---|---|
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 |
Result DTOs
Section titled “Result DTOs”| DTO | Purpose |
|---|---|
PaymentIntentResult | Payment intent creation result with client_secret |
PaymentResult | Charge/confirm result with success/failure info |
SetupIntentResult | Setup intent for saving payment methods |
RefundResult | Refund transaction result |
WebhookResult | Webhook processing result |
Payment Configuration
Section titled “Payment Configuration”Environment Variables
Section titled “Environment Variables”# Stripe (via Laravel Cashier)STRIPE_KEY=pk_test_xxxSTRIPE_SECRET=sk_test_xxxSTRIPE_WEBHOOK_SECRET=whsec_xxx
# Payment settingsPAYMENT_DEFAULT_CURRENCY=EURPAYMENT_BALANCE_DAYS_BEFORE=7Database Tables
Section titled “Database Tables”payment_gateways- Gateway definitions (stripe, adyen, redsys)payment_methods- Method types (card, apple_pay, sepa_debit)market_payment_methods- Market-specific gateway/method configurationpayments- Payment transactionsclient_payment_methods- Saved payment methods per client
Testing
Section titled “Testing”Tests use dedicated factories for payment components.
Source: backend/tests/Feature/Services/ and backend/tests/Feature/Models/
# 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=AddressDataTestRelated
Section titled “Related”- Stripe Local Setup - Environment setup, webhooks, and test cards
- Checkout API - Frontend checkout flow
- Booking Model - Booking payment status
- Source:
backend/app/Services/Payment/