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,742 @@
1
+ require 'spec_helper'
2
+ require 'webmock'
3
+ require 'support/order_ready_for_payment'
4
+
5
+ RSpec.describe SolidusBraintree::Gateway do
6
+ let(:gateway) do
7
+ new_gateway.tap(&:save)
8
+ end
9
+
10
+ let(:braintree) { gateway.braintree }
11
+
12
+ let(:user) { create :user }
13
+
14
+ let(:source) do
15
+ SolidusBraintree::Source.create!(
16
+ nonce: 'fake-valid-nonce',
17
+ user: user,
18
+ payment_type: payment_type,
19
+ payment_method: gateway
20
+ )
21
+ end
22
+
23
+ let(:payment_type) { SolidusBraintree::Source::PAYPAL }
24
+
25
+ describe "saving preference hashes as strings" do
26
+ subject { gateway.update(update_params) }
27
+
28
+ context "with valid hash syntax" do
29
+ let(:update_params) do
30
+ {
31
+ preferred_merchant_currency_map: '{"EUR" => "test_merchant_account_id"}',
32
+ preferred_paypal_payee_email_map: '{"CAD" => "bruce+wayne@example.com"}'
33
+ }
34
+ end
35
+
36
+ it "successfully updates the preference" do
37
+ subject
38
+ expect(gateway.preferred_merchant_currency_map).to eq({ "EUR" => "test_merchant_account_id" })
39
+ expect(gateway.preferred_paypal_payee_email_map).to eq({ "CAD" => "bruce+wayne@example.com" })
40
+ end
41
+ end
42
+
43
+ context "with invalid user input" do
44
+ let(:update_params) do
45
+ { preferred_merchant_currency_map: '{this_is_not_a_valid_hash}' }
46
+ end
47
+
48
+ it "raise a JSON parser error" do
49
+ expect{ subject }.to raise_error(JSON::ParserError)
50
+ end
51
+ end
52
+ end
53
+
54
+ describe 'making a payment on an order', vcr: {
55
+ cassette_name: 'gateway/complete',
56
+ match_requests_on: [:braintree_uri]
57
+ } do
58
+ include_context 'when order is ready for payment'
59
+
60
+ before do
61
+ order.update(number: "ORDER0")
62
+ payment.update(number: "PAYMENT0")
63
+ end
64
+
65
+ let(:payment) do
66
+ order.payments.create!(
67
+ payment_method: gateway,
68
+ source: source,
69
+ amount: 55
70
+ )
71
+ end
72
+
73
+ it 'can complete an order' do
74
+ order.payments.reset
75
+
76
+ expect(order.total).to eq 55
77
+
78
+ expect(payment.capture_events.count).to eq 0
79
+
80
+ order.next!
81
+ expect(order.state).to eq "confirm"
82
+
83
+ order.complete!
84
+ expect(order.state).to eq "complete"
85
+
86
+ expect(order.outstanding_balance).to eq 0.0
87
+
88
+ expect(payment.capture_events.count).to eq 1
89
+ end
90
+ end
91
+
92
+ describe "instance methods" do
93
+ shared_examples "successful response" do
94
+ it 'returns a successful billing response', aggregate_failures: true do
95
+ expect(subject).to be_a ActiveMerchant::Billing::Response
96
+ expect(subject).to be_success
97
+ end
98
+ end
99
+
100
+ shared_examples "protects against connection errors" do
101
+ context 'when a timeout error happens' do
102
+ it 'raises ActiveMerchant::ConnectionError' do
103
+ expect_any_instance_of(Braintree::TransactionGateway).to receive(gateway_action) do
104
+ raise Braintree::BraintreeError
105
+ end
106
+
107
+ expect { subject }.to raise_error ActiveMerchant::ConnectionError
108
+ end
109
+ end
110
+ end
111
+
112
+ let(:authorized_id) do
113
+ braintree.transaction.sale(
114
+ amount: 40,
115
+ payment_method_nonce: source.nonce
116
+ ).transaction.id
117
+ end
118
+
119
+ let(:sale_id) do
120
+ braintree.transaction.sale(
121
+ amount: 40,
122
+ payment_method_nonce: source.nonce,
123
+ options: {
124
+ submit_for_settlement: true
125
+ }
126
+ ).transaction.id
127
+ end
128
+
129
+ let(:settled_id) do
130
+ braintree.testing.settle(sale_id).transaction.id
131
+ end
132
+
133
+ let(:currency) { 'USD' }
134
+
135
+ let(:gateway_options) do
136
+ {
137
+ currency: currency,
138
+ shipping_address: {
139
+ name: "Bruce Wayne",
140
+ address1: "42 Spruce Lane",
141
+ address2: "Apt 312",
142
+ city: "Gotham",
143
+ state: "CA",
144
+ zip: "90210",
145
+ country: "US"
146
+ },
147
+ billing_address: {
148
+ name: "Dick Grayson",
149
+ address1: "15 Robin Walk",
150
+ address2: "Apt 123",
151
+ city: "Blüdhaven",
152
+ state: "CA",
153
+ zip: "90210",
154
+ country: "US"
155
+ }
156
+ }
157
+ end
158
+
159
+ describe "#method_type" do
160
+ subject { gateway.method_type }
161
+
162
+ it { is_expected.to eq "braintree" }
163
+ end
164
+
165
+ describe '#gateway_options' do
166
+ subject(:gateway_options) { gateway.gateway_options }
167
+
168
+ it 'includes http_open_timeout' do
169
+ expect(subject).to have_key(:http_open_timeout)
170
+ expect(gateway_options[:http_open_timeout]).to eq(60)
171
+ end
172
+
173
+ it 'includes http_read_timeout' do
174
+ expect(subject).to have_key(:http_read_timeout)
175
+ expect(gateway_options[:http_read_timeout]).to eq(60)
176
+ end
177
+ end
178
+
179
+ describe '#purchase' do
180
+ subject(:purchase) { gateway.purchase(1000, source, gateway_options) }
181
+
182
+ context 'with successful purchase', vcr: {
183
+ cassette_name: 'gateway/purchase',
184
+ match_requests_on: [:braintree_uri]
185
+ } do
186
+ include_examples "successful response"
187
+
188
+ it 'submits the transaction for settlement', aggregate_failures: true do
189
+ expect(purchase.message).to eq 'submitted_for_settlement'
190
+ expect(purchase.authorization).to be_present
191
+ end
192
+ end
193
+
194
+ include_examples "protects against connection errors" do
195
+ let(:gateway_action) { :sale }
196
+ end
197
+ end
198
+
199
+ describe "#authorize" do
200
+ subject(:authorize) { gateway.authorize(1000, source, gateway_options) }
201
+
202
+ include_examples "protects against connection errors" do
203
+ let(:gateway_action) { :sale }
204
+ end
205
+
206
+ context 'with successful authorization', vcr: {
207
+ cassette_name: 'gateway/authorize',
208
+ match_requests_on: [:braintree_uri]
209
+ } do
210
+ include_examples "successful response"
211
+
212
+ it 'passes "Solidus" as the channel parameter in the request' do
213
+ expect_any_instance_of(Braintree::TransactionGateway).
214
+ to receive(:sale).
215
+ with(hash_including({ channel: "Solidus" })).and_call_original
216
+ authorize
217
+ end
218
+
219
+ it 'authorizes the transaction', aggregate_failures: true do
220
+ expect(authorize.message).to eq 'authorized'
221
+ expect(authorize.authorization).to be_present
222
+ end
223
+ end
224
+
225
+ context 'with different merchant account for currency', vcr: {
226
+ cassette_name: 'gateway/authorize/merchant_account/EUR',
227
+ match_requests_on: [:braintree_uri]
228
+ } do
229
+ let(:currency) { 'EUR' }
230
+
231
+ it 'settles with the correct currency' do
232
+ transaction = braintree.transaction.find(authorize.authorization)
233
+ expect(transaction.merchant_account_id).to eq 'stembolt_EUR'
234
+ end
235
+ end
236
+
237
+ context 'with different paypal payee email for currency', vcr: {
238
+ cassette_name: 'gateway/authorize/paypal/EUR',
239
+ match_requests_on: [:braintree_uri]
240
+ } do
241
+ let(:currency) { 'EUR' }
242
+
243
+ it 'uses the correct payee email' do
244
+ expect_any_instance_of(Braintree::TransactionGateway).
245
+ to receive(:sale).
246
+ with(hash_including({
247
+ options: {
248
+ store_in_vault_on_success: true,
249
+ paypal: {
250
+ payee_email: ENV.fetch('BRAINTREE_PAYPAL_PAYEE_EMAIL')
251
+ }
252
+ }
253
+ })).and_call_original
254
+ authorize
255
+ end
256
+
257
+ context "with PayPal transaction", vcr: {
258
+ cassette_name: 'gateway/authorize/paypal/address',
259
+ match_requests_on: [:braintree_uri]
260
+ } do
261
+ it 'includes the shipping address in the request' do
262
+ expect_any_instance_of(Braintree::TransactionGateway).
263
+ to receive(:sale).
264
+ with(hash_including({
265
+ shipping: {
266
+ first_name: "Bruce",
267
+ last_name: "Wayne",
268
+ street_address: "42 Spruce Lane Apt 312",
269
+ locality: "Gotham",
270
+ postal_code: "90210",
271
+ region: "CA",
272
+ country_code_alpha2: "US"
273
+ }
274
+ })).and_call_original
275
+ authorize
276
+ end
277
+ end
278
+ end
279
+
280
+ context "with CreditCard transaction", vcr: {
281
+ cassette_name: 'gateway/authorize/credit_card/address',
282
+ match_requests_on: [:braintree_uri]
283
+ } do
284
+ let(:payment_type) { SolidusBraintree::Source::CREDIT_CARD }
285
+
286
+ it 'includes the billing address in the request' do
287
+ expect_any_instance_of(Braintree::TransactionGateway).to receive(:sale).
288
+ with(hash_including({
289
+ billing: {
290
+ first_name: "Dick",
291
+ last_name: "Grayson",
292
+ street_address: "15 Robin Walk Apt 123",
293
+ locality: "Blüdhaven",
294
+ postal_code: "90210",
295
+ region: "CA",
296
+ country_code_alpha2: "US"
297
+ }
298
+ })).and_call_original
299
+ authorize
300
+ end
301
+ end
302
+ end
303
+
304
+ describe "#capture" do
305
+ subject(:capture) { gateway.capture(1000, authorized_id, {}) }
306
+
307
+ context 'with successful capture', vcr: {
308
+ cassette_name: 'gateway/capture',
309
+ match_requests_on: [:braintree_uri]
310
+ } do
311
+ include_examples "successful response"
312
+
313
+ it 'submits the transaction for settlement' do
314
+ expect(capture.message).to eq "submitted_for_settlement"
315
+ end
316
+ end
317
+
318
+ context 'with authorized transaction', vcr: {
319
+ cassette_name: 'gateway/authorized_transaction',
320
+ match_requests_on: [:braintree_uri]
321
+ } do
322
+ include_examples "protects against connection errors" do
323
+ let(:gateway_action) { :submit_for_settlement }
324
+ end
325
+ end
326
+ end
327
+
328
+ describe "#credit" do
329
+ subject(:credit) { gateway.credit(2000, source, settled_id, {}) }
330
+
331
+ context 'with successful credit', vcr: {
332
+ cassette_name: 'gateway/credit',
333
+ match_requests_on: [:braintree_uri]
334
+ } do
335
+ include_examples "successful response"
336
+
337
+ it 'credits the transaction' do
338
+ expect(credit.message).to eq 'submitted_for_settlement'
339
+ end
340
+ end
341
+
342
+ context 'with settled transaction', vcr: {
343
+ cassette_name: 'gateway/settled_transaction',
344
+ match_requests_on: [:braintree_uri]
345
+ } do
346
+ include_examples "protects against connection errors" do
347
+ let(:gateway_action) { :refund }
348
+ end
349
+ end
350
+ end
351
+
352
+ describe "#void" do
353
+ subject(:void) { gateway.void(authorized_id, source, {}) }
354
+
355
+ context 'when successfully voided', vcr: {
356
+ cassette_name: 'gateway/void',
357
+ match_requests_on: [:braintree_uri]
358
+ } do
359
+ include_examples "successful response"
360
+
361
+ it 'voids the transaction' do
362
+ expect(void.message).to eq 'voided'
363
+ end
364
+ end
365
+
366
+ context 'with authorized transaction', vcr: {
367
+ cassette_name: 'gateway/authorized_transaction',
368
+ match_requests_on: [:braintree_uri]
369
+ } do
370
+ include_examples "protects against connection errors" do
371
+ let(:gateway_action) { :void }
372
+ end
373
+ end
374
+ end
375
+
376
+ describe "#cancel", vcr: {
377
+ cassette_name: 'gateway/cancel',
378
+ match_requests_on: [:braintree_uri]
379
+ } do
380
+ subject(:cancel) { gateway.cancel(transaction_id) }
381
+
382
+ let(:transaction_id) { "fake_transaction_id" }
383
+
384
+ context "when the transaction is found" do
385
+ context "when it is voidable", vcr: {
386
+ cassette_name: 'gateway/cancel/void',
387
+ match_requests_on: [:braintree_uri]
388
+ } do
389
+ let(:transaction_id) { authorized_id }
390
+
391
+ include_examples "successful response"
392
+
393
+ it 'voids the transaction' do
394
+ expect(cancel.message).to eq 'voided'
395
+ end
396
+ end
397
+
398
+ context "when it is not voidable", vcr: {
399
+ cassette_name: 'gateway/cancel/refunds',
400
+ match_requests_on: [:braintree_uri]
401
+ } do
402
+ let(:transaction_id) { settled_id }
403
+
404
+ include_examples "successful response"
405
+
406
+ it 'refunds the transaction' do
407
+ expect(cancel.message).to eq 'submitted_for_settlement'
408
+ end
409
+ end
410
+ end
411
+
412
+ context "when the transaction is not found", vcr: {
413
+ cassette_name: 'gateway/cancel/missing',
414
+ match_requests_on: [:braintree_uri]
415
+ } do
416
+ it 'raises an error' do
417
+ expect{ cancel }.to raise_error ActiveMerchant::ConnectionError
418
+ end
419
+ end
420
+ end
421
+
422
+ describe '#try_void' do
423
+ subject { gateway.try_void(instance_double(Spree::Payment, response_code: source.token)) }
424
+
425
+ let(:transaction_request) do
426
+ class_double(Braintree::Transaction,
427
+ find: transaction_response)
428
+ end
429
+
430
+ before do
431
+ client = instance_double(Braintree::Gateway)
432
+ allow(client).to receive(:transaction) { transaction_request }
433
+ allow(gateway).to receive(:braintree) { client }
434
+ end
435
+
436
+ context 'with voidable payment' do
437
+ let(:transaction_response) do
438
+ instance_double(Braintree::Transaction,
439
+ status: Braintree::Transaction::Status::Authorized)
440
+ end
441
+
442
+ it 'voids the payment' do
443
+ expect(gateway).to receive(:void)
444
+ subject
445
+ end
446
+
447
+ context 'with error response mentioning an unvoidable transaction' do
448
+ before do
449
+ allow(gateway).to receive(:void) do
450
+ raise ActiveMerchant::ConnectionError.new(
451
+ 'Transaction can only be voided if status is authorized',
452
+ double
453
+ )
454
+ end
455
+ end
456
+
457
+ it { is_expected.to be(false) }
458
+ end
459
+
460
+ context 'with other error response' do
461
+ before do
462
+ allow(gateway).to receive(:void) do
463
+ raise ActiveMerchant::ConnectionError.new(
464
+ 'Server unreachable',
465
+ double
466
+ )
467
+ end
468
+ end
469
+
470
+ it { expect { subject }.to raise_error ActiveMerchant::ConnectionError }
471
+ end
472
+ end
473
+
474
+ context 'with voidable paypal payment' do
475
+ let(:transaction_response) do
476
+ instance_double(Braintree::Transaction,
477
+ status: Braintree::Transaction::Status::SettlementPending)
478
+ end
479
+
480
+ it 'voids the payment' do
481
+ expect(gateway).to receive(:void)
482
+ subject
483
+ end
484
+ end
485
+
486
+ context 'with non-voidable payment' do
487
+ let(:transaction_response) do
488
+ instance_double(Braintree::Transaction,
489
+ status: Braintree::Transaction::Status::Settled)
490
+ end
491
+
492
+ it { is_expected.to be(false) }
493
+ end
494
+ end
495
+
496
+ describe "#create_profile" do
497
+ subject(:profile) { gateway.create_profile(payment) }
498
+
499
+ let(:payment) do
500
+ build(:payment, {
501
+ payment_method: gateway,
502
+ source: source
503
+ })
504
+ end
505
+
506
+ cassette_options = {
507
+ cassette_name: "braintree/create_profile",
508
+ match_requests_on: [:braintree_uri]
509
+ }
510
+ context "with no existing customer profile", vcr: cassette_options do
511
+ it 'creates and returns a new customer profile', aggregate_failures: true do
512
+ expect(profile).to be_a SolidusBraintree::Customer
513
+ expect(profile.sources).to eq [source]
514
+ expect(profile.braintree_customer_id).to be_present
515
+ end
516
+
517
+ it "sets a token on the payment source" do
518
+ expect{ subject }.to change(source, :token)
519
+ end
520
+ end
521
+
522
+ context "when the source already has a token" do
523
+ before { source.token = "totally-a-valid-token" }
524
+
525
+ it "does not create a new customer profile" do
526
+ expect(profile).to be_nil
527
+ end
528
+ end
529
+
530
+ context "when the source already has a customer" do
531
+ before { source.build_customer }
532
+
533
+ it "does not create a new customer profile" do
534
+ expect(profile).to be_nil
535
+ end
536
+ end
537
+
538
+ context "when the source has no nonce" do
539
+ before { source.nonce = nil }
540
+
541
+ it "does not create a new customer profile" do
542
+ expect(profile).to be_nil
543
+ end
544
+ end
545
+ end
546
+
547
+ describe '#customer_profile_params' do
548
+ subject(:params) { gateway.send(:customer_profile_params, payment) }
549
+
550
+ let(:payment) do
551
+ build(:payment, {
552
+ payment_method: gateway,
553
+ source: source
554
+ })
555
+ end
556
+
557
+ context 'when payment does not belong to an order' do
558
+ before { allow(payment).to receive(:order).and_return(nil) }
559
+
560
+ it 'has the email param as nil' do
561
+ expect(subject[:email]).to be_nil
562
+ end
563
+ end
564
+
565
+ context 'when payment belongs to an order' do
566
+ it 'has no email param' do
567
+ expect(subject[:email]).to eq(payment.order.email)
568
+ end
569
+ end
570
+ end
571
+
572
+ describe "Braintree Customer" do
573
+ subject(:customer) { braintree.customer.create(params).customer }
574
+
575
+ let(:params) { gateway.send(:customer_profile_params, payment) }
576
+
577
+ let(:payment) do
578
+ build(:payment, {
579
+ payment_method: gateway,
580
+ source: source
581
+ })
582
+ end
583
+
584
+ cassette_options = {
585
+ cassette_name: 'gateway/customer',
586
+ match_requests_on: [:braintree_uri]
587
+ }
588
+
589
+ context "with customer", vcr: cassette_options do
590
+ it 'saves the customer email correctly' do
591
+ allow(payment.order).to receive(:email).and_return('braintree@customers.com')
592
+ expect(subject.email).to eq(payment.order.email)
593
+ end
594
+ end
595
+ end
596
+
597
+ shared_examples "sources_by_order" do
598
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
599
+ let(:gateway) { new_gateway.tap(&:save!) }
600
+
601
+ let(:other_payment_method) { FactoryBot.create(:payment_method) }
602
+
603
+ let(:source_without_profile) do
604
+ SolidusBraintree::Source.create!(
605
+ payment_method_id: gateway.id,
606
+ payment_type: payment_type,
607
+ user_id: user.id
608
+ )
609
+ end
610
+
611
+ let(:source_with_profile) do
612
+ SolidusBraintree::Source.create!(
613
+ payment_method_id: gateway.id,
614
+ payment_type: payment_type,
615
+ user_id: user.id
616
+ ).tap do |source|
617
+ source.create_customer!(user: user)
618
+ source.save!
619
+ end
620
+ end
621
+
622
+ before do
623
+ create(:payment, order: order, payment_method_id: payment_method_id, source: source)
624
+ end
625
+
626
+ context "when the order has payments with the braintree payment method" do
627
+ let(:payment_method_id) { gateway.id }
628
+
629
+ context "when the payment has a saved source with a profile" do
630
+ let(:source) { source_with_profile }
631
+
632
+ it "returns the source" do
633
+ expect(subject.to_a).to eql([source])
634
+ end
635
+ end
636
+
637
+ context "when the payment has a saved source without a profile" do
638
+ let(:source) { source_without_profile }
639
+
640
+ it "returns no result" do
641
+ expect(subject.to_a).to eql([])
642
+ end
643
+ end
644
+ end
645
+
646
+ context "when the order has no payments with the braintree payment method" do
647
+ let(:payment_method_id) { other_payment_method.id }
648
+ let(:source) { FactoryBot.create :credit_card }
649
+
650
+ it "returns no results" do
651
+ expect(subject.to_a).to eql([])
652
+ end
653
+ end
654
+ end
655
+
656
+ describe "#sources_by_order" do
657
+ subject { gateway.sources_by_order(order) }
658
+
659
+ let(:gateway) { new_gateway.tap(&:save!) }
660
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
661
+
662
+ include_examples "sources_by_order"
663
+ end
664
+
665
+ describe "#reusable_sources" do
666
+ subject { gateway.reusable_sources(order) }
667
+
668
+ let(:order) { FactoryBot.build :order, user: user }
669
+ let(:gateway) { new_gateway.tap(&:save!) }
670
+
671
+ context "when an order is completed" do
672
+ include_examples "sources_by_order"
673
+ end
674
+
675
+ context "when an order is not completed" do
676
+ context "when the order has a user id" do
677
+ let(:user) { FactoryBot.create(:user) }
678
+
679
+ let!(:source_without_profile) do
680
+ SolidusBraintree::Source.create!(
681
+ payment_method_id: gateway.id,
682
+ payment_type: payment_type,
683
+ user_id: user.id
684
+ )
685
+ end
686
+
687
+ let!(:source_with_profile) do
688
+ SolidusBraintree::Source.create!(
689
+ payment_method_id: gateway.id,
690
+ payment_type: payment_type,
691
+ user_id: user.id
692
+ ).tap do |source|
693
+ source.create_customer!(user: user)
694
+ source.save!
695
+ end
696
+ end
697
+
698
+ it "includes saved sources with payment profiles" do
699
+ expect(subject).to include(source_with_profile)
700
+ end
701
+
702
+ it "excludes saved sources without payment profiles" do
703
+ expect(subject).not_to include(source_without_profile)
704
+ end
705
+ end
706
+
707
+ context "when the order does not have a user" do
708
+ let(:user) { nil }
709
+
710
+ it "returns no sources for guest users" do
711
+ expect(subject).to eql([])
712
+ end
713
+ end
714
+ end
715
+ end
716
+ end
717
+
718
+ describe '.generate_token' do
719
+ subject do
720
+ # dont VCR ignore generate token request, use the existing cassette
721
+ allow(VCR.request_ignorer.hooks).to receive(:[]).with(:ignore_request).and_return([])
722
+ gateway.generate_token
723
+ end
724
+
725
+ context 'with connection enabled', vcr: {
726
+ cassette_name: 'braintree/generate_token',
727
+ match_requests_on: [:braintree_uri]
728
+ } do
729
+ it { is_expected.to be_a(String).and be_present }
730
+ end
731
+
732
+ context 'when token generation is disabled' do
733
+ let(:gateway) do
734
+ gateway = described_class.create!(name: 'braintree')
735
+ gateway.preferred_token_generation_enabled = false
736
+ gateway
737
+ end
738
+
739
+ it { expect { subject }.to raise_error SolidusBraintree::Gateway::TokenGenerationDisabledError }
740
+ end
741
+ end
742
+ end