solidus_paypal_braintree 0.4.0 → 1.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +40 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.gitignore +18 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +68 -0
  8. data/CHANGELOG.md +258 -0
  9. data/Gemfile +43 -0
  10. data/LICENSE +2 -2
  11. data/README.md +77 -23
  12. data/Rakefile +4 -25
  13. data/app/assets/javascripts/solidus_paypal_braintree/checkout.js +32 -3
  14. data/app/assets/javascripts/solidus_paypal_braintree/client.js +23 -4
  15. data/app/assets/javascripts/solidus_paypal_braintree/constants.js +19 -0
  16. data/app/assets/javascripts/solidus_paypal_braintree/frontend.js +1 -0
  17. data/app/assets/javascripts/solidus_paypal_braintree/hosted_form.js +15 -5
  18. data/app/assets/javascripts/solidus_paypal_braintree/paypal_button.js +47 -20
  19. data/app/assets/javascripts/solidus_paypal_braintree/paypal_messaging.js +22 -0
  20. data/app/decorators/controllers/solidus_paypal_braintree/admin_payments_controller_decorator.rb +11 -0
  21. data/app/decorators/controllers/solidus_paypal_braintree/checkout_controller_decorator.rb +11 -0
  22. data/app/decorators/controllers/solidus_paypal_braintree/client_tokens_controller.rb +41 -0
  23. data/app/decorators/controllers/solidus_paypal_braintree/orders_controller_decorator.rb +11 -0
  24. data/app/decorators/models/solidus_paypal_braintree/spree/store_decorator.rb +20 -0
  25. data/app/decorators/models/solidus_paypal_braintree/spree/user_decorator.rb +13 -0
  26. data/app/helpers/solidus_paypal_braintree/braintree_admin_helper.rb +23 -0
  27. data/app/helpers/solidus_paypal_braintree/braintree_checkout_helper.rb +46 -0
  28. data/app/models/application_record.rb +2 -0
  29. data/app/models/solidus_paypal_braintree/address.rb +30 -0
  30. data/app/models/solidus_paypal_braintree/configuration.rb +26 -3
  31. data/app/models/solidus_paypal_braintree/customer.rb +7 -3
  32. data/app/models/solidus_paypal_braintree/gateway.rb +52 -20
  33. data/app/models/solidus_paypal_braintree/response.rb +3 -2
  34. data/app/models/solidus_paypal_braintree/source.rb +21 -7
  35. data/app/models/solidus_paypal_braintree/transaction.rb +2 -0
  36. data/app/models/solidus_paypal_braintree/transaction_address.rb +30 -12
  37. data/app/models/solidus_paypal_braintree/transaction_import.rb +13 -9
  38. data/app/views/spree/api/payments/source_views/_paypal_braintree.json.jbuilder +3 -0
  39. data/app/views/spree/checkout/existing_payment/_paypal_braintree.html.erb +10 -0
  40. data/app/views/spree/shared/_apple_pay_button.html.erb +2 -2
  41. data/app/views/spree/shared/_braintree_errors.html.erb +11 -17
  42. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +24 -9
  43. data/app/views/spree/shared/_paypal_braintree_head_scripts.html.erb +9 -6
  44. data/app/views/spree/shared/_paypal_cart_button.html.erb +16 -2
  45. data/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
  46. data/bin/console +17 -0
  47. data/bin/rails +15 -0
  48. data/bin/setup +8 -0
  49. data/config/locales/en.yml +10 -0
  50. data/config/locales/it.yml +51 -8
  51. data/config/routes.rb +2 -0
  52. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +3 -1
  53. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +5 -7
  54. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +3 -1
  55. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
  56. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
  57. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +20 -5
  58. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +25 -21
  59. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +55 -51
  60. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +7 -5
  61. data/lib/solidus_paypal_braintree.rb +4 -0
  62. data/lib/solidus_paypal_braintree/country_mapper.rb +4 -2
  63. data/lib/solidus_paypal_braintree/engine.rb +11 -11
  64. data/lib/solidus_paypal_braintree/factories.rb +8 -4
  65. data/lib/solidus_paypal_braintree/request_protection.rb +3 -0
  66. data/lib/solidus_paypal_braintree/version.rb +3 -1
  67. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +30 -5
  68. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +2 -2
  69. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +2 -2
  70. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +2 -2
  71. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +4 -2
  72. data/lib/views/frontend/spree/shared/_paypal_checkout_button.html.erb +30 -0
  73. data/solidus_paypal_braintree.gemspec +42 -0
  74. data/spec/controllers/solidus_paypal_braintree/checkouts_controller_spec.rb +99 -0
  75. data/spec/controllers/solidus_paypal_braintree/client_tokens_controller_spec.rb +55 -0
  76. data/spec/controllers/solidus_paypal_braintree/configurations_controller_spec.rb +73 -0
  77. data/spec/controllers/solidus_paypal_braintree/transactions_controller_spec.rb +183 -0
  78. data/spec/features/backend/configuration_spec.rb +23 -0
  79. data/spec/features/backend/new_payment_spec.rb +137 -0
  80. data/spec/features/frontend/braintree_credit_card_checkout_spec.rb +187 -0
  81. data/spec/features/frontend/paypal_checkout_spec.rb +166 -0
  82. data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
  83. data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
  84. data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
  85. data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
  86. data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
  87. data/spec/fixtures/cassettes/braintree/token.yml +63 -0
  88. data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
  89. data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
  90. data/spec/fixtures/cassettes/checkout/update.yml +71 -0
  91. data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +156 -0
  92. data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
  93. data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
  94. data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
  95. data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
  96. data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
  97. data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
  98. data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
  99. data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
  100. data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
  101. data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
  102. data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
  103. data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
  104. data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
  105. data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
  106. data/spec/fixtures/cassettes/gateway/void.yml +137 -0
  107. data/spec/fixtures/cassettes/source/card_type.yml +267 -0
  108. data/spec/fixtures/cassettes/source/last4.yml +267 -0
  109. data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
  110. data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
  111. data/spec/fixtures/views/spree/orders/edit.html.erb +50 -0
  112. data/spec/helpers/solidus_paypal_braintree/braintree_admin_helper_spec.rb +17 -0
  113. data/spec/models/solidus_paypal_braintree/address_spec.rb +51 -0
  114. data/spec/models/solidus_paypal_braintree/avs_result_spec.rb +317 -0
  115. data/spec/models/solidus_paypal_braintree/gateway_spec.rb +692 -0
  116. data/spec/models/solidus_paypal_braintree/response_spec.rb +280 -0
  117. data/spec/models/solidus_paypal_braintree/source_spec.rb +360 -0
  118. data/spec/models/solidus_paypal_braintree/transaction_address_spec.rb +253 -0
  119. data/spec/models/solidus_paypal_braintree/transaction_import_spec.rb +283 -0
  120. data/spec/models/solidus_paypal_braintree/transaction_spec.rb +85 -0
  121. data/spec/models/spree/store_spec.rb +14 -0
  122. data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
  123. data/spec/spec_helper.rb +26 -0
  124. data/spec/support/capybara.rb +7 -0
  125. data/spec/support/factories.rb +2 -0
  126. data/spec/support/gateway_helpers.rb +29 -0
  127. data/spec/support/order_ready_for_payment.rb +37 -0
  128. data/spec/support/vcr.rb +42 -0
  129. data/spec/support/views.rb +1 -0
  130. metadata +182 -194
  131. data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +0 -22
  132. data/app/helpers/braintree_admin_helper.rb +0 -18
  133. data/app/models/spree/store_decorator.rb +0 -11
  134. data/app/views/spree/shared/_paypal_checkout_button.html.erb +0 -27
  135. data/config/initializers/braintree.rb +0 -1
@@ -0,0 +1,692 @@
1
+ require 'spec_helper'
2
+ require 'webmock'
3
+ require 'support/order_ready_for_payment'
4
+
5
+ RSpec.describe SolidusPaypalBraintree::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
+ SolidusPaypalBraintree::Source.new(
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) { SolidusPaypalBraintree::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 "paypal_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) { SolidusPaypalBraintree::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 SolidusPaypalBraintree::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
+ shared_examples "sources_by_order" do
548
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
549
+ let(:gateway) { new_gateway.tap(&:save!) }
550
+
551
+ let(:other_payment_method) { FactoryBot.create(:payment_method) }
552
+
553
+ let(:source_without_profile) do
554
+ SolidusPaypalBraintree::Source.create!(
555
+ payment_method_id: gateway.id,
556
+ payment_type: payment_type,
557
+ user_id: user.id
558
+ )
559
+ end
560
+
561
+ let(:source_with_profile) do
562
+ SolidusPaypalBraintree::Source.create!(
563
+ payment_method_id: gateway.id,
564
+ payment_type: payment_type,
565
+ user_id: user.id
566
+ ).tap do |source|
567
+ source.create_customer!(user: user)
568
+ source.save!
569
+ end
570
+ end
571
+
572
+ before do
573
+ create(:payment, order: order, payment_method_id: payment_method_id, source: source)
574
+ end
575
+
576
+ context "when the order has payments with the braintree payment method" do
577
+ let(:payment_method_id) { gateway.id }
578
+
579
+ context "when the payment has a saved source with a profile" do
580
+ let(:source) { source_with_profile }
581
+
582
+ it "returns the source" do
583
+ expect(subject.to_a).to eql([source])
584
+ end
585
+ end
586
+
587
+ context "when the payment has a saved source without a profile" do
588
+ let(:source) { source_without_profile }
589
+
590
+ it "returns no result" do
591
+ expect(subject.to_a).to eql([])
592
+ end
593
+ end
594
+ end
595
+
596
+ context "when the order has no payments with the braintree payment method" do
597
+ let(:payment_method_id) { other_payment_method.id }
598
+ let(:source) { FactoryBot.create :credit_card }
599
+
600
+ it "returns no results" do
601
+ expect(subject.to_a).to eql([])
602
+ end
603
+ end
604
+ end
605
+
606
+ describe "#sources_by_order" do
607
+ subject { gateway.sources_by_order(order) }
608
+
609
+ let(:gateway) { new_gateway.tap(&:save!) }
610
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
611
+
612
+ include_examples "sources_by_order"
613
+ end
614
+
615
+ describe "#reusable_sources" do
616
+ subject { gateway.reusable_sources(order) }
617
+
618
+ let(:order) { FactoryBot.build :order, user: user }
619
+ let(:gateway) { new_gateway.tap(&:save!) }
620
+
621
+ context "when an order is completed" do
622
+ include_examples "sources_by_order"
623
+ end
624
+
625
+ context "when an order is not completed" do
626
+ context "when the order has a user id" do
627
+ let(:user) { FactoryBot.create(:user) }
628
+
629
+ let!(:source_without_profile) do
630
+ SolidusPaypalBraintree::Source.create!(
631
+ payment_method_id: gateway.id,
632
+ payment_type: payment_type,
633
+ user_id: user.id
634
+ )
635
+ end
636
+
637
+ let!(:source_with_profile) do
638
+ SolidusPaypalBraintree::Source.create!(
639
+ payment_method_id: gateway.id,
640
+ payment_type: payment_type,
641
+ user_id: user.id
642
+ ).tap do |source|
643
+ source.create_customer!(user: user)
644
+ source.save!
645
+ end
646
+ end
647
+
648
+ it "includes saved sources with payment profiles" do
649
+ expect(subject).to include(source_with_profile)
650
+ end
651
+
652
+ it "excludes saved sources without payment profiles" do
653
+ expect(subject).not_to include(source_without_profile)
654
+ end
655
+ end
656
+
657
+ context "when the order does not have a user" do
658
+ let(:user) { nil }
659
+
660
+ it "returns no sources for guest users" do
661
+ expect(subject).to eql([])
662
+ end
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ describe '.generate_token' do
669
+ subject do
670
+ # dont VCR ignore generate token request, use the existing cassette
671
+ allow(VCR.request_ignorer.hooks).to receive(:[]).with(:ignore_request).and_return([])
672
+ gateway.generate_token
673
+ end
674
+
675
+ context 'with connection enabled', vcr: {
676
+ cassette_name: 'braintree/generate_token',
677
+ match_requests_on: [:braintree_uri]
678
+ } do
679
+ it { is_expected.to be_a(String).and be_present }
680
+ end
681
+
682
+ context 'when token generation is disabled' do
683
+ let(:gateway) do
684
+ gateway = described_class.create!(name: 'braintree')
685
+ gateway.preferred_token_generation_enabled = false
686
+ gateway
687
+ end
688
+
689
+ it { expect { subject }.to raise_error SolidusPaypalBraintree::Gateway::TokenGenerationDisabledError }
690
+ end
691
+ end
692
+ end