spree_adyen 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +9 -0
- data/README.md +59 -0
- data/Rakefile +26 -0
- data/app/assets/config/spree_adyen_manifest.js +5 -0
- data/app/controllers/spree_adyen/apple_pay_domain_verification_controller.rb +11 -0
- data/app/controllers/spree_adyen/payment_sessions_controller.rb +67 -0
- data/app/controllers/spree_adyen/store_controller_decorator.rb +9 -0
- data/app/controllers/spree_adyen/webhooks_controller.rb +46 -0
- data/app/helpers/spree_adyen/base_helper.rb +18 -0
- data/app/javascript/spree_adyen/application.js +17 -0
- data/app/javascript/spree_adyen/controllers/checkout_adyen_controller.js +80 -0
- data/app/javascript/spree_adyen/controllers/redirect_adyen_controller.js +35 -0
- data/app/jobs/spree_adyen/add_allowed_origin_job.rb +24 -0
- data/app/jobs/spree_adyen/base_job.rb +5 -0
- data/app/jobs/spree_adyen/webhooks/process_authorisation_event_job.rb +10 -0
- data/app/jobs/spree_adyen/webhooks/process_cancellation_event_job.rb +10 -0
- data/app/jobs/spree_adyen/webhooks/process_capture_event_job.rb +10 -0
- data/app/models/spree/payment_sessions/adyen.rb +59 -0
- data/app/models/spree_adyen/base.rb +6 -0
- data/app/models/spree_adyen/custom_domain_decorator.rb +17 -0
- data/app/models/spree_adyen/gateway/payment_sessions.rb +151 -0
- data/app/models/spree_adyen/gateway.rb +487 -0
- data/app/models/spree_adyen/order_decorator.rb +23 -0
- data/app/models/spree_adyen/payment_decorator.rb +66 -0
- data/app/models/spree_adyen/payment_method_decorator.rb +15 -0
- data/app/models/spree_adyen/payment_session.rb +121 -0
- data/app/models/spree_adyen/payment_sources/ach_direct_debit.rb +17 -0
- data/app/models/spree_adyen/payment_sources/affirm.rb +17 -0
- data/app/models/spree_adyen/payment_sources/afterpay.rb +17 -0
- data/app/models/spree_adyen/payment_sources/alipay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/alipay_hk.rb +17 -0
- data/app/models/spree_adyen/payment_sources/alma.rb +17 -0
- data/app/models/spree_adyen/payment_sources/ancv.rb +17 -0
- data/app/models/spree_adyen/payment_sources/apple_pay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/atome.rb +17 -0
- data/app/models/spree_adyen/payment_sources/bacs.rb +17 -0
- data/app/models/spree_adyen/payment_sources/bancontact.rb +19 -0
- data/app/models/spree_adyen/payment_sources/bank_transfer.rb +17 -0
- data/app/models/spree_adyen/payment_sources/base.rb +7 -0
- data/app/models/spree_adyen/payment_sources/bcmc.rb +17 -0
- data/app/models/spree_adyen/payment_sources/bcmc_mobile.rb +17 -0
- data/app/models/spree_adyen/payment_sources/benefit.rb +17 -0
- data/app/models/spree_adyen/payment_sources/billie.rb +17 -0
- data/app/models/spree_adyen/payment_sources/bizum.rb +17 -0
- data/app/models/spree_adyen/payment_sources/blik.rb +15 -0
- data/app/models/spree_adyen/payment_sources/boleto.rb +17 -0
- data/app/models/spree_adyen/payment_sources/cash_app_afterpay.rb +17 -0
- data/app/models/spree_adyen/payment_sources/cashapp.rb +17 -0
- data/app/models/spree_adyen/payment_sources/clearpay.rb +17 -0
- data/app/models/spree_adyen/payment_sources/dana.rb +17 -0
- data/app/models/spree_adyen/payment_sources/doku.rb +19 -0
- data/app/models/spree_adyen/payment_sources/duitnow.rb +17 -0
- data/app/models/spree_adyen/payment_sources/ebanking_fi.rb +19 -0
- data/app/models/spree_adyen/payment_sources/eft_directdebit_ca.rb +19 -0
- data/app/models/spree_adyen/payment_sources/eftpos_australia.rb +19 -0
- data/app/models/spree_adyen/payment_sources/elo.rb +19 -0
- data/app/models/spree_adyen/payment_sources/eps.rb +19 -0
- data/app/models/spree_adyen/payment_sources/fastlane.rb +19 -0
- data/app/models/spree_adyen/payment_sources/fpx.rb +19 -0
- data/app/models/spree_adyen/payment_sources/gcash.rb +19 -0
- data/app/models/spree_adyen/payment_sources/gift_cards.rb +19 -0
- data/app/models/spree_adyen/payment_sources/giropay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/givex.rb +19 -0
- data/app/models/spree_adyen/payment_sources/gopay_wallet.rb +19 -0
- data/app/models/spree_adyen/payment_sources/grabpay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/grabpay_paylater.rb +19 -0
- data/app/models/spree_adyen/payment_sources/hipercard.rb +19 -0
- data/app/models/spree_adyen/payment_sources/ideal.rb +15 -0
- data/app/models/spree_adyen/payment_sources/kakaopay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/kcp_naverpay.rb +19 -0
- data/app/models/spree_adyen/payment_sources/klarna.rb +32 -0
- data/app/models/spree_adyen/payment_sources/oney.rb +13 -0
- data/app/models/spree_adyen/payment_sources/online_banking_czech_republic.rb +15 -0
- data/app/models/spree_adyen/payment_sources/online_banking_poland.rb +15 -0
- data/app/models/spree_adyen/payment_sources/pay_by_bank.rb +15 -0
- data/app/models/spree_adyen/payment_sources/paypal.rb +15 -0
- data/app/models/spree_adyen/payment_sources/paypo.rb +15 -0
- data/app/models/spree_adyen/payment_sources/paysafecard.rb +15 -0
- data/app/models/spree_adyen/payment_sources/rate_pay_direct_debit.rb +15 -0
- data/app/models/spree_adyen/payment_sources/riverty.rb +15 -0
- data/app/models/spree_adyen/payment_sources/samsung_pay.rb +15 -0
- data/app/models/spree_adyen/payment_sources/scalapay.rb +13 -0
- data/app/models/spree_adyen/payment_sources/sepa_direct_debit.rb +15 -0
- data/app/models/spree_adyen/payment_sources/trustly.rb +15 -0
- data/app/models/spree_adyen/payment_sources/unknown.rb +19 -0
- data/app/models/spree_adyen/payment_sources/wechat_pay.rb +15 -0
- data/app/models/spree_adyen/store_decorator.rb +17 -0
- data/app/presenters/spree_adyen/address_presenter.rb +21 -0
- data/app/presenters/spree_adyen/application_info_presenter.rb +21 -0
- data/app/presenters/spree_adyen/cancel_payload_presenter.rb +31 -0
- data/app/presenters/spree_adyen/capture_payload_presenter.rb +36 -0
- data/app/presenters/spree_adyen/checkout_presenter.rb +51 -0
- data/app/presenters/spree_adyen/payment_sessions/request_payload_presenter.rb +140 -0
- data/app/presenters/spree_adyen/payments/request_payload_presenter.rb +85 -0
- data/app/presenters/spree_adyen/refund_payload_presenter.rb +38 -0
- data/app/presenters/spree_adyen/webhook_payload_presenter.rb +28 -0
- data/app/presenters/spree_adyen/webhooks/credit_card_presenter.rb +43 -0
- data/app/services/spree_adyen/gateways/add_allowed_origin.rb +36 -0
- data/app/services/spree_adyen/gateways/configuration.rb +41 -0
- data/app/services/spree_adyen/gateways/configure.rb +60 -0
- data/app/services/spree_adyen/payment_sessions/find_or_create.rb +51 -0
- data/app/services/spree_adyen/payment_sessions/process_with_result.rb +49 -0
- data/app/services/spree_adyen/webhooks/actions/create_source.rb +112 -0
- data/app/services/spree_adyen/webhooks/actions/find_or_create_credit_card.rb +33 -0
- data/app/services/spree_adyen/webhooks/event.rb +104 -0
- data/app/services/spree_adyen/webhooks/event_processors/authorisation_event_processor.rb +69 -0
- data/app/services/spree_adyen/webhooks/event_processors/cancellation_event_processor.rb +49 -0
- data/app/services/spree_adyen/webhooks/event_processors/capture_event_processor.rb +48 -0
- data/app/services/spree_adyen/webhooks/handle_event.rb +43 -0
- data/app/services/spree_adyen/webhooks/standard_hmac_validator.rb +37 -0
- data/app/views/layouts/spree_adyen/default.html.erb +14 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_adyen.html.erb +14 -0
- data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_adyen.html.erb +18 -0
- data/app/views/spree/admin/payment_methods/descriptions/_spree_adyen.html.erb +12 -0
- data/app/views/spree/admin/payments/source_forms/_spree_adyen.html.erb +7 -0
- data/app/views/spree/checkout/payment/_spree_adyen.html.erb +40 -0
- data/app/views/spree_adyen/_drop_in.html.erb +12 -0
- data/app/views/spree_adyen/_head.html.erb +2 -0
- data/app/views/spree_adyen/payment_sessions/redirect.html.erb +5 -0
- data/config/importmap.rb +7 -0
- data/config/initializers/spree.rb +34 -0
- data/config/initializers/spree_permitted_attributes.rb +5 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +35 -0
- data/db/migrate/20250630150000_setup_spree_adyen_models.rb +17 -0
- data/db/migrate/20250811140113_add_channel_to_adyen_payment_sessions.rb +10 -0
- data/db/migrate/20250813152608_add_return_url_to_spree_adyen_payment_sessions.rb +15 -0
- data/lib/generators/spree_adyen/install/install_generator.rb +20 -0
- data/lib/spree_adyen/configuration.rb +42 -0
- data/lib/spree_adyen/engine.rb +79 -0
- data/lib/spree_adyen/factories.rb +3 -0
- data/lib/spree_adyen/testing_support/factories/gateway_factory.rb +29 -0
- data/lib/spree_adyen/testing_support/factories/payment_session_factory.rb +40 -0
- data/lib/spree_adyen/version.rb +7 -0
- data/lib/spree_adyen.rb +39 -0
- data/lib/spree_api_v2/spree/api/v2/storefront/adyen/base_controller.rb +21 -0
- data/lib/spree_api_v2/spree/api/v2/storefront/adyen/payment_sessions_controller.rb +72 -0
- data/lib/spree_api_v2/spree_adyen/api/v2/storefront/payment_session_serializer.rb +21 -0
- data/vendor/javascript/@adyen--adyen-web.js +4 -0
- data/vendor/stylesheets/adyen.css +106 -0
- metadata +301 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module SpreeAdyen
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentSessions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def session_required?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def payment_session_class
|
|
11
|
+
Spree::PaymentSessions::Adyen
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Creates a new Adyen payment session via the Adyen Sessions API
|
|
15
|
+
# and persists a Spree::PaymentSessions::Adyen record.
|
|
16
|
+
#
|
|
17
|
+
# @param order [Spree::Order] the order to create a session for
|
|
18
|
+
# @param amount [BigDecimal, nil] the amount (defaults to order total minus store credits)
|
|
19
|
+
# @param external_data [Hash] additional data (e.g., channel, return_url)
|
|
20
|
+
# @return [Spree::PaymentSessions::Adyen] the created payment session record
|
|
21
|
+
def create_payment_session(order:, amount: nil, external_data: {})
|
|
22
|
+
total = amount.presence || order.total_minus_store_credits
|
|
23
|
+
channel = external_data[:channel] || external_data['channel'] || Spree::PaymentSessions::Adyen::AVAILABLE_CHANNELS[:web]
|
|
24
|
+
return_url = external_data[:return_url] || external_data['return_url'] || default_return_url(order)
|
|
25
|
+
|
|
26
|
+
response = create_adyen_session(total, order, channel, return_url)
|
|
27
|
+
|
|
28
|
+
payment_session_class.create!(
|
|
29
|
+
order: order,
|
|
30
|
+
payment_method: self,
|
|
31
|
+
amount: total,
|
|
32
|
+
currency: order.currency,
|
|
33
|
+
status: 'pending',
|
|
34
|
+
external_id: response.params['id'],
|
|
35
|
+
customer: order.user,
|
|
36
|
+
expires_at: response.params['expiresAt'],
|
|
37
|
+
external_data: {
|
|
38
|
+
'session_data' => response.params['sessionData'],
|
|
39
|
+
'channel' => channel,
|
|
40
|
+
'return_url' => return_url
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Updates an existing payment session amount.
|
|
46
|
+
#
|
|
47
|
+
# @param payment_session [Spree::PaymentSessions::Adyen] the session to update
|
|
48
|
+
# @param amount [BigDecimal, nil] new amount
|
|
49
|
+
# @param external_data [Hash] additional data to merge
|
|
50
|
+
def update_payment_session(payment_session:, amount: nil, external_data: {})
|
|
51
|
+
attrs = {}
|
|
52
|
+
attrs[:amount] = amount if amount.present?
|
|
53
|
+
|
|
54
|
+
if external_data.present?
|
|
55
|
+
attrs[:external_data] = (payment_session.external_data || {}).merge(external_data.stringify_keys)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
payment_session.update!(attrs) if attrs.any?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Completes a payment session by checking the session result with Adyen,
|
|
62
|
+
# creating the Payment record, and transitioning the session accordingly.
|
|
63
|
+
#
|
|
64
|
+
# Does NOT complete the order — that is handled by Carts::Complete
|
|
65
|
+
# (called by the storefront or by the webhook handler).
|
|
66
|
+
#
|
|
67
|
+
# @param payment_session [Spree::PaymentSessions::Adyen] the session to complete
|
|
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 is provided
|
|
70
|
+
def complete_payment_session(payment_session:, params: {})
|
|
71
|
+
session_result = params[:session_result] || params['session_result']
|
|
72
|
+
|
|
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
|
+
|
|
77
|
+
raise Spree::Core::GatewayError, 'session_result or redirect_result is required' if redirect_result.blank?
|
|
78
|
+
|
|
79
|
+
return complete_payment_session_from_redirect(payment_session, redirect_result)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
complete_payment_session_from_result(payment_session, session_result)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def complete_payment_session_from_result(payment_session, session_result)
|
|
88
|
+
response = payment_session_result(payment_session.external_id, session_result)
|
|
89
|
+
status = response.params.fetch('status')
|
|
90
|
+
process_payment_session_status(payment_session, status)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def complete_payment_session_from_redirect(payment_session, redirect_result)
|
|
94
|
+
response = send_request do
|
|
95
|
+
client.checkout.payments_api.payments_details({ details: { redirectResult: redirect_result } })
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
result_code = response.response&.dig('resultCode')
|
|
99
|
+
status = map_result_code_to_status(result_code)
|
|
100
|
+
process_payment_session_status(payment_session, status)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def map_result_code_to_status(result_code)
|
|
104
|
+
case result_code
|
|
105
|
+
when 'Authorised' then 'completed'
|
|
106
|
+
when 'Pending', 'Received' then 'paymentPending'
|
|
107
|
+
when 'Cancelled' then 'canceled'
|
|
108
|
+
when 'Refused', 'Error' then 'refused'
|
|
109
|
+
else result_code
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def process_payment_session_status(payment_session, status)
|
|
114
|
+
payment_session.order.with_lock do
|
|
115
|
+
payment = payment_session.order.payments.where(
|
|
116
|
+
payment_method: payment_session.payment_method,
|
|
117
|
+
response_code: payment_session.external_id
|
|
118
|
+
).first_or_initialize
|
|
119
|
+
|
|
120
|
+
payment.update!(amount: payment_session.amount, skip_source_requirement: true)
|
|
121
|
+
payment.started_processing! if payment.checkout?
|
|
122
|
+
|
|
123
|
+
case status
|
|
124
|
+
when 'completed'
|
|
125
|
+
payment_session.complete if payment_session.can_complete?
|
|
126
|
+
payment.confirm!
|
|
127
|
+
when 'canceled'
|
|
128
|
+
payment.void! if payment.can_void?
|
|
129
|
+
payment_session.cancel if payment_session.can_cancel?
|
|
130
|
+
when 'refused', 'expired'
|
|
131
|
+
payment.failure! unless payment.failed?
|
|
132
|
+
payment_session.fail if payment_session.can_fail?
|
|
133
|
+
when 'paymentPending'
|
|
134
|
+
payment_session.process if payment_session.can_process?
|
|
135
|
+
else
|
|
136
|
+
Rails.error.unexpected('Unexpected Adyen payment status', context: { order_id: payment_session.order.id, status: status },
|
|
137
|
+
source: 'spree_adyen')
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def default_return_url(order)
|
|
143
|
+
if SpreeAdyen::Config[:use_legacy_adyen_payment_sessions]
|
|
144
|
+
Spree::Core::Engine.routes.url_helpers.redirect_adyen_payment_session_url(host: order.store.url_or_custom_domain)
|
|
145
|
+
else
|
|
146
|
+
"#{order.store.storefront_url}/adyen/payment_sessions/redirect"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
module SpreeAdyen
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
include PaymentSessions
|
|
4
|
+
|
|
5
|
+
CAPTURE_PSP_REFERENCE_METAFIELD_KEY = 'adyen.capture_psp_reference'.freeze
|
|
6
|
+
CANCELLATION_PSP_REFERENCE_METAFIELD_KEY = 'adyen.cancellation_psp_reference'.freeze
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# Attributes
|
|
10
|
+
#
|
|
11
|
+
attribute :skip_auto_configuration, :boolean, default: false
|
|
12
|
+
attribute :skip_api_key_validation, :boolean, default: false
|
|
13
|
+
|
|
14
|
+
preference :api_key, :password
|
|
15
|
+
preference :merchant_account, :string
|
|
16
|
+
preference :client_key, :password
|
|
17
|
+
preference :hmac_key, :password
|
|
18
|
+
preference :test_mode, :boolean, default: true
|
|
19
|
+
preference :webhook_id, :string
|
|
20
|
+
preference :live_url_prefix, :string
|
|
21
|
+
|
|
22
|
+
has_one_attached :apple_developer_merchantid_domain_association, service: Spree.private_storage_service_name
|
|
23
|
+
|
|
24
|
+
store_accessor :private_metadata, :previous_hmac_key
|
|
25
|
+
#
|
|
26
|
+
# Validations
|
|
27
|
+
#
|
|
28
|
+
validates :preferred_api_key, presence: true
|
|
29
|
+
validates :preferred_live_url_prefix, presence: true, unless: :preferred_test_mode
|
|
30
|
+
validate :validate_api_key, if: -> { preferred_api_key_changed? }, unless: :skip_api_key_validation
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Callbacks
|
|
34
|
+
#
|
|
35
|
+
after_commit :configure, if: :preferred_api_key_previously_changed?, unless: :skip_auto_configuration
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# Associations
|
|
39
|
+
#
|
|
40
|
+
has_many :payment_sessions, class_name: 'SpreeAdyen::PaymentSession',
|
|
41
|
+
foreign_key: 'payment_method_id',
|
|
42
|
+
dependent: :delete_all,
|
|
43
|
+
inverse_of: :payment_method
|
|
44
|
+
|
|
45
|
+
# @param amount_in_cents [Integer] the amount in cents to capture
|
|
46
|
+
# @param payment_source [Spree::CreditCard | Spree::PaymentSource]
|
|
47
|
+
# @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
|
|
48
|
+
def purchase(amount_in_cents, payment_source, gateway_options = {})
|
|
49
|
+
handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param amount_in_cents [Integer] the amount in cents to capture
|
|
53
|
+
# @param payment_source [Spree::CreditCard | Spree::PaymentSource]
|
|
54
|
+
# @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
|
|
55
|
+
def authorize(amount_in_cents, payment_source, gateway_options = {})
|
|
56
|
+
handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options = {})
|
|
60
|
+
payload = SpreeAdyen::Payments::RequestPayloadPresenter.new(
|
|
61
|
+
source: payment_source,
|
|
62
|
+
amount_in_cents: amount_in_cents,
|
|
63
|
+
manual_capture: !auto_capture?,
|
|
64
|
+
gateway_options: gateway_options
|
|
65
|
+
).to_h
|
|
66
|
+
|
|
67
|
+
response = send_request do
|
|
68
|
+
client.checkout.payments_api.payments(payload, headers: { 'Idempotency-Key' => SecureRandom.uuid })
|
|
69
|
+
end
|
|
70
|
+
response_body = response.response
|
|
71
|
+
|
|
72
|
+
if response.status.to_i == 200
|
|
73
|
+
success(response_body.pspReference, response_body)
|
|
74
|
+
else
|
|
75
|
+
failure(response_body.slice('pspReference', 'message').values.join(' - '))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cancel(id, payment)
|
|
80
|
+
transaction_id = id
|
|
81
|
+
payment ||= Spree::Payment.find_by(response_code: id)
|
|
82
|
+
if payment.completed?
|
|
83
|
+
amount = payment.credit_allowed
|
|
84
|
+
return success(transaction_id, {}) if amount.zero?
|
|
85
|
+
# Don't create a refund if the payment is for a shipment, we will create a refund for the whole shipping cost instead
|
|
86
|
+
return success(transaction_id, {}) if payment.respond_to?(:for_shipment?) && payment.for_shipment?
|
|
87
|
+
|
|
88
|
+
refund = payment.refunds.create!(
|
|
89
|
+
amount: amount,
|
|
90
|
+
reason: Spree::RefundReason.order_canceled_reason,
|
|
91
|
+
refunder_id: payment.order.canceler_id
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Spree::Refund#response has the response from the `credit` action
|
|
95
|
+
# For the authorization ID we need to use the payment.response_code
|
|
96
|
+
# Otherwise we'll overwrite the payment authorization with the refund ID
|
|
97
|
+
success(transaction_id, refund.response.params)
|
|
98
|
+
else
|
|
99
|
+
payment.void!
|
|
100
|
+
success(transaction_id, {})
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def credit(amount_in_cents, _source, payment_id, gateway_options = {})
|
|
105
|
+
refund = gateway_options[:originator]
|
|
106
|
+
payment = refund.present? ? refund.payment : Spree::Payment.find_by(response_code: payment_id)
|
|
107
|
+
|
|
108
|
+
return failure("#{payment_id} - Payment not found") unless payment
|
|
109
|
+
|
|
110
|
+
payload = SpreeAdyen::RefundPayloadPresenter.new(
|
|
111
|
+
payment: payment,
|
|
112
|
+
amount_in_cents: amount_in_cents,
|
|
113
|
+
payment_method: self,
|
|
114
|
+
currency: payment.currency,
|
|
115
|
+
refund: refund
|
|
116
|
+
).to_h
|
|
117
|
+
|
|
118
|
+
response = send_request do
|
|
119
|
+
client.checkout.modifications_api.refund_captured_payment(payload, payment.transaction_id, headers: { 'Idempotency-Key' => SecureRandom.uuid })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if response.status.to_i == 201
|
|
123
|
+
success(response.response['pspReference'], response)
|
|
124
|
+
else
|
|
125
|
+
failure(response.response.slice('pspReference', 'message').values.join(' - '))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def request_capture(amount_in_cents, response_code, _gateway_options = {})
|
|
130
|
+
payment = Spree::Payment.find_by(response_code: response_code)
|
|
131
|
+
|
|
132
|
+
return failure("#{response_code} - Payment not found") if payment.blank?
|
|
133
|
+
return failure("#{response_code} - Payment is already captured") if payment.completed?
|
|
134
|
+
|
|
135
|
+
payload = SpreeAdyen::CapturePayloadPresenter.new(
|
|
136
|
+
amount_in_cents: amount_in_cents,
|
|
137
|
+
payment: payment,
|
|
138
|
+
payment_method: self
|
|
139
|
+
).to_h
|
|
140
|
+
|
|
141
|
+
response = send_request do
|
|
142
|
+
client.checkout.modifications_api.capture_authorised_payment(
|
|
143
|
+
payload,
|
|
144
|
+
payment.response_code,
|
|
145
|
+
headers: { 'Idempotency-Key' => SecureRandom.uuid }
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if response.status.to_i == 201
|
|
150
|
+
success(response.response['paymentPspReference'], response)
|
|
151
|
+
else
|
|
152
|
+
failure(response.response.slice('paymentPspReference', 'message').values.join(' - '))
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# This only checks if the capture was successful by checking the presence of the capture PSP reference
|
|
157
|
+
# The actual capture is requested in #request_capture and handled in the SpreeAdyen::Webhooks::EventProcessors::CaptureEventProcessor
|
|
158
|
+
def capture(amount_in_cents, response_code, _gateway_options = {})
|
|
159
|
+
payment = Spree::Payment.find_by(response_code: response_code)
|
|
160
|
+
|
|
161
|
+
return failure("#{response_code} - Payment not found") if payment.blank?
|
|
162
|
+
return failure("#{response_code} - Payment is already captured") if payment.completed?
|
|
163
|
+
return failure("#{response_code} - Capture PSP reference not found") unless payment.has_metafield?(CAPTURE_PSP_REFERENCE_METAFIELD_KEY)
|
|
164
|
+
|
|
165
|
+
success(payment.response_code, {})
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def request_void(response_code, _source, _gateway_options)
|
|
169
|
+
payment = Spree::Payment.find_by(response_code: response_code)
|
|
170
|
+
|
|
171
|
+
return failure("#{response_code} - Payment not found") if payment.blank?
|
|
172
|
+
return failure("#{response_code} - Payment is already void") if payment.void?
|
|
173
|
+
|
|
174
|
+
payload = SpreeAdyen::CancelPayloadPresenter.new(
|
|
175
|
+
payment: payment,
|
|
176
|
+
payment_method: self
|
|
177
|
+
).to_h
|
|
178
|
+
|
|
179
|
+
response = send_request do
|
|
180
|
+
client.checkout.modifications_api.cancel_authorised_payment_by_psp_reference(
|
|
181
|
+
payload,
|
|
182
|
+
payment.response_code,
|
|
183
|
+
headers: { 'Idempotency-Key' => SecureRandom.uuid }
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if response.status.to_i == 201
|
|
188
|
+
success(response.response['paymentPspReference'], response)
|
|
189
|
+
else
|
|
190
|
+
failure(response.response.slice('paymentPspReference', 'message').values.join(' - '))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# This only checks if the void was successful by checking the presence of the cancellation PSP reference
|
|
195
|
+
# The actual void is requested in #request_void and handled in the SpreeAdyen::Webhooks::EventProcessors::CancellationEventProcessor
|
|
196
|
+
def void(response_code, _source, _gateway_options)
|
|
197
|
+
payment = Spree::Payment.find_by(response_code: response_code)
|
|
198
|
+
|
|
199
|
+
return failure("#{response_code} - Payment not found") if payment.blank?
|
|
200
|
+
return failure("#{response_code} - Payment is already void") if payment.void?
|
|
201
|
+
return failure("#{response_code} - Cancellation PSP reference not found") unless payment.has_metafield?(CANCELLATION_PSP_REFERENCE_METAFIELD_KEY)
|
|
202
|
+
|
|
203
|
+
success(payment.response_code, {})
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def webhook_url
|
|
207
|
+
store = stores.first
|
|
208
|
+
return nil unless store
|
|
209
|
+
|
|
210
|
+
if SpreeAdyen::Config[:use_legacy_webhook_handlers]
|
|
211
|
+
"#{store.formatted_url}/adyen/webhooks"
|
|
212
|
+
else
|
|
213
|
+
"#{store.formatted_url}/api/v3/webhooks/payments/#{prefixed_id}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_webhook_event(raw_body, headers)
|
|
218
|
+
payload = JSON.parse(raw_body).with_indifferent_access
|
|
219
|
+
event = SpreeAdyen::Webhooks::Event.new(event_data: payload)
|
|
220
|
+
|
|
221
|
+
# Verify HMAC signature
|
|
222
|
+
webhook_request_item = payload.dig('notificationItems', 0, 'NotificationRequestItem') || {}
|
|
223
|
+
unless valid_hmac?(webhook_request_item)
|
|
224
|
+
raise Spree::PaymentMethod::WebhookSignatureError, 'Invalid HMAC signature'
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Find payment session — scoped to this gateway
|
|
228
|
+
payment_session = event.session_id.present? ?
|
|
229
|
+
Spree::PaymentSessions::Adyen.find_by(payment_method: self, external_id: event.session_id) : nil
|
|
230
|
+
|
|
231
|
+
return nil unless payment_session
|
|
232
|
+
|
|
233
|
+
case event.code
|
|
234
|
+
when 'AUTHORISATION'
|
|
235
|
+
action = event.success? ? :authorized : :failed
|
|
236
|
+
{ action: action, payment_session: payment_session, metadata: { adyen_event: event } }
|
|
237
|
+
when 'CAPTURE'
|
|
238
|
+
{ action: :captured, payment_session: payment_session, metadata: { adyen_event: event } }
|
|
239
|
+
when 'CANCELLATION'
|
|
240
|
+
{ action: :canceled, payment_session: payment_session, metadata: { adyen_event: event } }
|
|
241
|
+
else
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def provider_class
|
|
247
|
+
self.class
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def environment
|
|
251
|
+
if preferred_test_mode
|
|
252
|
+
:test
|
|
253
|
+
else
|
|
254
|
+
:live
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def create_profile(payment); end
|
|
259
|
+
|
|
260
|
+
def payment_session_result(payment_session_id, session_result)
|
|
261
|
+
response = send_request do
|
|
262
|
+
client.checkout.payments_api.get_result_of_payment_session(payment_session_id, query_params: { sessionResult: session_result })
|
|
263
|
+
end
|
|
264
|
+
response_body = response.response
|
|
265
|
+
|
|
266
|
+
if response.status.to_i == 200
|
|
267
|
+
success(response_body.id, response_body)
|
|
268
|
+
else
|
|
269
|
+
failure(response_body.slice('pspReference', 'message').values.join(' - '))
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Creates an Adyen session via the Adyen Sessions API.
|
|
274
|
+
# Used internally by the v3 PaymentSessions module and by the legacy SpreeAdyen::PaymentSession model.
|
|
275
|
+
#
|
|
276
|
+
# @param amount [BigDecimal] the amount
|
|
277
|
+
# @param order [Spree::Order] the order to create a session for
|
|
278
|
+
# @param channel [String] the channel (Web, iOS, Android)
|
|
279
|
+
# @param return_url [String] the return URL after redirect flow
|
|
280
|
+
# @return [Spree::PaymentResponse] the response from the session creation
|
|
281
|
+
def create_adyen_session(amount, order, channel, return_url)
|
|
282
|
+
payload = SpreeAdyen::PaymentSessions::RequestPayloadPresenter.new(
|
|
283
|
+
order: order,
|
|
284
|
+
amount: amount,
|
|
285
|
+
user: order.user,
|
|
286
|
+
merchant_account: preferred_merchant_account,
|
|
287
|
+
payment_method: self,
|
|
288
|
+
channel: channel,
|
|
289
|
+
return_url: return_url
|
|
290
|
+
).to_h
|
|
291
|
+
|
|
292
|
+
response = send_request do
|
|
293
|
+
client.checkout.payments_api.sessions(payload, headers: { 'Idempotency-Key' => SecureRandom.uuid })
|
|
294
|
+
end
|
|
295
|
+
response_body = response.response
|
|
296
|
+
|
|
297
|
+
if response.status.to_i == 201
|
|
298
|
+
success(response_body.id, response_body)
|
|
299
|
+
else
|
|
300
|
+
failure(response_body.slice('pspReference', 'message').values.join(' - '))
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# @return [Boolean] whether payment profiles are supported
|
|
305
|
+
# this is used by spree to determine whenever payment source must be passed to gateway methods
|
|
306
|
+
def payment_profiles_supported?
|
|
307
|
+
true
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def default_name
|
|
311
|
+
'Adyen'
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def method_type
|
|
315
|
+
'spree_adyen'
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def payment_icon_name
|
|
319
|
+
'adyen'
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def description_partial_name
|
|
323
|
+
'spree_adyen'
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def configuration_guide_partial_name
|
|
327
|
+
'spree_adyen'
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def custom_form_fields_partial_name
|
|
331
|
+
'spree_adyen'
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def gateway_dashboard_payment_url(payment)
|
|
335
|
+
return if payment.transaction_id.blank?
|
|
336
|
+
|
|
337
|
+
"https://ca-#{environment}.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=#{payment.transaction_id}&txType=Payment"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def reusable_sources(order)
|
|
341
|
+
if order.completed?
|
|
342
|
+
sources_by_order order
|
|
343
|
+
elsif order.user.present?
|
|
344
|
+
credit_cards.where(user_id: order.user_id)
|
|
345
|
+
else
|
|
346
|
+
[]
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def get_api_credential_details
|
|
351
|
+
response = client.management.my_api_credential_api.get_api_credential_details
|
|
352
|
+
|
|
353
|
+
if response.status.to_i == 200
|
|
354
|
+
success(response.response.id, response.response)
|
|
355
|
+
else
|
|
356
|
+
failure(response.response.message)
|
|
357
|
+
end
|
|
358
|
+
rescue Adyen::AuthenticationError, Adyen::PermissionError
|
|
359
|
+
raise
|
|
360
|
+
rescue Adyen::AdyenError => e
|
|
361
|
+
failure(parse_adyen_error_response(e)['message'])
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def add_allowed_origin(domain)
|
|
365
|
+
response = client.management.my_api_credential_api.add_allowed_origin({ domain: domain })
|
|
366
|
+
|
|
367
|
+
if response.status.to_i == 200
|
|
368
|
+
success(response.response.id, response.response)
|
|
369
|
+
else
|
|
370
|
+
failure(response.response)
|
|
371
|
+
end
|
|
372
|
+
rescue Adyen::AdyenError => e
|
|
373
|
+
failure(parse_adyen_error_response(e))
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def set_up_webhook(url)
|
|
377
|
+
payload = SpreeAdyen::WebhookPayloadPresenter.new(url).to_h
|
|
378
|
+
response = client.management.webhooks_merchant_level_api.set_up_webhook(payload, preferred_merchant_account)
|
|
379
|
+
|
|
380
|
+
if response.status.to_i == 200
|
|
381
|
+
success(response.response.id, response.response)
|
|
382
|
+
else
|
|
383
|
+
failure(response.response)
|
|
384
|
+
end
|
|
385
|
+
rescue Adyen::AdyenError => e
|
|
386
|
+
failure(parse_adyen_error_response(e)['message'])
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def test_webhook
|
|
390
|
+
response = client.management.webhooks_merchant_level_api.test_webhook({ types: ['AUTHORISATION'] }, preferred_merchant_account,
|
|
391
|
+
preferred_webhook_id)
|
|
392
|
+
|
|
393
|
+
if response.status.to_i == 200 && response.response.dig('data', 0, 'status') == 'success'
|
|
394
|
+
success(nil, response.response)
|
|
395
|
+
else
|
|
396
|
+
failure(response.response)
|
|
397
|
+
end
|
|
398
|
+
rescue Adyen::AdyenError => e
|
|
399
|
+
failure(parse_adyen_error_response(e)['message'])
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def generate_hmac_key
|
|
403
|
+
response = client.management.webhooks_merchant_level_api.generate_hmac_key(preferred_merchant_account, preferred_webhook_id)
|
|
404
|
+
|
|
405
|
+
if response.status.to_i == 200
|
|
406
|
+
success(response.response.hmacKey, response.response)
|
|
407
|
+
else
|
|
408
|
+
failure(response.response)
|
|
409
|
+
end
|
|
410
|
+
rescue Adyen::AdyenError => e
|
|
411
|
+
failure(parse_adyen_error_response(e)['message'])
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def generate_client_key
|
|
415
|
+
response = client.management.my_api_credential_api.generate_client_key
|
|
416
|
+
|
|
417
|
+
if response.status.to_i == 200
|
|
418
|
+
success(response.response.clientKey, response.response)
|
|
419
|
+
else
|
|
420
|
+
failure(response.response.message)
|
|
421
|
+
end
|
|
422
|
+
rescue Adyen::AdyenError => e
|
|
423
|
+
failure(parse_adyen_error_response(e)['message'])
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def apple_domain_association_file_content
|
|
427
|
+
@apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
private
|
|
431
|
+
|
|
432
|
+
def validate_api_key
|
|
433
|
+
return if preferred_api_key.blank?
|
|
434
|
+
|
|
435
|
+
get_api_credential_details
|
|
436
|
+
rescue Adyen::AuthenticationError => e
|
|
437
|
+
errors.add(:preferred_api_key, "is invalid. Response: #{e.msg}")
|
|
438
|
+
rescue Adyen::PermissionError => e
|
|
439
|
+
errors.add(:preferred_api_key, "has insufficient permissions. Add missing roles to API credential. Response: #{e.msg}")
|
|
440
|
+
rescue Adyen::AdyenError => e
|
|
441
|
+
errors.add(:preferred_api_key, "An error occurred. Response: #{e.msg}")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def configure
|
|
445
|
+
return if preferred_api_key.blank?
|
|
446
|
+
|
|
447
|
+
SpreeAdyen::Gateways::Configure.new(self).call
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def client
|
|
451
|
+
@client ||= Adyen::Client.new.tap do |client|
|
|
452
|
+
client.api_key = preferred_api_key
|
|
453
|
+
client.env = environment
|
|
454
|
+
client.live_url_prefix = preferred_live_url_prefix if environment == :live
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def send_request
|
|
459
|
+
yield
|
|
460
|
+
rescue Adyen::AdyenError => e
|
|
461
|
+
raise Spree::Core::GatewayError, e.msg
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def parse_adyen_error_response(error)
|
|
465
|
+
JSON.parse(error.response)
|
|
466
|
+
rescue JSON::ParserError, TypeError
|
|
467
|
+
{ 'message' => error.msg }
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def valid_hmac?(webhook_request_item)
|
|
471
|
+
hmac_keys = [preferred_hmac_key, previous_hmac_key].compact
|
|
472
|
+
return false if hmac_keys.empty?
|
|
473
|
+
|
|
474
|
+
hmac_keys.any? do |key|
|
|
475
|
+
Adyen::Utils::HmacValidator.new.valid_webhook_hmac?(webhook_request_item, key)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def success(authorization, full_response)
|
|
480
|
+
Spree::PaymentResponse.new(true, nil, full_response.as_json, authorization: authorization)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def failure(error = nil)
|
|
484
|
+
Spree::PaymentResponse.new(false, error)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SpreeAdyen
|
|
2
|
+
module OrderDecorator
|
|
3
|
+
def self.prepended(base)
|
|
4
|
+
base.has_many :adyen_payment_sessions, class_name: 'SpreeAdyen::PaymentSession', dependent: :destroy
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def outdate_payment_sessions
|
|
8
|
+
return unless SpreeAdyen::Config[:use_legacy_adyen_payment_sessions]
|
|
9
|
+
|
|
10
|
+
adyen_payment_sessions
|
|
11
|
+
.where.not(currency: currency).or(adyen_payment_sessions.where.not(amount: total_minus_store_credits))
|
|
12
|
+
.with_status(:initial)
|
|
13
|
+
.each(&:destroy)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def can_create_adyen_payment_session?
|
|
17
|
+
state.in?(%w[confirm payment])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Spree::Order.prepend(SpreeAdyen::OrderDecorator)
|
|
23
|
+
Spree::Order.register_update_hook :outdate_payment_sessions
|