solidus_stripe 4.4.0 → 5.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +95 -41
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +94 -4
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +1 -265
  7. data/Gemfile +10 -30
  8. data/LICENSE +2 -2
  9. data/Procfile.dev +3 -0
  10. data/README.md +145 -215
  11. data/Rakefile +7 -6
  12. data/app/assets/javascripts/spree/backend/solidus_stripe.js +2 -0
  13. data/app/assets/stylesheets/spree/backend/solidus_stripe.css +4 -0
  14. data/app/controllers/solidus_stripe/intents_controller.rb +36 -52
  15. data/app/controllers/solidus_stripe/webhooks_controller.rb +28 -0
  16. data/app/models/concerns/solidus_stripe/log_entries.rb +31 -0
  17. data/app/models/solidus_stripe/customer.rb +21 -0
  18. data/app/models/solidus_stripe/gateway.rb +231 -0
  19. data/app/models/solidus_stripe/payment_intent.rb +111 -0
  20. data/app/models/solidus_stripe/payment_method.rb +106 -0
  21. data/app/models/solidus_stripe/payment_source.rb +31 -0
  22. data/app/models/solidus_stripe/slug_entry.rb +20 -0
  23. data/app/models/solidus_stripe.rb +7 -0
  24. data/app/subscribers/solidus_stripe/webhook/charge_subscriber.rb +28 -0
  25. data/app/subscribers/solidus_stripe/webhook/payment_intent_subscriber.rb +112 -0
  26. data/app/views/spree/admin/payments/source_forms/_stripe.html.erb +29 -0
  27. data/app/views/spree/admin/payments/source_forms/existing_payment/_stripe.html.erb +14 -0
  28. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_card.html.erb +8 -0
  29. data/app/views/spree/admin/payments/source_forms/existing_payment/stripe/_default.html.erb +7 -0
  30. data/app/views/spree/admin/payments/source_views/_stripe.html.erb +15 -0
  31. data/app/views/spree/api/payments/source_views/_stripe.json.jbuilder +8 -0
  32. data/bin/dev +13 -0
  33. data/bin/dummy-app +29 -0
  34. data/bin/rails +38 -3
  35. data/bin/rails-dummy-app +3 -0
  36. data/bin/rails-engine +1 -11
  37. data/bin/rails-new +55 -0
  38. data/bin/rails-sandbox +1 -14
  39. data/bin/rspec +10 -0
  40. data/bin/sandbox +12 -74
  41. data/bin/setup +1 -0
  42. data/bin/update-migrations +56 -0
  43. data/codecov.yml +12 -0
  44. data/config/locales/en.yml +16 -1
  45. data/config/routes.rb +5 -11
  46. data/coverage.rb +42 -0
  47. data/db/migrate/20230109183332_create_solidus_stripe_payment_sources.rb +10 -0
  48. data/db/migrate/20230303154931_create_solidus_stripe_setup_intent.rb +10 -0
  49. data/db/migrate/20230306105520_create_solidus_stripe_payment_intents.rb +10 -0
  50. data/db/migrate/20230308122414_create_solidus_stripe_webhook_endpoints.rb +10 -0
  51. data/db/migrate/20230310152615_add_payment_method_reference_to_stripe_intents.rb +6 -0
  52. data/db/migrate/20230310171444_normalize_stripe_intent_id_attributes.rb +6 -0
  53. data/db/migrate/20230313150008_create_solidus_stripe_customers.rb +16 -0
  54. data/db/migrate/20230323154931_drop_solidus_stripe_setup_intent.rb +13 -0
  55. data/db/migrate/20230403094916_rename_webhook_endpoint_to_payment_method_slug_entries.rb +5 -0
  56. data/db/seeds.rb +6 -24
  57. data/lib/generators/solidus_stripe/install/install_generator.rb +121 -14
  58. data/lib/generators/solidus_stripe/install/templates/app/assets/stylesheets/spree/frontend/solidus_stripe.css +13 -0
  59. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_confirm_controller.js +39 -0
  60. data/lib/generators/solidus_stripe/install/templates/app/javascript/controllers/solidus_stripe_payment_controller.js +89 -0
  61. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/_stripe.html.erb +16 -0
  62. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_card.html.erb +8 -0
  63. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/existing_payment/stripe/_default.html.erb +7 -0
  64. data/lib/generators/solidus_stripe/install/templates/app/views/checkouts/payment/_stripe.html.erb +39 -0
  65. data/lib/generators/solidus_stripe/install/templates/app/views/orders/payment_info/_stripe.html.erb +20 -0
  66. data/lib/generators/solidus_stripe/install/templates/config/initializers/solidus_stripe.rb +31 -0
  67. data/lib/solidus_stripe/configuration.rb +24 -3
  68. data/lib/solidus_stripe/engine.rb +19 -6
  69. data/lib/solidus_stripe/money_to_stripe_amount_converter.rb +109 -0
  70. data/lib/solidus_stripe/refunds_synchronizer.rb +96 -0
  71. data/lib/solidus_stripe/seeds.rb +19 -0
  72. data/lib/solidus_stripe/testing_support/factories.rb +153 -0
  73. data/lib/solidus_stripe/version.rb +1 -1
  74. data/lib/solidus_stripe/webhook/event.rb +90 -0
  75. data/lib/solidus_stripe.rb +0 -2
  76. data/solidus_stripe.gemspec +29 -6
  77. data/spec/lib/solidus_stripe/configuration_spec.rb +21 -0
  78. data/spec/lib/solidus_stripe/money_to_stripe_amount_converter_spec.rb +133 -0
  79. data/spec/lib/solidus_stripe/refunds_synchronizer_spec.rb +238 -0
  80. data/spec/lib/solidus_stripe/seeds_spec.rb +43 -0
  81. data/spec/lib/solidus_stripe/webhook/event_spec.rb +134 -0
  82. data/spec/models/concerns/solidus_stripe/log_entries_spec.rb +54 -0
  83. data/spec/models/solidus_stripe/customer_spec.rb +47 -0
  84. data/spec/models/solidus_stripe/gateway_spec.rb +283 -0
  85. data/spec/models/solidus_stripe/payment_intent_spec.rb +17 -0
  86. data/spec/models/solidus_stripe/payment_method_spec.rb +137 -0
  87. data/spec/models/solidus_stripe/payment_source_spec.rb +25 -0
  88. data/spec/requests/solidus_stripe/intents_controller_spec.rb +29 -0
  89. data/spec/requests/solidus_stripe/webhooks_controller/charge/refunded_spec.rb +31 -0
  90. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/canceled_spec.rb +23 -0
  91. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/payment_failed_spec.rb +23 -0
  92. data/spec/requests/solidus_stripe/webhooks_controller/payment_intent/succeeded_spec.rb +29 -0
  93. data/spec/requests/solidus_stripe/webhooks_controller_spec.rb +52 -0
  94. data/spec/solidus_stripe_spec_helper.rb +10 -0
  95. data/spec/subscribers/solidus_stripe/webhook/charge_subscriber_spec.rb +33 -0
  96. data/spec/subscribers/solidus_stripe/webhook/payment_intent_subscriber_spec.rb +297 -0
  97. data/spec/support/solidus_stripe/backend_test_helper.rb +210 -0
  98. data/spec/support/solidus_stripe/checkout_test_helper.rb +339 -0
  99. data/spec/support/solidus_stripe/factories.rb +5 -0
  100. data/spec/support/solidus_stripe/webhook/data_fixtures.rb +106 -0
  101. data/spec/support/solidus_stripe/webhook/event_with_context_factory.rb +82 -0
  102. data/spec/support/solidus_stripe/webhook/request_helper.rb +32 -0
  103. data/spec/system/backend/solidus_stripe/orders/payments_spec.rb +119 -0
  104. data/spec/system/frontend/.keep +0 -0
  105. data/spec/system/frontend/solidus_stripe/checkout_spec.rb +187 -0
  106. data/tmp/.keep +0 -0
  107. metadata +201 -75
  108. data/.rubocop_todo.yml +0 -298
  109. data/.travis.yml +0 -28
  110. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-cart-page-checkout.js +0 -122
  111. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-elements.js +0 -148
  112. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-init.js +0 -20
  113. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-intents.js +0 -84
  114. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment-request-button-shared.js +0 -160
  115. data/app/assets/javascripts/spree/frontend/solidus_stripe/stripe-payment.js +0 -16
  116. data/app/assets/javascripts/spree/frontend/solidus_stripe.js +0 -6
  117. data/app/controllers/solidus_stripe/payment_request_controller.rb +0 -52
  118. data/app/controllers/spree/stripe_controller.rb +0 -13
  119. data/app/decorators/models/spree/order_update_attributes_decorator.rb +0 -39
  120. data/app/decorators/models/spree/payment_decorator.rb +0 -11
  121. data/app/decorators/models/spree/refund_decorator.rb +0 -9
  122. data/app/models/solidus_stripe/address_from_params_service.rb +0 -72
  123. data/app/models/solidus_stripe/create_intents_payment_service.rb +0 -114
  124. data/app/models/solidus_stripe/prepare_order_for_payment_service.rb +0 -46
  125. data/app/models/solidus_stripe/shipping_rates_service.rb +0 -46
  126. data/app/models/spree/payment_method/stripe_credit_card.rb +0 -230
  127. data/bin/r +0 -13
  128. data/bin/sandbox_rails +0 -18
  129. data/db/migrate/20181010123508_update_stripe_payment_method_type_to_credit_card.rb +0 -21
  130. data/lib/assets/stylesheets/spree/frontend/solidus_stripe.scss +0 -11
  131. data/lib/solidus_stripe/testing_support/card_input_helper.rb +0 -34
  132. data/lib/tasks/solidus_stripe/db/seed.rake +0 -14
  133. data/lib/views/api/spree/api/payments/source_views/_stripe.json.jbuilder +0 -3
  134. data/lib/views/backend/spree/admin/log_entries/_stripe.html.erb +0 -28
  135. data/lib/views/backend/spree/admin/payments/source_forms/_stripe.html.erb +0 -1
  136. data/lib/views/backend/spree/admin/payments/source_views/_stripe.html.erb +0 -1
  137. data/lib/views/frontend/spree/checkout/existing_payment/_stripe.html.erb +0 -1
  138. data/lib/views/frontend/spree/checkout/payment/_stripe.html.erb +0 -8
  139. data/lib/views/frontend/spree/checkout/payment/v2/_javascript.html.erb +0 -78
  140. data/lib/views/frontend/spree/checkout/payment/v3/_elements.html.erb +0 -1
  141. data/lib/views/frontend/spree/checkout/payment/v3/_form_elements.html.erb +0 -40
  142. data/lib/views/frontend/spree/checkout/payment/v3/_intents.html.erb +0 -1
  143. data/lib/views/frontend/spree/checkout/payment/v3/_stripe.html.erb +0 -2
  144. data/lib/views/frontend/spree/orders/_stripe_payment_request_button.html.erb +0 -14
  145. data/spec/features/stripe_checkout_spec.rb +0 -486
  146. data/spec/models/solidus_stripe/address_from_params_service_spec.rb +0 -87
  147. data/spec/models/solidus_stripe/create_intents_payment_service_spec.rb +0 -127
  148. data/spec/models/solidus_stripe/prepare_order_for_payment_service_spec.rb +0 -65
  149. data/spec/models/solidus_stripe/shipping_rates_service_spec.rb +0 -54
  150. data/spec/models/spree/payment_method/stripe_credit_card_spec.rb +0 -350
  151. data/spec/requests/payment_requests_spec.rb +0 -152
  152. data/spec/spec_helper.rb +0 -37
  153. data/spec/support/solidus_address_helper.rb +0 -15
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe_spec_helper"
4
+
5
+ RSpec.describe SolidusStripe::Webhook::ChargeSubscriber do
6
+ describe "#sync_refunds" do
7
+ it "synchronizes refunds" do
8
+ SolidusStripe::Seeds.refund_reasons
9
+ payment_method = create(:stripe_payment_method)
10
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
11
+ payment = create(:payment,
12
+ amount: 10,
13
+ payment_method: payment_method,
14
+ response_code: stripe_payment_intent.id,
15
+ state: "completed")
16
+ stripe_charge = Stripe::Charge.construct_from(id: "ch_123", payment_intent: "pi_123")
17
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
18
+ Stripe::ListObject.construct_from(
19
+ data: [{ id: "re_123", amount: 1000, currency: "usd", metadata: {} }]
20
+ )
21
+ )
22
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
23
+ payment_method: payment_method,
24
+ object: stripe_charge,
25
+ type: "charge.refunded"
26
+ ).solidus_stripe_object
27
+
28
+ described_class.new.sync_refunds(event)
29
+
30
+ expect(payment.refunds.count).to be(1)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_stripe_spec_helper"
4
+
5
+ RSpec.describe SolidusStripe::Webhook::PaymentIntentSubscriber do
6
+ describe "#capture_payment" do
7
+ context "when a full capture is performed" do
8
+ it "completes a pending payment" do
9
+ payment_method = create(:stripe_payment_method)
10
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
11
+ id: "pi_123",
12
+ amount: 1000,
13
+ amount_received: 1000,
14
+ currency: "usd"
15
+ )
16
+ payment = create(:payment,
17
+ amount: 10,
18
+ payment_method: payment_method,
19
+ response_code: stripe_payment_intent.id,
20
+ state: "pending")
21
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
22
+ payment_method: payment_method,
23
+ object: stripe_payment_intent,
24
+ type: "payment_intent.succeeded"
25
+ ).solidus_stripe_object
26
+
27
+ described_class.new.capture_payment(event)
28
+
29
+ expect(payment.reload.state).to eq "completed"
30
+ end
31
+
32
+ it "adds a log entry to the payment" do
33
+ payment_method = create(:stripe_payment_method)
34
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
35
+ id: "pi_123",
36
+ amount: 1000,
37
+ amount_received: 1000,
38
+ currency: "usd"
39
+ )
40
+ payment = create(:payment,
41
+ amount: 10,
42
+ payment_method: payment_method,
43
+ response_code: stripe_payment_intent.id,
44
+ state: "pending")
45
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
46
+ payment_method: payment_method,
47
+ object: stripe_payment_intent,
48
+ type: "payment_intent.succeeded"
49
+ ).solidus_stripe_object
50
+
51
+ described_class.new.capture_payment(event)
52
+
53
+ details = payment.log_entries.last.parsed_details
54
+ expect(details.success?).to be(true)
55
+ expect(
56
+ details.message
57
+ ).to eq "Capture was successful after payment_intent.succeeded webhook"
58
+ end
59
+ end
60
+
61
+ context "when a partial capture is performed" do
62
+ it "completes a pending payment" do
63
+ SolidusStripe::Seeds.refund_reasons
64
+ payment_method = create(:stripe_payment_method)
65
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
66
+ id: "pi_123",
67
+ amount: 1000,
68
+ amount_received: 700,
69
+ currency: "usd"
70
+ )
71
+ payment = create(:payment,
72
+ amount: 10,
73
+ payment_method: payment_method,
74
+ response_code: stripe_payment_intent.id,
75
+ state: "pending")
76
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
77
+ Stripe::ListObject.construct_from(
78
+ data: [{ id: "re_123", amount: 700, currency: "usd", metadata: {} }]
79
+ )
80
+ )
81
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
82
+ payment_method: payment_method,
83
+ object: stripe_payment_intent,
84
+ type: "payment_intent.succeeded"
85
+ ).solidus_stripe_object
86
+
87
+ described_class.new.capture_payment(event)
88
+
89
+ expect(payment.reload.state).to eq "completed"
90
+ end
91
+
92
+ it "synchronizes refunds" do
93
+ SolidusStripe::Seeds.refund_reasons
94
+ payment_method = create(:stripe_payment_method)
95
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
96
+ id: "pi_123",
97
+ amount: 1000,
98
+ amount_received: 700,
99
+ currency: "usd"
100
+ )
101
+ payment = create(:payment,
102
+ amount: 7,
103
+ payment_method: payment_method,
104
+ response_code: stripe_payment_intent.id,
105
+ state: "pending")
106
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
107
+ Stripe::ListObject.construct_from(
108
+ data: [{ id: "re_123", amount: 200, currency: "usd", metadata: {} }]
109
+ )
110
+ )
111
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
112
+ payment_method: payment_method,
113
+ object: stripe_payment_intent,
114
+ type: "payment_intent.succeeded"
115
+ ).solidus_stripe_object
116
+
117
+ described_class.new.capture_payment(event)
118
+
119
+ expect(payment.refunds.count).to be(1)
120
+ end
121
+
122
+ it "adds a log entry for the captured payment" do
123
+ SolidusStripe::Seeds.refund_reasons
124
+ payment_method = create(:stripe_payment_method)
125
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(
126
+ id: "pi_123",
127
+ amount: 1000,
128
+ amount_received: 700,
129
+ currency: "usd"
130
+ )
131
+ payment = create(:payment,
132
+ amount: 10,
133
+ payment_method: payment_method,
134
+ response_code: stripe_payment_intent.id,
135
+ state: "pending")
136
+ allow(Stripe::Refund).to receive(:list).with(payment_intent: stripe_payment_intent.id).and_return(
137
+ Stripe::ListObject.construct_from(
138
+ data: [{ id: "re_123", amount: 700, currency: "usd", metadata: {} }]
139
+ )
140
+ )
141
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
142
+ payment_method: payment_method,
143
+ object: stripe_payment_intent,
144
+ type: "payment_intent.succeeded"
145
+ ).solidus_stripe_object
146
+
147
+ described_class.new.capture_payment(event)
148
+
149
+ log_entries = payment.log_entries.map { [_1.parsed_details.success?, _1.parsed_details.message] }
150
+ expect(
151
+ log_entries
152
+ ).to include([true, "Capture was successful after payment_intent.succeeded webhook"])
153
+ end
154
+ end
155
+
156
+ it "does nothing if the payment is already completed" do
157
+ payment_method = create(:stripe_payment_method)
158
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
159
+ payment = create(:payment,
160
+ payment_method: payment_method,
161
+ response_code: stripe_payment_intent.id,
162
+ state: "completed")
163
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
164
+ payment_method: payment_method,
165
+ object: stripe_payment_intent,
166
+ type: "payment_intent.succeeded",
167
+ ).solidus_stripe_object
168
+
169
+ described_class.new.capture_payment(event)
170
+
171
+ expect(payment.reload.state).to eq "completed"
172
+ expect(payment.log_entries.count).to be(0)
173
+ end
174
+ end
175
+
176
+ describe "#fail_payment" do
177
+ it "fails a pending payment" do
178
+ payment_method = create(:stripe_payment_method)
179
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
180
+ payment = create(:payment,
181
+ payment_method: payment_method,
182
+ response_code: stripe_payment_intent.id,
183
+ state: "pending")
184
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
185
+ payment_method: payment_method,
186
+ object: stripe_payment_intent,
187
+ type: "payment_intent.payment_failed"
188
+ ).solidus_stripe_object
189
+
190
+ described_class.new.fail_payment(event)
191
+
192
+ expect(payment.reload.state).to eq "failed"
193
+ end
194
+
195
+ it "adds a log entry to the payment" do
196
+ payment_method = create(:stripe_payment_method)
197
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
198
+ payment = create(:payment,
199
+ payment_method: payment_method,
200
+ response_code: stripe_payment_intent.id,
201
+ state: "pending")
202
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
203
+ payment_method: payment_method,
204
+ object: stripe_payment_intent,
205
+ type: "payment_intent.payment_failed"
206
+ ).solidus_stripe_object
207
+
208
+ described_class.new.fail_payment(event)
209
+
210
+ details = payment.log_entries.last.parsed_details
211
+ expect(details.success?).to be(false)
212
+ expect(
213
+ details.message
214
+ ).to eq "Payment was marked as failed after payment_intent.failed webhook"
215
+ end
216
+
217
+ it "does nothing if the payment is already failed" do
218
+ payment_method = create(:stripe_payment_method)
219
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123")
220
+ payment = create(:payment,
221
+ payment_method: payment_method,
222
+ response_code: stripe_payment_intent.id,
223
+ state: "failed")
224
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
225
+ payment_method: payment_method,
226
+ object: stripe_payment_intent,
227
+ type: "payment_intent.payment_failed",
228
+ ).solidus_stripe_object
229
+
230
+ described_class.new.fail_payment(event)
231
+
232
+ expect(payment.reload.state).to eq "failed"
233
+ expect(payment.log_entries.count).to be(0)
234
+ end
235
+ end
236
+
237
+ describe "#void_payment" do
238
+ it "voids a pending payment" do
239
+ payment_method = create(:stripe_payment_method)
240
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
241
+ payment = create(:payment,
242
+ payment_method: payment_method,
243
+ response_code: stripe_payment_intent.id,
244
+ state: "pending")
245
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
246
+ payment_method: payment_method,
247
+ object: stripe_payment_intent,
248
+ type: "payment_intent.canceled"
249
+ ).solidus_stripe_object
250
+
251
+ described_class.new.void_payment(event)
252
+
253
+ expect(payment.reload.state).to eq "void"
254
+ end
255
+
256
+ it "adds a log entry to the payment" do
257
+ payment_method = create(:stripe_payment_method)
258
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
259
+ payment = create(:payment,
260
+ payment_method: payment_method,
261
+ response_code: stripe_payment_intent.id,
262
+ state: "pending")
263
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
264
+ payment_method: payment_method,
265
+ object: stripe_payment_intent,
266
+ type: "payment_intent.canceled"
267
+ ).solidus_stripe_object
268
+
269
+ described_class.new.void_payment(event)
270
+
271
+ details = payment.log_entries.last.parsed_details
272
+ expect(details.success?).to be(true)
273
+ expect(
274
+ details.message
275
+ ).to eq "Payment was voided after payment_intent.voided webhook (duplicate)"
276
+ end
277
+
278
+ it "does nothing if the payment is already voided" do
279
+ payment_method = create(:stripe_payment_method)
280
+ stripe_payment_intent = Stripe::PaymentIntent.construct_from(id: "pi_123", cancellation_reason: "duplicate")
281
+ payment = create(:payment,
282
+ payment_method: payment_method,
283
+ response_code: stripe_payment_intent.id,
284
+ state: "void")
285
+ event = SolidusStripe::Webhook::EventWithContextFactory.from_object(
286
+ payment_method: payment_method,
287
+ object: stripe_payment_intent,
288
+ type: "payment_intent.payment_void",
289
+ ).solidus_stripe_object
290
+
291
+ described_class.new.void_payment(event)
292
+
293
+ expect(payment.reload.state).to eq "void"
294
+ expect(payment.log_entries.count).to be(0)
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusStripe::BackendTestHelper
4
+ def create_payment_method(setup_future_usage: 'off_session', auto_capture: false)
5
+ @payment_method = create(
6
+ :stripe_payment_method,
7
+ preferred_setup_future_usage: setup_future_usage,
8
+ auto_capture: auto_capture
9
+ )
10
+ end
11
+
12
+ def payment_method
13
+ # Memoize the payment method id to avoid fetching it multiple times
14
+ @payment_method ||= SolidusStripe::PaymentMethod.first || create_payment_method
15
+ end
16
+
17
+ def order
18
+ @order ||= create(:completed_order_with_totals, line_items_price: 50)
19
+ end
20
+
21
+ def last_valid_payment
22
+ order.payments.valid.last
23
+ end
24
+
25
+ # Stripe-related helper methods for creating and fetching Stripe objects
26
+
27
+ def create_stripe_payment_method(card_number)
28
+ payment_method.gateway.request do
29
+ Stripe::PaymentMethod.create({
30
+ type: 'card',
31
+ card: {
32
+ number: card_number,
33
+ exp_month: 12,
34
+ exp_year: (Time.zone.now.year + 1),
35
+ cvc: '123',
36
+ },
37
+ })
38
+ end
39
+ end
40
+
41
+ def create_stripe_payment_intent(stripe_payment_method_id)
42
+ payment_method.gateway.request do
43
+ Stripe::PaymentIntent.create(
44
+ amount: (order.outstanding_balance * 100).to_i,
45
+ capture_method: 'manual',
46
+ confirm: true,
47
+ currency: 'usd',
48
+ payment_method: stripe_payment_method_id
49
+ )
50
+ end
51
+ end
52
+
53
+ def fetch_stripe_refund(payment)
54
+ payment_method.gateway.request do
55
+ Stripe::Refund.list(payment_intent: payment.response_code).data.first
56
+ end
57
+ end
58
+
59
+ def fetch_stripe_payment_intent(payment)
60
+ payment_method.gateway.request do
61
+ Stripe::PaymentIntent.retrieve(payment.response_code)
62
+ end
63
+ end
64
+
65
+ # Navigation helper methods for interacting with the admin panel
66
+
67
+ def visit_payments_page
68
+ visit "/admin/orders/#{order.number}/payments"
69
+ end
70
+
71
+ def visit_payment_page(payment)
72
+ visit_payments_page
73
+ click_on payment.number
74
+ end
75
+
76
+ # Action helper methods for performing operations on payments
77
+
78
+ def create_authorized_payment(opts = {})
79
+ stripe_payment_method = create_stripe_payment_method(opts[:card_number] || '4242424242424242')
80
+ payment_intent = create_stripe_payment_intent(stripe_payment_method.id)
81
+
82
+ create(:stripe_payment,
83
+ :authorized,
84
+ order: order,
85
+ response_code: payment_intent.id,
86
+ stripe_payment_method_id: stripe_payment_method.id,
87
+ payment_method: payment_method)
88
+ end
89
+
90
+ def create_captured_payment(card_number: '4242424242424242')
91
+ payment = create_authorized_payment(card_number: card_number)
92
+ payment.capture!
93
+ payment
94
+ end
95
+
96
+ def create_valid_payment_source
97
+ stripe_payment_method = create_stripe_payment_method('4242424242424242')
98
+ SolidusStripe::PaymentSource.create!(
99
+ stripe_payment_method_id: stripe_payment_method.id,
100
+ payment_method_id: payment_method.id
101
+ )
102
+ end
103
+
104
+ def create_order_with_existing_payment_source
105
+ payment_source = create_valid_payment_source
106
+ @order = create(:order_with_line_items, line_items_price: 50)
107
+ order.user.wallet.add(payment_source)
108
+ end
109
+
110
+ def complete_order_with_existing_payment_source
111
+ visit_payments_page
112
+ click_on "Continue"
113
+ click_on "Confirm"
114
+ click_on "Complete Order"
115
+ end
116
+
117
+ def refund_payment
118
+ refund_reason = create :refund_reason
119
+ click_icon(:"mail-reply") # Refund icon style reference in solidus_backend
120
+ within '.new_refund' do
121
+ select refund_reason.name, from: 'Reason'
122
+ click_button 'Refund'
123
+ end
124
+ end
125
+
126
+ def partially_refund_payment(refund_reason, amount)
127
+ click_icon(:"mail-reply") # Refund icon style reference in solidus_backend
128
+ within '.new_refund' do
129
+ select refund_reason.name, from: 'Reason'
130
+ fill_in 'Amount', with: amount
131
+ click_button 'Refund'
132
+ end
133
+ end
134
+
135
+ def void_payment
136
+ click_icon(:void)
137
+ end
138
+
139
+ def capture_payment
140
+ click_icon(:capture)
141
+ end
142
+
143
+ def cancel_order
144
+ visit "/admin/orders/#{order.number}/edit"
145
+ accept_alert do
146
+ click_on 'Cancel'
147
+ end
148
+ end
149
+
150
+ # Helper methods for checking expected outcomes and states
151
+
152
+ def expects_page_to_display_successfully_captured_payment(payment)
153
+ expect(page).to have_content('Payment Updated')
154
+ expect(payment.reload.state).to eq('completed')
155
+ expect(payment.capture_events.first.amount).to eq(payment.amount)
156
+ end
157
+
158
+ def expects_page_to_display_successfully_refunded_payment(payment)
159
+ expect(page).to have_content('Refund has been successfully created!')
160
+ expect(payment).to be_fully_refunded
161
+ end
162
+
163
+ def expects_page_to_display_successfully_partially_refunded_payment(payment, amount)
164
+ expect(page).to have_content('Refund has been successfully created!')
165
+ expect(payment.reload.state).to eq('completed')
166
+ expect(payment.refunds.first.amount).to eq(amount)
167
+ end
168
+
169
+ def expects_page_to_display_successfully_voided_payment(payment)
170
+ expect(page).to have_content('Payment Updated')
171
+ expect(payment.reload.state).to eq('void')
172
+ end
173
+
174
+ def expects_page_to_display_successfully_canceled_order_payment(payment)
175
+ expect(page).to have_content('Order canceled')
176
+ expect(payment.reload.state).to eq('void')
177
+ end
178
+
179
+ def expects_page_to_display_capture_fail_message(payment, message)
180
+ expect(page).to have_content(message)
181
+ expect(payment.reload.state).to eq('failed')
182
+ end
183
+
184
+ def expects_page_to_display_payment(payment)
185
+ click_on 'Payments'
186
+ expect(page).to have_content(payment.number)
187
+ end
188
+
189
+ def expects_page_to_display_log_messages(log_messages = [])
190
+ expect(page).to have_content('Log Entries')
191
+ log_messages.each do |log_message|
192
+ expect(page).to have_content(log_message)
193
+ end
194
+ end
195
+
196
+ def expects_payment_to_be_refunded_on_stripe(payment, amount)
197
+ stripe_refund = fetch_stripe_refund(payment)
198
+ expect(stripe_refund.amount).to eq(amount * 100)
199
+ end
200
+
201
+ def expects_payment_to_be_voided_on_stripe(payment)
202
+ stripe_payment_intent = fetch_stripe_payment_intent(payment)
203
+ expect(stripe_payment_intent.status).to eq('canceled')
204
+ end
205
+
206
+ def expects_payment_to_have_correct_capture_amount_on_stripe(payment, amount)
207
+ stripe_payment_intent = fetch_stripe_payment_intent(payment)
208
+ expect(stripe_payment_intent.amount_received).to eq(amount * 100)
209
+ end
210
+ end