solidus_paypal_braintree 0.1.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 +26 -0
- data/README.md +196 -0
- data/Rakefile +30 -0
- data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +66 -0
- data/app/assets/javascripts/spree/braintree_hosted_form.js +98 -0
- data/app/assets/javascripts/spree/checkout/braintree.js +60 -0
- data/app/assets/javascripts/spree/frontend/paypal_button.js +182 -0
- data/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +188 -0
- data/app/assets/stylesheets/spree/backend/solidus_paypal_braintree.scss +28 -0
- data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +16 -0
- data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +21 -0
- data/app/helpers/braintree_admin_helper.rb +18 -0
- data/app/models/application_record.rb +3 -0
- data/app/models/solidus_paypal_braintree/configuration.rb +5 -0
- data/app/models/solidus_paypal_braintree/customer.rb +4 -0
- data/app/models/solidus_paypal_braintree/gateway.rb +323 -0
- data/app/models/solidus_paypal_braintree/response.rb +52 -0
- data/app/models/solidus_paypal_braintree/source.rb +73 -0
- data/app/models/solidus_paypal_braintree/transaction.rb +30 -0
- data/app/models/solidus_paypal_braintree/transaction_address.rb +66 -0
- data/app/models/solidus_paypal_braintree/transaction_import.rb +92 -0
- data/app/models/spree/store_decorator.rb +11 -0
- data/app/overrides/admin_navigation_menu.rb +6 -0
- data/app/views/spree/shared/_braintree_hosted_fields.html.erb +26 -0
- data/config/initializers/braintree.rb +1 -0
- data/config/locales/en.yml +30 -0
- data/config/routes.rb +12 -0
- data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
- data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +11 -0
- data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
- data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +9 -0
- data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
- data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +30 -0
- data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +11 -0
- data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +30 -0
- data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +27 -0
- data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +61 -0
- data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +37 -0
- data/lib/solidus_paypal_braintree.rb +10 -0
- data/lib/solidus_paypal_braintree/country_mapper.rb +35 -0
- data/lib/solidus_paypal_braintree/engine.rb +53 -0
- data/lib/solidus_paypal_braintree/factories.rb +18 -0
- data/lib/solidus_paypal_braintree/version.rb +3 -0
- data/lib/views/backend/solidus_paypal_braintree/configurations/_admin_tab.html.erb +3 -0
- data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +30 -0
- data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
- data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +34 -0
- data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
- data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +52 -0
- metadata +350 -0
@@ -0,0 +1,323 @@
|
|
1
|
+
require 'braintree'
|
2
|
+
|
3
|
+
module SolidusPaypalBraintree
|
4
|
+
class Gateway < ::Spree::PaymentMethod
|
5
|
+
TOKEN_GENERATION_DISABLED_MESSAGE = 'Token generation is disabled.' \
|
6
|
+
' To re-enable set the `token_generation_enabled` preference on the' \
|
7
|
+
' gateway to `true`.'.freeze
|
8
|
+
|
9
|
+
ALLOWED_BRAINTREE_OPTIONS = [
|
10
|
+
:device_data,
|
11
|
+
:device_session_id,
|
12
|
+
:merchant_account_id,
|
13
|
+
:order_id
|
14
|
+
]
|
15
|
+
|
16
|
+
VOIDABLE_STATUSES = [
|
17
|
+
Braintree::Transaction::Status::SubmittedForSettlement,
|
18
|
+
Braintree::Transaction::Status::Authorized
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
# This is useful in feature tests to avoid rate limited requests from
|
22
|
+
# Braintree
|
23
|
+
preference(:client_sdk_enabled, :boolean, default: true)
|
24
|
+
|
25
|
+
preference(:token_generation_enabled, :boolean, default: true)
|
26
|
+
|
27
|
+
# Preferences for configuration of Braintree credentials
|
28
|
+
preference(:environment, :string, default: 'sandbox')
|
29
|
+
preference(:merchant_id, :string, default: nil)
|
30
|
+
preference(:public_key, :string, default: nil)
|
31
|
+
preference(:private_key, :string, default: nil)
|
32
|
+
preference(:merchant_currency_map, :hash, default: {})
|
33
|
+
preference(:paypal_payee_email_map, :hash, default: {})
|
34
|
+
|
35
|
+
def partial_name
|
36
|
+
"paypal_braintree"
|
37
|
+
end
|
38
|
+
alias_method :method_type, :partial_name
|
39
|
+
|
40
|
+
def payment_source_class
|
41
|
+
Source
|
42
|
+
end
|
43
|
+
|
44
|
+
def braintree
|
45
|
+
@braintree ||= Braintree::Gateway.new(gateway_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def gateway_options
|
49
|
+
{
|
50
|
+
environment: preferred_environment.to_sym,
|
51
|
+
merchant_id: preferred_merchant_id,
|
52
|
+
public_key: preferred_public_key,
|
53
|
+
private_key: preferred_private_key,
|
54
|
+
logger: Braintree::Configuration.logger.clone
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a payment and submit it for settlement all at once.
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
# @param money_cents [Number, String] amount to authorize
|
62
|
+
# @param source [Source] payment source
|
63
|
+
# @params gateway_options [Hash]
|
64
|
+
# extra options to send along. e.g.: device data for fraud prevention
|
65
|
+
# @return [Response]
|
66
|
+
def purchase(money_cents, source, gateway_options)
|
67
|
+
result = braintree.transaction.sale(
|
68
|
+
amount: dollars(money_cents),
|
69
|
+
**transaction_options(source, gateway_options, true)
|
70
|
+
)
|
71
|
+
|
72
|
+
Response.build(result)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Authorize a payment to be captured later.
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
# @param money_cents [Number, String] amount to authorize
|
79
|
+
# @param source [Source] payment source
|
80
|
+
# @params gateway_options [Hash]
|
81
|
+
# extra options to send along. e.g.: device data for fraud prevention
|
82
|
+
# @return [Response]
|
83
|
+
def authorize(money_cents, source, gateway_options)
|
84
|
+
result = braintree.transaction.sale(
|
85
|
+
amount: dollars(money_cents),
|
86
|
+
**transaction_options(source, gateway_options)
|
87
|
+
)
|
88
|
+
|
89
|
+
Response.build(result)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Collect funds from an authorized payment.
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
# @param money_cents [Number, String]
|
96
|
+
# amount to capture (partial settlements are supported by the gateway)
|
97
|
+
# @param response_code [String] the transaction id of the payment to capture
|
98
|
+
# @return [Response]
|
99
|
+
def capture(money_cents, response_code, _gateway_options)
|
100
|
+
result = braintree.transaction.submit_for_settlement(
|
101
|
+
response_code,
|
102
|
+
dollars(money_cents)
|
103
|
+
)
|
104
|
+
Response.build(result)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Used to refeund a customer for an already settled transaction.
|
108
|
+
#
|
109
|
+
# @api public
|
110
|
+
# @param money_cents [Number, String] amount to refund
|
111
|
+
# @param response_code [String] the transaction id of the payment to refund
|
112
|
+
# @return [Response]
|
113
|
+
def credit(money_cents, _source, response_code, _gateway_options)
|
114
|
+
result = braintree.transaction.refund(
|
115
|
+
response_code,
|
116
|
+
dollars(money_cents)
|
117
|
+
)
|
118
|
+
Response.build(result)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Used to cancel a transaction before it is settled.
|
122
|
+
#
|
123
|
+
# @api public
|
124
|
+
# @param response_code [String] the transaction id of the payment to void
|
125
|
+
# @return [Response]
|
126
|
+
def void(response_code, _source, _gateway_options)
|
127
|
+
result = braintree.transaction.void(response_code)
|
128
|
+
Response.build(result)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Will either refund or void the payment depending on its state.
|
132
|
+
#
|
133
|
+
# If the transaction has not yet been settled, we can void the transaction.
|
134
|
+
# Otherwise, we need to issue a refund.
|
135
|
+
#
|
136
|
+
# @api public
|
137
|
+
# @param response_code [String] the transaction id of the payment to void
|
138
|
+
# @return [Response]
|
139
|
+
def cancel(response_code)
|
140
|
+
transaction = braintree.transaction.find(response_code)
|
141
|
+
if VOIDABLE_STATUSES.include?(transaction.status)
|
142
|
+
void(response_code, nil, {})
|
143
|
+
else
|
144
|
+
credit(cents(transaction.amount), nil, response_code, {})
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Creates a new customer profile in Braintree
|
149
|
+
#
|
150
|
+
# @api public
|
151
|
+
# @param payment [Spree::Payment]
|
152
|
+
# @return [SolidusPaypalBraintree::Customer]
|
153
|
+
def create_profile(payment)
|
154
|
+
source = payment.source
|
155
|
+
|
156
|
+
return if source.token.present? || source.customer.present? || source.nonce.nil?
|
157
|
+
|
158
|
+
result = braintree.customer.create(customer_profile_params(payment))
|
159
|
+
fail Spree::Core::GatewayError, result.message unless result.success?
|
160
|
+
|
161
|
+
customer = result.customer
|
162
|
+
|
163
|
+
source.create_customer!(braintree_customer_id: customer.id).tap do
|
164
|
+
if customer.payment_methods.any?
|
165
|
+
source.token = customer.payment_methods.last.token
|
166
|
+
end
|
167
|
+
|
168
|
+
source.save!
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# @return [String]
|
173
|
+
# The token that should be used along with the Braintree js-client sdk.
|
174
|
+
#
|
175
|
+
# returns an error message if `preferred_token_generation_enabled` is
|
176
|
+
# set to false.
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# <script>
|
180
|
+
# var token = #{Spree::Braintree::Gateway.first!.generate_token}
|
181
|
+
#
|
182
|
+
# braintree.client.create(
|
183
|
+
# {
|
184
|
+
# authorization: token
|
185
|
+
# },
|
186
|
+
# function(clientError, clientInstance) {
|
187
|
+
# ...
|
188
|
+
# }
|
189
|
+
# );
|
190
|
+
# </script>
|
191
|
+
def generate_token
|
192
|
+
return TOKEN_GENERATION_DISABLED_MESSAGE unless preferred_token_generation_enabled
|
193
|
+
braintree.client_token.generate
|
194
|
+
end
|
195
|
+
|
196
|
+
def payment_profiles_supported?
|
197
|
+
true
|
198
|
+
end
|
199
|
+
|
200
|
+
def sources_by_order(order)
|
201
|
+
source_ids = order.payments.where(payment_method_id: id).pluck(:source_id).uniq
|
202
|
+
payment_source_class.where(id: source_ids).with_payment_profile
|
203
|
+
end
|
204
|
+
|
205
|
+
def reusable_sources(order)
|
206
|
+
if order.completed?
|
207
|
+
sources_by_order(order)
|
208
|
+
elsif order.user_id
|
209
|
+
payment_source_class.where(
|
210
|
+
payment_method_id: id,
|
211
|
+
user_id: order.user_id
|
212
|
+
).with_payment_profile
|
213
|
+
else
|
214
|
+
[]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def dollars(cents)
|
221
|
+
Money.new(cents).dollars
|
222
|
+
end
|
223
|
+
|
224
|
+
def cents(dollars)
|
225
|
+
dollars.to_money.cents
|
226
|
+
end
|
227
|
+
|
228
|
+
def to_hash(preference_string)
|
229
|
+
JSON.parse(preference_string.gsub("=>", ":"))
|
230
|
+
end
|
231
|
+
|
232
|
+
def convert_preference_value(value, type)
|
233
|
+
if type == :hash && value.is_a?(String)
|
234
|
+
value = to_hash(value)
|
235
|
+
end
|
236
|
+
super
|
237
|
+
end
|
238
|
+
|
239
|
+
def transaction_options(source, options, submit_for_settlement = false)
|
240
|
+
params = options.select do |key, _|
|
241
|
+
ALLOWED_BRAINTREE_OPTIONS.include?(key)
|
242
|
+
end
|
243
|
+
|
244
|
+
params[:channel] = "Solidus"
|
245
|
+
params[:options] = { store_in_vault_on_success: true }
|
246
|
+
|
247
|
+
if submit_for_settlement
|
248
|
+
params[:options][:submit_for_settlement] = true
|
249
|
+
end
|
250
|
+
|
251
|
+
if paypal_email = paypal_payee_email_for(source, options)
|
252
|
+
params[:options][:paypal] = { payee_email: paypal_email }
|
253
|
+
end
|
254
|
+
|
255
|
+
if merchant_account_id = merchant_account_for(source, options)
|
256
|
+
params[:merchant_account_id] = merchant_account_id
|
257
|
+
end
|
258
|
+
|
259
|
+
if source.token
|
260
|
+
params[:payment_method_token] = source.token
|
261
|
+
else
|
262
|
+
params[:payment_method_nonce] = source.nonce
|
263
|
+
end
|
264
|
+
|
265
|
+
if source.paypal?
|
266
|
+
params[:shipping] = braintree_shipping_address(options)
|
267
|
+
end
|
268
|
+
|
269
|
+
if source.credit_card?
|
270
|
+
params[:billing] = braintree_billing_address(options)
|
271
|
+
end
|
272
|
+
|
273
|
+
if source.customer.present?
|
274
|
+
params[:customer_id] = source.customer.braintree_customer_id
|
275
|
+
end
|
276
|
+
|
277
|
+
params
|
278
|
+
end
|
279
|
+
|
280
|
+
def braintree_shipping_address(options)
|
281
|
+
braintree_address_attributes(options[:shipping_address])
|
282
|
+
end
|
283
|
+
|
284
|
+
def braintree_billing_address(options)
|
285
|
+
braintree_address_attributes(options[:billing_address])
|
286
|
+
end
|
287
|
+
|
288
|
+
def braintree_address_attributes(address)
|
289
|
+
first, last = address[:name].split(" ", 2)
|
290
|
+
{
|
291
|
+
first_name: first,
|
292
|
+
last_name: last,
|
293
|
+
street_address: [address[:address1], address[:address2]].compact.join(" "),
|
294
|
+
locality: address[:city],
|
295
|
+
postal_code: address[:zip],
|
296
|
+
region: address[:state],
|
297
|
+
country_code_alpha2: address[:country]
|
298
|
+
}
|
299
|
+
end
|
300
|
+
|
301
|
+
def merchant_account_for(_source, options)
|
302
|
+
if options[:currency]
|
303
|
+
preferred_merchant_currency_map[options[:currency]]
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def paypal_payee_email_for(source, options)
|
308
|
+
if source.paypal?
|
309
|
+
preferred_paypal_payee_email_map[options[:currency]]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def customer_profile_params(payment)
|
314
|
+
params = {}
|
315
|
+
|
316
|
+
if payment.source.try(:nonce)
|
317
|
+
params[:payment_method_nonce] = payment.source.nonce
|
318
|
+
end
|
319
|
+
|
320
|
+
params
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Response object that all actions on the gateway should return
|
2
|
+
module SolidusPaypalBraintree
|
3
|
+
class Response < ActiveMerchant::Billing::Response
|
4
|
+
# def initialize(success, message, params = {}, options = {})
|
5
|
+
|
6
|
+
class << self
|
7
|
+
private :new
|
8
|
+
|
9
|
+
# @param result [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
10
|
+
def build(result)
|
11
|
+
result.success? ? build_success(result) : build_failure(result)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_success(result)
|
17
|
+
transaction = result.transaction
|
18
|
+
|
19
|
+
test = true
|
20
|
+
authorization = transaction.id
|
21
|
+
fraud_review = nil
|
22
|
+
avs_result = nil
|
23
|
+
cvv_result = nil
|
24
|
+
|
25
|
+
options = {
|
26
|
+
test: test,
|
27
|
+
authorization: authorization,
|
28
|
+
fraud_review: fraud_review,
|
29
|
+
avs_result: avs_result,
|
30
|
+
cvv_result: cvv_result
|
31
|
+
}
|
32
|
+
|
33
|
+
new(true, transaction.status, {}, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_failure(result)
|
37
|
+
new(false, error_message(result))
|
38
|
+
end
|
39
|
+
|
40
|
+
def error_message(result)
|
41
|
+
if result.errors.any?
|
42
|
+
result.errors.map { |e| "#{e.message} (#{e.code})" }.join(" ")
|
43
|
+
else
|
44
|
+
[result.transaction.status,
|
45
|
+
result.transaction.gateway_rejection_reason,
|
46
|
+
result.transaction.processor_settlement_response_code,
|
47
|
+
result.transaction.processor_settlement_response_text].compact.join(" ")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module SolidusPaypalBraintree
|
2
|
+
class Source < ApplicationRecord
|
3
|
+
PAYPAL = "PayPalAccount"
|
4
|
+
APPLE_PAY = "ApplePayCard"
|
5
|
+
CREDIT_CARD = "CreditCard"
|
6
|
+
|
7
|
+
belongs_to :user, class_name: Spree::UserClassHandle.new
|
8
|
+
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
|
9
|
+
has_many :payments, as: :source, class_name: "Spree::Payment"
|
10
|
+
|
11
|
+
belongs_to :customer, class_name: "SolidusPaypalBraintree::Customer"
|
12
|
+
|
13
|
+
validates :payment_type, inclusion: [PAYPAL, APPLE_PAY, CREDIT_CARD]
|
14
|
+
|
15
|
+
scope(:with_payment_profile, -> { joins(:customer) })
|
16
|
+
scope(:credit_card, -> { where(payment_type: CREDIT_CARD) })
|
17
|
+
|
18
|
+
delegate :last_4, :card_type, to: :braintree_payment_method, allow_nil: true
|
19
|
+
alias_method :last_digits, :last_4
|
20
|
+
|
21
|
+
# we are not currenctly supporting an "imported" flag
|
22
|
+
def imported
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def actions
|
27
|
+
%w[capture void credit]
|
28
|
+
end
|
29
|
+
|
30
|
+
def can_capture?(payment)
|
31
|
+
payment.pending? || payment.checkout?
|
32
|
+
end
|
33
|
+
|
34
|
+
def can_void?(payment)
|
35
|
+
!payment.failed? && !payment.void?
|
36
|
+
end
|
37
|
+
|
38
|
+
def can_credit?(payment)
|
39
|
+
payment.completed? && payment.credit_allowed > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def friendly_payment_type
|
43
|
+
I18n.t(payment_type.underscore, scope: "solidus_paypal_braintree.payment_type")
|
44
|
+
end
|
45
|
+
|
46
|
+
def apple_pay?
|
47
|
+
payment_type == APPLE_PAY
|
48
|
+
end
|
49
|
+
|
50
|
+
def paypal?
|
51
|
+
payment_type == PAYPAL
|
52
|
+
end
|
53
|
+
|
54
|
+
def credit_card?
|
55
|
+
payment_type == CREDIT_CARD
|
56
|
+
end
|
57
|
+
|
58
|
+
def display_number
|
59
|
+
"XXXX-XXXX-XXXX-#{last_digits}"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def braintree_payment_method
|
65
|
+
return unless braintree_client && credit_card?
|
66
|
+
@braintree_payment_method ||= braintree_client.payment_method.find(token)
|
67
|
+
end
|
68
|
+
|
69
|
+
def braintree_client
|
70
|
+
@braintree_client ||= payment_method.try(:braintree)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|