Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions lib/active_merchant/billing/gateways/paysafe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def initialize(options = {})
def purchase(money, payment, options = {})
post = {}
add_auth_purchase_params(post, money, payment, options)
add_airline_travel_details(post, options)
add_split_pay_details(post, options)
post[:settleWithAuth] = true

Expand Down Expand Up @@ -129,6 +128,8 @@ def add_auth_purchase_params(post, money, payment, options)
add_three_d_secure(post, payment, options) if options[:three_d_secure]
add_stored_credential(post, options) if options[:stored_credential]
add_funding_transaction(post, options)
add_airline_travel_details(post, options)
add_cruiseline_travel_details(post, options)
end

# Customer data can be included in transactions where the payment method is a credit card
Expand Down Expand Up @@ -230,55 +231,137 @@ def add_three_d_secure(post, payment, options)
post[:authentication][:directoryServerTransactionId] = three_d_secure[:ds_transaction_id] unless payment.is_a?(String) || !mastercard?(payment)
end

def add_cruiseline_travel_details(post, options)
return unless cruiseline_data = options[:cruiseline_travel_details]

post[:cruiselineTravelDetails] = {}
post[:cruiselineTravelDetails][:cruiseShipName] = cruiseline_data[:cruise_ship_name] if cruiseline_data[:cruise_ship_name]
post[:cruiselineTravelDetails][:passengerName] = cruiseline_data[:passenger_name] if cruiseline_data[:passenger_name]
post[:cruiselineTravelDetails][:departureDate] = cruiseline_data[:departure_date] if cruiseline_data[:departure_date]
post[:cruiselineTravelDetails][:returnDate] = cruiseline_data[:return_date] if cruiseline_data[:return_date]
post[:cruiselineTravelDetails][:country] = cruiseline_data[:country] if cruiseline_data[:country]
post[:cruiselineTravelDetails][:state] = cruiseline_data[:state] if cruiseline_data[:state]
post[:cruiselineTravelDetails][:originCity] = cruiseline_data[:origin_city] if cruiseline_data[:origin_city]
post[:cruiselineTravelDetails][:roomRate] = cruiseline_data[:room_rate] if cruiseline_data[:room_rate]
post[:cruiselineTravelDetails][:travelPackageApplication] = cruiseline_data[:travel_package_application] if cruiseline_data[:travel_package_application]

add_cruiseline_ticket_details(post, options)
add_cruiseline_passenger_details(post, options)
add_cruisline_trip_leg_details(post, options)
end

def add_cruiseline_ticket_details(post, options)
return unless ticket = options[:cruiseline_travel_details][:ticket]

post[:cruiselineTravelDetails][:ticket] = {}
post[:cruiselineTravelDetails][:ticket][:ticketNumber] = ticket[:ticket_number] if ticket[:ticket_number]
post[:cruiselineTravelDetails][:ticket][:isRestrictedTicket] = ticket[:is_restricted_ticket].to_s.present?
end

def add_cruiseline_passenger_details(post, options)
return unless passengers = options[:cruiseline_travel_details][:passengers]

passengers_hash = {}
passengers.each.with_index(1) do |passenger, i|
my_passenger = "passenger#{i}".to_sym
details = add_passenger_details(my_passenger, passenger[1])

passengers_hash[my_passenger] = details
end
post[:cruiselineTravelDetails][:passengers] = passengers_hash
end

def add_passenger_details(obj, passenger)
details = {}

details[:ticketNumber] = passenger[:ticket_number] if passenger[:ticket_number]
details[:firstName] = passenger[:first_name] if passenger[:first_name]
details[:lastName] = passenger[:last_name] if passenger[:last_name]
details[:phoneNumber] = passenger[:phone_number] if passenger[:phone_number]
details[:passengerCode] = passenger[:passenger_code] if passenger[:passenger_code]
details[:gender] = passenger[:gender] if passenger[:gender]

details
end

def add_cruisline_trip_leg_details(post, options)
return unless trip_legs = options[:cruiseline_travel_details][:trip_legs]

trip_legs_hash = {}
trip_legs.each.with_index(1) do |leg, i|
my_leg = "leg#{i}".to_sym
details = add_cruiseline_leg_details(my_leg, leg[1])

trip_legs_hash[my_leg] = details
end
post[:cruiselineTravelDetails][:tripLegs] = trip_legs_hash
end

def add_cruiseline_leg_details(obj, leg)
details = {}

details[:serviceClass] = leg[:service_class] if leg[:service_class]
details[:departureCity] = leg[:departure_city] if leg[:departure_city]
details[:destinationCity] = leg[:destination_city] if leg[:destination_city]
details[:fare] = leg[:fare] if leg[:fare]
details[:departureDate] = leg[:departure_date] if leg[:departure_date]

details
end

def add_airline_travel_details(post, options)
return unless options[:airline_travel_details]
return unless airline_data = options[:airline_travel_details]

post[:airlineTravelDetails] = {}
post[:airlineTravelDetails][:passengerName] = options[:airline_travel_details][:passenger_name] if options[:airline_travel_details][:passenger_name]
post[:airlineTravelDetails][:departureDate] = options[:airline_travel_details][:departure_date] if options[:airline_travel_details][:departure_date]
post[:airlineTravelDetails][:origin] = options[:airline_travel_details][:origin] if options[:airline_travel_details][:origin]
post[:airlineTravelDetails][:computerizedReservationSystem] = options[:airline_travel_details][:computerized_reservation_system] if options[:airline_travel_details][:computerized_reservation_system]
post[:airlineTravelDetails][:customerReferenceNumber] = options[:airline_travel_details][:customer_reference_number] if options[:airline_travel_details][:customer_reference_number]
post[:airlineTravelDetails][:passengerName] = airline_data[:passenger_name] if airline_data[:passenger_name]
post[:airlineTravelDetails][:departureDate] = airline_data[:departure_date] if airline_data[:departure_date]
post[:airlineTravelDetails][:origin] = airline_data[:origin] if airline_data[:origin]
post[:airlineTravelDetails][:computerizedReservationSystem] = airline_data[:computerized_reservation_system] if airline_data[:computerized_reservation_system]
post[:airlineTravelDetails][:customerReferenceNumber] = airline_data[:customer_reference_number] if airline_data[:customer_reference_number]

add_ticket_details(post, options)
add_travel_agency_details(post, options)
add_trip_legs(post, options)
add_airline_ticket_details(post, options)
add_airline_travel_agency_details(post, options)
add_airline_trip_legs(post, options)
end

def add_ticket_details(post, options)
def add_airline_ticket_details(post, options)
return unless ticket = options[:airline_travel_details][:ticket]

post[:airlineTravelDetails][:ticket] = {}
post[:airlineTravelDetails][:ticket][:ticketNumber] = ticket[:ticket_number] if ticket[:ticket_number]
post[:airlineTravelDetails][:ticket][:isRestrictedTicket] = ticket[:is_restricted_ticket] if ticket[:is_restricted_ticket]
post[:airlineTravelDetails][:ticket][:isRestrictedTicket] = ticket[:is_restricted_ticket].to_s.present?
post[:airlineTravelDetails][:ticket][:cityOfTicketIssuing] = ticket[:city_of_ticket_issuing] if ticket[:city_of_ticket_issuing]
post[:airlineTravelDetails][:ticket][:ticketDeliveryMethod] = ticket[:ticket_delivery_method] if ticket[:ticket_delivery_method]
post[:airlineTravelDetails][:ticket][:ticketIssueDate] = ticket[:ticket_issue_date] if ticket[:ticket_issue_date]
post[:airlineTravelDetails][:ticket][:numberOfPax] = ticket[:number_of_pax] if ticket[:number_of_pax]
end

def add_travel_agency_details(post, options)
def add_airline_travel_agency_details(post, options)
return unless agency = options[:airline_travel_details][:travel_agency]

post[:airlineTravelDetails][:travelAgency] = {}
post[:airlineTravelDetails][:travelAgency][:name] = agency[:name] if agency[:name]
post[:airlineTravelDetails][:travelAgency][:code] = agency[:code] if agency[:code]
end

def add_trip_legs(post, options)
def add_airline_trip_legs(post, options)
return unless trip_legs = options[:airline_travel_details][:trip_legs]

trip_legs_hash = {}
trip_legs.each.with_index(1) do |leg, i|
my_leg = "leg#{i}".to_sym
details = add_leg_details(my_leg, leg[1])
details = add_airline_leg_details(my_leg, leg[1])

trip_legs_hash[my_leg] = details
end
post[:airlineTravelDetails][:tripLegs] = trip_legs_hash
end

def add_leg_details(obj, leg)
def add_airline_leg_details(obj, leg)
details = {}
add_flight_details(details, obj, leg)
details[:serviceClass] = leg[:service_class] if leg[:service_class]
details[:isStopOverAllowed] = leg[:is_stop_over_allowed] if leg[:is_stop_over_allowed]
details[:isStopOverAllowed] = leg[:is_stop_over_allowed].to_s.present?
details[:destination] = leg[:destination] if leg[:destination]
details[:fareBasis] = leg[:fare_basis] if leg[:fare_basis]
details[:departureDate] = leg[:departure_date] if leg[:departure_date]
Expand Down
69 changes: 68 additions & 1 deletion test/remote/gateways/remote_paysafe_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ def setup
computerized_reservation_system: 'DATS',
ticket: {
ticket_number: 9876789,
is_restricted_ticket: false
is_restricted_ticket: false,
city_of_ticket_issuing: 'Porto',
ticket_delivery_method: 'E_TICKET',
ticket_issue_date: '2026-01-26',
number_of_pax: 4
},
customer_reference_number: 107854099,
travel_agency: {
Expand Down Expand Up @@ -104,6 +108,58 @@ def setup
}
}
}

@cruiseline_details = {
cruiseline_travel_details: {
cruise_ship_name: 'Cruise Ship',
passenger_name: 'Joe Smith',
departure_date: '2026-11-30',
return_date: '2026-12-30',
country: 'US',
state: 'CA',
origin_city: 'SXF',
room_rate: 1200,
travel_package_application: 'CAR_RENTAL_RESERVATION',
ticket: {
ticket_number: 9876789,
is_restricted_ticket: false
},
passengers: {
passenger1: {
ticket_number: '12J13J1',
first_name: 'Joe',
last_name: 'Smith',
phone_number: '8888994222',
passenger_code: 'INF',
gender: 'M'
},
passenger2: {
ticket_number: '12J12J1',
first_name: 'Jane',
last_name: 'Smith',
phone_number: '8888994223',
passenger_code: 'INF',
gender: 'F'
}
},
trip_legs: {
leg1: {
service_class: 'F',
departure_city: 'DOM',
destination_city: 'BDS',
fare: 10,
departure_date: '2026-11-30'
},
leg2: {
service_class: 'F',
departure_city: 'DOM',
destination_city: 'BDS',
fare: 10,
departure_date: '2026-11-30'
}
}
}
}
end

def test_successful_purchase
Expand Down Expand Up @@ -150,6 +206,17 @@ def test_successful_purchase_with_airline_details
assert_equal 'F', response.params['airlineTravelDetails']['tripLegs']['leg2']['serviceClass']
end

def test_successful_purchase_with_cruiseline_details
response = @gateway.purchase(@amount, @credit_card, @options.merge(@cruiseline_details))

assert_success response
assert_equal 'COMPLETED', response.message
assert_equal 'M', response.params['cruiselineTravelDetails']['passengers']['passenger1']['gender']
assert_equal 'INF', response.params['cruiselineTravelDetails']['passengers']['passenger2']['passengerCode']
assert_equal 'BDS', response.params['cruiselineTravelDetails']['tripLegs']['leg1']['destinationCity']
assert_equal 'F', response.params['cruiselineTravelDetails']['tripLegs']['leg2']['serviceClass']
end

def test_successful_purchase_with_truncated_address
options = {
billing_address: {
Expand Down
64 changes: 64 additions & 0 deletions test/unit/gateways/paysafe_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,70 @@ def test_successful_purchase_with_airline_details
assert_success response
end

def test_successful_purchase_with_cruiseline_details
cruiseline_details = {
cruiseline_travel_details: {
cruise_ship_name: 'CruiseShip',
passenger_name: 'Joe Smith',
departure_date: '2026-11-30',
return_date: '2026-12-30',
country: 'US',
state: 'CA',
origin_city: 'SXF',
room_rate: 1200,
travel_package_application: 'CAR_RENTAL_RESERVATION',
ticket: {
ticket_number: 9876789,
is_restricted_ticket: false
},
passengers: {
passenger1: {
ticket_number: '12J13J1',
first_name: 'Joe',
last_name: 'Smith',
phone_number: '8888994222',
passenger_code: 'INF',
gender: 'M'
},
passenger2: {
ticket_number: '12J12J1',
first_name: 'Jane',
last_name: 'Smith',
phone_number: '8888994223',
passenger_code: 'INF',
gender: 'F'
}
},
trip_legs: {
leg1: {
service_class: 'F',
departure_city: 'DOM',
destination_city: 'BDS',
fare: 10,
departure_date: '2026-11-30'
},
leg2: {
service_class: 'F',
departure_city: 'DOM',
destination_city: 'BDS',
fare: 10,
departure_date: '2026-11-30'
}
}
}
}
response = stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@amount, @credit_card, cruiseline_details)
end.check_request do |_method, _endpoint, data, _headers|
assert_match(/"cruiselineTravelDetails"/, data)
assert_match(/"cruiseShipName":"CruiseShip"/, data)
assert_match(/"tripLegs":{"leg1":{"serviceClass":"F"/, data)
assert_match(/"passengers":{"passenger1":{"ticketNumber":"12J13J1"/, data)
end.respond_with(successful_purchase_response)

assert_success response
end

def test_successful_purchase_with_stored_credentials
stored_credential_options = {
stored_credential: {
Expand Down