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.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +9 -0
  3. data/README.md +59 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/config/spree_adyen_manifest.js +5 -0
  6. data/app/controllers/spree_adyen/apple_pay_domain_verification_controller.rb +11 -0
  7. data/app/controllers/spree_adyen/payment_sessions_controller.rb +67 -0
  8. data/app/controllers/spree_adyen/store_controller_decorator.rb +9 -0
  9. data/app/controllers/spree_adyen/webhooks_controller.rb +46 -0
  10. data/app/helpers/spree_adyen/base_helper.rb +18 -0
  11. data/app/javascript/spree_adyen/application.js +17 -0
  12. data/app/javascript/spree_adyen/controllers/checkout_adyen_controller.js +80 -0
  13. data/app/javascript/spree_adyen/controllers/redirect_adyen_controller.js +35 -0
  14. data/app/jobs/spree_adyen/add_allowed_origin_job.rb +24 -0
  15. data/app/jobs/spree_adyen/base_job.rb +5 -0
  16. data/app/jobs/spree_adyen/webhooks/process_authorisation_event_job.rb +10 -0
  17. data/app/jobs/spree_adyen/webhooks/process_cancellation_event_job.rb +10 -0
  18. data/app/jobs/spree_adyen/webhooks/process_capture_event_job.rb +10 -0
  19. data/app/models/spree/payment_sessions/adyen.rb +59 -0
  20. data/app/models/spree_adyen/base.rb +6 -0
  21. data/app/models/spree_adyen/custom_domain_decorator.rb +17 -0
  22. data/app/models/spree_adyen/gateway/payment_sessions.rb +151 -0
  23. data/app/models/spree_adyen/gateway.rb +487 -0
  24. data/app/models/spree_adyen/order_decorator.rb +23 -0
  25. data/app/models/spree_adyen/payment_decorator.rb +66 -0
  26. data/app/models/spree_adyen/payment_method_decorator.rb +15 -0
  27. data/app/models/spree_adyen/payment_session.rb +121 -0
  28. data/app/models/spree_adyen/payment_sources/ach_direct_debit.rb +17 -0
  29. data/app/models/spree_adyen/payment_sources/affirm.rb +17 -0
  30. data/app/models/spree_adyen/payment_sources/afterpay.rb +17 -0
  31. data/app/models/spree_adyen/payment_sources/alipay.rb +19 -0
  32. data/app/models/spree_adyen/payment_sources/alipay_hk.rb +17 -0
  33. data/app/models/spree_adyen/payment_sources/alma.rb +17 -0
  34. data/app/models/spree_adyen/payment_sources/ancv.rb +17 -0
  35. data/app/models/spree_adyen/payment_sources/apple_pay.rb +19 -0
  36. data/app/models/spree_adyen/payment_sources/atome.rb +17 -0
  37. data/app/models/spree_adyen/payment_sources/bacs.rb +17 -0
  38. data/app/models/spree_adyen/payment_sources/bancontact.rb +19 -0
  39. data/app/models/spree_adyen/payment_sources/bank_transfer.rb +17 -0
  40. data/app/models/spree_adyen/payment_sources/base.rb +7 -0
  41. data/app/models/spree_adyen/payment_sources/bcmc.rb +17 -0
  42. data/app/models/spree_adyen/payment_sources/bcmc_mobile.rb +17 -0
  43. data/app/models/spree_adyen/payment_sources/benefit.rb +17 -0
  44. data/app/models/spree_adyen/payment_sources/billie.rb +17 -0
  45. data/app/models/spree_adyen/payment_sources/bizum.rb +17 -0
  46. data/app/models/spree_adyen/payment_sources/blik.rb +15 -0
  47. data/app/models/spree_adyen/payment_sources/boleto.rb +17 -0
  48. data/app/models/spree_adyen/payment_sources/cash_app_afterpay.rb +17 -0
  49. data/app/models/spree_adyen/payment_sources/cashapp.rb +17 -0
  50. data/app/models/spree_adyen/payment_sources/clearpay.rb +17 -0
  51. data/app/models/spree_adyen/payment_sources/dana.rb +17 -0
  52. data/app/models/spree_adyen/payment_sources/doku.rb +19 -0
  53. data/app/models/spree_adyen/payment_sources/duitnow.rb +17 -0
  54. data/app/models/spree_adyen/payment_sources/ebanking_fi.rb +19 -0
  55. data/app/models/spree_adyen/payment_sources/eft_directdebit_ca.rb +19 -0
  56. data/app/models/spree_adyen/payment_sources/eftpos_australia.rb +19 -0
  57. data/app/models/spree_adyen/payment_sources/elo.rb +19 -0
  58. data/app/models/spree_adyen/payment_sources/eps.rb +19 -0
  59. data/app/models/spree_adyen/payment_sources/fastlane.rb +19 -0
  60. data/app/models/spree_adyen/payment_sources/fpx.rb +19 -0
  61. data/app/models/spree_adyen/payment_sources/gcash.rb +19 -0
  62. data/app/models/spree_adyen/payment_sources/gift_cards.rb +19 -0
  63. data/app/models/spree_adyen/payment_sources/giropay.rb +19 -0
  64. data/app/models/spree_adyen/payment_sources/givex.rb +19 -0
  65. data/app/models/spree_adyen/payment_sources/gopay_wallet.rb +19 -0
  66. data/app/models/spree_adyen/payment_sources/grabpay.rb +19 -0
  67. data/app/models/spree_adyen/payment_sources/grabpay_paylater.rb +19 -0
  68. data/app/models/spree_adyen/payment_sources/hipercard.rb +19 -0
  69. data/app/models/spree_adyen/payment_sources/ideal.rb +15 -0
  70. data/app/models/spree_adyen/payment_sources/kakaopay.rb +19 -0
  71. data/app/models/spree_adyen/payment_sources/kcp_naverpay.rb +19 -0
  72. data/app/models/spree_adyen/payment_sources/klarna.rb +32 -0
  73. data/app/models/spree_adyen/payment_sources/oney.rb +13 -0
  74. data/app/models/spree_adyen/payment_sources/online_banking_czech_republic.rb +15 -0
  75. data/app/models/spree_adyen/payment_sources/online_banking_poland.rb +15 -0
  76. data/app/models/spree_adyen/payment_sources/pay_by_bank.rb +15 -0
  77. data/app/models/spree_adyen/payment_sources/paypal.rb +15 -0
  78. data/app/models/spree_adyen/payment_sources/paypo.rb +15 -0
  79. data/app/models/spree_adyen/payment_sources/paysafecard.rb +15 -0
  80. data/app/models/spree_adyen/payment_sources/rate_pay_direct_debit.rb +15 -0
  81. data/app/models/spree_adyen/payment_sources/riverty.rb +15 -0
  82. data/app/models/spree_adyen/payment_sources/samsung_pay.rb +15 -0
  83. data/app/models/spree_adyen/payment_sources/scalapay.rb +13 -0
  84. data/app/models/spree_adyen/payment_sources/sepa_direct_debit.rb +15 -0
  85. data/app/models/spree_adyen/payment_sources/trustly.rb +15 -0
  86. data/app/models/spree_adyen/payment_sources/unknown.rb +19 -0
  87. data/app/models/spree_adyen/payment_sources/wechat_pay.rb +15 -0
  88. data/app/models/spree_adyen/store_decorator.rb +17 -0
  89. data/app/presenters/spree_adyen/address_presenter.rb +21 -0
  90. data/app/presenters/spree_adyen/application_info_presenter.rb +21 -0
  91. data/app/presenters/spree_adyen/cancel_payload_presenter.rb +31 -0
  92. data/app/presenters/spree_adyen/capture_payload_presenter.rb +36 -0
  93. data/app/presenters/spree_adyen/checkout_presenter.rb +51 -0
  94. data/app/presenters/spree_adyen/payment_sessions/request_payload_presenter.rb +140 -0
  95. data/app/presenters/spree_adyen/payments/request_payload_presenter.rb +85 -0
  96. data/app/presenters/spree_adyen/refund_payload_presenter.rb +38 -0
  97. data/app/presenters/spree_adyen/webhook_payload_presenter.rb +28 -0
  98. data/app/presenters/spree_adyen/webhooks/credit_card_presenter.rb +43 -0
  99. data/app/services/spree_adyen/gateways/add_allowed_origin.rb +36 -0
  100. data/app/services/spree_adyen/gateways/configuration.rb +41 -0
  101. data/app/services/spree_adyen/gateways/configure.rb +60 -0
  102. data/app/services/spree_adyen/payment_sessions/find_or_create.rb +51 -0
  103. data/app/services/spree_adyen/payment_sessions/process_with_result.rb +49 -0
  104. data/app/services/spree_adyen/webhooks/actions/create_source.rb +112 -0
  105. data/app/services/spree_adyen/webhooks/actions/find_or_create_credit_card.rb +33 -0
  106. data/app/services/spree_adyen/webhooks/event.rb +104 -0
  107. data/app/services/spree_adyen/webhooks/event_processors/authorisation_event_processor.rb +69 -0
  108. data/app/services/spree_adyen/webhooks/event_processors/cancellation_event_processor.rb +49 -0
  109. data/app/services/spree_adyen/webhooks/event_processors/capture_event_processor.rb +48 -0
  110. data/app/services/spree_adyen/webhooks/handle_event.rb +43 -0
  111. data/app/services/spree_adyen/webhooks/standard_hmac_validator.rb +37 -0
  112. data/app/views/layouts/spree_adyen/default.html.erb +14 -0
  113. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_adyen.html.erb +14 -0
  114. data/app/views/spree/admin/payment_methods/custom_form_fields/_spree_adyen.html.erb +18 -0
  115. data/app/views/spree/admin/payment_methods/descriptions/_spree_adyen.html.erb +12 -0
  116. data/app/views/spree/admin/payments/source_forms/_spree_adyen.html.erb +7 -0
  117. data/app/views/spree/checkout/payment/_spree_adyen.html.erb +40 -0
  118. data/app/views/spree_adyen/_drop_in.html.erb +12 -0
  119. data/app/views/spree_adyen/_head.html.erb +2 -0
  120. data/app/views/spree_adyen/payment_sessions/redirect.html.erb +5 -0
  121. data/config/importmap.rb +7 -0
  122. data/config/initializers/spree.rb +34 -0
  123. data/config/initializers/spree_permitted_attributes.rb +5 -0
  124. data/config/locales/en.yml +11 -0
  125. data/config/routes.rb +35 -0
  126. data/db/migrate/20250630150000_setup_spree_adyen_models.rb +17 -0
  127. data/db/migrate/20250811140113_add_channel_to_adyen_payment_sessions.rb +10 -0
  128. data/db/migrate/20250813152608_add_return_url_to_spree_adyen_payment_sessions.rb +15 -0
  129. data/lib/generators/spree_adyen/install/install_generator.rb +20 -0
  130. data/lib/spree_adyen/configuration.rb +42 -0
  131. data/lib/spree_adyen/engine.rb +79 -0
  132. data/lib/spree_adyen/factories.rb +3 -0
  133. data/lib/spree_adyen/testing_support/factories/gateway_factory.rb +29 -0
  134. data/lib/spree_adyen/testing_support/factories/payment_session_factory.rb +40 -0
  135. data/lib/spree_adyen/version.rb +7 -0
  136. data/lib/spree_adyen.rb +39 -0
  137. data/lib/spree_api_v2/spree/api/v2/storefront/adyen/base_controller.rb +21 -0
  138. data/lib/spree_api_v2/spree/api/v2/storefront/adyen/payment_sessions_controller.rb +72 -0
  139. data/lib/spree_api_v2/spree_adyen/api/v2/storefront/payment_session_serializer.rb +21 -0
  140. data/vendor/javascript/@adyen--adyen-web.js +4 -0
  141. data/vendor/stylesheets/adyen.css +106 -0
  142. metadata +301 -0
@@ -0,0 +1,49 @@
1
+ module SpreeAdyen
2
+ module PaymentSessions
3
+ class ProcessWithResult
4
+ def initialize(payment_session:, session_result:)
5
+ @payment_session = payment_session
6
+ @session_result = session_result
7
+ end
8
+
9
+ def call
10
+ response = payment_session.payment_method.payment_session_result(payment_session.adyen_id, session_result)
11
+ status = response.params.fetch('status')
12
+
13
+ order.with_lock do
14
+ payment = order.payments.where(
15
+ payment_method: payment_session.payment_method,
16
+ response_code: payment_session.adyen_id
17
+ ).first_or_initialize
18
+
19
+ payment.update!(amount: payment_session.amount, skip_source_requirement: true)
20
+ payment.started_processing! if payment.checkout? # it can be already changed by webhook
21
+
22
+ case status
23
+ when 'completed'
24
+ payment_session.complete! if payment_session.can_complete?
25
+ payment.confirm!
26
+ Spree::Dependencies.checkout_complete_service.constantize.call(order: order) unless order.completed?
27
+ when 'canceled'
28
+ payment.void! if payment.can_void?
29
+ payment_session.cancel! unless payment_session.canceled?
30
+ when 'refused', 'expired'
31
+ payment.failure! unless payment.failed?
32
+ payment_session.refuse! unless payment_session.refused?
33
+ when 'paymentPending'
34
+ payment_session.pending! if payment_session.can_pending? # this can have other status after
35
+ else
36
+ Rails.error.unexpected('Unexpected payment status', context: { order_id: order.id, status: status },
37
+ source: 'spree_adyen')
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :payment_session, :session_result
45
+
46
+ delegate :order, to: :payment_session
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,112 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ module Actions
4
+ class CreateSource
5
+ SOURCE_KLASS_MAP = {
6
+ affirm: SpreeAdyen::PaymentSources::Affirm,
7
+ alipay: SpreeAdyen::PaymentSources::Alipay,
8
+ bacs: SpreeAdyen::PaymentSources::Bacs,
9
+ bankTransfer_IBAN: SpreeAdyen::PaymentSources::BankTransfer,
10
+ klarna_b2b: SpreeAdyen::PaymentSources::Billie,
11
+ blik: SpreeAdyen::PaymentSources::Blik,
12
+ clearpay: SpreeAdyen::PaymentSources::Clearpay,
13
+ eps: SpreeAdyen::PaymentSources::Eps,
14
+ ideal: SpreeAdyen::PaymentSources::Ideal,
15
+ facilypay_3x: SpreeAdyen::PaymentSources::Oney,
16
+ facilypay_4x: SpreeAdyen::PaymentSources::Oney,
17
+ facilypay_6x: SpreeAdyen::PaymentSources::Oney,
18
+ facilypay_10x: SpreeAdyen::PaymentSources::Oney,
19
+ facilypay_12x: SpreeAdyen::PaymentSources::Oney,
20
+ scalapay_3x: SpreeAdyen::PaymentSources::Scalapay,
21
+ klarna: SpreeAdyen::PaymentSources::Klarna,
22
+ klarna_account: SpreeAdyen::PaymentSources::Klarna,
23
+ klarna_paynow: SpreeAdyen::PaymentSources::Klarna,
24
+ klarna_paylater: SpreeAdyen::PaymentSources::Klarna,
25
+ klarna_payovertime: SpreeAdyen::PaymentSources::Klarna,
26
+ onlineBanking_CZ: SpreeAdyen::PaymentSources::OnlineBankingCzechRepublic,
27
+ onlineBanking_PL: SpreeAdyen::PaymentSources::OnlineBankingPoland,
28
+ paybybank: SpreeAdyen::PaymentSources::PayByBank,
29
+ paypal: SpreeAdyen::PaymentSources::Paypal,
30
+ paypo: SpreeAdyen::PaymentSources::Paypo,
31
+ paysafecard: SpreeAdyen::PaymentSources::Paysafecard,
32
+ ratepay_directdebit: SpreeAdyen::PaymentSources::RatePayDirectDebit,
33
+ riverty: SpreeAdyen::PaymentSources::Riverty,
34
+ samsungpay: SpreeAdyen::PaymentSources::SamsungPay,
35
+ sepadirectdebit: SpreeAdyen::PaymentSources::SepaDirectDebit,
36
+ trustly: SpreeAdyen::PaymentSources::Trustly,
37
+ wechatpaySDK: SpreeAdyen::PaymentSources::WechatPay,
38
+ wechatpayQR: SpreeAdyen::PaymentSources::WechatPay,
39
+ ach: SpreeAdyen::PaymentSources::AchDirectDebit,
40
+ afterpaytouch: SpreeAdyen::PaymentSources::Afterpay,
41
+ afterpaytouch_US: SpreeAdyen::PaymentSources::CashAppAfterpay,
42
+ alipay_hk: SpreeAdyen::PaymentSources::AlipayHk,
43
+ alma: SpreeAdyen::PaymentSources::Alma,
44
+ ancv: SpreeAdyen::PaymentSources::Ancv,
45
+ atome: SpreeAdyen::PaymentSources::Atome,
46
+ benefit: SpreeAdyen::PaymentSources::Benefit,
47
+ bcmc: SpreeAdyen::PaymentSources::Bancontact,
48
+ bcmc_mobile: SpreeAdyen::PaymentSources::Bancontact,
49
+ bizum: SpreeAdyen::PaymentSources::Bizum,
50
+ boleto: SpreeAdyen::PaymentSources::Boleto,
51
+ cashapp: SpreeAdyen::PaymentSources::Cashapp,
52
+ doku_alfamart: SpreeAdyen::PaymentSources::Doku,
53
+ doku_indomaret: SpreeAdyen::PaymentSources::Doku,
54
+ dana: SpreeAdyen::PaymentSources::Dana,
55
+ duitnow: SpreeAdyen::PaymentSources::Duitnow,
56
+ fastlane: SpreeAdyen::PaymentSources::Fastlane,
57
+ molpay_ebanking_fpx_MY: SpreeAdyen::PaymentSources::Fpx,
58
+ gcash: SpreeAdyen::PaymentSources::Gcash,
59
+ givex: SpreeAdyen::PaymentSources::GiftCards,
60
+ genericgiftcard: SpreeAdyen::PaymentSources::GiftCards,
61
+ valuelink: SpreeAdyen::PaymentSources::GiftCards,
62
+ svs: SpreeAdyen::PaymentSources::GiftCards,
63
+ giropay: SpreeAdyen::PaymentSources::Giropay,
64
+ grabpay_MY: SpreeAdyen::PaymentSources::Grabpay,
65
+ grabpay_PH: SpreeAdyen::PaymentSources::Grabpay,
66
+ grabpay_SG: SpreeAdyen::PaymentSources::Grabpay
67
+ }.freeze
68
+
69
+ def initialize(event:, payment_method:, user:)
70
+ @event = event
71
+ @payment_method = payment_method
72
+ @user = user
73
+ end
74
+
75
+ def call
76
+ if event.payment_method_reference.in?(SpreeAdyen::Config.credit_card_sources)
77
+ find_or_create_credit_card
78
+ else
79
+ find_or_create_source
80
+ end
81
+ end
82
+
83
+ def find_or_create_source
84
+ source_klass_factory.find_or_create_by(
85
+ gateway_payment_profile_id: event.stored_payment_method_id.presence || event.psp_reference,
86
+ payment_method: payment_method
87
+ ) do |source|
88
+ source.user = user
89
+ end
90
+ end
91
+
92
+ def find_or_create_credit_card
93
+ SpreeAdyen::Webhooks::Actions::FindOrCreateCreditCard.new(
94
+ event: event,
95
+ gateway: payment_method,
96
+ user: user
97
+ ).call
98
+ end
99
+
100
+ private
101
+
102
+ attr_reader :event, :payment_method, :user
103
+
104
+ delegate :payment_method_reference, to: :event
105
+
106
+ def source_klass_factory
107
+ SOURCE_KLASS_MAP[event.payment_method_reference] || SpreeAdyen::PaymentSources::Unknown
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,33 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ module Actions
4
+ class FindOrCreateCreditCard
5
+ def initialize(event:, gateway:, user:)
6
+ @event = event
7
+ @gateway = gateway
8
+ @user = user
9
+ end
10
+
11
+ def call
12
+ gateway.credit_cards.capturable.find_or_create_by(find_by_hash) do |cc|
13
+ cc.assign_attributes(credit_card_attributes)
14
+ cc.user = user
15
+ cc.payment_method = gateway
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :event, :gateway, :user
22
+
23
+ def credit_card_attributes
24
+ @credit_card_attributes ||= SpreeAdyen::Webhooks::CreditCardPresenter.new(event).to_h
25
+ end
26
+
27
+ def find_by_hash
28
+ credit_card_attributes.slice(:gateway_payment_profile_id).compact
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,104 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ class Event
4
+ def initialize(event_data:)
5
+ @event_data = event_data.to_h.with_indifferent_access
6
+ end
7
+
8
+ def id
9
+ @id ||= body['pspReference']
10
+ end
11
+
12
+ def payload
13
+ event_data
14
+ end
15
+
16
+ def order_number
17
+ @order_number ||= merchant_reference.split('_')[0]
18
+ end
19
+
20
+ def payment_method_id
21
+ @payment_method_id ||= merchant_reference.split('_')[1]
22
+ end
23
+
24
+ def code
25
+ @code ||= body['eventCode']
26
+ end
27
+
28
+ def success?
29
+ body['success'] == 'true'
30
+ end
31
+
32
+ def billing_address
33
+ @billing_address ||= begin
34
+ return {} unless additional_data['billingAddress']
35
+
36
+ SpreeAdyen::AddressPresenter.new(additional_data['billingAddress']).to_h
37
+ end
38
+ end
39
+
40
+ def shipping_address
41
+ @shipping_address ||= begin
42
+ return {} unless additional_data['deliveryAddress']
43
+
44
+ SpreeAdyen::AddressPresenter.new(additional_data['deliveryAddress']).to_h
45
+ end
46
+ end
47
+
48
+ def payment_method_reference
49
+ @payment_method_reference ||= body['paymentMethod'].to_sym
50
+ end
51
+
52
+ def stored_payment_method_id
53
+ @stored_payment_method_id ||= additional_data['tokenization.storedPaymentMethodId'] ||
54
+ additional_data['storedPaymentMethodId'] ||
55
+ additional_data['recurring.recurringDetailReference']
56
+ end
57
+
58
+ def card_details
59
+ @card_details ||= additional_data.slice('expiryDate', 'cardSummary', 'type')
60
+ end
61
+
62
+ def amount
63
+ @amount ||= begin
64
+ amount_data = body['amount']
65
+ return nil unless amount_data&.key?('value') && amount_data.key?('currency')
66
+
67
+ Spree::Money.new(amount_data['value'] / 100.0, currency: amount_data['currency'])
68
+ end
69
+ end
70
+
71
+ def merchant_reference
72
+ @merchant_reference ||= body['merchantReference']
73
+ end
74
+
75
+ def session_id
76
+ @session_id ||= additional_data['checkoutSessionId']
77
+ end
78
+
79
+ def event_date
80
+ @event_date ||= body['eventDate'].to_datetime
81
+ end
82
+
83
+ def psp_reference
84
+ @psp_reference ||= body['pspReference']
85
+ end
86
+
87
+ def fetch(attribute)
88
+ body[attribute]
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :event_data
94
+
95
+ def body
96
+ @body ||= event_data.dig('notificationItems', 0, 'NotificationRequestItem') || {}
97
+ end
98
+
99
+ def additional_data
100
+ @additional_data ||= body.fetch('additionalData', {})
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,69 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ module EventProcessors
4
+ class AuthorisationEventProcessor
5
+ def initialize(event)
6
+ @event = event
7
+ end
8
+
9
+ def call
10
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Started processing")
11
+ order = Spree::Order.find_by!(number: event.order_number)
12
+
13
+ order.with_lock do
14
+ payment_method = SpreeAdyen::Gateway.find(event.payment_method_id)
15
+ # session_id is available only for session based payments so payment_session can be nil
16
+ payment_session = order.adyen_payment_sessions.find_by(adyen_id: event.session_id)
17
+ source = SpreeAdyen::Webhooks::Actions::CreateSource.new(event: event, payment_method: payment_method, user: order.user).call
18
+ # create or find payment
19
+ # atm payment should be already created for web channel (but there is no payment for mobile channels)
20
+ # for web channel payment response code is updated from session_id to psp_reference
21
+ # psp_reference is required for refund flow but is not available before this event
22
+ payment_session&.lock!
23
+
24
+ payment = find_or_initialize_payment(payment_method)
25
+ payment.assign_attributes(
26
+ response_code: event.psp_reference,
27
+ amount: event.amount.to_d,
28
+ order: order,
29
+ source: source
30
+ )
31
+ payment.save!
32
+
33
+ payment.started_processing! if payment.checkout?
34
+
35
+ if event.success?
36
+ payment_session&.complete
37
+ payment.confirm!
38
+ Spree::Dependencies.checkout_complete_service.constantize.call(order: order) unless order.completed?
39
+ else
40
+ payment.failure!
41
+ payment_session&.refuse
42
+ if order.completed?
43
+ Rails.error.unexpected('Payment failed for previously completed order', context: { order_id: order.id, event: event.payload },
44
+ source: 'spree_adyen')
45
+ end
46
+ end
47
+ end
48
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Finished processing")
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :event
54
+
55
+ delegate :id, to: :event, prefix: true
56
+
57
+ def find_or_initialize_payment(payment_method)
58
+ if event.session_id.present?
59
+ Spree::Payment.where(response_code: event.session_id, payment_method: payment_method).or(
60
+ Spree::Payment.where(response_code: event.psp_reference, payment_method: payment_method)
61
+ ).first_or_initialize
62
+ else
63
+ Spree::Payment.where(response_code: event.psp_reference, payment_method: payment_method).first_or_initialize
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,49 @@
1
+ module SpreeAdyen
2
+ class CancellationError < StandardError; end
3
+
4
+ module Webhooks
5
+ module EventProcessors
6
+ class CancellationEventProcessor
7
+ class Error < StandardError; end
8
+
9
+ def initialize(event)
10
+ @event = event
11
+ end
12
+
13
+ def call
14
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Started processing")
15
+ order = Spree::Order.find_by!(number: event.order_number)
16
+
17
+ order.with_lock do
18
+ payment_method = SpreeAdyen::Gateway.find(event.payment_method_id)
19
+ payment = Spree::Payment.find_by!(response_code: event.fetch('originalReference'), payment_method: payment_method)
20
+
21
+ if event.success?
22
+ payment.set_metafield(SpreeAdyen::Gateway::CANCELLATION_PSP_REFERENCE_METAFIELD_KEY, event.psp_reference)
23
+ payment.started_processing! if payment.can_started_processing?
24
+ payment.void! unless payment.void?
25
+ else
26
+ payment.add_gateway_processing_error("Cancellation failed: #{event.fetch('reason')}")
27
+ payment.started_processing! if payment.can_started_processing?
28
+ payment.pend! if payment.can_pend?
29
+
30
+ Rails.error.report(
31
+ SpreeAdyen::CancellationError.new(event.fetch('reason')),
32
+ context: { order_id: order.id, event: event.payload },
33
+ source: 'spree_adyen'
34
+ )
35
+ end
36
+ end
37
+
38
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Finished processing")
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :event
44
+
45
+ delegate :id, to: :event, prefix: true
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ module SpreeAdyen
2
+ class CaptureError < StandardError; end
3
+
4
+ module Webhooks
5
+ module EventProcessors
6
+ class CaptureEventProcessor
7
+ class Error < StandardError; end
8
+
9
+ def initialize(event)
10
+ @event = event
11
+ end
12
+
13
+ def call
14
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Started processing")
15
+ order = Spree::Order.find_by!(number: event.order_number)
16
+
17
+ order.with_lock do
18
+ payment_method = SpreeAdyen::Gateway.find(event.payment_method_id)
19
+ payment = Spree::Payment.find_by!(response_code: event.fetch('originalReference'), payment_method: payment_method)
20
+
21
+ if event.success?
22
+ payment.set_metafield(SpreeAdyen::Gateway::CAPTURE_PSP_REFERENCE_METAFIELD_KEY, event.psp_reference)
23
+ payment.capture!
24
+ else
25
+ payment.add_gateway_processing_error("Capture failed: #{event.fetch('reason')}")
26
+ payment.started_processing! if payment.can_started_processing?
27
+ payment.failure! if payment.can_failure?
28
+
29
+ Rails.error.report(
30
+ SpreeAdyen::CaptureError.new(event.fetch('reason')),
31
+ context: { order_id: order.id, event: event.payload },
32
+ source: 'spree_adyen'
33
+ )
34
+ end
35
+ end
36
+
37
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Finished processing")
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :event
43
+
44
+ delegate :id, to: :event, prefix: true
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ class HandleEvent
4
+ def initialize(event_payload:)
5
+ @event_payload = event_payload
6
+ end
7
+
8
+ def call
9
+ # event not supported - skip
10
+ if event_class.nil?
11
+ Rails.logger.info("[SpreeAdyen][#{event_code}]: Skipping not supported event")
12
+ return
13
+ end
14
+
15
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Event received")
16
+ return unless event.code.in?(SpreeAdyen.event_handlers.keys)
17
+
18
+ Rails.logger.info("[SpreeAdyen][#{event_id}]: Event queued")
19
+ SpreeAdyen.event_handlers[event.code]
20
+ .set(wait: SpreeAdyen::Config.webhook_delay_in_seconds.seconds)
21
+ .perform_later(event.payload)
22
+ end
23
+
24
+ def event
25
+ @event ||= event_class.new(event_data: event_payload)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :event_payload
31
+
32
+ delegate :id, to: :event, prefix: true
33
+
34
+ def event_class
35
+ @event_class ||= SpreeAdyen.events[event_code]
36
+ end
37
+
38
+ def event_code
39
+ @event_code ||= event_payload.dig('notificationItems', 0, 'NotificationRequestItem', 'eventCode') || event_payload['type']
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ module SpreeAdyen
2
+ module Webhooks
3
+ class StandardHmacValidator
4
+ def initialize(request:, params:, gateway:)
5
+ @request = request
6
+ @params = params
7
+ @gateway = gateway
8
+ end
9
+
10
+ def call
11
+ return false if gateway.nil?
12
+
13
+ hmac_keys.any? do |hmac_key|
14
+ Adyen::Utils::HmacValidator.new.valid_webhook_hmac?(
15
+ webhook_request_item,
16
+ hmac_key
17
+ )
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :request, :params, :gateway
24
+
25
+ def hmac_keys
26
+ [
27
+ gateway.preferred_hmac_key,
28
+ gateway.previous_hmac_key
29
+ ].compact
30
+ end
31
+
32
+ def webhook_request_item
33
+ params.dig('notificationItems', 0, 'NotificationRequestItem') || {}
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ <% content_for(:head) do %>
2
+ <meta name="robots" content="noindex, nofollow">
3
+ <% end %>
4
+
5
+ <!doctype html>
6
+ <html lang="<%= I18n.locale %>">
7
+ <head>
8
+ <%= render 'spree/shared/head' %>
9
+ <%= render 'spree/shared/custom_head' %>
10
+ </head>
11
+ <body id="checkout-page" class="theme-<%= current_theme.class.name.demodulize.underscore %> w-full bg-background text-text">
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,14 @@
1
+ <div class="alert alert-info">
2
+ <div>
3
+ Please follow
4
+ <%= external_link_to 'how to configure Adyen payment method in Spree Commerce guide', 'https://spreecommerce.org/docs/integrations/payments/adyen', class: 'alert-link' %>.
5
+ </div>
6
+ </div>
7
+
8
+ <div class="alert alert-info">
9
+ <div>
10
+ <strong>Live URL Prefix</strong> is required for live payments. You can find it in your
11
+ <%= external_link_to 'Adyen Customer Area', 'https://ca-live.adyen.com', class: 'alert-link' %>
12
+ under Developers > API URLs > Prefix.
13
+ </div>
14
+ </div>
@@ -0,0 +1,18 @@
1
+ <% if f.object.persisted? %>
2
+ <div class="form-group mb-0">
3
+ <%= f.label :apple_developer_merchantid_domain_association, 'Apple Pay certificate', class: 'font-weight-bold' %>
4
+
5
+ <% if f.object.apple_developer_merchantid_domain_association.attached? %>
6
+ <div class="form-control d-flex align-items-center justify-content-between pr-1 mb-3 bg-light">
7
+ Certificate file uploaded!
8
+
9
+ <%= link_to main_app.rails_blob_path(f.object.apple_developer_merchantid_domain_association, disposition: 'attachment'), class: 'btn btn-light btn-sm' do %>
10
+ <%= icon 'download' %>
11
+ Download
12
+ <% end %>
13
+ </div>
14
+ <% end %>
15
+
16
+ <%= f.file_field :apple_developer_merchantid_domain_association, class: "form-control-file" %>
17
+ </div>
18
+ <% end %>
@@ -0,0 +1,12 @@
1
+ End-to-end payments, data, and financial management in a single solution. Meet the financial technology platform that helps you realize your ambitions faster.
2
+
3
+ <div class="d-flex align-items-center">
4
+ <%= payment_method_icon_tag 'visa', class: 'm-1' %>
5
+ <%= payment_method_icon_tag 'master', class: 'm-1' %>
6
+ <%= payment_method_icon_tag 'american_express', class: 'm-1' %>
7
+ <%= payment_method_icon_tag 'apple_pay', class: 'm-1' %>
8
+ <%= payment_method_icon_tag 'google_pay', class: 'm-1' %>
9
+ <%= payment_method_icon_tag 'klarna', class: 'm-1' %>
10
+ <%= payment_method_icon_tag 'afterpay', class: 'm-1' %>
11
+ <%= payment_method_icon_tag 'affirm', class: 'm-1' %>
12
+ </div>
@@ -0,0 +1,7 @@
1
+ <% if previous_cards.any? %>
2
+ <%= render 'spree/admin/payments/source_forms/previous_cards', previous_cards: previous_cards, f: f %>
3
+ <% else %>
4
+ <span class="text-muted">
5
+ <%= Spree.t(:no_saved_cards) %>
6
+ </span>
7
+ <% end %>
@@ -0,0 +1,40 @@
1
+
2
+ <% payment_sources = current_user&.credit_cards&.not_removed %>
3
+
4
+ <div data-controller="checkout-adyen"></div>
5
+
6
+ <% if payment_sources.present? %>
7
+ <div id="existing_cards">
8
+ <div class="form-group">
9
+ <ul class="list-group mb-5 border rounded-md" data-controller="reveal">
10
+ <% payment_sources.each_with_index do |card, index| %>
11
+ <li class="p-0 m-0 border-b" id="<%= spree_dom_id(card) %>">
12
+ <div class="custom-control custom-radio px-5 py-4 flex items-center">
13
+ <label class="flex flex-row items-center cursor-pointer">
14
+ <%= radio_button_tag "order[existing_card]", card.id, index == 0, { data: { hide: "dropin-container", checkout_adyen_target: index == 0 ? "defaultCard" : nil}, class: "mr-5" } %>
15
+ <div class="flex space-x-4 items-center">
16
+ <%= payment_method_icon_tag(card.cc_type) %>
17
+ <span>****<%= card.last_digits %></span>
18
+ <span><%= card.month.to_s.rjust(2, '0') %>/<%= card.year %></span>
19
+ </div>
20
+ </label>
21
+ </div>
22
+ </li>
23
+ <% end %>
24
+ <li>
25
+ <div class="custom-control custom-radio px-5 py-4 flex items-center w-full">
26
+ <label class="flex flex-row items-center w-full pr-8">
27
+ <%= radio_button_tag "order[existing_card]", nil, false, { data: { show: "dropin-container" }, class: "mr-5" } %>
28
+ <div class="w-full flex flex-col items-start space-y-4 justify-center">
29
+ <span class="cursor-pointer"><%= Spree.t(:add_new_credit_card) %></span>
30
+ <%= render 'spree_adyen/drop_in', payment_session: current_adyen_payment_session %>
31
+ </div>
32
+ </label>
33
+ </div>
34
+ </li>
35
+ </ul>
36
+ </div>
37
+ </div>
38
+ <% else %>
39
+ <%= render 'spree_adyen/drop_in', payment_session: current_adyen_payment_session, auto_mount: true %>
40
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <% auto_mount ||= false %>
2
+
3
+ <% if payment_session.present? %>
4
+ <div
5
+ class="<%= !auto_mount ? 'hidden' : '' %>"
6
+ id="dropin-container"
7
+ data-checkout-adyen-checkout-attrubutes="<%= SpreeAdyen::CheckoutPresenter.new(payment_session).to_json %>"
8
+ data-checkout-adyen-payment-session-path-value="<%= spree.adyen_payment_session_url(sessionId: payment_session.adyen_id) %>"
9
+ data-checkout-adyen-checkout-path-value="<%= spree.api_v2_storefront_checkout_path %>"
10
+ data-checkout-adyen-auto-mount-value="<%= auto_mount %>"
11
+ ></div>
12
+ <% end %>