diff --git a/lib/checkout_sdk/checkout_api.rb b/lib/checkout_sdk/checkout_api.rb index 245b528..ad2fef7 100644 --- a/lib/checkout_sdk/checkout_api.rb +++ b/lib/checkout_sdk/checkout_api.rb @@ -45,6 +45,8 @@ module CheckoutSdk # @return [CheckoutSdk::Payments::PaymentSessionsClient] # @!attribute payments_setups # @return [CheckoutSdk::Payments::PaymentSetupsClient] + # @!attribute flow + # @return [CheckoutSdk::Payments::FlowClient] # @!attribute forward # @return [CheckoutSdk::Forward::ForwardClient] class CheckoutApi @@ -56,6 +58,7 @@ class CheckoutApi :links, :payments, :payments_setups, + :flow, :reports, :sessions, :tokens, @@ -97,6 +100,7 @@ def initialize(configuration) @contexts = CheckoutSdk::Payments::PaymentContextsClient.new api_client, configuration @payment_sessions = CheckoutSdk::Payments::PaymentSessionsClient.new api_client, configuration @payments_setups = CheckoutSdk::Payments::PaymentSetupsClient.new api_client, configuration + @flow = CheckoutSdk::Payments::FlowClient.new api_client, configuration @forward = CheckoutSdk::Forward::ForwardClient.new(api_client, configuration) end diff --git a/lib/checkout_sdk/payments/flow/flow_client.rb b/lib/checkout_sdk/payments/flow/flow_client.rb new file mode 100644 index 0000000..ce22dc3 --- /dev/null +++ b/lib/checkout_sdk/payments/flow/flow_client.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # Client for Payment Flow API operations + # [Beta] + class FlowClient < Client + PAYMENT_SESSIONS_PATH = 'payment-sessions' + SUBMIT_PATH = 'submit' + COMPLETE_PATH = 'complete' + + # @param [ApiClient] api_client + # @param [CheckoutConfiguration] configuration + def initialize(api_client, configuration) + super(api_client, configuration, CheckoutSdk::AuthorizationType::SECRET_KEY_OR_OAUTH) + end + + # Creates a Payment Session. + # Use this endpoint to set up a payment session before collecting payment details from your customer. + # [Beta] + # + # @param [Hash] request_payment_session_request + def request_payment_session(request_payment_session_request) + api_client.invoke_post( + build_path(PAYMENT_SESSIONS_PATH), + sdk_authorization, + request_payment_session_request + ) + end + + # Submits a Payment Session. + # Use this endpoint to submit payment details and process the payment for an existing session. + # [Beta] + # + # @param [String] id - The unique identifier of the Payment Session + # @param [Hash] submit_payment_session_request + def submit_payment_session(id, submit_payment_session_request) + api_client.invoke_post( + build_path(PAYMENT_SESSIONS_PATH, id, SUBMIT_PATH), + sdk_authorization, + submit_payment_session_request + ) + end + + # Creates and submits a Payment Session in a single request. + # Use this endpoint to create a payment session and immediately process the payment. + # [Beta] + # + # @param [Hash] request_payment_session_with_payment_request + def request_payment_session_with_payment(request_payment_session_with_payment_request) + api_client.invoke_post( + build_path(PAYMENT_SESSIONS_PATH, COMPLETE_PATH), + sdk_authorization, + request_payment_session_with_payment_request + ) + end + end + end +end diff --git a/lib/checkout_sdk/payments/payments.rb b/lib/checkout_sdk/payments/payments.rb index 701c3e5..8cdfebc 100644 --- a/lib/checkout_sdk/payments/payments.rb +++ b/lib/checkout_sdk/payments/payments.rb @@ -180,3 +180,6 @@ # Payment Setups require 'checkout_sdk/payments/setups/payment_setups_client' + +# Payment Flow +require 'checkout_sdk/payments/flow/flow_client' diff --git a/spec/checkout_sdk/payments/flow/flow_helper.rb b/spec/checkout_sdk/payments/flow/flow_helper.rb new file mode 100644 index 0000000..ccedc8b --- /dev/null +++ b/spec/checkout_sdk/payments/flow/flow_helper.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +module FlowHelper + def request_payment_session_request(**opts) + amount = opts.fetch(:amount, 1000) + currency = opts.fetch(:currency, 'USD') + + { + amount: amount, + currency: currency, + payment_type: 'Regular', + billing: { + address: { + address_line1: '123 High St.', + address_line2: 'Flat 456', + city: 'London', + state: 'London', + zip: 'SW1A 1AA', + country: 'GB' + }, + phone: { + country_code: '+1', + number: '415 555 2671' + } + }, + reference: "ORD-#{SecureRandom.uuid[0..8]}", + description: 'Integration test payment session', + customer: { + email: "jia.tsang+#{SecureRandom.uuid[0..5]}@example.com", + name: 'Jia Tsang', + phone: { + country_code: '+1', + number: '415 555 2671' + } + }, + shipping: { + address: { + address_line1: '123 High St.', + address_line2: 'Flat 456', + city: 'London', + state: 'London', + zip: 'SW1A 1AA', + country: 'GB' + }, + phone: { + country_code: '+1', + number: '415 555 2671' + } + }, + processing_channel_id: ENV.fetch('CHECKOUT_PROCESSING_CHANNEL_ID'), + success_url: 'https://example.com/payments/success', + failure_url: 'https://example.com/payments/failure', + enabled_payment_methods: ['card'], + metadata: { + test_payment: 'flow_integration_test' + } + } + end + + def submit_payment_session_request(**opts) + amount = opts.fetch(:amount, 1000) + + { + session_data: 'string', + amount: amount, + reference: "SUBMIT-#{SecureRandom.uuid[0..8]}", + payment_type: 'Regular', + items: [{ + reference: "item-#{SecureRandom.uuid[0..5]}", + name: 'Test Item', + quantity: 1, + unit_price: amount, + total_amount: amount + }], + ip_address: '90.197.169.245' + } + end + + def request_payment_session_with_payment_request(**opts) + amount = opts.fetch(:amount, 1000) + currency = opts.fetch(:currency, 'USD') + + { + session_data: 'string', + amount: amount, + currency: currency, + payment_type: 'Regular', + billing: { + address: { + address_line1: '123 High St.', + address_line2: 'Flat 456', + city: 'London', + state: 'London', + zip: 'SW1A 1AA', + country: 'GB' + }, + phone: { + country_code: '+1', + number: '415 555 2671' + } + }, + reference: "CREATE-SUBMIT-#{SecureRandom.uuid[0..8]}", + description: 'Integration test create and submit', + customer: { + email: "jia.tsang+#{SecureRandom.uuid[0..5]}@example.com", + name: 'Jia Tsang', + phone: { + country_code: '+1', + number: '415 555 2671' + } + }, + processing_channel_id: ENV.fetch('CHECKOUT_PROCESSING_CHANNEL_ID'), + success_url: 'https://example.com/payments/success', + failure_url: 'https://example.com/payments/failure' + } + end + + def request_payment_session(**opts) + amount = opts.fetch(:amount, 1000) + currency = opts.fetch(:currency, 'USD') + + request = request_payment_session_request(amount: amount, currency: currency) + response = default_sdk.flow.request_payment_session(request) + expect(response).not_to be nil + expect(response.id).not_to be nil + response + end + + def submit_payment_session(session_id, **opts) + request = submit_payment_session_request(**opts) + response = default_sdk.flow.submit_payment_session(session_id, request) + expect(response).not_to be nil + response + end + + def request_payment_session_with_payment(**opts) + amount = opts.fetch(:amount, 1000) + currency = opts.fetch(:currency, 'USD') + + request = request_payment_session_with_payment_request(amount: amount, currency: currency) + response = default_sdk.flow.request_payment_session_with_payment(request) + expect(response).not_to be nil + response + end +end \ No newline at end of file diff --git a/spec/checkout_sdk/payments/flow/flow_integration_spec.rb b/spec/checkout_sdk/payments/flow/flow_integration_spec.rb new file mode 100644 index 0000000..799c4b4 --- /dev/null +++ b/spec/checkout_sdk/payments/flow/flow_integration_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +RSpec.describe CheckoutSdk::Payments do + include FlowHelper + + before(:all) do + @api = default_sdk + end + + describe '.request_payment_session' do + context 'when creating a payment session with hash request' do + it 'creates payment session successfully' do + response = request_payment_session + + # Only check the essential fields that are likely to be returned + assert_response(response, %w[id]) + expect(response.id).not_to be nil + # Check optional fields only if they exist + expect(response.amount).to eq(1000) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('USD') if response.respond_to?(:currency) && !response.currency.nil? + end + end + + context 'when creating with different currencies' do + it 'creates payment session with EUR' do + response = request_payment_session(amount: 2000, currency: 'EUR') + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(2000) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('EUR') if response.respond_to?(:currency) && !response.currency.nil? + end + + it 'creates payment session with GBP' do + response = request_payment_session(amount: 1500, currency: 'GBP') + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(1500) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('GBP') if response.respond_to?(:currency) && !response.currency.nil? + end + end + + context 'when creating with invalid data' do + it 'raises an exception for missing required fields' do + invalid_request = { + # Missing required fields like processing_channel_id, amount, currency + reference: 'INVALID-REF' + } + + expect { + @api.flow.request_payment_session(invalid_request) + }.to raise_error(CheckoutSdk::CheckoutApiException) + end + end + end + + describe '.submit_payment_session' do + context 'when submitting a valid payment session' do + xit 'submits payment session successfully (requires real Flow session_data)' do + # Create a payment session first + create_response = request_payment_session(amount: 1000) + + # Submit the payment session + submit_request = submit_payment_session_request(amount: 1000) + response = @api.flow.submit_payment_session(create_response.id, submit_request) + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(1000) if response.respond_to?(:amount) && !response.amount.nil? + end + end + + context 'when submitting with invalid payment method' do + xit 'raises an exception for invalid card details (requires real Flow session_data)' do + # Create a payment session first + create_response = request_payment_session(amount: 1000) + + invalid_submit_request = { + amount: 1000, + reference: 'INVALID-REF', + payment_type: 'InvalidType', # Invalid payment type + ip_address: '90.197.169.245' + } + + expect { + @api.flow.submit_payment_session(create_response.id, invalid_submit_request) + }.to raise_error(CheckoutSdk::CheckoutApiException) + end + end + + context 'when submitting to non-existent session' do + xit 'raises an exception (requires real Flow session_data)' do + fake_session_id = 'ps_non_existent_session' + submit_request = submit_payment_session_request(amount: 1000) + + expect { + @api.flow.submit_payment_session(fake_session_id, submit_request) + }.to raise_error(CheckoutSdk::CheckoutApiException) + end + end + end + + describe '.request_payment_session_with_payment' do + context 'when creating and submitting in one request' do + xit 'processes payment session successfully (requires real Flow session_data)' do + response = request_payment_session_with_payment(amount: 1000) + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(1000) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('USD') if response.respond_to?(:currency) && !response.currency.nil? + end + end + + context 'when creating and submitting with different currencies' do + xit 'processes payment session with EUR (requires real Flow session_data)' do + response = request_payment_session_with_payment(amount: 2000, currency: 'EUR') + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(2000) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('EUR') if response.respond_to?(:currency) && !response.currency.nil? + end + + xit 'processes payment session with GBP (requires real Flow session_data)' do + response = request_payment_session_with_payment(amount: 1500, currency: 'GBP') + + assert_response(response, %w[id]) + expect(response.id).not_to be nil + expect(response.amount).to eq(1500) if response.respond_to?(:amount) && !response.amount.nil? + expect(response.currency).to eq('GBP') if response.respond_to?(:currency) && !response.currency.nil? + end + end + + context 'when creating and submitting with invalid data' do + xit 'raises an exception for invalid payment method (requires real Flow session_data)' do + invalid_request = request_payment_session_with_payment_request(amount: 1000) + invalid_request[:processing_channel_id] = 'invalid_channel_id' # Invalid channel + + expect { + @api.flow.request_payment_session_with_payment(invalid_request) + }.to raise_error(CheckoutSdk::CheckoutApiException) + end + end + end + + describe 'Integration workflow' do + context 'when performing complete payment flow workflow' do + xit 'creates session, then submits payment separately (requires real Flow session_data)' do + # Step 1: Create payment session + create_response = request_payment_session(amount: 1000) + expect(create_response.id).not_to be nil + expect(create_response.amount).to eq(1000) if create_response.respond_to?(:amount) && !create_response.amount.nil? + expect(create_response.currency).to eq('USD') if create_response.respond_to?(:currency) && !create_response.currency.nil? + + # Step 2: Submit payment to the session + submit_request = submit_payment_session_request(amount: 1000) + submit_response = @api.flow.submit_payment_session(create_response.id, submit_request) + + expect(submit_response.id).not_to be nil + expect(submit_response.amount).to eq(1000) if submit_response.respond_to?(:amount) && !submit_response.amount.nil? + end + end + + context 'when comparing separate vs combined flow' do + xit 'produces similar results for both approaches (requires real Flow session_data)' do + # Separate flow: create then submit + create_response = request_payment_session(amount: 1000) + submit_request = submit_payment_session_request(amount: 1000) + separate_response = @api.flow.submit_payment_session(create_response.id, submit_request) + + # Combined flow: create and submit together + combined_response = request_payment_session_with_payment(amount: 1000) + + # Both should have similar structure + expect(separate_response.amount).to eq(combined_response.amount) if separate_response.respond_to?(:amount) && combined_response.respond_to?(:amount) + expect(separate_response.currency).to eq(combined_response.currency) if separate_response.respond_to?(:currency) && combined_response.respond_to?(:currency) + end + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0da3dbc..a74adde 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require './spec/checkout_sdk/payments/contexts/contexts_helper' require './spec/checkout_sdk/payments/sessions/sessions_helper' require './spec/checkout_sdk/payments/setups/payment_setups_helper' +require './spec/checkout_sdk/payments/flow/flow_helper' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure