solidus-adyen 0.1.2
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/.gitignore +26 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +325 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +238 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/spree/backend/solidus-adyen.js +1 -0
- data/app/assets/javascripts/spree/checkout/payment/adyen.js +10 -0
- data/app/assets/javascripts/spree/frontend/solidus-adyen.js +1 -0
- data/app/assets/stylesheets/spree/backend/solidus-adyen/buttons.scss +15 -0
- data/app/assets/stylesheets/spree/backend/solidus-adyen/communication.scss +121 -0
- data/app/assets/stylesheets/spree/backend/solidus-adyen/variables.scss +4 -0
- data/app/assets/stylesheets/spree/backend/solidus-adyen.css +5 -0
- data/app/assets/stylesheets/spree/frontend/solidus-adyen.css +3 -0
- data/app/controllers/concerns/spree/adyen/admin/refunds_controller.rb +59 -0
- data/app/controllers/spree/adyen/hpps_controller.rb +22 -0
- data/app/controllers/spree/adyen_notifications_controller.rb +34 -0
- data/app/controllers/spree/adyen_redirect_controller.rb +90 -0
- data/app/models/adyen_notification.rb +159 -0
- data/app/models/concerns/spree/adyen/order.rb +11 -0
- data/app/models/concerns/spree/adyen/payment.rb +95 -0
- data/app/models/spree/adyen/hpp_source.rb +101 -0
- data/app/models/spree/adyen/notification_processor.rb +102 -0
- data/app/models/spree/adyen/presenters/communication.rb +31 -0
- data/app/models/spree/adyen/presenters/communications/adyen_notification.rb +26 -0
- data/app/models/spree/adyen/presenters/communications/base.rb +50 -0
- data/app/models/spree/adyen/presenters/communications/hpp_source.rb +31 -0
- data/app/models/spree/adyen/presenters/communications/log_entry.rb +28 -0
- data/app/models/spree/adyen/presenters/communications.rb +9 -0
- data/app/models/spree/gateway/adyen_hpp.rb +95 -0
- data/app/overrides/spree/admin/shared/_order_summary.rb +6 -0
- data/app/views/spree/admin/payments/source_forms/_adyen.html.erb +1 -0
- data/app/views/spree/admin/payments/source_views/_adyen.html.erb +14 -0
- data/app/views/spree/adyen/_manual_refund.html.erb +9 -0
- data/app/views/spree/adyen/communication/_communication.html.erb +42 -0
- data/app/views/spree/adyen/hpps/directory.html.erb +10 -0
- data/app/views/spree/checkout/payment/_adyen.html.erb +33 -0
- data/bin/checkout.rb +111 -0
- data/bin/regen.sh +1 -0
- data/config/initializers/solidus_adyen.rb +1 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20131017040945_create_adyen_notifications.rb +29 -0
- data/db/migrate/20150911201942_add_index_adyen_notifications_psp_reference.rb +5 -0
- data/db/migrate/20150914162539_create_spree_adyen_hpp_sources.rb +21 -0
- data/db/migrate/20151007090519_add_days_to_ship_to_config.rb +5 -0
- data/db/migrate/20151020230830_remove_indices_on_adyen_notifications.rb +6 -0
- data/db/migrate/20151106093023_allow_merchant_reference_to_be_null_for_adyen_notification.rb +5 -0
- data/lib/solidus-adyen.rb +1 -0
- data/lib/spree/adyen/engine.rb +32 -0
- data/lib/spree/adyen/form.rb +135 -0
- data/lib/spree/adyen/hpp_check.rb +11 -0
- data/lib/spree/adyen/url.rb +30 -0
- data/lib/spree/adyen/version.rb +5 -0
- data/lib/spree/adyen.rb +6 -0
- data/solidus-adyen.gemspec +51 -0
- data/spec/controllers/concerns/spree/adyen/admin/refunds_controller_spec.rb +76 -0
- data/spec/controllers/spree/adyen/hpps_controller_spec.rb +43 -0
- data/spec/controllers/spree/adyen_notifications_controller_spec.rb +118 -0
- data/spec/controllers/spree/adyen_redirect_controller_spec.rb +154 -0
- data/spec/factories/active_merchant_billing_response.rb +23 -0
- data/spec/factories/adyen_notification.rb +91 -0
- data/spec/factories/spree_adyen_hpp_source.rb +15 -0
- data/spec/factories/spree_gateway_adyen_hpp.rb +23 -0
- data/spec/factories/spree_payment.rb +13 -0
- data/spec/lib/spree/adyen/form_spec.rb +214 -0
- data/spec/models/adyen_notification_spec.rb +86 -0
- data/spec/models/concerns/spree/adyen/order_spec.rb +22 -0
- data/spec/models/concerns/spree/adyen/payment_spec.rb +93 -0
- data/spec/models/spree/adyen/hpp_source_spec.rb +101 -0
- data/spec/models/spree/adyen/notification_processor_spec.rb +205 -0
- data/spec/models/spree/adyen/presenters/communication_spec.rb +62 -0
- data/spec/models/spree/gateway/adyen_hpp_spec.rb +76 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/support/shared_contexts/mock_adyen_api.rb +47 -0
- metadata +463 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@import 'variables';
|
|
2
|
+
@import 'bourbon';
|
|
3
|
+
|
|
4
|
+
$color-received-bg: mix($color-3, white, 10%);
|
|
5
|
+
$color-sent-bg: mix($color-3, white, 100%);
|
|
6
|
+
$notch-size-side: 12px;
|
|
7
|
+
$notch-size-top: 18px;
|
|
8
|
+
$timestamp-fade: 0.25;
|
|
9
|
+
|
|
10
|
+
@mixin notch($side, $color) {
|
|
11
|
+
#{$side}: -($notch-size-side);
|
|
12
|
+
border-#{$side}: $notch-size-side solid transparent;
|
|
13
|
+
border-top: $notch-size-top solid $color;
|
|
14
|
+
content: '';
|
|
15
|
+
padding-left: 0;
|
|
16
|
+
position: absolute;
|
|
17
|
+
top: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@mixin message($side, $color-bg, $color-text) {
|
|
21
|
+
background-color: $color-bg;
|
|
22
|
+
border-top-#{$side}-radius: 0;
|
|
23
|
+
box-shadow: 2px 2px 1px -1px fade-out(mix($color-bg, black, 70%), 0.2);
|
|
24
|
+
color: $color-text;
|
|
25
|
+
line-height: 1.6em;
|
|
26
|
+
|
|
27
|
+
margin-right: $notch-size-side;
|
|
28
|
+
margin-left: $notch-size-side;
|
|
29
|
+
|
|
30
|
+
@if $side == right {
|
|
31
|
+
float: right;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&:before {
|
|
35
|
+
@include notch($side, $color-bg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.adyen-comm-timestamp {
|
|
39
|
+
color: fade-out($color-text, $timestamp-fade);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* cannot use a real table here because spree clobbers all default style for
|
|
44
|
+
* tables and is super specific.
|
|
45
|
+
*/
|
|
46
|
+
.adyen-comm-content {
|
|
47
|
+
display: table;
|
|
48
|
+
border-spacing: 20px 10px
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.adyen-comm-content-cell {
|
|
52
|
+
display: table-cell;
|
|
53
|
+
|
|
54
|
+
&:first-child {
|
|
55
|
+
font-weight: bold;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.adyen-comm-content-row {
|
|
60
|
+
display: table-row;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.adyen-comm {
|
|
64
|
+
border-radius: 10px;
|
|
65
|
+
display: inline-block;
|
|
66
|
+
margin-bottom: 10px;
|
|
67
|
+
min-width: 350px;
|
|
68
|
+
padding: 5px 20px;
|
|
69
|
+
position: relative;
|
|
70
|
+
|
|
71
|
+
.adyen-comm-sent & {
|
|
72
|
+
@include message(right, $color-sent-bg, white);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.adyen-comm-received & {
|
|
76
|
+
@include message(left, $color-received-bg, black);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.adyen-comm-received,
|
|
81
|
+
.adyen-comm-sent {
|
|
82
|
+
& + & .adyen-comm {
|
|
83
|
+
border-top-right-radius: 10px;
|
|
84
|
+
border-top-left-radius: 10px;
|
|
85
|
+
&:before {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.adyen-comm-icon-success {
|
|
92
|
+
color: $color-success;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.adyen-comm-icon-failure {
|
|
96
|
+
color: $color-error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.adyen-comm-icon-unprocessed {
|
|
100
|
+
color: $color-safety-yellow;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.adyen-comm-icon {
|
|
104
|
+
@include display(flex);
|
|
105
|
+
@include flex-direction(column);
|
|
106
|
+
@include justify-content(space-between);
|
|
107
|
+
font-size: 30px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.adyen-comm-body {
|
|
111
|
+
@include display(flex);
|
|
112
|
+
@include align-items(center);
|
|
113
|
+
|
|
114
|
+
.adyen-comm-sent & {
|
|
115
|
+
@include flex-direction(row-reverse);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.adyen-comm-timestamp {
|
|
120
|
+
text-align: right;
|
|
121
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Adyen
|
|
3
|
+
module Admin
|
|
4
|
+
module RefundsController
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include Spree::Adyen::HppCheck
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
before_filter :adyen_create, only: [:create]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def adyen_create
|
|
13
|
+
if hpp_payment?(@payment)
|
|
14
|
+
# this sucks, but the attributes are not assigned until after
|
|
15
|
+
# callbacks if we're here we aren't going down the normal flow
|
|
16
|
+
# anyways
|
|
17
|
+
@refund.attributes = permitted_resource_params
|
|
18
|
+
|
|
19
|
+
# early exit if @refund is invalid, .create will have the error
|
|
20
|
+
# messages
|
|
21
|
+
return if @refund.invalid?
|
|
22
|
+
|
|
23
|
+
@payment.refunds.reset # we don't want to save the refund
|
|
24
|
+
@payment.credit!(cents, currency: currency)
|
|
25
|
+
|
|
26
|
+
respond
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def currency
|
|
33
|
+
money.currency.iso_code
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cents
|
|
37
|
+
money.cents
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def money
|
|
41
|
+
@refund.money.money
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# This is directly copied from .create's response, no way to make it
|
|
45
|
+
# any less awful than this as we still want it to have the same
|
|
46
|
+
# response.
|
|
47
|
+
def respond
|
|
48
|
+
respond_with(@object) do |format|
|
|
49
|
+
format.html do
|
|
50
|
+
flash[:success] = "Refund request was received"
|
|
51
|
+
redirect_to location_after_save
|
|
52
|
+
end
|
|
53
|
+
format.js { render :layout => false }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Adyen
|
|
3
|
+
class HppsController < ::ActionController::Base
|
|
4
|
+
load_resource :order, class: "Spree::Order", id_param: :order_id
|
|
5
|
+
load_resource(
|
|
6
|
+
:payment_method,
|
|
7
|
+
class: "Spree::PaymentMethod",
|
|
8
|
+
id_param: :payment_method_id)
|
|
9
|
+
|
|
10
|
+
def directory
|
|
11
|
+
@brands = Adyen::Form.payment_methods_from_directory(
|
|
12
|
+
@order,
|
|
13
|
+
@payment_method)
|
|
14
|
+
|
|
15
|
+
respond_to do |format|
|
|
16
|
+
format.html
|
|
17
|
+
format.json { render json: @brands }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class AdyenNotificationsController < StoreController
|
|
3
|
+
skip_before_filter :verify_authenticity_token
|
|
4
|
+
|
|
5
|
+
before_filter :authenticate
|
|
6
|
+
|
|
7
|
+
def notify
|
|
8
|
+
notification = AdyenNotification.build(params)
|
|
9
|
+
|
|
10
|
+
if notification.duplicate?
|
|
11
|
+
accept and return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
notification.save!
|
|
15
|
+
|
|
16
|
+
Spree::Adyen::NotificationProcessor.new(notification).process!
|
|
17
|
+
accept
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
# Enable HTTP basic authentication
|
|
22
|
+
def authenticate
|
|
23
|
+
authenticate_or_request_with_http_basic do |username, password|
|
|
24
|
+
username == ENV["ADYEN_NOTIFY_USER"] &&
|
|
25
|
+
password == ENV["ADYEN_NOTIFY_PASSWD"]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
def accept
|
|
31
|
+
render text: "[accepted]"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class AdyenRedirectController < StoreController
|
|
3
|
+
before_filter :restore_session
|
|
4
|
+
before_filter :check_signature, only: :confirm
|
|
5
|
+
|
|
6
|
+
skip_before_filter :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
# This is the entry point after an Adyen HPP payment is completed
|
|
9
|
+
def confirm
|
|
10
|
+
source = Adyen::HppSource.new(source_params(params))
|
|
11
|
+
|
|
12
|
+
unless source.authorised?
|
|
13
|
+
flash.notice = Spree.t(:payment_processing_failed)
|
|
14
|
+
redirect_to checkout_state_path(@order.state)
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# payment is created in a 'checkout' state so that the payment method
|
|
19
|
+
# can attempt to auth it. The payment of course is already auth'd and
|
|
20
|
+
# adyen hpp's authorize implementation just returns a dummy response.
|
|
21
|
+
payment =
|
|
22
|
+
@order.payments.create!(
|
|
23
|
+
amount: @order.total,
|
|
24
|
+
payment_method: @payment_method,
|
|
25
|
+
source: source,
|
|
26
|
+
response_code: params[:pspReference],
|
|
27
|
+
state: "checkout",
|
|
28
|
+
# Order is explicitly defined here because as of writing the
|
|
29
|
+
# Order -> Payments association does not have the inverse of defined
|
|
30
|
+
# when we call `order.complete` below payment.order will still
|
|
31
|
+
# refer to a previous state of the record.
|
|
32
|
+
#
|
|
33
|
+
# If the payment is auto captured only then the payment will completed
|
|
34
|
+
# in `process_outstanding!`, and because Payment calls
|
|
35
|
+
# .order.update_totals after save the order is saved with its
|
|
36
|
+
# previous values, causing payment_state and shipment_state to revert
|
|
37
|
+
# to nil.
|
|
38
|
+
order: @order
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if @order.complete
|
|
42
|
+
# We may have already recieved the authorization notification, so process
|
|
43
|
+
# it now
|
|
44
|
+
Spree::Adyen::NotificationProcessor.process_outstanding!(payment)
|
|
45
|
+
|
|
46
|
+
flash.notice = Spree.t(:order_processed_successfully)
|
|
47
|
+
redirect_to order_path(@order)
|
|
48
|
+
else
|
|
49
|
+
#TODO void/cancel payment
|
|
50
|
+
redirect_to checkout_state_path(@order.state)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_signature
|
|
57
|
+
unless ::Adyen::Form.redirect_signature_check(params, @payment_method.shared_secret)
|
|
58
|
+
raise "Payment Method not found."
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# We pass the guest token and payment method id in, pipe seperated in the
|
|
63
|
+
# merchantReturnData parameter so that we can recover the session.
|
|
64
|
+
def restore_session
|
|
65
|
+
guest_token, payment_method_id =
|
|
66
|
+
params.fetch(:merchantReturnData).split("|")
|
|
67
|
+
|
|
68
|
+
cookies.permanent.signed[:guest_token] = guest_token
|
|
69
|
+
|
|
70
|
+
@payment_method = Spree::PaymentMethod.find(payment_method_id)
|
|
71
|
+
|
|
72
|
+
@order =
|
|
73
|
+
Spree::Order.
|
|
74
|
+
incomplete.
|
|
75
|
+
find_by!(guest_token: cookies.signed[:guest_token])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def source_params params
|
|
79
|
+
params.permit(
|
|
80
|
+
:authResult,
|
|
81
|
+
:pspReference,
|
|
82
|
+
:merchantReference,
|
|
83
|
+
:skinCode,
|
|
84
|
+
:merchantSig,
|
|
85
|
+
:paymentMethod,
|
|
86
|
+
:shopperLocale,
|
|
87
|
+
:merchantReturnData)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# The +AdyenNotification+ class handles notifications sent by Adyen to your servers.
|
|
2
|
+
#
|
|
3
|
+
# Because notifications contain important payment status information, you should store
|
|
4
|
+
# these notifications in your database. For this reason, +AdyenNotification+ inherits
|
|
5
|
+
# from +ActiveRecord::Base+, and a migration is included to simply create a suitable table
|
|
6
|
+
# to store the notifications in.
|
|
7
|
+
#
|
|
8
|
+
# Adyen can either send notifications to you via HTTP POST requests, or SOAP requests.
|
|
9
|
+
# Because SOAP is not really well supported in Rails and setting up a SOAP server is
|
|
10
|
+
# not trivial, only handling HTTP POST notifications is currently supported.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# @notification = AdyenNotification.log(request)
|
|
14
|
+
# if @notification.successful_authorisation?
|
|
15
|
+
# @invoice = Invoice.find(@notification.merchant_reference)
|
|
16
|
+
# @invoice.set_paid!
|
|
17
|
+
# end
|
|
18
|
+
class AdyenNotification < ActiveRecord::Base
|
|
19
|
+
AUTO_CAPTURE_ONLY_METHODS = ["ideal", "c_cash", "directEbanking"].freeze
|
|
20
|
+
|
|
21
|
+
AUTHORISATION = "AUTHORISATION".freeze
|
|
22
|
+
CANCELLATION = "CANCELLATION".freeze
|
|
23
|
+
REFUND = "REFUND".freeze
|
|
24
|
+
CANCEL_OR_REFUND = "CANCEL_OR_REFUND".freeze
|
|
25
|
+
CAPTURE = "CAPTURE".freeze
|
|
26
|
+
CAPTURE_FAILED = "CAPTURE_FAILED".freeze
|
|
27
|
+
REFUND_FAILED = "REFUND_FAILED".freeze
|
|
28
|
+
REFUNDED_REVERSED = "REFUNDED_REVERSED".freeze
|
|
29
|
+
|
|
30
|
+
belongs_to :prev,
|
|
31
|
+
class_name: self,
|
|
32
|
+
foreign_key: :original_reference,
|
|
33
|
+
primary_key: :psp_reference,
|
|
34
|
+
inverse_of: :next
|
|
35
|
+
|
|
36
|
+
# Auth will have no original reference, all successive notifications with
|
|
37
|
+
# reference the first auth notification
|
|
38
|
+
has_many :next,
|
|
39
|
+
class_name: self,
|
|
40
|
+
foreign_key: :original_reference,
|
|
41
|
+
primary_key: :psp_reference,
|
|
42
|
+
inverse_of: :prev
|
|
43
|
+
|
|
44
|
+
belongs_to :order,
|
|
45
|
+
class_name: Spree::Order,
|
|
46
|
+
primary_key: :number,
|
|
47
|
+
foreign_key: :merchant_reference
|
|
48
|
+
|
|
49
|
+
scope :as_dispatched, -> { order(event_date: :desc) }
|
|
50
|
+
scope :processed, -> { where processed: true }
|
|
51
|
+
scope :unprocessed, -> { where processed: false }
|
|
52
|
+
scope :authorisation, -> { where event_code: "AUTHORISATION" }
|
|
53
|
+
|
|
54
|
+
validates_presence_of :event_code
|
|
55
|
+
validates_presence_of :psp_reference
|
|
56
|
+
validates_uniqueness_of :success, scope: [:psp_reference, :event_code]
|
|
57
|
+
|
|
58
|
+
# Logs an incoming notification into the database.
|
|
59
|
+
#
|
|
60
|
+
# @param [Hash] params The notification parameters that should be stored in the database.
|
|
61
|
+
# @return [Adyen::Notification] The initiated and persisted notification instance.
|
|
62
|
+
# @raise This method will raise an exception if the notification cannot be stored.
|
|
63
|
+
# @see Adyen::Notification::HttpPost.log
|
|
64
|
+
def self.build(params)
|
|
65
|
+
# Assign explicit each attribute from CamelCase notation to notification
|
|
66
|
+
# For example, merchantReference will be converted to merchant_reference
|
|
67
|
+
self.new.tap do |notification|
|
|
68
|
+
params.each do |key, value|
|
|
69
|
+
setter = "#{key.to_s.underscore}="
|
|
70
|
+
|
|
71
|
+
# don't assign if value is empty string.
|
|
72
|
+
if notification.respond_to?(setter) && value.present?
|
|
73
|
+
notification.send(setter, value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def payment
|
|
80
|
+
Spree::Payment.find_by response_code: original_reference || psp_reference
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns true if this notification is an AUTHORISATION notification
|
|
84
|
+
# @return [true, false] true iff event_code == 'AUTHORISATION'
|
|
85
|
+
# @see Adyen.notification#successful_authorisation?
|
|
86
|
+
def authorisation?
|
|
87
|
+
event_code == AUTHORISATION
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def capture?
|
|
91
|
+
event_code == CAPTURE
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cancel_or_refund?
|
|
95
|
+
event_code == CANCEL_OR_REFUND
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def refund?
|
|
99
|
+
event_code == REFUND
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def processed!
|
|
103
|
+
update! processed: true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def actions
|
|
107
|
+
if operations
|
|
108
|
+
operations.
|
|
109
|
+
split(",").
|
|
110
|
+
map(&:downcase)
|
|
111
|
+
|
|
112
|
+
else
|
|
113
|
+
[]
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# https://docs.adyen.com/display/TD/Notification+fields
|
|
119
|
+
def modification_event?
|
|
120
|
+
[ CANCELLATION,
|
|
121
|
+
REFUND,
|
|
122
|
+
CANCEL_OR_REFUND,
|
|
123
|
+
CAPTURE,
|
|
124
|
+
CAPTURE_FAILED,
|
|
125
|
+
REFUND_FAILED,
|
|
126
|
+
REFUNDED_REVERSED
|
|
127
|
+
].member? self.event_code
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normal_event?
|
|
131
|
+
AUTHORISATION == self.event_code
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def bank_transfer?
|
|
135
|
+
self.payment_method.match(/^bankTransfer/)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def duplicate?
|
|
139
|
+
self.class.exists?(
|
|
140
|
+
psp_reference: self.psp_reference,
|
|
141
|
+
event_code: self.event_code,
|
|
142
|
+
success: self.success
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def auto_captured?
|
|
147
|
+
payment_method_auto_capture_only? || bank_transfer?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def payment_method_auto_capture_only?
|
|
151
|
+
AUTO_CAPTURE_ONLY_METHODS.member?(self.payment_method)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def money
|
|
155
|
+
::Money.new(value, currency)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
alias_method :authorization?, :authorisation?
|
|
159
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Because adyen payment modifications are delayed we don't actually know if
|
|
2
|
+
# the request succeeded after doing it. For that reason we can't use the
|
|
3
|
+
# standard capture! and friends as they change the payment state and would
|
|
4
|
+
# result is false positives (payment could fail after capture).
|
|
5
|
+
module Spree
|
|
6
|
+
module Adyen
|
|
7
|
+
module Payment
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
include Spree::Adyen::HppCheck
|
|
10
|
+
|
|
11
|
+
# capture! :: bool | error
|
|
12
|
+
def capture!
|
|
13
|
+
if hpp_payment?
|
|
14
|
+
amount = money.money.cents
|
|
15
|
+
process do
|
|
16
|
+
payment_method.send(
|
|
17
|
+
:capture, amount, response_code, gateway_options)
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# credit! :: bool | error
|
|
25
|
+
#
|
|
26
|
+
# Issue a request to credit the payment, this does NOT perform validation
|
|
27
|
+
# on the amount to be credited, which is assumed to have been done prior
|
|
28
|
+
# to this.
|
|
29
|
+
#
|
|
30
|
+
# credit! is only implemented for hpp payments, because of the delayed
|
|
31
|
+
# manner of Adyen api communications. If this method is called on a
|
|
32
|
+
# payment that is not from Adyen then it should fail. This is crummy way
|
|
33
|
+
# of getting around the fact that Payment methods cannot specifiy these
|
|
34
|
+
# methods.
|
|
35
|
+
def credit! amount, options
|
|
36
|
+
if hpp_payment?
|
|
37
|
+
process { payment_method.credit(amount, response_code, options) }
|
|
38
|
+
else
|
|
39
|
+
fail NotImplementedError, "Spree::Payment does not implement credit!"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# cancel! :: bool | error
|
|
44
|
+
#
|
|
45
|
+
# Borrowed from handle_void_response, this has been modified so that it
|
|
46
|
+
# won't actually void the payment _yet_.
|
|
47
|
+
def cancel!
|
|
48
|
+
if hpp_payment?
|
|
49
|
+
if source.requires_manual_refund?
|
|
50
|
+
log_manual_refund
|
|
51
|
+
else
|
|
52
|
+
process { payment_method.cancel response_code }
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def log_manual_refund
|
|
62
|
+
message = I18n.t("solidus-adyen.manual_refund.log_message")
|
|
63
|
+
record_response(
|
|
64
|
+
OpenStruct.new(
|
|
65
|
+
success?: false,
|
|
66
|
+
message: message))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def process &block
|
|
70
|
+
check_environment
|
|
71
|
+
response = nil
|
|
72
|
+
|
|
73
|
+
Spree::Payment.transaction do
|
|
74
|
+
protect_from_connection_error do
|
|
75
|
+
started_processing!
|
|
76
|
+
response = yield(block)
|
|
77
|
+
fail ActiveRecord::Rollback unless response.success?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
record_response(response)
|
|
82
|
+
|
|
83
|
+
if response.success?
|
|
84
|
+
# The payments controller's fire action expects a truthy value to
|
|
85
|
+
# indicate success
|
|
86
|
+
true
|
|
87
|
+
else
|
|
88
|
+
# this is done to be consistent with capture, but we might actually
|
|
89
|
+
# want them to just return to the previous state
|
|
90
|
+
gateway_error(response)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|