Skip to content

Commit 6d2fd8e

Browse files
Fix return URL for headless storefronts and handle redirect result server-side (#57)
* Fix return URL for headless storefronts and handle redirect result server-side **Default return URL**: Use `store.storefront_url` (first AllowedOrigin, falling back to formatted_url) instead of the Spree engine route helper which generates URLs with incorrect scheme/port for headless setups. Legacy mode (`use_legacy_adyen_payment_sessions`) keeps the old engine route behaviour. **Redirect result handling**: `complete_payment_session` now accepts `redirect_result` in `external_data` for redirect-based payment methods (Klarna, iDEAL, 3DS). When the storefront passes the `redirectResult` from Adyen's return URL, the gateway resolves it to a `sessionResult` server-side via `/payments/details`, keeping the storefront provider-agnostic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: fail fast on missing session_result, drop unused param - Raise GatewayError when neither session_result nor redirect_result resolves, instead of passing nil to the Adyen API - Remove unused payment_session param from resolve_session_result_from_redirect - Fix hash indentation in payments_details call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add tests for complete_payment_session redirect_result handling - with session_result: completes normally - with redirect_result in external_data: resolves via /payments/details then completes - with neither: raises GatewayError - when redirect_result cannot be resolved: raises GatewayError Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 59a75c6 commit 6d2fd8e

5 files changed

Lines changed: 96 additions & 10 deletions

File tree

app/models/spree_adyen/gateway/payment_sessions.rb

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,19 @@ def update_payment_session(payment_session:, amount: nil, external_data: {})
6565
# (called by the storefront or by the webhook handler).
6666
#
6767
# @param payment_session [Spree::PaymentSessions::Adyen] the session to complete
68-
# @param params [Hash] must include :session_result
68+
# @param params [Hash] must include :session_result or external_data with :redirect_result
69+
# @raise [Spree::Core::GatewayError] if neither session_result nor redirect_result can be resolved
6970
def complete_payment_session(payment_session:, params: {})
7071
session_result = params[:session_result] || params['session_result']
7172

73+
if session_result.blank?
74+
external_data = params[:external_data] || params['external_data'] || {}
75+
redirect_result = external_data[:redirect_result] || external_data['redirect_result']
76+
session_result = resolve_session_result_from_redirect(redirect_result) if redirect_result.present?
77+
end
78+
79+
raise Spree::Core::GatewayError, 'session_result or redirect_result is required' if session_result.blank?
80+
7281
response = payment_session_result(payment_session.external_id, session_result)
7382
status = response.params.fetch('status')
7483

@@ -102,8 +111,24 @@ def complete_payment_session(payment_session:, params: {})
102111

103112
private
104113

114+
# Resolves a sessionResult from a redirectResult by calling Adyen's /payments/details endpoint.
115+
# In the sessions flow, Adyen processes the redirect server-side. We call /payments/details
116+
# with the redirectResult to finalize the payment and get the sessionResult.
117+
def resolve_session_result_from_redirect(redirect_result)
118+
response = send_request do
119+
client.checkout.payments_api.payments_details(
120+
details: { redirectResult: redirect_result }
121+
)
122+
end
123+
response.response&.dig('sessionResult')
124+
end
125+
105126
def default_return_url(order)
106-
Spree::Core::Engine.routes.url_helpers.redirect_adyen_payment_session_url(host: order.store.url_or_custom_domain)
127+
if SpreeAdyen::Config[:use_legacy_adyen_payment_sessions]
128+
Spree::Core::Engine.routes.url_helpers.redirect_adyen_payment_session_url(host: order.store.url_or_custom_domain)
129+
else
130+
"#{order.store.storefront_url}/adyen/payment_sessions/redirect"
131+
end
107132
end
108133
end
109134
end

app/models/spree_adyen/payment_session.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def set_default_channel
8282
def set_return_url
8383
return if order.blank?
8484

85-
self.return_url = Spree::Core::Engine.routes.url_helpers.redirect_adyen_payment_session_url(host: order.store.url_or_custom_domain)
85+
self.return_url = "#{order.store.storefront_url}/adyen/payment_sessions/redirect"
8686
end
8787

8888
def expiration_date_cannot_be_in_the_past_or_later_than_24_hours

spec/models/spree_adyen/gateway_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,71 @@
658658
end
659659
end
660660

661+
describe '#complete_payment_session' do
662+
let(:order) { create(:order_with_line_items, store: store) }
663+
let!(:payment_session) do
664+
Spree::PaymentSessions::Adyen.create!(
665+
order: order,
666+
payment_method: gateway,
667+
amount: order.total,
668+
currency: order.currency,
669+
status: 'pending',
670+
external_id: 'CS4FBB6F827EC53AC7',
671+
external_data: { 'session_data' => 'test', 'channel' => 'Web' }
672+
)
673+
end
674+
675+
context 'with session_result' do
676+
it 'completes the payment session' do
677+
VCR.use_cassette('payment_session_results/success/completed') do
678+
gateway.complete_payment_session(payment_session: payment_session, params: { session_result: 'resultData' })
679+
expect(payment_session.reload.status).to eq('completed')
680+
end
681+
end
682+
end
683+
684+
context 'with redirect_result in external_data' do
685+
before do
686+
allow(gateway).to receive(:resolve_session_result_from_redirect)
687+
.with('redirectResultToken')
688+
.and_return('resultData')
689+
end
690+
691+
it 'resolves session_result from redirect_result and completes' do
692+
VCR.use_cassette('payment_session_results/success/completed') do
693+
gateway.complete_payment_session(
694+
payment_session: payment_session,
695+
params: { external_data: { redirect_result: 'redirectResultToken' } }
696+
)
697+
expect(payment_session.reload.status).to eq('completed')
698+
end
699+
end
700+
end
701+
702+
context 'with neither session_result nor redirect_result' do
703+
it 'raises a gateway error' do
704+
expect {
705+
gateway.complete_payment_session(payment_session: payment_session, params: {})
706+
}.to raise_error(Spree::Core::GatewayError, 'session_result or redirect_result is required')
707+
end
708+
end
709+
710+
context 'when redirect_result cannot be resolved' do
711+
before do
712+
allow(gateway).to receive(:resolve_session_result_from_redirect).and_return(nil)
713+
end
714+
715+
it 'raises a gateway error' do
716+
expect {
717+
gateway.complete_payment_session(
718+
payment_session: payment_session,
719+
params: { external_data: { redirect_result: 'bad_token' } }
720+
)
721+
}.to raise_error(Spree::Core::GatewayError, 'session_result or redirect_result is required')
722+
end
723+
end
724+
end
725+
661726
describe '#parse_webhook_event' do
662727
let(:order) { create(:order_with_line_items, store: store) }
663728
let!(:payment_session) do

spec/models/spree_adyen/payment_session_spec.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,8 @@
172172
end
173173

174174
describe 'set_return_url' do
175-
before do
176-
allow(store).to receive(:url_or_custom_domain).and_return('url-or-custom-domain.com')
177-
end
178-
179-
it 'sets the redirect url' do
180-
expect { payment_session.validate }.to change(payment_session, :return_url).to('http://url-or-custom-domain.com/adyen/payment_sessions/redirect')
175+
it 'sets the redirect url using store storefront_url' do
176+
expect { payment_session.validate }.to change(payment_session, :return_url).to("#{store.storefront_url}/adyen/payment_sessions/redirect")
181177
end
182178
end
183179
end

spec/requests/spree/api/v2/storefront/adyen/payment_sessions_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
expect(json_data['attributes']['client_key']).to eq('test_client_key')
9999
expect(json_data['attributes']['adyen_data']).to be_present
100100
expect(json_data['attributes']['channel']).to eq('Web') # default channel
101-
expect(json_data['attributes']['return_url']).to eq('http://www.example.com/adyen/payment_sessions/redirect') # default channel
101+
expect(json_data['attributes']['return_url']).to eq("#{store.storefront_url}/adyen/payment_sessions/redirect")
102102

103103
# Verify relationships
104104
expect(json_data['relationships']['order']['data']['id']).to eq(order.id.to_s)

0 commit comments

Comments
 (0)