solidus_paypal_braintree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +196 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +66 -0
  6. data/app/assets/javascripts/spree/braintree_hosted_form.js +98 -0
  7. data/app/assets/javascripts/spree/checkout/braintree.js +60 -0
  8. data/app/assets/javascripts/spree/frontend/paypal_button.js +182 -0
  9. data/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +188 -0
  10. data/app/assets/stylesheets/spree/backend/solidus_paypal_braintree.scss +28 -0
  11. data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +16 -0
  12. data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +21 -0
  13. data/app/helpers/braintree_admin_helper.rb +18 -0
  14. data/app/models/application_record.rb +3 -0
  15. data/app/models/solidus_paypal_braintree/configuration.rb +5 -0
  16. data/app/models/solidus_paypal_braintree/customer.rb +4 -0
  17. data/app/models/solidus_paypal_braintree/gateway.rb +323 -0
  18. data/app/models/solidus_paypal_braintree/response.rb +52 -0
  19. data/app/models/solidus_paypal_braintree/source.rb +73 -0
  20. data/app/models/solidus_paypal_braintree/transaction.rb +30 -0
  21. data/app/models/solidus_paypal_braintree/transaction_address.rb +66 -0
  22. data/app/models/solidus_paypal_braintree/transaction_import.rb +92 -0
  23. data/app/models/spree/store_decorator.rb +11 -0
  24. data/app/overrides/admin_navigation_menu.rb +6 -0
  25. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +26 -0
  26. data/config/initializers/braintree.rb +1 -0
  27. data/config/locales/en.yml +30 -0
  28. data/config/routes.rb +12 -0
  29. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  30. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +11 -0
  31. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  32. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +9 -0
  33. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  34. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +30 -0
  35. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +11 -0
  36. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +30 -0
  37. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +27 -0
  38. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +61 -0
  39. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +37 -0
  40. data/lib/solidus_paypal_braintree.rb +10 -0
  41. data/lib/solidus_paypal_braintree/country_mapper.rb +35 -0
  42. data/lib/solidus_paypal_braintree/engine.rb +53 -0
  43. data/lib/solidus_paypal_braintree/factories.rb +18 -0
  44. data/lib/solidus_paypal_braintree/version.rb +3 -0
  45. data/lib/views/backend/solidus_paypal_braintree/configurations/_admin_tab.html.erb +3 -0
  46. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +30 -0
  47. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  48. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +34 -0
  49. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  50. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +52 -0
  51. metadata +350 -0
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,5 @@
1
+ class SolidusPaypalBraintree::Configuration < ApplicationRecord
2
+ belongs_to :store, class_name: 'Spree::Store'
3
+
4
+ validates :store, presence: true
5
+ end
@@ -0,0 +1,4 @@
1
+ class SolidusPaypalBraintree::Customer < ApplicationRecord
2
+ belongs_to :user, class_name: Spree::UserClassHandle.new
3
+ has_many :sources, class_name: "SolidusPaypalBraintree::Source", inverse_of: :customer
4
+ end
@@ -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