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,280 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusPaypalBraintree::Response do
4
+ let(:failed_transaction) { nil }
5
+ let(:error) do
6
+ instance_double(
7
+ 'Braintree::ValidationError',
8
+ code: '12345',
9
+ message: "Cannot refund a transaction unless it is settled."
10
+ )
11
+ end
12
+
13
+ let(:error_result) do
14
+ instance_double(
15
+ 'Braintree::ErrorResult',
16
+ success?: false,
17
+ errors: [error],
18
+ transaction: failed_transaction,
19
+ params: { some: 'error' }
20
+ )
21
+ end
22
+
23
+ let(:error_response) do
24
+ described_class.build(error_result)
25
+ end
26
+
27
+ let(:successful_result) do
28
+ transaction = instance_double(
29
+ 'Braintree::Transaction',
30
+ status: 'ok',
31
+ id: 'abcdef',
32
+ avs_error_response_code: nil,
33
+ avs_street_address_response_code: 'M',
34
+ avs_postal_code_response_code: 'M',
35
+ cvv_response_code: 'I'
36
+ )
37
+
38
+ instance_double(
39
+ 'Braintree::SuccessfulResult',
40
+ success?: true,
41
+ transaction: transaction
42
+ )
43
+ end
44
+
45
+ let(:successful_response) do
46
+ described_class.build(successful_result)
47
+ end
48
+
49
+ describe '.new' do
50
+ it 'is private' do
51
+ expect { described_class.new }.to raise_error(/private method/)
52
+ end
53
+ end
54
+
55
+ describe '.build' do
56
+ it { expect(error_response).to be_a ActiveMerchant::Billing::Response }
57
+ it { expect(successful_response).to be_a ActiveMerchant::Billing::Response }
58
+ end
59
+
60
+ describe '#success?' do
61
+ it { expect(error_response.success?).to be false }
62
+ it { expect(successful_response.success?).to be true }
63
+ end
64
+
65
+ describe "#message" do
66
+ context "with a success response" do
67
+ subject { successful_response.message }
68
+
69
+ it { is_expected.to eq "ok" }
70
+ end
71
+
72
+ context "with an error response" do
73
+ subject { error_response.message }
74
+
75
+ context "with a Braintree error" do
76
+ it { is_expected.to eq "Cannot refund a transaction unless it is settled. (12345)" }
77
+ end
78
+
79
+ context "with a settlement_declined status" do
80
+ let(:error) { nil }
81
+ let(:failed_transaction) do
82
+ instance_double(
83
+ 'Braintree::Transaction',
84
+ id: 'abcdef',
85
+ status: "settlement_declined",
86
+ processor_settlement_response_code: "4001",
87
+ processor_settlement_response_text: "Settlement Declined",
88
+ avs_error_response_code: nil,
89
+ avs_street_address_response_code: nil,
90
+ avs_postal_code_response_code: nil,
91
+ cvv_response_code: nil
92
+ )
93
+ end
94
+
95
+ it { is_expected.to eq "Settlement Declined (4001)" }
96
+ end
97
+
98
+ context "with a gateway_rejected status" do
99
+ let(:error) { nil }
100
+ let(:failed_transaction) do
101
+ instance_double(
102
+ 'Braintree::Transaction',
103
+ id: 'abcdef',
104
+ status: "gateway_rejected",
105
+ gateway_rejection_reason: "cvv",
106
+ avs_error_response_code: nil,
107
+ avs_street_address_response_code: nil,
108
+ avs_postal_code_response_code: nil,
109
+ cvv_response_code: nil
110
+ )
111
+ end
112
+
113
+ it { is_expected.to eq "CVV check failed." }
114
+ end
115
+
116
+ context "with a processor_declined status" do
117
+ let(:error) { nil }
118
+ let(:failed_transaction) do
119
+ instance_double(
120
+ 'Braintree::Transaction',
121
+ id: 'abcdef',
122
+ status: "processor_declined",
123
+ processor_response_code: '2001',
124
+ processor_response_text: 'Insufficient Funds',
125
+ avs_error_response_code: nil,
126
+ avs_street_address_response_code: nil,
127
+ avs_postal_code_response_code: nil,
128
+ cvv_response_code: nil
129
+ )
130
+ end
131
+
132
+ it { is_expected.to eq "Insufficient Funds (2001)" }
133
+ end
134
+
135
+ context 'with other transaction status' do
136
+ let(:error) { nil }
137
+ let(:failed_transaction) do
138
+ instance_double(
139
+ 'Braintree::Transaction',
140
+ id: 'abcdef',
141
+ status: "authorization_expired",
142
+ avs_error_response_code: nil,
143
+ avs_street_address_response_code: nil,
144
+ avs_postal_code_response_code: nil,
145
+ cvv_response_code: nil
146
+ )
147
+ end
148
+
149
+ it { is_expected.to eq 'Payment authorization has expired.' }
150
+ end
151
+
152
+ context 'with other transaction status that is not translated' do
153
+ let(:error) { nil }
154
+ let(:failed_transaction) do
155
+ instance_double(
156
+ 'Braintree::Transaction',
157
+ id: 'abcdef',
158
+ status: "something_bad_happened",
159
+ avs_error_response_code: nil,
160
+ avs_street_address_response_code: nil,
161
+ avs_postal_code_response_code: nil,
162
+ cvv_response_code: nil
163
+ )
164
+ end
165
+
166
+ it { is_expected.to eq 'Something bad happened' }
167
+ end
168
+ end
169
+ end
170
+
171
+ describe '#avs_result' do
172
+ context 'with a successful result' do
173
+ subject { described_class.build(successful_result).avs_result }
174
+
175
+ it 'includes AVS response code' do
176
+ expect(subject['code']).to eq 'M'
177
+ end
178
+
179
+ it 'includes AVS response message' do
180
+ expect(subject['message']).to eq 'Street address and postal code match.'
181
+ end
182
+ end
183
+
184
+ context 'with an error result' do
185
+ subject { described_class.build(error_result).avs_result }
186
+
187
+ let(:failed_transaction) do
188
+ instance_double(
189
+ 'Braintree::Transaction',
190
+ id: 'abcdef',
191
+ avs_error_response_code: 'E',
192
+ avs_street_address_response_code: nil,
193
+ avs_postal_code_response_code: nil,
194
+ cvv_response_code: nil
195
+ )
196
+ end
197
+
198
+ it 'includes AVS response code' do
199
+ expect(subject['code']).to eq 'E'
200
+ end
201
+
202
+ it 'includes AVS response message' do
203
+ expect(subject['message']).to eq 'AVS data is invalid or AVS is not allowed for this card type.'
204
+ end
205
+
206
+ context 'without transaction' do
207
+ let(:failed_transaction) { nil }
208
+
209
+ it 'includes no AVS response' do
210
+ expect(subject['message']).to be_nil
211
+ expect(subject['code']).to be_nil
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ describe '#cvv_result' do
218
+ context 'with a successful result' do
219
+ subject { described_class.build(successful_result).cvv_result }
220
+
221
+ it 'does not include CVV response code' do
222
+ expect(subject['code']).to be_nil
223
+ end
224
+
225
+ it 'does not include CVV response message' do
226
+ expect(subject['message']).to be_nil
227
+ end
228
+ end
229
+
230
+ context 'with an error result' do
231
+ subject { described_class.build(error_result).cvv_result }
232
+
233
+ let(:failed_transaction) do
234
+ instance_double(
235
+ 'Braintree::Transaction',
236
+ id: 'abcdef',
237
+ avs_error_response_code: nil,
238
+ avs_street_address_response_code: nil,
239
+ avs_postal_code_response_code: nil,
240
+ cvv_response_code: 'N'
241
+ )
242
+ end
243
+
244
+ it 'includes CVV response code' do
245
+ expect(subject['code']).to eq 'N'
246
+ end
247
+
248
+ it 'includes CVV response message' do
249
+ expect(subject['message']).to eq 'CVV does not match'
250
+ end
251
+
252
+ context 'without transaction' do
253
+ let(:failed_transaction) { nil }
254
+
255
+ it 'includes no CVV response' do
256
+ expect(subject['message']).to be_nil
257
+ expect(subject['code']).to be_nil
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ describe '#params' do
264
+ context "with an error response" do
265
+ subject { error_response.params }
266
+
267
+ it 'includes request params' do
268
+ expect(subject).to eq({ 'some' => 'error' })
269
+ end
270
+ end
271
+ end
272
+
273
+ describe '#authorization' do
274
+ it 'is whatever the b.t. transaction id was' do
275
+ expect(successful_response.authorization).to eq 'abcdef'
276
+ end
277
+
278
+ it { expect(error_response.authorization).to be_nil }
279
+ end
280
+ end
@@ -0,0 +1,360 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SolidusPaypalBraintree::Source, type: :model do
4
+ include_context 'when order is ready for payment'
5
+
6
+ it 'is invalid without a payment_type set' do
7
+ expect(described_class.new).to be_invalid
8
+ end
9
+
10
+ it 'is invalid with payment_type set to unknown type' do
11
+ expect(described_class.new(payment_type: 'AndroidPay')).to be_invalid
12
+ end
13
+
14
+ describe '#payment_method' do
15
+ it 'uses spree_payment_method' do
16
+ expect(described_class.new.build_payment_method).to be_a Spree::PaymentMethod
17
+ end
18
+ end
19
+
20
+ describe '#imported' do
21
+ it 'is always false' do
22
+ expect(described_class.new.imported).not_to be_truthy
23
+ end
24
+ end
25
+
26
+ describe "#actions" do
27
+ it "supports capture, void, and credit" do
28
+ expect(described_class.new.actions).to eq %w[capture void credit]
29
+ end
30
+ end
31
+
32
+ describe "#can_capture?" do
33
+ subject { described_class.new.can_capture?(payment) }
34
+
35
+ context "when the payment state is pending" do
36
+ let(:payment) { build(:payment, state: "pending") }
37
+
38
+ it { is_expected.to be_truthy }
39
+ end
40
+
41
+ context "when the payment state is checkout" do
42
+ let(:payment) { build(:payment, state: "checkout") }
43
+
44
+ it { is_expected.to be_truthy }
45
+ end
46
+
47
+ context "when the payment is completed" do
48
+ let(:payment) { build(:payment, state: "completed") }
49
+
50
+ it { is_expected.not_to be_truthy }
51
+ end
52
+ end
53
+
54
+ describe '#can_void?' do
55
+ subject { payment_source.can_void?(payment) }
56
+
57
+ let(:payment_source) { described_class.new }
58
+ let(:payment) { build(:payment) }
59
+
60
+ let(:transaction_response) do
61
+ double(:response, status: Braintree::Transaction::Status::SubmittedForSettlement)
62
+ end
63
+
64
+ let(:transaction_request) do
65
+ double(:request, find: transaction_response)
66
+ end
67
+
68
+ before do
69
+ allow(payment_source).to receive(:braintree_client) do
70
+ double(:transaction, transaction: transaction_request)
71
+ end
72
+ end
73
+
74
+ context 'when transaction id is not present' do
75
+ let(:payment) { build(:payment, response_code: nil) }
76
+
77
+ it { is_expected.to be(false) }
78
+ end
79
+
80
+ context 'when transaction has voidable status' do
81
+ it { is_expected.to be(true) }
82
+ end
83
+
84
+ context 'when transaction has non voidable status' do
85
+ let(:transaction_response) do
86
+ double(:response, status: Braintree::Transaction::Status::Settled)
87
+ end
88
+
89
+ it { is_expected.to be(false) }
90
+ end
91
+
92
+ context 'when transaction is not found at Braintreee' do
93
+ before do
94
+ allow(transaction_request).to \
95
+ receive(:find).and_raise(Braintree::NotFoundError)
96
+ end
97
+
98
+ it { is_expected.to be(false) }
99
+ end
100
+ end
101
+
102
+ describe "#can_credit?" do
103
+ subject { described_class.new.can_credit?(payment) }
104
+
105
+ context "when the payment is completed" do
106
+ context "when the credit allowed is 100" do
107
+ let(:payment) { build(:payment, state: "completed", amount: 100) }
108
+
109
+ it { is_expected.to be_truthy }
110
+ end
111
+
112
+ context "when the credit allowed is 0" do
113
+ let(:payment) { build(:payment, state: "completed", amount: 0) }
114
+
115
+ it { is_expected.not_to be_truthy }
116
+ end
117
+ end
118
+
119
+ context "when the payment has not been completed" do
120
+ let(:payment) { build(:payment, state: "checkout") }
121
+
122
+ it { is_expected.not_to be_truthy }
123
+ end
124
+ end
125
+
126
+ describe "#friendly_payment_type" do
127
+ subject { described_class.new(payment_type: type).friendly_payment_type }
128
+
129
+ context "when then payment type is PayPal" do
130
+ let(:type) { "PayPalAccount" }
131
+
132
+ it "returns the translated payment type" do
133
+ expect(subject).to eq "PayPal"
134
+ end
135
+ end
136
+
137
+ context "when the payment type is Apple Pay" do
138
+ let(:type) { "ApplePayCard" }
139
+
140
+ it "returns the translated payment type" do
141
+ expect(subject).to eq "Apple Pay"
142
+ end
143
+ end
144
+
145
+ context "when the payment type is Credit Card" do
146
+ let(:type) { "CreditCard" }
147
+
148
+ it "returns the translated payment type" do
149
+ expect(subject).to eq "Credit Card"
150
+ end
151
+ end
152
+ end
153
+
154
+ describe "#apple_pay?" do
155
+ subject { described_class.new(payment_type: type).apple_pay? }
156
+
157
+ context "when the payment type is Apple Pay" do
158
+ let(:type) { "ApplePayCard" }
159
+
160
+ it { is_expected.to be true }
161
+ end
162
+
163
+ context "when the payment type is not Apple Pay" do
164
+ let(:type) { "DogeCoin" }
165
+
166
+ it { is_expected.to be false }
167
+ end
168
+ end
169
+
170
+ describe "#paypal?" do
171
+ subject { described_class.new(payment_type: type).paypal? }
172
+
173
+ context "when the payment type is PayPal" do
174
+ let(:type) { "PayPalAccount" }
175
+
176
+ it { is_expected.to be true }
177
+ end
178
+
179
+ context "when the payment type is not PayPal" do
180
+ let(:type) { "MonopolyMoney" }
181
+
182
+ it { is_expected.to be false }
183
+ end
184
+ end
185
+
186
+ describe "#credit_card?" do
187
+ subject { described_class.new(payment_type: type).credit_card? }
188
+
189
+ context "when the payment type is CreditCard" do
190
+ let(:type) { "CreditCard" }
191
+
192
+ it { is_expected.to be true }
193
+ end
194
+
195
+ context "when the payment type is not CreditCard" do
196
+ let(:type) { "MonopolyMoney" }
197
+
198
+ it { is_expected.to be false }
199
+ end
200
+ end
201
+
202
+ shared_context 'with unknown source token' do
203
+ let(:braintree_payment_method) { double }
204
+
205
+ before do
206
+ allow(braintree_payment_method).to receive(:find) do
207
+ raise Braintree::NotFoundError
208
+ end
209
+ allow(payment_source).to receive(:braintree_client) do
210
+ instance_double(payment_method: braintree_payment_method)
211
+ end
212
+ end
213
+ end
214
+
215
+ shared_context 'with nil source token' do
216
+ let(:braintree_payment_method) { double }
217
+
218
+ before do
219
+ allow(braintree_payment_method).to receive(:find) do
220
+ raise ArgumentError
221
+ end
222
+ allow(payment_source).to receive(:braintree_client) do
223
+ instance_double(payment_method: braintree_payment_method)
224
+ end
225
+ end
226
+ end
227
+
228
+ describe "#last_4" do
229
+ subject { payment_source.last_4 }
230
+
231
+ let(:method) { new_gateway.tap(&:save!) }
232
+ let(:payment_source) { described_class.create!(payment_type: "CreditCard", payment_method: method) }
233
+ let(:braintree_client) { method.braintree }
234
+
235
+ context 'when token is known at braintree', vcr: {
236
+ cassette_name: "source/last4",
237
+ match_requests_on: [:braintree_uri]
238
+ } do
239
+ before do
240
+ customer = braintree_client.customer.create
241
+
242
+ method = braintree_client.payment_method.create({
243
+ payment_method_nonce: "fake-valid-country-of-issuance-usa-nonce",
244
+ customer_id: customer.customer.id
245
+ })
246
+
247
+ payment_source.update!(token: method.payment_method.token)
248
+ end
249
+
250
+ it "delegates to the braintree payment method" do
251
+ method = braintree_client.payment_method.find(payment_source.token)
252
+ expect(subject).to eql(method.last_4)
253
+ end
254
+ end
255
+
256
+ context 'when the source token is not known at Braintree' do
257
+ include_context 'with unknown source token'
258
+
259
+ it { is_expected.to be(nil) }
260
+ end
261
+
262
+ context 'when the source token is nil' do
263
+ include_context 'with nil source token'
264
+
265
+ it { is_expected.to be(nil) }
266
+ end
267
+ end
268
+
269
+ describe "#display_number" do
270
+ subject { payment_source.display_number }
271
+
272
+ let(:type) { nil }
273
+ let(:payment_source) { described_class.new(payment_type: type) }
274
+
275
+ context "when last_digits is a number" do
276
+ before do
277
+ allow(payment_source).to receive(:last_digits).and_return('1234')
278
+ end
279
+
280
+ it { is_expected.to eq 'XXXX-XXXX-XXXX-1234' }
281
+ end
282
+
283
+ context "when last_digits is nil" do
284
+ before do
285
+ allow(payment_source).to receive(:last_digits).and_return(nil)
286
+ end
287
+
288
+ it { is_expected.to eq 'XXXX-XXXX-XXXX-XXXX' }
289
+ end
290
+
291
+ context "when is a PayPal source" do
292
+ let(:type) { "PayPalAccount" }
293
+
294
+ before do
295
+ allow(payment_source).to receive(:email).and_return('user@example.com')
296
+ end
297
+
298
+ it { is_expected.to eq 'user@example.com' }
299
+ end
300
+ end
301
+
302
+ describe "#card_type" do
303
+ subject { payment_source.card_type }
304
+
305
+ let(:method) { new_gateway.tap(&:save!) }
306
+ let(:payment_source) { described_class.create!(payment_type: "CreditCard", payment_method: method) }
307
+ let(:braintree_client) { method.braintree }
308
+
309
+ context "when the token is known at braintree", vcr: {
310
+ cassette_name: "source/card_type",
311
+ match_requests_on: [:braintree_uri]
312
+ } do
313
+ before do
314
+ customer = braintree_client.customer.create
315
+
316
+ method = braintree_client.payment_method.create({
317
+ payment_method_nonce: "fake-valid-country-of-issuance-usa-nonce", customer_id: customer.customer.id
318
+ })
319
+
320
+ payment_source.update!(token: method.payment_method.token)
321
+ end
322
+
323
+ it "delegates to the braintree payment method" do
324
+ method = braintree_client.payment_method.find(payment_source.token)
325
+ expect(subject).to eql(method.card_type)
326
+ end
327
+ end
328
+
329
+ context 'when the source token is not known at Braintree' do
330
+ include_context 'with unknown source token'
331
+
332
+ it { is_expected.to be_nil }
333
+ end
334
+
335
+ context 'when the source token is nil' do
336
+ include_context 'with nil source token'
337
+
338
+ it { is_expected.to be_nil }
339
+ end
340
+ end
341
+
342
+ describe '#reusable?' do
343
+ subject { payment_source.reusable? }
344
+
345
+ let(:payment_source) { described_class.new(token: token, nonce: nonce) }
346
+ let(:nonce) { 'nonce67890' }
347
+
348
+ context 'when source token is present' do
349
+ let(:token) { 'token12345' }
350
+
351
+ it { is_expected.to be_truthy }
352
+ end
353
+
354
+ context 'when source token is nil' do
355
+ let(:token) { nil }
356
+
357
+ it { is_expected.to be_falsy }
358
+ end
359
+ end
360
+ end