solidus_braintree 1.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +78 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +1 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +21 -10
  7. data/.rspec +1 -1
  8. data/.rubocop.yml +86 -0
  9. data/CHANGELOG.md +186 -18
  10. data/Gemfile +35 -17
  11. data/LICENSE +26 -0
  12. data/README.md +389 -24
  13. data/Rakefile +6 -16
  14. data/app/assets/config/solidus_braintree_manifest.js +0 -0
  15. data/app/assets/javascripts/spree/backend/solidus_braintree/client.js +239 -0
  16. data/app/assets/javascripts/spree/backend/solidus_braintree/constants.js +89 -0
  17. data/app/assets/javascripts/spree/backend/solidus_braintree/hosted_form.js +46 -0
  18. data/app/assets/javascripts/spree/backend/solidus_braintree/promise.js +20 -0
  19. data/app/assets/javascripts/spree/backend/solidus_braintree.js +96 -0
  20. data/app/assets/stylesheets/spree/backend/solidus_braintree.scss +28 -0
  21. data/app/decorators/controllers/solidus_braintree/admin_payments_controller_decorator.rb +11 -0
  22. data/app/decorators/controllers/solidus_braintree/client_tokens_controller.rb +41 -0
  23. data/app/decorators/models/solidus_braintree/spree/store_decorator.rb +20 -0
  24. data/app/decorators/models/solidus_braintree/spree/user_decorator.rb +13 -0
  25. data/app/helpers/solidus_braintree/braintree_admin_helper.rb +23 -0
  26. data/app/models/application_record.rb +5 -0
  27. data/app/models/solidus_braintree/address.rb +64 -0
  28. data/app/models/solidus_braintree/avs_result.rb +69 -0
  29. data/app/models/solidus_braintree/configuration.rb +39 -0
  30. data/app/models/solidus_braintree/customer.rb +8 -0
  31. data/app/models/solidus_braintree/gateway.rb +437 -0
  32. data/app/models/solidus_braintree/response.rb +80 -0
  33. data/app/models/solidus_braintree/source.rb +140 -0
  34. data/app/models/solidus_braintree/transaction.rb +31 -0
  35. data/app/models/solidus_braintree/transaction_address.rb +88 -0
  36. data/app/models/solidus_braintree/transaction_import.rb +98 -0
  37. data/app/views/spree/api/payments/source_views/_braintree.json.jbuilder +3 -0
  38. data/bin/console +4 -1
  39. data/bin/dummy-app +37 -0
  40. data/bin/rails +5 -5
  41. data/bin/rails-dummy-app +17 -0
  42. data/bin/rails-engine +13 -0
  43. data/bin/rails-sandbox +16 -0
  44. data/bin/rake +7 -0
  45. data/bin/rspec +11 -0
  46. data/bin/sandbox +61 -0
  47. data/bin/setup +5 -4
  48. data/config/locales/en.yml +94 -2
  49. data/config/locales/it.yml +56 -0
  50. data/config/routes.rb +12 -3
  51. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  52. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +13 -0
  53. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  54. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +7 -0
  55. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  56. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +38 -0
  57. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +14 -0
  58. data/db/migrate/20190705115327_add_paypal_button_preferences_to_braintree_configurations.rb +5 -0
  59. data/db/migrate/20190911141712_add_3d_secure_to_braintree_configuration.rb +5 -0
  60. data/db/migrate/20211222170950_add_paypal_funding_source_to_solidus_paypal_braintree_sources.rb +5 -0
  61. data/db/migrate/20220104150301_add_venmo_to_braintree_configuration.rb +5 -0
  62. data/db/migrate/20230109080950_rename_solidus_paypal_braintree_source_type.rb +31 -0
  63. data/db/migrate/20230210104310_add_device_data_to_braintree_sources.rb +5 -0
  64. data/lib/controllers/backend/solidus_braintree/configurations_controller.rb +48 -0
  65. data/lib/generators/solidus_braintree/install/install_generator.rb +155 -19
  66. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_280x48.svg +19 -0
  67. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_320x48.svg +19 -0
  68. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_blue_button_375x48.svg +19 -0
  69. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_280x48.svg +19 -0
  70. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_320x48.svg +19 -0
  71. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_active_white_button_375x48.svg +19 -0
  72. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_acceptance_mark.svg +15 -0
  73. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_280x48.svg +19 -0
  74. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_320x48.svg +19 -0
  75. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_button_375x48.svg +19 -0
  76. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_blue_logo.svg +18 -0
  77. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_acceptance_mark.svg +20 -0
  78. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_280x48.svg +19 -0
  79. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_320x48.svg +19 -0
  80. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_button_375x48.svg +19 -0
  81. data/lib/generators/solidus_braintree/install/templates/app/assets/images/solidus_braintree/venmo/venmo_white_logo.svg +18 -0
  82. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/paypal_button.js +34 -0
  83. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/ajax.js +13 -0
  84. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/apple_pay_button.js +179 -0
  85. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/checkout.js +113 -0
  86. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/client.js +239 -0
  87. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/constants.js +89 -0
  88. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/frontend.js +15 -0
  89. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/hosted_form.js +48 -0
  90. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/paypal_button.js +178 -0
  91. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/paypal_messaging.js +22 -0
  92. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/promise.js +20 -0
  93. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree/venmo_button.js +86 -0
  94. data/lib/generators/solidus_braintree/install/templates/app/assets/javascripts/spree/frontend/solidus_braintree.js +1 -0
  95. data/lib/generators/solidus_braintree/install/templates/app/assets/stylesheets/spree/frontend/solidus_braintree.scss +62 -0
  96. data/lib/generators/solidus_braintree/install/templates/app/controllers/solidus_braintree/checkouts_controller.rb +31 -0
  97. data/lib/generators/solidus_braintree/install/templates/app/controllers/solidus_braintree/transactions_controller.rb +67 -0
  98. data/lib/generators/solidus_braintree/install/templates/app/helpers/solidus_braintree/braintree_checkout_helper.rb +60 -0
  99. data/lib/generators/solidus_braintree/install/templates/app/views/checkouts/existing_payment/_braintree.html.erb +2 -0
  100. data/lib/generators/solidus_braintree/install/templates/app/views/checkouts/payment/_braintree.html.erb +23 -0
  101. data/lib/generators/solidus_braintree/install/templates/app/views/payments/_braintree_payment_details.html.erb +9 -0
  102. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_apple_pay_button.html.erb +27 -0
  103. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_errors.html.erb +16 -0
  104. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_head_scripts.html.erb +26 -0
  105. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_braintree_hosted_fields.html.erb +40 -0
  106. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_cart_button.html.erb +38 -0
  107. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_checkout_button.html.erb +32 -0
  108. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_paypal_messaging.html.erb +13 -0
  109. data/lib/generators/solidus_braintree/install/templates/app/views/spree/shared/_venmo_button.html.erb +33 -0
  110. data/lib/generators/solidus_braintree/install/templates/config/initializers/solidus_braintree.rb +6 -0
  111. data/lib/solidus_braintree/country_mapper.rb +37 -0
  112. data/lib/solidus_braintree/engine.rb +61 -11
  113. data/lib/solidus_braintree/extension_configuration.rb +23 -0
  114. data/lib/solidus_braintree/request_protection.rb +21 -0
  115. data/lib/solidus_braintree/version.rb +3 -1
  116. data/lib/solidus_braintree.rb +14 -2
  117. data/lib/solidus_paypal_braintree.rb +6 -0
  118. data/lib/views/backend/solidus_braintree/configurations/list.html.erb +63 -0
  119. data/lib/views/backend/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  120. data/lib/views/backend/spree/admin/payments/source_views/_braintree.html.erb +39 -0
  121. data/lib/views/backend/spree/admin/shared/preference_fields/_preference_select.html.erb +13 -0
  122. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_braintree.html.erb +16 -0
  123. data/lib/views/backend_v2.4/spree/admin/shared/preference_fields/_hash.html.erb +12 -0
  124. data/solidus_braintree.gemspec +37 -38
  125. data/spec/controllers/solidus_braintree/checkouts_controller_spec.rb +99 -0
  126. data/spec/controllers/solidus_braintree/client_tokens_controller_spec.rb +55 -0
  127. data/spec/controllers/solidus_braintree/configurations_controller_spec.rb +73 -0
  128. data/spec/controllers/solidus_braintree/transactions_controller_spec.rb +183 -0
  129. data/spec/fixtures/cassettes/admin/invalid_credit_card.yml +63 -0
  130. data/spec/fixtures/cassettes/admin/resubmit_credit_card.yml +352 -0
  131. data/spec/fixtures/cassettes/admin/valid_credit_card.yml +412 -0
  132. data/spec/fixtures/cassettes/braintree/create_profile.yml +71 -0
  133. data/spec/fixtures/cassettes/braintree/generate_token.yml +63 -0
  134. data/spec/fixtures/cassettes/braintree/token.yml +63 -0
  135. data/spec/fixtures/cassettes/checkout/invalid_credit_card.yml +63 -0
  136. data/spec/fixtures/cassettes/checkout/resubmit_credit_card.yml +216 -0
  137. data/spec/fixtures/cassettes/checkout/update.yml +71 -0
  138. data/spec/fixtures/cassettes/checkout/valid_credit_card.yml +171 -0
  139. data/spec/fixtures/cassettes/checkout/valid_venmo_transaction.yml +599 -0
  140. data/spec/fixtures/cassettes/gateway/authorize/credit_card/address.yml +86 -0
  141. data/spec/fixtures/cassettes/gateway/authorize/merchant_account/EUR.yml +154 -0
  142. data/spec/fixtures/cassettes/gateway/authorize/paypal/EUR.yml +90 -0
  143. data/spec/fixtures/cassettes/gateway/authorize/paypal/address.yml +90 -0
  144. data/spec/fixtures/cassettes/gateway/authorize.yml +86 -0
  145. data/spec/fixtures/cassettes/gateway/authorized_transaction.yml +73 -0
  146. data/spec/fixtures/cassettes/gateway/cancel/missing.yml +63 -0
  147. data/spec/fixtures/cassettes/gateway/cancel/refunds.yml +272 -0
  148. data/spec/fixtures/cassettes/gateway/cancel/void.yml +201 -0
  149. data/spec/fixtures/cassettes/gateway/capture.yml +141 -0
  150. data/spec/fixtures/cassettes/gateway/complete.yml +157 -0
  151. data/spec/fixtures/cassettes/gateway/credit.yml +208 -0
  152. data/spec/fixtures/cassettes/gateway/customer.yml +79 -0
  153. data/spec/fixtures/cassettes/gateway/purchase.yml +87 -0
  154. data/spec/fixtures/cassettes/gateway/settled_transaction.yml +140 -0
  155. data/spec/fixtures/cassettes/gateway/void.yml +137 -0
  156. data/spec/fixtures/cassettes/source/bin.yml +295 -0
  157. data/spec/fixtures/cassettes/source/card_type.yml +267 -0
  158. data/spec/fixtures/cassettes/source/last4.yml +267 -0
  159. data/spec/fixtures/cassettes/transaction/import/valid/capture.yml +224 -0
  160. data/spec/fixtures/cassettes/transaction/import/valid.yml +71 -0
  161. data/spec/fixtures/views/carts/_cart_footer.html.erb +18 -0
  162. data/spec/helpers/solidus_braintree/braintree_admin_helper_spec.rb +17 -0
  163. data/spec/helpers/solidus_braintree/braintree_checkout_helper_spec.rb +70 -0
  164. data/spec/models/solidus_braintree/address_spec.rb +71 -0
  165. data/spec/models/solidus_braintree/avs_result_spec.rb +317 -0
  166. data/spec/models/solidus_braintree/gateway_spec.rb +774 -0
  167. data/spec/models/solidus_braintree/response_spec.rb +280 -0
  168. data/spec/models/solidus_braintree/source_spec.rb +555 -0
  169. data/spec/models/solidus_braintree/transaction_address_spec.rb +235 -0
  170. data/spec/models/solidus_braintree/transaction_import_spec.rb +302 -0
  171. data/spec/models/solidus_braintree/transaction_spec.rb +86 -0
  172. data/spec/models/spree/store_spec.rb +14 -0
  173. data/spec/requests/spree/api/orders_controller_spec.rb +36 -0
  174. data/spec/solidus_braintree_helper.rb +7 -0
  175. data/spec/support/solidus_braintree/capybara.rb +7 -0
  176. data/spec/support/solidus_braintree/factories.rb +55 -0
  177. data/spec/support/solidus_braintree/gateway_helpers.rb +29 -0
  178. data/spec/support/solidus_braintree/order_ready_for_payment.rb +44 -0
  179. data/spec/support/solidus_braintree/order_walkthrough.rb +87 -0
  180. data/spec/support/solidus_braintree/vcr.rb +42 -0
  181. data/spec/support/solidus_braintree/with_prepended_view_fixtures.rb +19 -0
  182. data/spec/system/backend/configuration_spec.rb +23 -0
  183. data/spec/system/backend/new_payment_spec.rb +136 -0
  184. data/spec/system/frontend/braintree_credit_card_checkout_spec.rb +199 -0
  185. data/spec/system/frontend/paypal_checkout_spec.rb +169 -0
  186. data/spec/system/frontend/venmo_checkout_spec.rb +193 -0
  187. metadata +289 -255
  188. data/.travis.yml +0 -41
  189. data/LICENSE.txt +0 -21
  190. data/app/controllers/spree/api/braintree_client_token_controller.rb +0 -13
  191. data/app/helpers/braintree_view_helpers.rb +0 -20
  192. data/app/models/concerns/solidus_braintree/add_name_validation_concern.rb +0 -8
  193. data/app/models/concerns/solidus_braintree/inject_device_data_concern.rb +0 -18
  194. data/app/models/concerns/solidus_braintree/payment_braintree_nonce_concern.rb +0 -8
  195. data/app/models/concerns/solidus_braintree/permitted_attributes_concern.rb +0 -11
  196. data/app/models/concerns/solidus_braintree/skip_require_card_numbers_concern.rb +0 -14
  197. data/app/models/concerns/solidus_braintree/use_data_field_concern.rb +0 -23
  198. data/app/models/credit_card_decorator.rb +0 -3
  199. data/app/models/payment_decorator.rb +0 -2
  200. data/app/models/permitted_attributes_decorator.rb +0 -1
  201. data/app/models/solidus/gateway/braintree_gateway.rb +0 -306
  202. data/app/overrides/spree/checkout/_confirm/braintree_security.html.erb.deface +0 -9
  203. data/app/views/spree/admin/payments/source_forms/_braintree.html.erb +0 -38
  204. data/app/views/spree/admin/payments/source_views/_braintree.html.erb +0 -30
  205. data/app/views/spree/checkout/payment/_braintree.html.erb +0 -55
  206. data/app/views/spree/checkout/payment/_braintree_initialization.html.erb +0 -12
  207. data/config/initializers/braintree.rb +0 -1
  208. data/db/migrate/20150910170527_add_data_to_credit_card.rb +0 -5
  209. data/db/migrate/20160426221931_add_braintree_device_data_to_order.rb +0 -5
  210. data/lib/assets/javascripts/spree/backend/braintree/solidus_braintree.js +0 -59
  211. data/lib/assets/javascripts/spree/frontend/braintree/solidus_braintree.js +0 -144
  212. data/lib/assets/javascripts/vendor/braintree.js +0 -8
  213. data/lib/assets/stylesheets/spree/frontend/solidus_braintree.scss +0 -26
@@ -0,0 +1,774 @@
1
+ require 'solidus_braintree_helper'
2
+ require 'webmock'
3
+ require 'support/solidus_braintree/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
+ device_data: 'fake-device-data'
21
+ )
22
+ end
23
+
24
+ let(:payment_type) { SolidusBraintree::Source::PAYPAL }
25
+
26
+ describe "saving preference hashes as strings" do
27
+ subject { gateway.update(update_params) }
28
+
29
+ context "with valid hash syntax" do
30
+ let(:update_params) do
31
+ {
32
+ preferred_merchant_currency_map: '{"EUR" => "test_merchant_account_id"}',
33
+ preferred_paypal_payee_email_map: '{"CAD" => "bruce+wayne@example.com"}'
34
+ }
35
+ end
36
+
37
+ it "successfully updates the preference" do
38
+ subject
39
+ expect(gateway.preferred_merchant_currency_map).to eq({ "EUR" => "test_merchant_account_id" })
40
+ expect(gateway.preferred_paypal_payee_email_map).to eq({ "CAD" => "bruce+wayne@example.com" })
41
+ end
42
+ end
43
+
44
+ context "with invalid user input" do
45
+ let(:update_params) do
46
+ { preferred_merchant_currency_map: '{this_is_not_a_valid_hash}' }
47
+ end
48
+
49
+ it "raise a JSON parser error" do
50
+ expect{ subject }.to raise_error(JSON::ParserError)
51
+ end
52
+ end
53
+ end
54
+
55
+ describe 'making a payment on an order', vcr: {
56
+ cassette_name: 'gateway/complete',
57
+ match_requests_on: [:braintree_uri]
58
+ } do
59
+ include_context 'when order is ready for payment'
60
+
61
+ before do
62
+ order.update(number: "ORDER0")
63
+ payment.update(number: "PAYMENT0")
64
+ end
65
+
66
+ let(:payment) do
67
+ order.payments.create!(
68
+ payment_method: gateway,
69
+ source: source,
70
+ amount: 55
71
+ )
72
+ end
73
+
74
+ it 'can complete an order' do
75
+ order.payments.reset
76
+
77
+ expect(order.total).to eq 55
78
+
79
+ expect(payment.capture_events.count).to eq 0
80
+
81
+ order.next!
82
+ expect(order.state).to eq "confirm"
83
+
84
+ order.complete!
85
+ expect(order.state).to eq "complete"
86
+
87
+ expect(order.outstanding_balance).to eq 0.0
88
+
89
+ expect(payment.capture_events.count).to eq 1
90
+ end
91
+ end
92
+
93
+ describe "instance methods" do
94
+ shared_examples "successful response" do
95
+ it 'returns a successful billing response', aggregate_failures: true do
96
+ expect(subject).to be_a ActiveMerchant::Billing::Response
97
+ expect(subject).to be_success
98
+ end
99
+ end
100
+
101
+ shared_examples "protects against connection errors" do
102
+ context 'when a timeout error happens' do
103
+ it 'raises ActiveMerchant::ConnectionError' do
104
+ expect_any_instance_of(Braintree::TransactionGateway).to receive(gateway_action) do
105
+ raise Braintree::BraintreeError
106
+ end
107
+
108
+ expect { subject }.to raise_error ActiveMerchant::ConnectionError
109
+ end
110
+ end
111
+ end
112
+
113
+ let(:authorized_id) do
114
+ braintree.transaction.sale(
115
+ amount: 40,
116
+ payment_method_nonce: source.nonce
117
+ ).transaction.id
118
+ end
119
+
120
+ let(:sale_id) do
121
+ braintree.transaction.sale(
122
+ amount: 40,
123
+ payment_method_nonce: source.nonce,
124
+ options: {
125
+ submit_for_settlement: true
126
+ }
127
+ ).transaction.id
128
+ end
129
+
130
+ let(:settled_id) do
131
+ braintree.testing.settle(sale_id).transaction.id
132
+ end
133
+
134
+ let(:currency) { 'USD' }
135
+
136
+ let(:gateway_options) do
137
+ {
138
+ currency: currency,
139
+ shipping_address: {
140
+ name: "Bruce Wayne",
141
+ address1: "42 Spruce Lane",
142
+ address2: "Apt 312",
143
+ city: "Gotham",
144
+ state: "CA",
145
+ zip: "90210",
146
+ country: "US"
147
+ },
148
+ billing_address: {
149
+ name: "Dick Grayson",
150
+ address1: "15 Robin Walk",
151
+ address2: "Apt 123",
152
+ city: "Blüdhaven",
153
+ state: "CA",
154
+ zip: "90210",
155
+ country: "US"
156
+ }
157
+ }
158
+ end
159
+
160
+ describe "#method_type" do
161
+ subject { gateway.method_type }
162
+
163
+ it { is_expected.to eq "braintree" }
164
+ end
165
+
166
+ describe '#gateway_options' do
167
+ subject(:gateway_options) { gateway.gateway_options }
168
+
169
+ it 'includes http_open_timeout' do
170
+ expect(subject).to have_key(:http_open_timeout)
171
+ expect(gateway_options[:http_open_timeout]).to eq(60)
172
+ end
173
+
174
+ it 'includes http_read_timeout' do
175
+ expect(subject).to have_key(:http_read_timeout)
176
+ expect(gateway_options[:http_read_timeout]).to eq(60)
177
+ end
178
+ end
179
+
180
+ describe '#purchase' do
181
+ subject(:purchase) { gateway.purchase(1000, source, gateway_options) }
182
+
183
+ context 'with successful purchase', vcr: {
184
+ cassette_name: 'gateway/purchase',
185
+ match_requests_on: [:braintree_uri]
186
+ } do
187
+ include_examples "successful response"
188
+
189
+ it 'submits the transaction for settlement', aggregate_failures: true do
190
+ expect(purchase.message).to eq 'submitted_for_settlement'
191
+ expect(purchase.authorization).to be_present
192
+ end
193
+ end
194
+
195
+ include_examples "protects against connection errors" do
196
+ let(:gateway_action) { :sale }
197
+ end
198
+ end
199
+
200
+ describe "#authorize" do
201
+ subject(:authorize) { gateway.authorize(1000, source, gateway_options) }
202
+
203
+ include_examples "protects against connection errors" do
204
+ let(:gateway_action) { :sale }
205
+ end
206
+
207
+ context 'with successful authorization', vcr: {
208
+ cassette_name: 'gateway/authorize',
209
+ match_requests_on: [:braintree_uri]
210
+ } do
211
+ include_examples "successful response"
212
+
213
+ it 'passes "Solidus" as the channel parameter in the request' do
214
+ expect_any_instance_of(Braintree::TransactionGateway).
215
+ to receive(:sale).
216
+ with(hash_including({ channel: "Solidus" })).and_call_original
217
+ authorize
218
+ end
219
+
220
+ it 'authorizes the transaction', aggregate_failures: true do
221
+ expect(authorize.message).to eq 'authorized'
222
+ expect(authorize.authorization).to be_present
223
+ end
224
+
225
+ context 'with available device data' do
226
+ it 'passes the device data as a parameter in the request' do
227
+ expect_any_instance_of(Braintree::TransactionGateway).
228
+ to receive(:sale).
229
+ with(hash_including({ device_data: "fake-device-data" })).and_call_original
230
+ authorize
231
+ end
232
+ end
233
+
234
+ context 'without device_data' do
235
+ let(:source) do
236
+ SolidusBraintree::Source.create!(
237
+ nonce: 'fake-valid-nonce',
238
+ user: user,
239
+ payment_type: payment_type,
240
+ payment_method: gateway
241
+ )
242
+ end
243
+
244
+ before do
245
+ allow_any_instance_of(Braintree::TransactionGateway).to receive(:sale).and_call_original
246
+ end
247
+
248
+ it 'does not pass any device data in the request' do
249
+ expect_any_instance_of(Braintree::TransactionGateway)
250
+ .not_to receive(:sale).with(hash_including({ device_data: "" }))
251
+
252
+ authorize
253
+ end
254
+ end
255
+ end
256
+
257
+ context 'with different merchant account for currency', vcr: {
258
+ cassette_name: 'gateway/authorize/merchant_account/EUR',
259
+ match_requests_on: [:braintree_uri]
260
+ } do
261
+ let(:currency) { 'EUR' }
262
+
263
+ it 'settles with the correct currency' do
264
+ transaction = braintree.transaction.find(authorize.authorization)
265
+ expect(transaction.merchant_account_id).to eq 'stembolt_EUR'
266
+ end
267
+ end
268
+
269
+ context 'with different paypal payee email for currency', vcr: {
270
+ cassette_name: 'gateway/authorize/paypal/EUR',
271
+ match_requests_on: [:braintree_uri]
272
+ } do
273
+ let(:currency) { 'EUR' }
274
+
275
+ it 'uses the correct payee email' do
276
+ expect_any_instance_of(Braintree::TransactionGateway).
277
+ to receive(:sale).
278
+ with(hash_including({
279
+ options: {
280
+ store_in_vault_on_success: true,
281
+ paypal: {
282
+ payee_email: ENV.fetch('BRAINTREE_PAYPAL_PAYEE_EMAIL')
283
+ }
284
+ }
285
+ })).and_call_original
286
+ authorize
287
+ end
288
+
289
+ context "with PayPal transaction", vcr: {
290
+ cassette_name: 'gateway/authorize/paypal/address',
291
+ match_requests_on: [:braintree_uri]
292
+ } do
293
+ it 'includes the shipping address in the request' do
294
+ expect_any_instance_of(Braintree::TransactionGateway).
295
+ to receive(:sale).
296
+ with(hash_including({
297
+ shipping: {
298
+ first_name: "Bruce",
299
+ last_name: "Wayne",
300
+ street_address: "42 Spruce Lane Apt 312",
301
+ locality: "Gotham",
302
+ postal_code: "90210",
303
+ region: "CA",
304
+ country_code_alpha2: "US"
305
+ }
306
+ })).and_call_original
307
+ authorize
308
+ end
309
+ end
310
+ end
311
+
312
+ context "with CreditCard transaction", vcr: {
313
+ cassette_name: 'gateway/authorize/credit_card/address',
314
+ match_requests_on: [:braintree_uri]
315
+ } do
316
+ let(:payment_type) { SolidusBraintree::Source::CREDIT_CARD }
317
+
318
+ it 'includes the billing address in the request' do
319
+ expect_any_instance_of(Braintree::TransactionGateway).to receive(:sale).
320
+ with(hash_including({
321
+ billing: {
322
+ first_name: "Dick",
323
+ last_name: "Grayson",
324
+ street_address: "15 Robin Walk Apt 123",
325
+ locality: "Blüdhaven",
326
+ postal_code: "90210",
327
+ region: "CA",
328
+ country_code_alpha2: "US"
329
+ }
330
+ })).and_call_original
331
+ authorize
332
+ end
333
+ end
334
+ end
335
+
336
+ describe "#capture" do
337
+ subject(:capture) { gateway.capture(1000, authorized_id, {}) }
338
+
339
+ context 'with successful capture', vcr: {
340
+ cassette_name: 'gateway/capture',
341
+ match_requests_on: [:braintree_uri]
342
+ } do
343
+ include_examples "successful response"
344
+
345
+ it 'submits the transaction for settlement' do
346
+ expect(capture.message).to eq "submitted_for_settlement"
347
+ end
348
+ end
349
+
350
+ context 'with authorized transaction', vcr: {
351
+ cassette_name: 'gateway/authorized_transaction',
352
+ match_requests_on: [:braintree_uri]
353
+ } do
354
+ include_examples "protects against connection errors" do
355
+ let(:gateway_action) { :submit_for_settlement }
356
+ end
357
+ end
358
+ end
359
+
360
+ describe "#credit" do
361
+ subject(:credit) { gateway.credit(2000, source, settled_id, {}) }
362
+
363
+ context 'with successful credit', vcr: {
364
+ cassette_name: 'gateway/credit',
365
+ match_requests_on: [:braintree_uri]
366
+ } do
367
+ include_examples "successful response"
368
+
369
+ it 'credits the transaction' do
370
+ expect(credit.message).to eq 'submitted_for_settlement'
371
+ end
372
+ end
373
+
374
+ context 'with settled transaction', vcr: {
375
+ cassette_name: 'gateway/settled_transaction',
376
+ match_requests_on: [:braintree_uri]
377
+ } do
378
+ include_examples "protects against connection errors" do
379
+ let(:gateway_action) { :refund }
380
+ end
381
+ end
382
+ end
383
+
384
+ describe "#void" do
385
+ subject(:void) { gateway.void(authorized_id, source, {}) }
386
+
387
+ context 'when successfully voided', vcr: {
388
+ cassette_name: 'gateway/void',
389
+ match_requests_on: [:braintree_uri]
390
+ } do
391
+ include_examples "successful response"
392
+
393
+ it 'voids the transaction' do
394
+ expect(void.message).to eq 'voided'
395
+ end
396
+ end
397
+
398
+ context 'with authorized transaction', vcr: {
399
+ cassette_name: 'gateway/authorized_transaction',
400
+ match_requests_on: [:braintree_uri]
401
+ } do
402
+ include_examples "protects against connection errors" do
403
+ let(:gateway_action) { :void }
404
+ end
405
+ end
406
+ end
407
+
408
+ describe "#cancel", vcr: {
409
+ cassette_name: 'gateway/cancel',
410
+ match_requests_on: [:braintree_uri]
411
+ } do
412
+ subject(:cancel) { gateway.cancel(transaction_id) }
413
+
414
+ let(:transaction_id) { "fake_transaction_id" }
415
+
416
+ context "when the transaction is found" do
417
+ context "when it is voidable", vcr: {
418
+ cassette_name: 'gateway/cancel/void',
419
+ match_requests_on: [:braintree_uri]
420
+ } do
421
+ let(:transaction_id) { authorized_id }
422
+
423
+ include_examples "successful response"
424
+
425
+ it 'voids the transaction' do
426
+ expect(cancel.message).to eq 'voided'
427
+ end
428
+ end
429
+
430
+ context "when it is not voidable", vcr: {
431
+ cassette_name: 'gateway/cancel/refunds',
432
+ match_requests_on: [:braintree_uri]
433
+ } do
434
+ let(:transaction_id) { settled_id }
435
+
436
+ include_examples "successful response"
437
+
438
+ it 'refunds the transaction' do
439
+ expect(cancel.message).to eq 'submitted_for_settlement'
440
+ end
441
+ end
442
+ end
443
+
444
+ context "when the transaction is not found", vcr: {
445
+ cassette_name: 'gateway/cancel/missing',
446
+ match_requests_on: [:braintree_uri]
447
+ } do
448
+ it 'raises an error' do
449
+ expect{ cancel }.to raise_error ActiveMerchant::ConnectionError
450
+ end
451
+ end
452
+ end
453
+
454
+ describe '#try_void' do
455
+ subject { gateway.try_void(instance_double(Spree::Payment, response_code: source.token)) }
456
+
457
+ let(:transaction_request) do
458
+ class_double(Braintree::Transaction,
459
+ find: transaction_response)
460
+ end
461
+
462
+ before do
463
+ client = instance_double(Braintree::Gateway)
464
+ allow(client).to receive(:transaction) { transaction_request }
465
+ allow(gateway).to receive(:braintree) { client }
466
+ end
467
+
468
+ context 'with voidable payment' do
469
+ let(:transaction_response) do
470
+ instance_double(Braintree::Transaction,
471
+ status: Braintree::Transaction::Status::Authorized)
472
+ end
473
+
474
+ it 'voids the payment' do
475
+ expect(gateway).to receive(:void)
476
+ subject
477
+ end
478
+
479
+ context 'with error response mentioning an unvoidable transaction' do
480
+ before do
481
+ allow(gateway).to receive(:void) do
482
+ raise ActiveMerchant::ConnectionError.new(
483
+ 'Transaction can only be voided if status is authorized',
484
+ double
485
+ )
486
+ end
487
+ end
488
+
489
+ it { is_expected.to be(false) }
490
+ end
491
+
492
+ context 'with other error response' do
493
+ before do
494
+ allow(gateway).to receive(:void) do
495
+ raise ActiveMerchant::ConnectionError.new(
496
+ 'Server unreachable',
497
+ double
498
+ )
499
+ end
500
+ end
501
+
502
+ it { expect { subject }.to raise_error ActiveMerchant::ConnectionError }
503
+ end
504
+ end
505
+
506
+ context 'with voidable paypal payment' do
507
+ let(:transaction_response) do
508
+ instance_double(Braintree::Transaction,
509
+ status: Braintree::Transaction::Status::SettlementPending)
510
+ end
511
+
512
+ it 'voids the payment' do
513
+ expect(gateway).to receive(:void)
514
+ subject
515
+ end
516
+ end
517
+
518
+ context 'with non-voidable payment' do
519
+ let(:transaction_response) do
520
+ instance_double(Braintree::Transaction,
521
+ status: Braintree::Transaction::Status::Settled)
522
+ end
523
+
524
+ it { is_expected.to be(false) }
525
+ end
526
+ end
527
+
528
+ describe "#create_profile" do
529
+ subject(:profile) { gateway.create_profile(payment) }
530
+
531
+ let(:payment) do
532
+ build(:payment, {
533
+ payment_method: gateway,
534
+ source: source
535
+ })
536
+ end
537
+
538
+ cassette_options = {
539
+ cassette_name: "braintree/create_profile",
540
+ match_requests_on: [:braintree_uri]
541
+ }
542
+ context "with no existing customer profile", vcr: cassette_options do
543
+ it 'creates and returns a new customer profile', aggregate_failures: true do
544
+ expect(profile).to be_a SolidusBraintree::Customer
545
+ expect(profile.sources).to eq [source]
546
+ expect(profile.braintree_customer_id).to be_present
547
+ end
548
+
549
+ it "sets a token on the payment source" do
550
+ expect{ subject }.to change(source, :token)
551
+ end
552
+ end
553
+
554
+ context "when the source already has a token" do
555
+ before { source.token = "totally-a-valid-token" }
556
+
557
+ it "does not create a new customer profile" do
558
+ expect(profile).to be_nil
559
+ end
560
+ end
561
+
562
+ context "when the source already has a customer" do
563
+ before { source.build_customer }
564
+
565
+ it "does not create a new customer profile" do
566
+ expect(profile).to be_nil
567
+ end
568
+ end
569
+
570
+ context "when the source has no nonce" do
571
+ before { source.nonce = nil }
572
+
573
+ it "does not create a new customer profile" do
574
+ expect(profile).to be_nil
575
+ end
576
+ end
577
+ end
578
+
579
+ describe '#customer_profile_params' do
580
+ subject(:params) { gateway.send(:customer_profile_params, payment) }
581
+
582
+ let(:payment) do
583
+ build(:payment, {
584
+ payment_method: gateway,
585
+ source: source
586
+ })
587
+ end
588
+
589
+ context 'when payment does not belong to an order' do
590
+ before { allow(payment).to receive(:order).and_return(nil) }
591
+
592
+ it 'has the email param as nil' do
593
+ expect(subject[:email]).to be_nil
594
+ end
595
+ end
596
+
597
+ context 'when payment belongs to an order' do
598
+ it 'has no email param' do
599
+ expect(subject[:email]).to eq(payment.order.email)
600
+ end
601
+ end
602
+ end
603
+
604
+ describe "Braintree Customer" do
605
+ subject(:customer) { braintree.customer.create(params).customer }
606
+
607
+ let(:params) { gateway.send(:customer_profile_params, payment) }
608
+
609
+ let(:payment) do
610
+ build(:payment, {
611
+ payment_method: gateway,
612
+ source: source
613
+ })
614
+ end
615
+
616
+ cassette_options = {
617
+ cassette_name: 'gateway/customer',
618
+ match_requests_on: [:braintree_uri]
619
+ }
620
+
621
+ context "with customer", vcr: cassette_options do
622
+ it 'saves the customer email correctly' do
623
+ allow(payment.order).to receive(:email).and_return('braintree@customers.com')
624
+ expect(subject.email).to eq(payment.order.email)
625
+ end
626
+ end
627
+ end
628
+
629
+ shared_examples "sources_by_order" do
630
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
631
+ let(:gateway) { new_gateway.tap(&:save!) }
632
+
633
+ let(:other_payment_method) { FactoryBot.create(:payment_method) }
634
+
635
+ let(:source_without_profile) do
636
+ SolidusBraintree::Source.create!(
637
+ payment_method_id: gateway.id,
638
+ payment_type: payment_type,
639
+ user_id: user.id
640
+ )
641
+ end
642
+
643
+ let(:source_with_profile) do
644
+ SolidusBraintree::Source.create!(
645
+ payment_method_id: gateway.id,
646
+ payment_type: payment_type,
647
+ user_id: user.id
648
+ ).tap do |source|
649
+ source.create_customer!(user: user)
650
+ source.save!
651
+ end
652
+ end
653
+
654
+ before do
655
+ create(:payment, order: order, payment_method_id: payment_method_id, source: source)
656
+ end
657
+
658
+ context "when the order has payments with the braintree payment method" do
659
+ let(:payment_method_id) { gateway.id }
660
+
661
+ context "when the payment has a saved source with a profile" do
662
+ let(:source) { source_with_profile }
663
+
664
+ it "returns the source" do
665
+ expect(subject.to_a).to eql([source])
666
+ end
667
+ end
668
+
669
+ context "when the payment has a saved source without a profile" do
670
+ let(:source) { source_without_profile }
671
+
672
+ it "returns no result" do
673
+ expect(subject.to_a).to eql([])
674
+ end
675
+ end
676
+ end
677
+
678
+ context "when the order has no payments with the braintree payment method" do
679
+ let(:payment_method_id) { other_payment_method.id }
680
+ let(:source) { FactoryBot.create :credit_card }
681
+
682
+ it "returns no results" do
683
+ expect(subject.to_a).to eql([])
684
+ end
685
+ end
686
+ end
687
+
688
+ describe "#sources_by_order" do
689
+ subject { gateway.sources_by_order(order) }
690
+
691
+ let(:gateway) { new_gateway.tap(&:save!) }
692
+ let(:order) { FactoryBot.create :order, user: user, state: "complete", completed_at: Time.current }
693
+
694
+ include_examples "sources_by_order"
695
+ end
696
+
697
+ describe "#reusable_sources" do
698
+ subject { gateway.reusable_sources(order) }
699
+
700
+ let(:order) { FactoryBot.build :order, user: user }
701
+ let(:gateway) { new_gateway.tap(&:save!) }
702
+
703
+ context "when an order is completed" do
704
+ include_examples "sources_by_order"
705
+ end
706
+
707
+ context "when an order is not completed" do
708
+ context "when the order has a user id" do
709
+ let(:user) { FactoryBot.create(:user) }
710
+
711
+ let!(:source_without_profile) do
712
+ SolidusBraintree::Source.create!(
713
+ payment_method_id: gateway.id,
714
+ payment_type: payment_type,
715
+ user_id: user.id
716
+ )
717
+ end
718
+
719
+ let!(:source_with_profile) do
720
+ SolidusBraintree::Source.create!(
721
+ payment_method_id: gateway.id,
722
+ payment_type: payment_type,
723
+ user_id: user.id
724
+ ).tap do |source|
725
+ source.create_customer!(user: user)
726
+ source.save!
727
+ end
728
+ end
729
+
730
+ it "includes saved sources with payment profiles" do
731
+ expect(subject).to include(source_with_profile)
732
+ end
733
+
734
+ it "excludes saved sources without payment profiles" do
735
+ expect(subject).not_to include(source_without_profile)
736
+ end
737
+ end
738
+
739
+ context "when the order does not have a user" do
740
+ let(:user) { nil }
741
+
742
+ it "returns no sources for guest users" do
743
+ expect(subject).to eql([])
744
+ end
745
+ end
746
+ end
747
+ end
748
+ end
749
+
750
+ describe '.generate_token' do
751
+ subject do
752
+ # dont VCR ignore generate token request, use the existing cassette
753
+ allow(VCR.request_ignorer.hooks).to receive(:[]).with(:ignore_request).and_return([])
754
+ gateway.generate_token
755
+ end
756
+
757
+ context 'with connection enabled', vcr: {
758
+ cassette_name: 'braintree/generate_token',
759
+ match_requests_on: [:braintree_uri]
760
+ } do
761
+ it { is_expected.to be_a(String).and be_present }
762
+ end
763
+
764
+ context 'when token generation is disabled' do
765
+ let(:gateway) do
766
+ gateway = described_class.create!(name: 'braintree')
767
+ gateway.preferred_token_generation_enabled = false
768
+ gateway
769
+ end
770
+
771
+ it { expect { subject }.to raise_error SolidusBraintree::Gateway::TokenGenerationDisabledError }
772
+ end
773
+ end
774
+ end