Class: SpreeAdyen::Gateway

Inherits:
Spree::Gateway
  • Object
show all
Includes:
PaymentSessions
Defined in:
app/models/spree_adyen/gateway.rb,
app/models/spree_adyen/gateway/payment_sessions.rb

Defined Under Namespace

Modules: PaymentSessions

Constant Summary collapse

CAPTURE_PSP_REFERENCE_METAFIELD_KEY =
'adyen.capture_psp_reference'.freeze
CANCELLATION_PSP_REFERENCE_METAFIELD_KEY =
'adyen.cancellation_psp_reference'.freeze

Instance Method Summary collapse

Methods included from PaymentSessions

#complete_payment_session, #create_payment_session, #payment_session_class, #session_required?, #update_payment_session

Instance Method Details

#add_allowed_origin(domain) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
# File 'app/models/spree_adyen/gateway.rb', line 364

def add_allowed_origin(domain)
  response = client.management.my_api_credential_api.add_allowed_origin({ domain: domain })

  if response.status.to_i == 200
    success(response.response.id, response.response)
  else
    failure(response.response)
  end
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e))
end

#apple_domain_association_file_contentObject



426
427
428
# File 'app/models/spree_adyen/gateway.rb', line 426

def apple_domain_association_file_content
  @apple_domain_association_file_content ||= apple_developer_merchantid_domain_association&.download
end

#authorize(amount_in_cents, payment_source, gateway_options = {}) ⇒ Object

Parameters:

  • amount_in_cents (Integer)

    the amount in cents to capture

  • payment_source (Spree::CreditCard | Spree::PaymentSource)
  • gateway_options (Hash) (defaults to: {})

    this is an instance of Spree::Payment::GatewayOptions.to_hash



55
56
57
# File 'app/models/spree_adyen/gateway.rb', line 55

def authorize(amount_in_cents, payment_source, gateway_options = {})
  handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
end

#cancel(id, payment) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/models/spree_adyen/gateway.rb', line 79

def cancel(id, payment)
  transaction_id = id
  payment ||= Spree::Payment.find_by(response_code: id)
  if payment.completed?
    amount = payment.credit_allowed
    return success(transaction_id, {}) if amount.zero?
    # Don't create a refund if the payment is for a shipment, we will create a refund for the whole shipping cost instead
    return success(transaction_id, {}) if payment.respond_to?(:for_shipment?) && payment.for_shipment?

    refund = payment.refunds.create!(
      amount: amount,
      reason: Spree::RefundReason.order_canceled_reason,
      refunder_id: payment.order.canceler_id
    )

    # Spree::Refund#response has the response from the `credit` action
    # For the authorization ID we need to use the payment.response_code
    # Otherwise we'll overwrite the payment authorization with the refund ID
    success(transaction_id, refund.response.params)
  else
    payment.void!
    success(transaction_id, {})
  end
end

#capture(amount_in_cents, response_code, _gateway_options = {}) ⇒ Object

This only checks if the capture was successful by checking the presence of the capture PSP reference The actual capture is requested in #request_capture and handled in the SpreeAdyen::Webhooks::EventProcessors::CaptureEventProcessor



158
159
160
161
162
163
164
165
166
# File 'app/models/spree_adyen/gateway.rb', line 158

def capture(amount_in_cents, response_code, _gateway_options = {})
  payment = Spree::Payment.find_by(response_code: response_code)

  return failure("#{response_code} - Payment not found") if payment.blank?
  return failure("#{response_code} - Payment is already captured") if payment.completed?
  return failure("#{response_code} - Capture PSP reference not found") unless payment.has_metafield?(CAPTURE_PSP_REFERENCE_METAFIELD_KEY)

  success(payment.response_code, {})
end

#configuration_guide_partial_nameObject



326
327
328
# File 'app/models/spree_adyen/gateway.rb', line 326

def configuration_guide_partial_name
  'spree_adyen'
end

#create_adyen_session(amount, order, channel, return_url) ⇒ Spree::PaymentResponse

Creates an Adyen session via the Adyen Sessions API. Used internally by the v3 PaymentSessions module and by the legacy SpreeAdyen::PaymentSession model.

Parameters:

  • amount (BigDecimal)

    the amount

  • order (Spree::Order)

    the order to create a session for

  • channel (String)

    the channel (Web, iOS, Android)

  • return_url (String)

    the return URL after redirect flow

Returns:

  • (Spree::PaymentResponse)

    the response from the session creation



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/spree_adyen/gateway.rb', line 281

def create_adyen_session(amount, order, channel, return_url)
  payload = SpreeAdyen::PaymentSessions::RequestPayloadPresenter.new(
    order: order,
    amount: amount,
    user: order.user,
    merchant_account: ,
    payment_method: self,
    channel: channel,
    return_url: return_url
  ).to_h

  response = send_request do
    client.checkout.payments_api.sessions(payload, headers: { 'Idempotency-Key' => SecureRandom.uuid })
  end
  response_body = response.response

  if response.status.to_i == 201
    success(response_body.id, response_body)
  else
    failure(response_body.slice('pspReference', 'message').values.join(' - '))
  end
end

#create_profile(payment) ⇒ Object



258
# File 'app/models/spree_adyen/gateway.rb', line 258

def create_profile(payment); end

#credit(amount_in_cents, _source, payment_id, gateway_options = {}) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'app/models/spree_adyen/gateway.rb', line 104

def credit(amount_in_cents, _source, payment_id, gateway_options = {})
  refund = gateway_options[:originator]
  payment = refund.present? ? refund.payment : Spree::Payment.find_by(response_code: payment_id)

  return failure("#{payment_id} - Payment not found") unless payment

  payload = SpreeAdyen::RefundPayloadPresenter.new(
    payment: payment,
    amount_in_cents: amount_in_cents,
    payment_method: self,
    currency: payment.currency,
    refund: refund
  ).to_h

  response = send_request do
    client.checkout.modifications_api.refund_captured_payment(payload, payment.transaction_id, headers: { 'Idempotency-Key' => SecureRandom.uuid })
  end

  if response.status.to_i == 201
    success(response.response['pspReference'], response)
  else
    failure(response.response.slice('pspReference', 'message').values.join(' - '))
  end
end

#custom_form_fields_partial_nameObject



330
331
332
# File 'app/models/spree_adyen/gateway.rb', line 330

def custom_form_fields_partial_name
  'spree_adyen'
end

#default_nameObject



310
311
312
# File 'app/models/spree_adyen/gateway.rb', line 310

def default_name
  'Adyen'
end

#description_partial_nameObject



322
323
324
# File 'app/models/spree_adyen/gateway.rb', line 322

def description_partial_name
  'spree_adyen'
end

#environmentObject



250
251
252
253
254
255
256
# File 'app/models/spree_adyen/gateway.rb', line 250

def environment
  if preferred_test_mode
    :test
  else
    :live
  end
end

#gateway_dashboard_payment_url(payment) ⇒ Object



334
335
336
337
338
# File 'app/models/spree_adyen/gateway.rb', line 334

def gateway_dashboard_payment_url(payment)
  return if payment.transaction_id.blank?

  "https://ca-#{environment}.adyen.com/ca/ca/accounts/showTx.shtml?pspReference=#{payment.transaction_id}&txType=Payment"
end

#generate_client_keyObject



414
415
416
417
418
419
420
421
422
423
424
# File 'app/models/spree_adyen/gateway.rb', line 414

def generate_client_key
  response = client.management.my_api_credential_api.generate_client_key

  if response.status.to_i == 200
    success(response.response.clientKey, response.response)
  else
    failure(response.response.message)
  end
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e)['message'])
end

#generate_hmac_keyObject



402
403
404
405
406
407
408
409
410
411
412
# File 'app/models/spree_adyen/gateway.rb', line 402

def generate_hmac_key
  response = client.management.webhooks_merchant_level_api.generate_hmac_key(, preferred_webhook_id)

  if response.status.to_i == 200
    success(response.response.hmacKey, response.response)
  else
    failure(response.response)
  end
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e)['message'])
end

#get_api_credential_detailsObject



350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'app/models/spree_adyen/gateway.rb', line 350

def get_api_credential_details
  response = client.management.my_api_credential_api.get_api_credential_details

  if response.status.to_i == 200
    success(response.response.id, response.response)
  else
    failure(response.response.message)
  end
rescue Adyen::AuthenticationError, Adyen::PermissionError
  raise
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e)['message'])
end

#handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options = {}) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'app/models/spree_adyen/gateway.rb', line 59

def handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options = {})
  payload = SpreeAdyen::Payments::RequestPayloadPresenter.new(
    source: payment_source,
    amount_in_cents: amount_in_cents,
    manual_capture: !auto_capture?,
    gateway_options: gateway_options
  ).to_h

  response = send_request do
    client.checkout.payments_api.payments(payload, headers: { 'Idempotency-Key' => SecureRandom.uuid })
  end
  response_body = response.response

  if response.status.to_i == 200
    success(response_body.pspReference, response_body)
  else
    failure(response_body.slice('pspReference', 'message').values.join(' - '))
  end
end

#method_typeObject



314
315
316
# File 'app/models/spree_adyen/gateway.rb', line 314

def method_type
  'spree_adyen'
end

#parse_webhook_event(raw_body, headers) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/models/spree_adyen/gateway.rb', line 217

def parse_webhook_event(raw_body, headers)
  payload = JSON.parse(raw_body).with_indifferent_access
  event = SpreeAdyen::Webhooks::Event.new(event_data: payload)

  # Verify HMAC signature
  webhook_request_item = payload.dig('notificationItems', 0, 'NotificationRequestItem') || {}
  unless valid_hmac?(webhook_request_item)
    raise Spree::PaymentMethod::WebhookSignatureError, 'Invalid HMAC signature'
  end

  # Find payment session — scoped to this gateway
  payment_session = event.session_id.present? ?
    Spree::PaymentSessions::Adyen.find_by(payment_method: self, external_id: event.session_id) : nil

  return nil unless payment_session

  case event.code
  when 'AUTHORISATION'
    action = event.success? ? :authorized : :failed
    { action: action, payment_session: payment_session, metadata: { adyen_event: event } }
  when 'CAPTURE'
    { action: :captured, payment_session: payment_session, metadata: { adyen_event: event } }
  when 'CANCELLATION'
    { action: :canceled, payment_session: payment_session, metadata: { adyen_event: event } }
  else
    nil
  end
end

#payment_icon_nameObject



318
319
320
# File 'app/models/spree_adyen/gateway.rb', line 318

def payment_icon_name
  'adyen'
end

#payment_profiles_supported?Boolean

this is used by spree to determine whenever payment source must be passed to gateway methods

Returns:

  • (Boolean)

    whether payment profiles are supported



306
307
308
# File 'app/models/spree_adyen/gateway.rb', line 306

def payment_profiles_supported?
  true
end

#payment_session_result(payment_session_id, session_result) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
# File 'app/models/spree_adyen/gateway.rb', line 260

def payment_session_result(payment_session_id, session_result)
  response = send_request do
    client.checkout.payments_api.get_result_of_payment_session(payment_session_id, query_params: { sessionResult: session_result })
  end
  response_body = response.response

  if response.status.to_i == 200
    success(response_body.id, response_body)
  else
    failure(response_body.slice('pspReference', 'message').values.join(' - '))
  end
end

#provider_classObject



246
247
248
# File 'app/models/spree_adyen/gateway.rb', line 246

def provider_class
  self.class
end

#purchase(amount_in_cents, payment_source, gateway_options = {}) ⇒ Object

Parameters:

  • amount_in_cents (Integer)

    the amount in cents to capture

  • payment_source (Spree::CreditCard | Spree::PaymentSource)
  • gateway_options (Hash) (defaults to: {})

    this is an instance of Spree::Payment::GatewayOptions.to_hash



48
49
50
# File 'app/models/spree_adyen/gateway.rb', line 48

def purchase(amount_in_cents, payment_source, gateway_options = {})
  handle_authorize_or_purchase(amount_in_cents, payment_source, gateway_options)
end

#request_capture(amount_in_cents, response_code, _gateway_options = {}) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/models/spree_adyen/gateway.rb', line 129

def request_capture(amount_in_cents, response_code, _gateway_options = {})
  payment = Spree::Payment.find_by(response_code: response_code)

  return failure("#{response_code} - Payment not found") if payment.blank?
  return failure("#{response_code} - Payment is already captured") if payment.completed?

  payload = SpreeAdyen::CapturePayloadPresenter.new(
    amount_in_cents: amount_in_cents,
    payment: payment,
    payment_method: self
  ).to_h

  response = send_request do
    client.checkout.modifications_api.capture_authorised_payment(
      payload,
      payment.response_code,
      headers: { 'Idempotency-Key' => SecureRandom.uuid }
    )
  end

  if response.status.to_i == 201
    success(response.response['paymentPspReference'], response)
  else
    failure(response.response.slice('paymentPspReference', 'message').values.join(' - '))
  end
end

#request_void(response_code, _source, _gateway_options) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'app/models/spree_adyen/gateway.rb', line 168

def request_void(response_code, _source, _gateway_options)
  payment = Spree::Payment.find_by(response_code: response_code)

  return failure("#{response_code} - Payment not found") if payment.blank?
  return failure("#{response_code} - Payment is already void") if payment.void?

  payload = SpreeAdyen::CancelPayloadPresenter.new(
    payment: payment,
    payment_method: self
  ).to_h

  response = send_request do
    client.checkout.modifications_api.cancel_authorised_payment_by_psp_reference(
      payload,
      payment.response_code,
      headers: { 'Idempotency-Key' => SecureRandom.uuid }
    )
  end

  if response.status.to_i == 201
    success(response.response['paymentPspReference'], response)
  else
    failure(response.response.slice('paymentPspReference', 'message').values.join(' - '))
  end
end

#reusable_sources(order) ⇒ Object



340
341
342
343
344
345
346
347
348
# File 'app/models/spree_adyen/gateway.rb', line 340

def reusable_sources(order)
  if order.completed?
    sources_by_order order
  elsif order.user.present?
    credit_cards.where(user_id: order.user_id)
  else
    []
  end
end

#set_up_webhook(url) ⇒ Object



376
377
378
379
380
381
382
383
384
385
386
387
# File 'app/models/spree_adyen/gateway.rb', line 376

def set_up_webhook(url)
  payload = SpreeAdyen::WebhookPayloadPresenter.new(url).to_h
  response = client.management.webhooks_merchant_level_api.set_up_webhook(payload, )

  if response.status.to_i == 200
    success(response.response.id, response.response)
  else
    failure(response.response)
  end
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e)['message'])
end

#test_webhookObject



389
390
391
392
393
394
395
396
397
398
399
400
# File 'app/models/spree_adyen/gateway.rb', line 389

def test_webhook
  response = client.management.webhooks_merchant_level_api.test_webhook({ types: ['AUTHORISATION'] }, ,
                                                                        preferred_webhook_id)

  if response.status.to_i == 200 && response.response.dig('data', 0, 'status') == 'success'
    success(nil, response.response)
  else
    failure(response.response)
  end
rescue Adyen::AdyenError => e
  failure(parse_adyen_error_response(e)['message'])
end

#void(response_code, _source, _gateway_options) ⇒ Object

This only checks if the void was successful by checking the presence of the cancellation PSP reference The actual void is requested in #request_void and handled in the SpreeAdyen::Webhooks::EventProcessors::CancellationEventProcessor



196
197
198
199
200
201
202
203
204
# File 'app/models/spree_adyen/gateway.rb', line 196

def void(response_code, _source, _gateway_options)
  payment = Spree::Payment.find_by(response_code: response_code)

  return failure("#{response_code} - Payment not found") if payment.blank?
  return failure("#{response_code} - Payment is already void") if payment.void?
  return failure("#{response_code} - Cancellation PSP reference not found") unless payment.has_metafield?(CANCELLATION_PSP_REFERENCE_METAFIELD_KEY)

  success(payment.response_code, {})
end

#webhook_urlObject



206
207
208
209
210
211
212
213
214
215
# File 'app/models/spree_adyen/gateway.rb', line 206

def webhook_url
  store = stores.first
  return nil unless store

  if SpreeAdyen::Config[:use_legacy_webhook_handlers]
    "#{store.formatted_url}/adyen/webhooks"
  else
    "#{store.formatted_url}/api/v3/webhooks/payments/#{prefixed_id}"
  end
end