solidus_braintree 1.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +78 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +1 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +21 -10
  7. data/.rspec +1 -1
  8. data/.rubocop.yml +86 -0
  9. data/CHANGELOG.md +186 -18
  10. data/Gemfile +35 -17
  11. data/LICENSE +26 -0
  12. data/README.md +389 -24
  13. data/Rakefile +6 -16
  14. data/app/assets/config/solidus_braintree_manifest.js +0 -0
  15. data/app/assets/javascripts/spree/backend/solidus_braintree/client.js +239 -0
  16. data/app/assets/javascripts/spree/backend/solidus_braintree/constants.js +89 -0
  17. data/app/assets/javascripts/spree/backend/solidus_braintree/hosted_form.js +46 -0
  18. data/app/assets/javascripts/spree/backend/solidus_braintree/promise.js +20 -0
  19. data/app/assets/javascripts/spree/backend/solidus_braintree.js +96 -0
  20. data/app/assets/stylesheets/spree/backend/solidus_braintree.scss +28 -0
  21. data/app/decorators/controllers/solidus_braintree/admin_payments_controller_decorator.rb +11 -0
  22. data/app/decorators/controllers/solidus_braintree/client_tokens_controller.rb +41 -0
  23. data/app/decorators/models/solidus_braintree/spree/store_decorator.rb +20 -0
  24. data/app/decorators/models/solidus_braintree/spree/user_decorator.rb +13 -0
  25. data/app/helpers/solidus_braintree/braintree_admin_helper.rb +23 -0
  26. data/app/models/application_record.rb +5 -0
  27. data/app/models/solidus_braintree/address.rb +64 -0
  28. data/app/models/solidus_braintree/avs_result.rb +69 -0
  29. data/app/models/solidus_braintree/configuration.rb +39 -0
  30. data/app/models/solidus_braintree/customer.rb +8 -0
  31. data/app/models/solidus_braintree/gateway.rb +437 -0
  32. data/app/models/solidus_braintree/response.rb +80 -0
  33. data/app/models/solidus_braintree/source.rb +140 -0
  34. data/app/models/solidus_braintree/transaction.rb +31 -0
  35. data/app/models/solidus_braintree/transaction_address.rb +88 -0
  36. data/app/models/solidus_braintree/transaction_import.rb +98 -0
  37. data/app/views/spree/api/payments/source_views/_braintree.json.jbuilder +3 -0
  38. data/bin/console +4 -1
  39. data/bin/dummy-app +37 -0
  40. data/bin/rails +5 -5
  41. data/bin/rails-dummy-app +17 -0
  42. data/bin/rails-engine +13 -0
  43. data/bin/rails-sandbox +16 -0
  44. data/bin/rake +7 -0
  45. data/bin/rspec +11 -0
  46. data/bin/sandbox +61 -0
  47. data/bin/setup +5 -4
  48. data/config/locales/en.yml +94 -2
  49. data/config/locales/it.yml +56 -0
  50. data/config/routes.rb +12 -3
  51. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  52. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +13 -0
  53. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  54. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +7 -0
  55. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  56. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +38 -0
  57. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +14 -0
  58. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
  59. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
  60. data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +5 -0
  61. data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +5 -0
  62. data/db/migrate/20230109080950_rename_solidus_paypal_braintree_source_type.rb +31 -0
  63. data/db/migrate/20230210104310_add_device_data_to_braintree_sources.rb +5 -0
  64. data/lib/controllers/backend/solidus_braintree/configurations_controller.rb +48 -0
  65. data/lib/generators/solidus_braintree/install/install_generator.rb +155 -19
  66. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_280x48.svg +19 -0
  67. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_320x48.svg +19 -0
  68. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_375x48.svg +19 -0
  69. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_280x48.svg +19 -0
  70. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_320x48.svg +19 -0
  71. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_375x48.svg +19 -0
  72. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_acceptance_mark.svg +15 -0
  73. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_280x48.svg +19 -0
  74. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_320x48.svg +19 -0
  75. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_375x48.svg +19 -0
  76. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_logo.svg +18 -0
  77. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_acceptance_mark.svg +20 -0
  78. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_280x48.svg +19 -0
  79. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_320x48.svg +19 -0
  80. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_375x48.svg +19 -0
  81. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_logo.svg +18 -0
  82. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/paypal_button.js +34 -0
  83. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/ajax.js +13 -0
  84. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/apple_pay_button.js +179 -0
  85. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/checkout.js +113 -0
  86. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/client.js +239 -0
  87. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/constants.js +89 -0
  88. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/frontend.js +15 -0
  89. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/hosted_form.js +48 -0
  90. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/paypal_button.js +178 -0
  91. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/paypal_messaging.js +22 -0
  92. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/promise.js +20 -0
  93. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/venmo_button.js +86 -0
  94. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree.js +1 -0
  95. data/lib/generators/solidus_braintree/install/templates/app/assets/stylesheets/spree/frontend/solidus_braintree.scss +62 -0
  96. data/lib/generators/solidus_braintree/install/templates/app/controllers/solidus_braintree/checkouts_controller.rb +31 -0
  97. data/lib/generators/solidus_braintree/install/templates/app/controllers/solidus_braintree/transactions_controller.rb +67 -0
  98. data/lib/generators/solidus_braintree/install/templates/app/helpers/solidus_braintree/braintree_checkout_helper.rb +60 -0
  99. data/lib/generators/solidus_braintree/install/templates/app/views/checkouts/existing_payment/_braintree.html.erb +2 -0
  100. data/lib/generators/solidus_braintree/install/templates/app/views/checkouts/payment/_braintree.html.erb +23 -0
  101. data/lib/generators/solidus_braintree/install/templates/app/views/payments/_braintree_payment_details.html.erb +9 -0
  102. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_apple_pay_button.html.erb +27 -0
  103. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_errors.html.erb +16 -0
  104. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_head_scripts.html.erb +26 -0
  105. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_hosted_fields.html.erb +40 -0
  106. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_cart_button.html.erb +38 -0
  107. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_checkout_button.html.erb +32 -0
  108. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
  109. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_venmo_button.html.erb +33 -0
  110. data/lib/generators/solidus_braintree/install/templates/config/initializers/solidus_braintree.rb +6 -0
  111. data/lib/solidus_braintree/country_mapper.rb +37 -0
  112. data/lib/solidus_braintree/engine.rb +61 -11
  113. data/lib/solidus_braintree/extension_configuration.rb +23 -0
  114. data/lib/solidus_braintree/request_protection.rb +21 -0
  115. data/lib/solidus_braintree/version.rb +3 -1
  116. data/lib/solidus_braintree.rb +14 -2
  117. data/lib/solidus_paypal_braintree.rb +6 -0
  118. data/lib/views/backend/solidus_braintree/configurations/list.html.erb +63 -0
  119. data/lib/views/backend/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  120. data/lib/views/backend/spree/admin/payments/source_views/_braintree.html.erb +39 -0
  121. data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +13 -0
  122. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  123. data/lib/views/backend_v2.4/spree/admin/shared/preference_fields/_hash.html.erb +12 -0
  124. data/solidus_braintree.gemspec +37 -38
  125. data/spec/controllers/solidus_braintree/checkouts_controller_spec.rb +99 -0
  126. data/spec/controllers/solidus_braintree/client_tokens_controller_spec.rb +55 -0
  127. data/spec/controllers/solidus_braintree/configurations_controller_spec.rb +73 -0
  128. data/spec/controllers/solidus_braintree/transactions_controller_spec.rb +183 -0
  129. data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
  130. data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
  131. data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
  132. data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
  133. data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
  134. data/spec/fixtures/cassettes/braintree/token.yml +63 -0
  135. data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
  136. data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
  137. data/spec/fixtures/cassettes/checkout/update.yml +71 -0
  138. data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +171 -0
  139. data/spec/fixtures/cassettes/checkout/valid_venmo_transaction.yml +599 -0
  140. data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
  141. data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
  142. data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
  143. data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
  144. data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
  145. data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
  146. data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
  147. data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
  148. data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
  149. data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
  150. data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
  151. data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
  152. data/spec/fixtures/cassettes/gateway/customer.yml +79 -0
  153. data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
  154. data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
  155. data/spec/fixtures/cassettes/gateway/void.yml +137 -0
  156. data/spec/fixtures/cassettes/source/bin.yml +295 -0
  157. data/spec/fixtures/cassettes/source/card_type.yml +267 -0
  158. data/spec/fixtures/cassettes/source/last4.yml +267 -0
  159. data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
  160. data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
  161. data/spec/fixtures/views/carts/_cart_footer.html.erb +18 -0
  162. data/spec/helpers/solidus_braintree/braintree_admin_helper_spec.rb +17 -0
  163. data/spec/helpers/solidus_braintree/braintree_checkout_helper_spec.rb +70 -0
  164. data/spec/models/solidus_braintree/address_spec.rb +71 -0
  165. data/spec/models/solidus_braintree/avs_result_spec.rb +317 -0
  166. data/spec/models/solidus_braintree/gateway_spec.rb +774 -0
  167. data/spec/models/solidus_braintree/response_spec.rb +280 -0
  168. data/spec/models/solidus_braintree/source_spec.rb +555 -0
  169. data/spec/models/solidus_braintree/transaction_address_spec.rb +235 -0
  170. data/spec/models/solidus_braintree/transaction_import_spec.rb +302 -0
  171. data/spec/models/solidus_braintree/transaction_spec.rb +86 -0
  172. data/spec/models/spree/store_spec.rb +14 -0
  173. data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
  174. data/spec/solidus_braintree_helper.rb +7 -0
  175. data/spec/support/solidus_braintree/capybara.rb +7 -0
  176. data/spec/support/solidus_braintree/factories.rb +55 -0
  177. data/spec/support/solidus_braintree/gateway_helpers.rb +29 -0
  178. data/spec/support/solidus_braintree/order_ready_for_payment.rb +44 -0
  179. data/spec/support/solidus_braintree/order_walkthrough.rb +87 -0
  180. data/spec/support/solidus_braintree/vcr.rb +42 -0
  181. data/spec/support/solidus_braintree/with_prepended_view_fixtures.rb +19 -0
  182. data/spec/system/backend/configuration_spec.rb +23 -0
  183. data/spec/system/backend/new_payment_spec.rb +136 -0
  184. data/spec/system/frontend/braintree_credit_card_checkout_spec.rb +199 -0
  185. data/spec/system/frontend/paypal_checkout_spec.rb +169 -0
  186. data/spec/system/frontend/venmo_checkout_spec.rb +193 -0
  187. metadata +289 -255
  188. data/.travis.yml +0 -41
  189. data/LICENSE.txt +0 -21
  190. data/app/controllers/spree/api/braintree_client_token_controller.rb +0 -13
  191. data/app/helpers/braintree_view_helpers.rb +0 -20
  192. data/app/models/concerns/solidus_braintree/add_name_validation_concern.rb +0 -8
  193. data/app/models/concerns/solidus_braintree/inject_device_data_concern.rb +0 -18
  194. data/app/models/concerns/solidus_braintree/payment_braintree_nonce_concern.rb +0 -8
  195. data/app/models/concerns/solidus_braintree/permitted_attributes_concern.rb +0 -11
  196. data/app/models/concerns/solidus_braintree/skip_require_card_numbers_concern.rb +0 -14
  197. data/app/models/concerns/solidus_braintree/use_data_field_concern.rb +0 -23
  198. data/app/models/credit_card_decorator.rb +0 -3
  199. data/app/models/payment_decorator.rb +0 -2
  200. data/app/models/permitted_attributes_decorator.rb +0 -1
  201. data/app/models/solidus/gateway/braintree_gateway.rb +0 -306
  202. data/app/overrides/spree/checkout/_confirm/braintree_security.html.erb.deface +0 -9
  203. data/app/views/spree/admin/payments/source_forms/_braintree.html.erb +0 -38
  204. data/app/views/spree/admin/payments/source_views/_braintree.html.erb +0 -30
  205. data/app/views/spree/checkout/payment/_braintree.html.erb +0 -55
  206. data/app/views/spree/checkout/payment/_braintree_initialization.html.erb +0 -12
  207. data/config/initializers/braintree.rb +0 -1
  208. data/db/migrate/20150910170527_add_data_to_credit_card.rb +0 -5
  209. data/db/migrate/20160426221931_add_braintree_device_data_to_order.rb +0 -5
  210. data/lib/assets/javascripts/spree/backend/braintree/solidus_braintree.js +0 -59
  211. data/lib/assets/javascripts/spree/frontend/braintree/solidus_braintree.js +0 -144
  212. data/lib/assets/javascripts/vendor/braintree.js +0 -8
  213. data/lib/assets/stylesheets/spree/frontend/solidus_braintree.scss +0 -26
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBraintree
4
+ class Address
5
+ delegate :address1,
6
+ :address2,
7
+ :city,
8
+ :country,
9
+ :phone,
10
+ :state,
11
+ :zipcode,
12
+ to: :spree_address
13
+
14
+ def self.split_name(name)
15
+ if defined?(Spree::Address::Name)
16
+ address_name = Spree::Address::Name.new(name)
17
+ [address_name.first_name, address_name.last_name]
18
+ else
19
+ name.strip.split(' ', 2)
20
+ end
21
+ end
22
+
23
+ def initialize(spree_address)
24
+ @spree_address = spree_address
25
+ end
26
+
27
+ def to_json(*_args)
28
+ address_hash = {
29
+ line1: address1,
30
+ line2: address2,
31
+ city: city,
32
+ postalCode: zipcode,
33
+ countryCode: country.iso,
34
+ phone: phone,
35
+ recipientName: "#{firstname} #{lastname}"
36
+ }
37
+
38
+ if ::Spree::Config.address_requires_state && country.states_required
39
+ address_hash[:state] = state.name
40
+ end
41
+ address_hash.to_json
42
+ end
43
+
44
+ def firstname
45
+ if SolidusSupport.combined_first_and_last_name_in_address?
46
+ self.class.split_name(spree_address.name).first
47
+ else
48
+ spree_address.firstname
49
+ end
50
+ end
51
+
52
+ def lastname
53
+ if SolidusSupport.combined_first_and_last_name_in_address?
54
+ self.class.split_name(spree_address.name).last
55
+ else
56
+ spree_address.lastname
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :spree_address
63
+ end
64
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_merchant/billing/avs_result'
4
+
5
+ module SolidusBraintree
6
+ class AVSResult < ActiveMerchant::Billing::AVSResult
7
+ # Mapping took from ActiveMerchant::Billing::BraintreeBlueGateway
8
+ AVS_MAPPING = {
9
+ 'M' => {
10
+ 'M' => 'M',
11
+ 'N' => 'A',
12
+ 'U' => 'B',
13
+ 'I' => 'B',
14
+ 'A' => 'B'
15
+ },
16
+ 'N' => {
17
+ 'M' => 'Z',
18
+ 'N' => 'C',
19
+ 'U' => 'C',
20
+ 'I' => 'C',
21
+ 'A' => 'C'
22
+ },
23
+ 'U' => {
24
+ 'M' => 'P',
25
+ 'N' => 'N',
26
+ 'U' => 'I',
27
+ 'I' => 'I',
28
+ 'A' => 'I'
29
+ },
30
+ 'I' => {
31
+ 'M' => 'P',
32
+ 'N' => 'C',
33
+ 'U' => 'I',
34
+ 'I' => 'I',
35
+ 'A' => 'I'
36
+ },
37
+ 'A' => {
38
+ 'M' => 'P',
39
+ 'N' => 'C',
40
+ 'U' => 'I',
41
+ 'I' => 'I',
42
+ 'A' => 'I'
43
+ },
44
+ 'B' => {
45
+ 'B' => 'B'
46
+ },
47
+ nil => { nil => nil }
48
+ }.freeze
49
+
50
+ class << self
51
+ private :new
52
+
53
+ def build(transaction)
54
+ new(
55
+ code: avs_code_from(transaction),
56
+ street_match: transaction.avs_street_address_response_code,
57
+ postal_match: transaction.avs_postal_code_response_code
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ def avs_code_from(transaction)
64
+ transaction.avs_error_response_code ||
65
+ AVS_MAPPING[transaction.avs_street_address_response_code][transaction.avs_postal_code_response_code]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBraintree
4
+ class Configuration < ::Spree::Base
5
+ PAYPAL_BUTTON_PREFERENCES = {
6
+ color: { availables: %w[gold blue silver white black], default: 'white' },
7
+ shape: { availables: %w[pill rect], default: 'rect' },
8
+ label: { availables: %w[checkout credit pay buynow paypal installment], default: 'checkout' },
9
+ tagline: { availables: %w[true false], default: 'false' },
10
+ layout: { availables: %w[horizontal vertical], default: 'horizontal' },
11
+ messaging: { availables: %w[true false], default: 'false' }
12
+ }.freeze
13
+
14
+ include ::Spree::Preferences::Persistable
15
+
16
+ belongs_to :store, class_name: 'Spree::Store', optional: false
17
+
18
+ # Preferences for Paypal button
19
+ PAYPAL_BUTTON_PREFERENCES.each do |name, desc|
20
+ preference_name = "paypal_button_#{name}".to_sym
21
+ attribute_name = "preferred_#{preference_name}".to_sym
22
+
23
+ preference preference_name, :string, default: desc[:default]
24
+
25
+ validates attribute_name, inclusion: desc[:availables]
26
+ end
27
+
28
+ preference :venmo_button_color, :preference_select, default: 'blue'
29
+ preference :venmo_button_width, :preference_select, default: '320'
30
+
31
+ def preferred_venmo_button_color_options
32
+ [["Blue", "blue"], ["White", "white"]]
33
+ end
34
+
35
+ def preferred_venmo_button_width_options
36
+ [["280", "280"], ["320", "320"], ["375", "375"]]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusBraintree
4
+ class Customer < ApplicationRecord
5
+ belongs_to :user, class_name: ::Spree::UserClassHandle.new, optional: true
6
+ has_many :sources, class_name: "SolidusBraintree::Source", inverse_of: :customer, dependent: :destroy
7
+ end
8
+ end
@@ -0,0 +1,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'braintree'
4
+ require 'solidus_braintree/request_protection'
5
+
6
+ module SolidusBraintree
7
+ class Gateway < ::Spree::PaymentMethod
8
+ include RequestProtection
9
+
10
+ class TokenGenerationDisabledError < StandardError; end
11
+
12
+ # Error message from Braintree that gets returned by a non voidable transaction
13
+ NON_VOIDABLE_STATUS_ERROR_REGEXP = /can only be voided if status is authorized/.freeze
14
+
15
+ TOKEN_GENERATION_DISABLED_MESSAGE = 'Token generation is disabled. ' \
16
+ 'To re-enable set the `token_generation_enabled` preference on the ' \
17
+ 'gateway to `true`.'
18
+
19
+ ALLOWED_BRAINTREE_OPTIONS = [
20
+ :device_data,
21
+ :device_session_id,
22
+ :merchant_account_id,
23
+ :order_id
24
+ ].freeze
25
+
26
+ VOIDABLE_STATUSES = [
27
+ Braintree::Transaction::Status::SubmittedForSettlement,
28
+ Braintree::Transaction::Status::SettlementPending,
29
+ Braintree::Transaction::Status::Authorized
30
+ ].freeze
31
+
32
+ # This is useful in feature tests to avoid rate limited requests from
33
+ # Braintree
34
+ preference(:client_sdk_enabled, :boolean, default: true)
35
+
36
+ preference(:token_generation_enabled, :boolean, default: true)
37
+
38
+ # Preferences for configuration of Braintree credentials
39
+ preference(:environment, :string, default: 'sandbox')
40
+ preference(:merchant_id, :string, default: nil)
41
+ preference(:public_key, :string, default: nil)
42
+ preference(:private_key, :string, default: nil)
43
+ preference(:http_open_timeout, :integer, default: 60)
44
+ preference(:http_read_timeout, :integer, default: 60)
45
+ preference(:merchant_currency_map, :hash, default: {})
46
+ preference(:paypal_payee_email_map, :hash, default: {})
47
+
48
+ # Which checkout flow to use (vault/checkout)
49
+ preference(:paypal_flow, :string, default: 'vault')
50
+
51
+ # A hash that gets passed to the `style` key when initializing the credit card fields.
52
+ # See https://developers.braintreepayments.com/guides/hosted-fields/styling/javascript/v3
53
+ preference(:credit_card_fields_style, :hash, default: {})
54
+
55
+ # A hash that gets its keys passed to the associated braintree field placeholder tag.
56
+ # Example: { number: "Enter card number", cvv: "Enter CVV", expirationDate: "mm/yy" }
57
+ preference(:placeholder_text, :hash, default: {})
58
+
59
+ # Wether to use the JS device data collector
60
+ preference(:use_data_collector, :boolean, default: true)
61
+
62
+ # Useful for testing purposes, as PayPal will show funding sources based on the buyer's country;
63
+ # usually retrieved by their ip geolocation. I.e. Venmo will show for US buyers, but not European.
64
+ preference(:force_buyer_country, :string)
65
+
66
+ preference(:enable_venmo_funding, :boolean, default: false)
67
+
68
+ # When on mobile, paying with Venmo, the user may be returned to the same store tab
69
+ # depending on if their browser supports it, otherwise a new tab will be created
70
+ # However, returning to a new tab may break the payment checkout flow for some stores, for example,
71
+ # if they are single-page applications (SPA). Set this to false if this is the case
72
+ preference(:venmo_new_tab_support, :boolean, default: true)
73
+
74
+ def partial_name
75
+ "braintree"
76
+ end
77
+ alias_method :method_type, :partial_name
78
+
79
+ def payment_source_class
80
+ Source
81
+ end
82
+
83
+ def braintree
84
+ @braintree ||= Braintree::Gateway.new(gateway_options)
85
+ end
86
+
87
+ def gateway_options
88
+ {
89
+ environment: preferred_environment.to_sym,
90
+ merchant_id: preferred_merchant_id,
91
+ public_key: preferred_public_key,
92
+ private_key: preferred_private_key,
93
+ http_open_timeout: preferred_http_open_timeout,
94
+ http_read_timeout: preferred_http_read_timeout,
95
+ logger: logger
96
+ }
97
+ end
98
+
99
+ # Create a payment and submit it for settlement all at once.
100
+ #
101
+ # @api public
102
+ # @param money_cents [Number, String] amount to authorize
103
+ # @param source [Source] payment source
104
+ # @params gateway_options [Hash]
105
+ # extra options to send along. e.g.: device data for fraud prevention
106
+ # @return [Response]
107
+ def purchase(money_cents, source, gateway_options)
108
+ protected_request do
109
+ result = braintree.transaction.sale(
110
+ amount: dollars(money_cents),
111
+ **transaction_options(source, gateway_options, submit_for_settlement: true)
112
+ )
113
+
114
+ Response.build(result)
115
+ end
116
+ end
117
+
118
+ # Authorize a payment to be captured later.
119
+ #
120
+ # @api public
121
+ # @param money_cents [Number, String] amount to authorize
122
+ # @param source [Source] payment source
123
+ # @params gateway_options [Hash]
124
+ # extra options to send along. e.g.: device data for fraud prevention
125
+ # @return [Response]
126
+ def authorize(money_cents, source, gateway_options)
127
+ protected_request do
128
+ result = braintree.transaction.sale(
129
+ amount: dollars(money_cents),
130
+ **transaction_options(source, gateway_options)
131
+ )
132
+
133
+ Response.build(result)
134
+ end
135
+ end
136
+
137
+ # Collect funds from an authorized payment.
138
+ #
139
+ # @api public
140
+ # @param money_cents [Number, String]
141
+ # amount to capture (partial settlements are supported by the gateway)
142
+ # @param response_code [String] the transaction id of the payment to capture
143
+ # @return [Response]
144
+ def capture(money_cents, response_code, _gateway_options)
145
+ protected_request do
146
+ result = braintree.transaction.submit_for_settlement(
147
+ response_code,
148
+ dollars(money_cents)
149
+ )
150
+ Response.build(result)
151
+ end
152
+ end
153
+
154
+ # Used to refeund a customer for an already settled transaction.
155
+ #
156
+ # @api public
157
+ # @param money_cents [Number, String] amount to refund
158
+ # @param response_code [String] the transaction id of the payment to refund
159
+ # @return [Response]
160
+ def credit(money_cents, _source, response_code, _gateway_options)
161
+ protected_request do
162
+ result = braintree.transaction.refund(
163
+ response_code,
164
+ dollars(money_cents)
165
+ )
166
+ Response.build(result)
167
+ end
168
+ end
169
+
170
+ # Used to cancel a transaction before it is settled.
171
+ #
172
+ # @api public
173
+ # @param response_code [String] the transaction id of the payment to void
174
+ # @return [Response]
175
+ def void(response_code, _source, _gateway_options)
176
+ protected_request do
177
+ result = braintree.transaction.void(response_code)
178
+ Response.build(result)
179
+ end
180
+ end
181
+
182
+ # Will either refund or void the payment depending on its state.
183
+ #
184
+ # If the transaction has not yet been settled, we can void the transaction.
185
+ # Otherwise, we need to issue a refund.
186
+ #
187
+ # @api public
188
+ # @param response_code [String] the transaction id of the payment to void
189
+ # @return [Response]
190
+ def cancel(response_code)
191
+ transaction = protected_request do
192
+ braintree.transaction.find(response_code)
193
+ end
194
+ if VOIDABLE_STATUSES.include?(transaction.status)
195
+ void(response_code, nil, {})
196
+ else
197
+ credit(cents(transaction.amount), nil, response_code, {})
198
+ end
199
+ end
200
+
201
+ # Will void the payment depending on its state or return false
202
+ #
203
+ # Used by Solidus >= 2.4 instead of +cancel+
204
+ #
205
+ # If the transaction has not yet been settled, we can void the transaction.
206
+ # Otherwise, we return false so Solidus creates a refund instead.
207
+ #
208
+ # @api public
209
+ # @param payment [Spree::Payment] the payment to void
210
+ # @return [Response|FalseClass]
211
+ def try_void(payment)
212
+ transaction = braintree.transaction.find(payment.response_code)
213
+ if transaction.status.in? SolidusBraintree::Gateway::VOIDABLE_STATUSES
214
+ # Sometimes Braintree returns a voidable status although it is not voidable anymore.
215
+ # When we try to void that transaction we receive an error and need to return false
216
+ # so Solidus can create a refund instead.
217
+ begin
218
+ void(payment.response_code, nil, {})
219
+ rescue ActiveMerchant::ConnectionError => e
220
+ e.message.match(NON_VOIDABLE_STATUS_ERROR_REGEXP) ? false : raise(e)
221
+ end
222
+ else
223
+ false
224
+ end
225
+ end
226
+
227
+ # Creates a new customer profile in Braintree
228
+ #
229
+ # @api public
230
+ # @param payment [Spree::Payment]
231
+ # @return [SolidusBraintree::Customer]
232
+ def create_profile(payment)
233
+ source = payment.source
234
+
235
+ return if source.token.present? || source.customer.present? || source.nonce.nil?
236
+
237
+ result = braintree.customer.create(customer_profile_params(payment))
238
+ fail ::Spree::Core::GatewayError, result.message unless result.success?
239
+
240
+ customer = result.customer
241
+
242
+ source.create_customer!(braintree_customer_id: customer.id).tap do
243
+ if customer.payment_methods.any?
244
+ source.token = customer.payment_methods.last.token
245
+ end
246
+
247
+ source.save!
248
+ end
249
+ end
250
+
251
+ # @raise [TokenGenerationDisabledError]
252
+ # If `preferred_token_generation_enabled` is false
253
+ #
254
+ # @return [String]
255
+ # The token that should be used along with the Braintree js-client sdk.
256
+ #
257
+ # @example
258
+ # <script>
259
+ # var token = #{Spree::Braintree::Gateway.first!.generate_token}
260
+ #
261
+ # braintree.client.create(
262
+ # {
263
+ # authorization: token
264
+ # },
265
+ # function(clientError, clientInstance) {
266
+ # ...
267
+ # }
268
+ # );
269
+ # </script>
270
+ def generate_token
271
+ unless preferred_token_generation_enabled
272
+ raise TokenGenerationDisabledError, TOKEN_GENERATION_DISABLED_MESSAGE
273
+ end
274
+
275
+ braintree.client_token.generate
276
+ end
277
+
278
+ def payment_profiles_supported?
279
+ true
280
+ end
281
+
282
+ def sources_by_order(order)
283
+ source_ids = order.payments.where(payment_method_id: id).pluck(:source_id).uniq
284
+ payment_source_class.where(id: source_ids).with_payment_profile
285
+ end
286
+
287
+ def reusable_sources(order)
288
+ if order.completed?
289
+ sources_by_order(order)
290
+ elsif order.user_id
291
+ payment_source_class.where(
292
+ payment_method_id: id,
293
+ user_id: order.user_id
294
+ ).with_payment_profile
295
+ else
296
+ []
297
+ end
298
+ end
299
+
300
+ private
301
+
302
+ # Whether to store this payment method in the PayPal Vault. This only works when the checkout
303
+ # flow is "vault", so make sure to call +super+ if you override it.
304
+ def store_in_vault
305
+ preferred_paypal_flow == 'vault'
306
+ end
307
+
308
+ def logger
309
+ Braintree::Configuration.logger.clone.tap do |logger|
310
+ logger.level = Rails.logger.level
311
+ end
312
+ end
313
+
314
+ def dollars(cents)
315
+ Money.new(cents).dollars
316
+ end
317
+
318
+ def cents(dollars)
319
+ dollars.to_money.cents
320
+ end
321
+
322
+ def to_hash(preference_string)
323
+ JSON.parse(preference_string.gsub("=>", ":"))
324
+ end
325
+
326
+ def convert_preference_value(value, type, preference_encryptor = nil)
327
+ if type == :hash && value.is_a?(String)
328
+ value = to_hash(value)
329
+ end
330
+ if method(__method__).super_method.arity == 3
331
+ super
332
+ else
333
+ super(value, type)
334
+ end
335
+ end
336
+
337
+ def transaction_options(source, options, submit_for_settlement: false)
338
+ params = options.select do |key, _|
339
+ ALLOWED_BRAINTREE_OPTIONS.include?(key)
340
+ end
341
+
342
+ params[:channel] = "Solidus"
343
+ params[:options] = { store_in_vault_on_success: store_in_vault }
344
+
345
+ if submit_for_settlement
346
+ params[:options][:submit_for_settlement] = true
347
+ end
348
+
349
+ if paypal_email = paypal_payee_email_for(source, options)
350
+ params[:options][:paypal] = { payee_email: paypal_email }
351
+ end
352
+
353
+ if source.venmo? && venmo_business_profile_id
354
+ params[:options][:venmo] = { profile_id: venmo_business_profile_id }
355
+ end
356
+
357
+ if merchant_account_id = merchant_account_for(source, options)
358
+ params[:merchant_account_id] = merchant_account_id
359
+ end
360
+
361
+ if source.token
362
+ params[:payment_method_token] = source.token
363
+ else
364
+ params[:payment_method_nonce] = source.nonce
365
+ end
366
+
367
+ if source&.device_data
368
+ params[:device_data] = source.device_data
369
+ end
370
+
371
+ if source.paypal?
372
+ params[:shipping] = braintree_shipping_address(options)
373
+ end
374
+
375
+ if source.credit_card?
376
+ params[:billing] = braintree_billing_address(options)
377
+ end
378
+
379
+ if source.customer.present?
380
+ params[:customer_id] = source.customer.braintree_customer_id
381
+ end
382
+
383
+ params
384
+ end
385
+
386
+ def braintree_shipping_address(options)
387
+ braintree_address_attributes(options[:shipping_address])
388
+ end
389
+
390
+ def braintree_billing_address(options)
391
+ braintree_address_attributes(options[:billing_address])
392
+ end
393
+
394
+ def braintree_address_attributes(address)
395
+ first, last = address[:name].split(" ", 2)
396
+ {
397
+ first_name: first,
398
+ last_name: last,
399
+ street_address: [address[:address1], address[:address2]].compact.join(" "),
400
+ locality: address[:city],
401
+ postal_code: address[:zip],
402
+ region: address[:state],
403
+ country_code_alpha2: address[:country]
404
+ }
405
+ end
406
+
407
+ def merchant_account_for(_source, options)
408
+ return unless options[:currency]
409
+
410
+ preferred_merchant_currency_map[options[:currency]]
411
+ end
412
+
413
+ def paypal_payee_email_for(source, options)
414
+ return unless source.paypal?
415
+
416
+ preferred_paypal_payee_email_map[options[:currency]]
417
+ end
418
+
419
+ def customer_profile_params(payment)
420
+ params = {}
421
+
422
+ params[:email] = payment&.order&.email
423
+
424
+ if store_in_vault && payment.source.try(:nonce)
425
+ params[:payment_method_nonce] = payment.source.nonce
426
+ end
427
+
428
+ params
429
+ end
430
+
431
+ # override with the Venmo business profile that you want to use for transactions,
432
+ # or leave it to be nil if want Braintree to use your default account
433
+ def venmo_business_profile_id
434
+ nil
435
+ end
436
+ end
437
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_merchant/billing/response'
4
+ require_relative 'avs_result'
5
+
6
+ # Response object that all actions on the gateway should return
7
+ module SolidusBraintree
8
+ class Response < ActiveMerchant::Billing::Response
9
+ # def initialize(success, message, params = {}, options = {})
10
+
11
+ class << self
12
+ private :new
13
+
14
+ # @param result [Braintree::SuccessfulResult, Braintree::ErrorResult]
15
+ def build(result)
16
+ result.success? ? build_success(result) : build_failure(result)
17
+ end
18
+
19
+ private
20
+
21
+ def build_success(result)
22
+ transaction = result.transaction
23
+ new(true, transaction.status, {}, response_options(transaction))
24
+ end
25
+
26
+ def build_failure(result)
27
+ transaction = result.transaction
28
+ options = response_options(transaction).update(
29
+ # For error responses we want to have the CVV code
30
+ cvv_result: transaction&.cvv_response_code
31
+ )
32
+ new(false, error_message(result), result.params.deep_stringify_keys, options)
33
+ end
34
+
35
+ def response_options(transaction)
36
+ # Some error responses do not have a transaction
37
+ return {} if transaction.nil?
38
+
39
+ {
40
+ authorization: transaction.id,
41
+ avs_result: SolidusBraintree::AVSResult.build(transaction),
42
+ # As we do not provide the CVV while submitting the transaction (for PCI compliance reasons),
43
+ # we need to ignore the only response we get back (I = not provided).
44
+ # Otherwise Solidus thinks this payment is risky.
45
+ cvv_result: nil
46
+ }
47
+ end
48
+
49
+ def error_message(result)
50
+ if result.errors.any?
51
+ result.errors.map { |e| "#{e.message} (#{e.code})" }.join(" ")
52
+ else
53
+ transaction_error_message(result.transaction)
54
+ end
55
+ end
56
+
57
+ # Human readable error message for transaction responses
58
+ def transaction_error_message(transaction)
59
+ case transaction.status
60
+ when 'gateway_rejected'
61
+ I18n.t(transaction.gateway_rejection_reason,
62
+ scope: 'solidus_braintree.gateway_rejection_reasons',
63
+ default: "#{transaction.status.humanize} #{transaction.gateway_rejection_reason.humanize}")
64
+ when 'processor_declined'
65
+ I18n.t(transaction.processor_response_code,
66
+ scope: 'solidus_braintree.processor_response_codes',
67
+ default: "#{transaction.processor_response_text} (#{transaction.processor_response_code})")
68
+ when 'settlement_declined'
69
+ I18n.t(transaction.processor_settlement_response_code,
70
+ scope: 'solidus_braintree.processor_settlement_response_codes',
71
+ default: "#{transaction.processor_settlement_response_text} (#{transaction.processor_settlement_response_code})") # rubocop:disable Layout/LineLength
72
+ else
73
+ I18n.t(transaction.status,
74
+ scope: 'solidus_braintree.transaction_statuses',
75
+ default: transaction.status.humanize)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end