solidus_paypal_braintree 1.1.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -16
  3. data/.github/stale.yml +1 -17
  4. data/.github_changelog_generator +2 -0
  5. data/.gitignore +4 -3
  6. data/.rubocop.yml +1 -2
  7. data/CHANGELOG.md +41 -0
  8. data/Gemfile +18 -20
  9. data/README.md +50 -17
  10. data/bin/rails +4 -12
  11. data/bin/rails-engine +13 -0
  12. data/bin/rails-sandbox +16 -0
  13. data/bin/rake +7 -0
  14. data/bin/sandbox +103 -0
  15. data/bin/setup +1 -1
  16. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +2 -31
  17. data/lib/solidus_paypal_braintree/engine.rb +1 -56
  18. data/lib/solidus_paypal_braintree/version.rb +1 -1
  19. data/lib/solidus_paypal_braintree.rb +1 -4
  20. data/solidus_paypal_braintree.gemspec +41 -39
  21. data/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb +99 -0
  22. data/spec/controllers/solidus_paypal_braintree/client_tokens_controller_spec.rb +55 -0
  23. data/spec/controllers/solidus_paypal_braintree/configurations_controller_spec.rb +73 -0
  24. data/spec/controllers/solidus_paypal_braintree/transactions_controller_spec.rb +183 -0
  25. data/spec/features/backend/configuration_spec.rb +23 -0
  26. data/spec/features/backend/new_payment_spec.rb +137 -0
  27. data/spec/features/frontend/braintree_credit_card_checkout_spec.rb +191 -0
  28. data/spec/features/frontend/paypal_checkout_spec.rb +166 -0
  29. data/spec/features/frontend/venmo_checkout_spec.rb +194 -0
  30. data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
  31. data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
  32. data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
  33. data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
  34. data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
  35. data/spec/fixtures/cassettes/braintree/token.yml +63 -0
  36. data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
  37. data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
  38. data/spec/fixtures/cassettes/checkout/update.yml +71 -0
  39. data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +171 -0
  40. data/spec/fixtures/cassettes/checkout/valid_venmo_transaction.yml +599 -0
  41. data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
  42. data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
  43. data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
  44. data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
  45. data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
  46. data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
  47. data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
  48. data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
  49. data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
  50. data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
  51. data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
  52. data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
  53. data/spec/fixtures/cassettes/gateway/customer.yml +79 -0
  54. data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
  55. data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
  56. data/spec/fixtures/cassettes/gateway/void.yml +137 -0
  57. data/spec/fixtures/cassettes/source/bin.yml +295 -0
  58. data/spec/fixtures/cassettes/source/card_type.yml +267 -0
  59. data/spec/fixtures/cassettes/source/last4.yml +267 -0
  60. data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
  61. data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
  62. data/spec/fixtures/views/spree/orders/edit.html.erb +50 -0
  63. data/spec/helpers/solidus_paypal_braintree/braintree_admin_helper_spec.rb +17 -0
  64. data/spec/helpers/solidus_paypal_braintree/braintree_checkout_helper_spec.rb +70 -0
  65. data/spec/models/solidus_paypal_braintree/address_spec.rb +71 -0
  66. data/spec/models/solidus_paypal_braintree/avs_result_spec.rb +317 -0
  67. data/spec/models/solidus_paypal_braintree/gateway_spec.rb +742 -0
  68. data/spec/models/solidus_paypal_braintree/response_spec.rb +280 -0
  69. data/spec/models/solidus_paypal_braintree/source_spec.rb +539 -0
  70. data/spec/models/solidus_paypal_braintree/transaction_address_spec.rb +235 -0
  71. data/spec/models/solidus_paypal_braintree/transaction_import_spec.rb +302 -0
  72. data/spec/models/solidus_paypal_braintree/transaction_spec.rb +86 -0
  73. data/spec/models/spree/store_spec.rb +14 -0
  74. data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
  75. data/spec/spec_helper.rb +32 -0
  76. data/spec/support/capybara.rb +7 -0
  77. data/spec/support/gateway_helpers.rb +29 -0
  78. data/spec/support/order_ready_for_payment.rb +37 -0
  79. data/spec/support/vcr.rb +42 -0
  80. data/spec/support/views.rb +1 -0
  81. metadata +144 -178
  82. data/app/assets/config/solidus_paypal_braintree_manifest.js +0 -1
  83. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_280x48.svg +0 -19
  84. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_320x48.svg +0 -19
  85. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_blue_button_375x48.svg +0 -19
  86. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_280x48.svg +0 -19
  87. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_320x48.svg +0 -19
  88. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_active_white_button_375x48.svg +0 -19
  89. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_acceptance_mark.svg +0 -15
  90. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_280x48.svg +0 -19
  91. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_320x48.svg +0 -19
  92. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_button_375x48.svg +0 -19
  93. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_blue_logo.svg +0 -18
  94. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_acceptance_mark.svg +0 -20
  95. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_280x48.svg +0 -19
  96. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_320x48.svg +0 -19
  97. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_button_375x48.svg +0 -19
  98. data/app/assets/images/solidus_paypal_braintree/venmo/venmo_white_logo.svg +0 -18
  99. data/app/assets/javascripts/solidus_paypal_braintree/apple_pay_button.js +0 -179
  100. data/app/assets/javascripts/solidus_paypal_braintree/checkout.js +0 -108
  101. data/app/assets/javascripts/solidus_paypal_braintree/client.js +0 -239
  102. data/app/assets/javascripts/solidus_paypal_braintree/constants.js +0 -89
  103. data/app/assets/javascripts/solidus_paypal_braintree/frontend.js +0 -14
  104. data/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js +0 -46
  105. data/app/assets/javascripts/solidus_paypal_braintree/paypal_button.js +0 -178
  106. data/app/assets/javascripts/solidus_paypal_braintree/paypal_messaging.js +0 -22
  107. data/app/assets/javascripts/solidus_paypal_braintree/promise.js +0 -20
  108. data/app/assets/javascripts/solidus_paypal_braintree/venmo_button.js +0 -86
  109. data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +0 -96
  110. data/app/assets/javascripts/spree/frontend/paypal_button.js +0 -34
  111. data/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +0 -1
  112. data/app/assets/stylesheets/spree/backend/solidus_paypal_braintree.scss +0 -28
  113. data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +0 -51
  114. data/app/decorators/controllers/solidus_paypal_braintree/admin_payments_controller_decorator.rb +0 -11
  115. data/app/decorators/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb +0 -11
  116. data/app/decorators/controllers/solidus_paypal_braintree/client_tokens_controller.rb +0 -41
  117. data/app/decorators/controllers/solidus_paypal_braintree/orders_controller_decorator.rb +0 -11
  118. data/app/decorators/models/solidus_paypal_braintree/spree/store_decorator.rb +0 -20
  119. data/app/decorators/models/solidus_paypal_braintree/spree/user_decorator.rb +0 -13
  120. data/app/helpers/solidus_paypal_braintree/braintree_admin_helper.rb +0 -23
  121. data/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb +0 -60
  122. data/app/models/application_record.rb +0 -5
  123. data/app/models/solidus_paypal_braintree/address.rb +0 -64
  124. data/app/models/solidus_paypal_braintree/avs_result.rb +0 -69
  125. data/app/models/solidus_paypal_braintree/configuration.rb +0 -41
  126. data/app/models/solidus_paypal_braintree/customer.rb +0 -8
  127. data/app/models/solidus_paypal_braintree/gateway.rb +0 -432
  128. data/app/models/solidus_paypal_braintree/response.rb +0 -80
  129. data/app/models/solidus_paypal_braintree/source.rb +0 -133
  130. data/app/models/solidus_paypal_braintree/transaction.rb +0 -31
  131. data/app/models/solidus_paypal_braintree/transaction_address.rb +0 -87
  132. data/app/models/solidus_paypal_braintree/transaction_import.rb +0 -98
  133. data/app/overrides/spree/payments/payment/add_paypal_funding_source_to_payment.rb +0 -9
  134. data/app/views/spree/api/payments/source_views/_paypal_braintree.json.jbuilder +0 -3
  135. data/app/views/spree/checkout/existing_payment/_paypal_braintree.html.erb +0 -10
  136. data/app/views/spree/shared/_apple_pay_button.html.erb +0 -27
  137. data/app/views/spree/shared/_braintree_errors.html.erb +0 -16
  138. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +0 -43
  139. data/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb +0 -26
  140. data/app/views/spree/shared/_paypal_cart_button.html.erb +0 -38
  141. data/app/views/spree/shared/_paypal_messaging.html.erb +0 -13
  142. data/app/views/spree/shared/_venmo_button.html.erb +0 -33
  143. data/config/locales/en.yml +0 -96
  144. data/config/locales/it.yml +0 -56
  145. data/config/routes.rb +0 -14
  146. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +0 -16
  147. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +0 -13
  148. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +0 -11
  149. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +0 -7
  150. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +0 -6
  151. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +0 -32
  152. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +0 -11
  153. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +0 -5
  154. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +0 -5
  155. data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +0 -5
  156. data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +0 -5
  157. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +0 -48
  158. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +0 -31
  159. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +0 -67
  160. data/lib/solidus_paypal_braintree/country_mapper.rb +0 -37
  161. data/lib/solidus_paypal_braintree/factories.rb +0 -53
  162. data/lib/solidus_paypal_braintree/request_protection.rb +0 -21
  163. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +0 -63
  164. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +0 -16
  165. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +0 -39
  166. data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +0 -13
  167. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +0 -16
  168. data/lib/views/backend_v2.4/spree/admin/shared/preference_fields/_hash.html.erb +0 -12
  169. data/lib/views/frontend/solidus_paypal_braintree/payments/_payment.html.erb +0 -12
  170. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +0 -23
  171. data/lib/views/frontend/spree/shared/_paypal_checkout_button.html.erb +0 -32
@@ -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