solidus_braintree 1.4.0 → 2.0.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +12 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github_changelog_generator +2 -0
  5. data/.gitignore +20 -11
  6. data/.rspec +1 -1
  7. data/.rubocop.yml +79 -0
  8. data/CHANGELOG.md +178 -18
  9. data/Gemfile +31 -23
  10. data/LICENSE +26 -0
  11. data/README.md +387 -26
  12. data/Rakefile +4 -19
  13. data/app/assets/config/solidus_braintree_manifest.js +1 -0
  14. data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_280x48.svg +19 -0
  15. data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_320x48.svg +19 -0
  16. data/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_375x48.svg +19 -0
  17. data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_280x48.svg +19 -0
  18. data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_320x48.svg +19 -0
  19. data/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_375x48.svg +19 -0
  20. data/app/assets/images/solidus_braintree/venmo/venmo_blue_acceptance_mark.svg +15 -0
  21. data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_280x48.svg +19 -0
  22. data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_320x48.svg +19 -0
  23. data/app/assets/images/solidus_braintree/venmo/venmo_blue_button_375x48.svg +19 -0
  24. data/app/assets/images/solidus_braintree/venmo/venmo_blue_logo.svg +18 -0
  25. data/app/assets/images/solidus_braintree/venmo/venmo_white_acceptance_mark.svg +20 -0
  26. data/app/assets/images/solidus_braintree/venmo/venmo_white_button_280x48.svg +19 -0
  27. data/app/assets/images/solidus_braintree/venmo/venmo_white_button_320x48.svg +19 -0
  28. data/app/assets/images/solidus_braintree/venmo/venmo_white_button_375x48.svg +19 -0
  29. data/app/assets/images/solidus_braintree/venmo/venmo_white_logo.svg +18 -0
  30. data/app/assets/javascripts/solidus_braintree/apple_pay_button.js +179 -0
  31. data/app/assets/javascripts/solidus_braintree/checkout.js +108 -0
  32. data/app/assets/javascripts/solidus_braintree/client.js +239 -0
  33. data/app/assets/javascripts/solidus_braintree/constants.js +89 -0
  34. data/app/assets/javascripts/solidus_braintree/frontend.js +14 -0
  35. data/app/assets/javascripts/solidus_braintree/hosted_form.js +46 -0
  36. data/app/assets/javascripts/solidus_braintree/paypal_button.js +178 -0
  37. data/app/assets/javascripts/solidus_braintree/paypal_messaging.js +22 -0
  38. data/app/assets/javascripts/solidus_braintree/promise.js +20 -0
  39. data/app/assets/javascripts/solidus_braintree/venmo_button.js +86 -0
  40. data/app/assets/javascripts/spree/backend/solidus_braintree.js +96 -0
  41. data/app/assets/javascripts/spree/frontend/paypal_button.js +34 -0
  42. data/app/assets/javascripts/spree/frontend/solidus_braintree.js +1 -0
  43. data/app/assets/stylesheets/spree/backend/solidus_braintree.scss +28 -0
  44. data/app/assets/stylesheets/spree/frontend/solidus_braintree.scss +51 -0
  45. data/app/decorators/controllers/solidus_braintree/admin_payments_controller_decorator.rb +11 -0
  46. data/app/decorators/controllers/solidus_braintree/checkout_controller_decorator.rb +11 -0
  47. data/app/decorators/controllers/solidus_braintree/client_tokens_controller.rb +41 -0
  48. data/app/decorators/controllers/solidus_braintree/orders_controller_decorator.rb +11 -0
  49. data/app/decorators/models/solidus_braintree/spree/store_decorator.rb +20 -0
  50. data/app/decorators/models/solidus_braintree/spree/user_decorator.rb +13 -0
  51. data/app/helpers/solidus_braintree/braintree_admin_helper.rb +23 -0
  52. data/app/helpers/solidus_braintree/braintree_checkout_helper.rb +60 -0
  53. data/app/models/application_record.rb +5 -0
  54. data/app/models/solidus_braintree/address.rb +64 -0
  55. data/app/models/solidus_braintree/avs_result.rb +69 -0
  56. data/app/models/solidus_braintree/configuration.rb +39 -0
  57. data/app/models/solidus_braintree/customer.rb +8 -0
  58. data/app/models/solidus_braintree/gateway.rb +433 -0
  59. data/app/models/solidus_braintree/response.rb +80 -0
  60. data/app/models/solidus_braintree/source.rb +135 -0
  61. data/app/models/solidus_braintree/transaction.rb +31 -0
  62. data/app/models/solidus_braintree/transaction_address.rb +88 -0
  63. data/app/models/solidus_braintree/transaction_import.rb +98 -0
  64. data/app/overrides/spree/payments/payment/add_paypal_funding_source_to_payment.rb +9 -0
  65. data/app/views/spree/api/payments/source_views/_braintree.json.jbuilder +1 -1
  66. data/app/views/spree/checkout/existing_payment/_braintree.html.erb +10 -0
  67. data/app/views/spree/shared/_apple_pay_button.html.erb +27 -0
  68. data/app/views/spree/shared/_braintree_errors.html.erb +16 -0
  69. data/app/views/spree/shared/_braintree_head_scripts.html.erb +26 -0
  70. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +43 -0
  71. data/app/views/spree/shared/_paypal_cart_button.html.erb +38 -0
  72. data/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
  73. data/app/views/spree/shared/_venmo_button.html.erb +33 -0
  74. data/bin/console +4 -1
  75. data/bin/rails +5 -5
  76. data/bin/rails-engine +13 -0
  77. data/bin/rails-sandbox +16 -0
  78. data/bin/rake +7 -0
  79. data/bin/sandbox +103 -0
  80. data/bin/setup +5 -4
  81. data/config/locales/en.yml +94 -2
  82. data/config/locales/it.yml +56 -0
  83. data/config/routes.rb +12 -3
  84. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  85. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +13 -0
  86. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  87. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +7 -0
  88. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  89. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +38 -0
  90. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +14 -0
  91. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
  92. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
  93. data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +5 -0
  94. data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +5 -0
  95. data/db/migrate/20230109080950_rename_solidus_paypal_braintree_source_type.rb +31 -0
  96. data/lib/controllers/backend/solidus_braintree/configurations_controller.rb +48 -0
  97. data/lib/controllers/frontend/solidus_braintree/checkouts_controller.rb +31 -0
  98. data/lib/controllers/frontend/solidus_braintree/transactions_controller.rb +67 -0
  99. data/lib/generators/solidus_braintree/install/install_generator.rb +54 -18
  100. data/lib/generators/solidus_braintree/install/templates/initializer.rb +6 -0
  101. data/lib/solidus_braintree/country_mapper.rb +37 -0
  102. data/lib/solidus_braintree/engine.rb +55 -10
  103. data/lib/solidus_braintree/extension_configuration.rb +23 -0
  104. data/lib/solidus_braintree/request_protection.rb +21 -0
  105. data/lib/solidus_braintree/testing_support/factories.rb +53 -0
  106. data/lib/solidus_braintree/version.rb +3 -1
  107. data/lib/solidus_braintree.rb +14 -2
  108. data/lib/solidus_paypal_braintree.rb +6 -0
  109. data/lib/views/backend/solidus_braintree/configurations/list.html.erb +63 -0
  110. data/lib/views/backend/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  111. data/lib/views/backend/spree/admin/payments/source_views/_braintree.html.erb +39 -0
  112. data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +13 -0
  113. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  114. data/lib/views/backend_v2.4/spree/admin/shared/preference_fields/_hash.html.erb +12 -0
  115. data/lib/views/frontend/solidus_braintree/payments/_payment.html.erb +12 -0
  116. data/lib/views/frontend/spree/checkout/payment/_braintree.html.erb +23 -0
  117. data/lib/views/frontend/spree/shared/_paypal_checkout_button.html.erb +32 -0
  118. data/solidus_braintree.gemspec +39 -38
  119. data/spec/controllers/solidus_braintree/checkouts_controller_spec.rb +99 -0
  120. data/spec/controllers/solidus_braintree/client_tokens_controller_spec.rb +55 -0
  121. data/spec/controllers/solidus_braintree/configurations_controller_spec.rb +73 -0
  122. data/spec/controllers/solidus_braintree/transactions_controller_spec.rb +183 -0
  123. data/spec/features/backend/configuration_spec.rb +23 -0
  124. data/spec/features/backend/new_payment_spec.rb +137 -0
  125. data/spec/features/frontend/braintree_credit_card_checkout_spec.rb +191 -0
  126. data/spec/features/frontend/paypal_checkout_spec.rb +166 -0
  127. data/spec/features/frontend/venmo_checkout_spec.rb +194 -0
  128. data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
  129. data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
  130. data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
  131. data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
  132. data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
  133. data/spec/fixtures/cassettes/braintree/token.yml +63 -0
  134. data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
  135. data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
  136. data/spec/fixtures/cassettes/checkout/update.yml +71 -0
  137. data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +171 -0
  138. data/spec/fixtures/cassettes/checkout/valid_venmo_transaction.yml +599 -0
  139. data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
  140. data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
  141. data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
  142. data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
  143. data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
  144. data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
  145. data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
  146. data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
  147. data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
  148. data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
  149. data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
  150. data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
  151. data/spec/fixtures/cassettes/gateway/customer.yml +79 -0
  152. data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
  153. data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
  154. data/spec/fixtures/cassettes/gateway/void.yml +137 -0
  155. data/spec/fixtures/cassettes/source/bin.yml +295 -0
  156. data/spec/fixtures/cassettes/source/card_type.yml +267 -0
  157. data/spec/fixtures/cassettes/source/last4.yml +267 -0
  158. data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
  159. data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
  160. data/spec/fixtures/views/spree/orders/edit.html.erb +50 -0
  161. data/spec/helpers/solidus_braintree/braintree_admin_helper_spec.rb +17 -0
  162. data/spec/helpers/solidus_braintree/braintree_checkout_helper_spec.rb +70 -0
  163. data/spec/models/solidus_braintree/address_spec.rb +71 -0
  164. data/spec/models/solidus_braintree/avs_result_spec.rb +317 -0
  165. data/spec/models/solidus_braintree/gateway_spec.rb +742 -0
  166. data/spec/models/solidus_braintree/response_spec.rb +280 -0
  167. data/spec/models/solidus_braintree/source_spec.rb +539 -0
  168. data/spec/models/solidus_braintree/transaction_address_spec.rb +235 -0
  169. data/spec/models/solidus_braintree/transaction_import_spec.rb +302 -0
  170. data/spec/models/solidus_braintree/transaction_spec.rb +86 -0
  171. data/spec/models/spree/store_spec.rb +14 -0
  172. data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
  173. data/spec/spec_helper.rb +32 -0
  174. data/spec/support/capybara.rb +7 -0
  175. data/spec/support/gateway_helpers.rb +29 -0
  176. data/spec/support/order_ready_for_payment.rb +37 -0
  177. data/spec/support/vcr.rb +42 -0
  178. data/spec/support/views.rb +1 -0
  179. metadata +276 -224
  180. data/LICENSE.txt +0 -21
  181. data/app/controllers/spree/api/braintree_client_token_controller.rb +0 -13
  182. data/app/decorators/lib/solidus_braintree/spree/permitted_attributes_decorator.rb +0 -9
  183. data/app/decorators/models/solidus_braintree/spree/credit_card_decorator.rb +0 -11
  184. data/app/decorators/models/solidus_braintree/spree/payment_decorator.rb +0 -10
  185. data/app/helpers/braintree_view_helpers.rb +0 -20
  186. data/app/models/concerns/solidus_braintree/add_name_validation_concern.rb +0 -8
  187. data/app/models/concerns/solidus_braintree/inject_device_data_concern.rb +0 -18
  188. data/app/models/concerns/solidus_braintree/payment_braintree_nonce_concern.rb +0 -8
  189. data/app/models/concerns/solidus_braintree/permitted_attributes_concern.rb +0 -11
  190. data/app/models/concerns/solidus_braintree/skip_require_card_numbers_concern.rb +0 -14
  191. data/app/models/concerns/solidus_braintree/use_data_field_concern.rb +0 -23
  192. data/app/models/solidus/gateway/braintree_gateway.rb +0 -306
  193. data/app/overrides/spree/checkout/_confirm/braintree_security.html.erb.deface +0 -9
  194. data/app/views/spree/admin/payments/source_forms/_braintree.html.erb +0 -38
  195. data/app/views/spree/admin/payments/source_views/_braintree.html.erb +0 -30
  196. data/app/views/spree/checkout/payment/_braintree.html.erb +0 -55
  197. data/app/views/spree/checkout/payment/_braintree_initialization.html.erb +0 -12
  198. data/config/initializers/braintree.rb +0 -3
  199. data/db/migrate/20150910170527_add_data_to_credit_card.rb +0 -5
  200. data/db/migrate/20160426221931_add_braintree_device_data_to_order.rb +0 -5
  201. data/lib/assets/javascripts/spree/backend/braintree/solidus_braintree.js +0 -59
  202. data/lib/assets/javascripts/spree/frontend/braintree/solidus_braintree.js +0 -144
  203. data/lib/assets/javascripts/vendor/braintree.js +0 -8
  204. data/lib/assets/stylesheets/spree/frontend/solidus_braintree.scss +0 -26
@@ -0,0 +1,433 @@
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.paypal?
368
+ params[:shipping] = braintree_shipping_address(options)
369
+ end
370
+
371
+ if source.credit_card?
372
+ params[:billing] = braintree_billing_address(options)
373
+ end
374
+
375
+ if source.customer.present?
376
+ params[:customer_id] = source.customer.braintree_customer_id
377
+ end
378
+
379
+ params
380
+ end
381
+
382
+ def braintree_shipping_address(options)
383
+ braintree_address_attributes(options[:shipping_address])
384
+ end
385
+
386
+ def braintree_billing_address(options)
387
+ braintree_address_attributes(options[:billing_address])
388
+ end
389
+
390
+ def braintree_address_attributes(address)
391
+ first, last = address[:name].split(" ", 2)
392
+ {
393
+ first_name: first,
394
+ last_name: last,
395
+ street_address: [address[:address1], address[:address2]].compact.join(" "),
396
+ locality: address[:city],
397
+ postal_code: address[:zip],
398
+ region: address[:state],
399
+ country_code_alpha2: address[:country]
400
+ }
401
+ end
402
+
403
+ def merchant_account_for(_source, options)
404
+ return unless options[:currency]
405
+
406
+ preferred_merchant_currency_map[options[:currency]]
407
+ end
408
+
409
+ def paypal_payee_email_for(source, options)
410
+ return unless source.paypal?
411
+
412
+ preferred_paypal_payee_email_map[options[:currency]]
413
+ end
414
+
415
+ def customer_profile_params(payment)
416
+ params = {}
417
+
418
+ params[:email] = payment&.order&.email
419
+
420
+ if store_in_vault && payment.source.try(:nonce)
421
+ params[:payment_method_nonce] = payment.source.nonce
422
+ end
423
+
424
+ params
425
+ end
426
+
427
+ # override with the Venmo business profile that you want to use for transactions,
428
+ # or leave it to be nil if want Braintree to use your default account
429
+ def venmo_business_profile_id
430
+ nil
431
+ end
432
+ end
433
+ 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, 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
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'solidus_braintree/request_protection'
4
+
5
+ module SolidusBraintree
6
+ class Source < ::Spree::PaymentSource
7
+ include RequestProtection
8
+
9
+ PAYPAL = "PayPalAccount"
10
+ APPLE_PAY = "ApplePayCard"
11
+ VENMO = "VenmoAccount"
12
+ CREDIT_CARD = "CreditCard"
13
+
14
+ enum paypal_funding_source: {
15
+ applepay: 0, bancontact: 1, blik: 2, boleto: 3, card: 4, credit: 5, eps: 6, giropay: 7, ideal: 8,
16
+ itau: 9, maxima: 10, mercadopago: 11, mybank: 12, oxxo: 13, p24: 14, paylater: 15, paypal: 16, payu: 17,
17
+ sepa: 18, sofort: 19, trustly: 20, venmo: 21, verkkopankki: 22, wechatpay: 23, zimpler: 24
18
+ }, _suffix: :funding
19
+
20
+ belongs_to :user, class_name: ::Spree::UserClassHandle.new, optional: true
21
+ belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
22
+ has_many :payments, as: :source, class_name: "Spree::Payment", dependent: :destroy
23
+
24
+ belongs_to :customer, class_name: "SolidusBraintree::Customer", optional: true
25
+
26
+ validates :payment_type, inclusion: [PAYPAL, APPLE_PAY, VENMO, CREDIT_CARD]
27
+
28
+ before_save :clear_paypal_funding_source, unless: :paypal?
29
+
30
+ scope(:with_payment_profile, -> { joins(:customer) })
31
+ scope(:credit_card, -> { where(payment_type: CREDIT_CARD) })
32
+
33
+ delegate :bin, :last_4, :card_type, :expiration_month, :expiration_year, :email,
34
+ :username, :source_description, to: :braintree_payment_method, allow_nil: true
35
+
36
+ # Aliases to match Spree::CreditCard's interface
37
+ alias_method :last_digits, :last_4
38
+ alias_method :month, :expiration_month
39
+ alias_method :year, :expiration_year
40
+ alias_method :cc_type, :card_type
41
+
42
+ # we are not currenctly supporting an "imported" flag
43
+ def imported
44
+ false
45
+ end
46
+
47
+ def actions
48
+ %w[capture void credit]
49
+ end
50
+
51
+ def can_capture?(payment)
52
+ payment.pending? || payment.checkout?
53
+ end
54
+
55
+ def can_void?(payment)
56
+ return false unless payment.response_code
57
+
58
+ transaction = protected_request do
59
+ braintree_client.transaction.find(payment.response_code)
60
+ end
61
+ Gateway::VOIDABLE_STATUSES.include?(transaction.status)
62
+ rescue ActiveMerchant::ConnectionError
63
+ false
64
+ end
65
+
66
+ def can_credit?(payment)
67
+ payment.completed? && payment.credit_allowed > 0
68
+ end
69
+
70
+ def friendly_payment_type
71
+ I18n.t(payment_type.underscore, scope: "solidus_braintree.payment_type")
72
+ end
73
+
74
+ def apple_pay?
75
+ payment_type == APPLE_PAY
76
+ end
77
+
78
+ def paypal?
79
+ payment_type == PAYPAL
80
+ end
81
+
82
+ def venmo?
83
+ payment_type == VENMO
84
+ end
85
+
86
+ def reusable?
87
+ token.present?
88
+ end
89
+
90
+ def credit_card?
91
+ payment_type == CREDIT_CARD
92
+ end
93
+
94
+ def display_number
95
+ if paypal?
96
+ email
97
+ elsif venmo?
98
+ username
99
+ else
100
+ "XXXX-XXXX-XXXX-#{last_digits.to_s.rjust(4, 'X')}"
101
+ end
102
+ end
103
+
104
+ def display_paypal_funding_source
105
+ I18n.t(paypal_funding_source,
106
+ scope: 'solidus_braintree.paypal_funding_sources',
107
+ default: paypal_funding_source)
108
+ end
109
+
110
+ def display_payment_type
111
+ "#{I18n.t('solidus_braintree.payment_type.label')}: #{friendly_payment_type}"
112
+ end
113
+
114
+ private
115
+
116
+ def braintree_payment_method
117
+ return unless braintree_client
118
+
119
+ @braintree_payment_method ||= protected_request do
120
+ braintree_client.payment_method.find(token)
121
+ end
122
+ rescue ActiveMerchant::ConnectionError, ArgumentError => e
123
+ Rails.logger.warn("#{e}: token unknown or missing for #{inspect}")
124
+ nil
125
+ end
126
+
127
+ def braintree_client
128
+ @braintree_client ||= payment_method.try(:braintree)
129
+ end
130
+
131
+ def clear_paypal_funding_source
132
+ self.paypal_funding_source = nil
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module SolidusBraintree
6
+ class Transaction
7
+ include ActiveModel::Model
8
+
9
+ attr_accessor :nonce, :payment_method, :payment_type, :paypal_funding_source, :address, :email, :phone
10
+
11
+ validates :nonce, presence: true
12
+ validates :payment_method, presence: true
13
+ validates :payment_type, presence: true
14
+ validates :email, presence: true
15
+
16
+ validate do
17
+ unless payment_method.is_a? SolidusBraintree::Gateway
18
+ errors.add(:payment_method, 'Must be braintree')
19
+ end
20
+ if address&.invalid?
21
+ address.errors.each do |error|
22
+ errors.add(:address, error.full_message)
23
+ end
24
+ end
25
+ end
26
+
27
+ def address_attributes=(attributes)
28
+ self.address = TransactionAddress.new attributes
29
+ end
30
+ end
31
+ end