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.
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