solidus_paypal_braintree 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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