spree_stripe 1.3.0 → 1.5.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -21
  3. data/Rakefile +5 -1
  4. data/app/assets/images/payment_icons/banktransfer.svg +3 -0
  5. data/app/assets/images/payment_icons/sepadebit.svg +22 -0
  6. data/app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb +1 -1
  7. data/app/controllers/spree_stripe/apple_pay_domain_verification_controller.rb +1 -1
  8. data/app/controllers/spree_stripe/payment_intents_controller.rb +4 -4
  9. data/app/controllers/spree_stripe/store_controller_decorator.rb +3 -1
  10. data/app/helpers/spree_stripe/checkout_helper_decorator.rb +3 -1
  11. data/app/jobs/spree_stripe/attach_customer_to_credit_card_job.rb +18 -0
  12. data/app/models/spree_stripe/gateway.rb +59 -27
  13. data/app/models/spree_stripe/order_decorator.rb +0 -8
  14. data/app/models/spree_stripe/payment_decorator.rb +2 -2
  15. data/app/models/spree_stripe/payment_intent.rb +16 -1
  16. data/app/models/spree_stripe/payment_method_decorator.rb +1 -1
  17. data/app/models/spree_stripe/payment_sources/bank_transfer.rb +17 -0
  18. data/app/models/spree_stripe/payment_sources/sepa_debit.rb +4 -0
  19. data/app/presenters/spree_stripe/payment_intent_presenter.rb +3 -0
  20. data/app/services/spree_stripe/complete_order.rb +9 -3
  21. data/app/services/spree_stripe/create_payment.rb +9 -0
  22. data/app/services/spree_stripe/create_source.rb +2 -0
  23. data/app/services/spree_stripe/register_domain.rb +2 -2
  24. data/app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb +1 -4
  25. data/app/subscribers/spree_stripe/order_completed_subscriber.rb +14 -0
  26. data/config/locales/en.yml +1 -0
  27. data/lib/spree_stripe/engine.rb +14 -3
  28. data/lib/spree_stripe/version.rb +1 -1
  29. data/lib/spree_stripe.rb +0 -1
  30. metadata +37 -55
  31. data/LICENSE.md +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f99142efe129494bc6d2bd1690e85dfae817eb652833217b24d14055811d778
4
- data.tar.gz: 9916be21b249554ec30e9b46e014634626a2763d1881d8808b14eb87079bc228
3
+ metadata.gz: '0685a047c48bfcef886c06c2c161c4f68606c55374809544179708b0bf0872a4'
4
+ data.tar.gz: 06a95c3d4f19e3919bf9a2e4ba7534d2aae694cc1dcb6bda340112e3412c53c5
5
5
  SHA512:
6
- metadata.gz: 5791e797836bf0b150915a01b61649ca3da6e5b4f758ec446842d1503919c59f4aaabc09767651e50e5d7c4deaa9bdda3bdb491529bbed20226331cca7fb15bf
7
- data.tar.gz: 9ec61e460685afd67d0f19aaba2af8d5a33716397e9f56d9939b4b24020cd569a7d11322e7489345bd0be8b1250d777bc69bead40e944de120db58177ea6cec2
6
+ metadata.gz: c8648f877e85f2d7a2ca4fc9bcb665d0917ed604787ecc389e50efffccaf293b787ddba10f08a932081362885abae8744cdb253608ce9f2e9057e9f8c89dc58c
7
+ data.tar.gz: ac48dc36a6279dc83dcd2babaa6109fa47741db163d0359cc54c08e4eb176a7b652ff267021746036bb70527e8c79a57891771bbb7ea81c1a6c0ca844c8999de
data/README.md CHANGED
@@ -15,10 +15,6 @@ If you like what you see, consider giving this repo a GitHub star :star:
15
15
 
16
16
  Thank you for supporting Spree open-source :heart:
17
17
 
18
- > [!IMPORTANT]
19
- > This Stripe integration for Spree is free to use for private projects but requires a [Commercial License](https://spreecommerce.org/why-consider-a-commercial-license-for-your-multi-tenant-or-saas-spree-based-project/) if you're planning to use it for your [SaaS](https://spreecommerce.org/multi-tenant-white-label-ecommerce/) or a [multi-tenant eCommerce](https://spreecommerce.org/multi-tenant-white-label-ecommerce/) website.
20
- > Feel free to [reach out](https://spreecommerce.org/get-started/) to learn more.
21
-
22
18
  > [!TIP]
23
19
  > Looking for a [Stripe Connect integration](#looking-for-a-stripe-connect-integration-for-spree) for Spree? It's available with the [Enterprise Edition](https://spreecommerce.org/spree-commerce-version-comparison-community-edition-vs-enterprise-edition/).
24
20
 
@@ -114,7 +110,7 @@ If you'd like to contribute, please take a look at the
114
110
  [instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
115
111
  pull request.
116
112
 
117
- Copyright (c) 2025 [Vendo Connect Inc.](https://getvendo.com), released under [AGPL 3.0 license](https://github.com/spree/spree_stripe/blob/main/LICENSE.md). Please refer to [this blog post](https://spreecommerce.org/why-spree-is-changing-its-open-source-license-to-agpl-3-0-and-introducing-a-commercial-license/) and [that blog post](https://spreecommerce.org/open-source-ecommerce-transparency/) to learn more about Spree licensing.
113
+ Copyright (c) 2026 [Vendo Connect Inc.](https://getvendo.com), released under [MIT](https://github.com/spree/spree_stripe/blob/main/LICENSE).
118
114
 
119
115
  ## Looking for a Stripe Connect integration for Spree?
120
116
 
@@ -128,19 +124,3 @@ Spree Commerce [Enterprise Edition](https://spreecommerce.org/spree-commerce-ver
128
124
  - Built-in fraud prevention tools
129
125
 
130
126
  Feel free to [reach out](https://spreecommerce.org/get-started/) to learn more.
131
-
132
- ## Spree 5 Announcement & Demo
133
-
134
- [![Spree Commerce 5 version](https://vendo-production-res.cloudinary.com/image/upload/w_2000/q_auto/v1742985405/docs/github/Spree_Commerce_open-source_eCommerce_myzurl.jpg)](https://spreecommerce.org/announcing-spree-5-the-biggest-open-source-release-ever/)
135
-
136
- We’re thrilled to unveil [Spree 5](https://spreecommerce.org/announcing-spree-5-the-biggest-open-source-release-ever/
137
- ) — the most powerful and feature-packed open-source release in Spree Commerce’s history, including:
138
- - A completely revamped Admin Dashboard experience: boost your team's productivity
139
- - A Mobile-First, No-code Customizable Storefront: raise conversions and loyalty
140
- - New integrations: a native [Stripe integration](https://github.com/spree/spree_stripe), and also Stripe Connect, Klaviyo integrations available with the Enterprise Edition
141
- - Enterprise Edition Admin Features: Audit Log, [Multi-Vendor Marketplace](https://spreecommerce.org/marketplace-ecommerce/), [Multi-tenant / White-label SaaS eCommerce](https://spreecommerce.org/multi-tenant-white-label-ecommerce/)
142
-
143
- Read the [full Spree 5 announcement here](https://spreecommerce.org/announcing-spree-5-the-biggest-open-source-release-ever/).
144
-
145
- Check out the [Spree 5 demo](https://demo.spreecommerce.org/) for yourself, including this Stripe integration.
146
-
data/Rakefile CHANGED
@@ -17,5 +17,9 @@ end
17
17
  desc 'Generates a dummy app for testing'
18
18
  task :test_app do
19
19
  ENV['LIB_NAME'] = 'spree_stripe'
20
- Rake::Task['extension:test_app'].invoke
20
+ Rake::Task['extension:test_app'].execute(
21
+ install_storefront: true,
22
+ install_admin: true
23
+ )
24
+ system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_api_v2:install')
21
25
  end
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z" />
3
+ </svg>
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" id="katman_1" x="0px" y="0px" style="enable-background:new 0 0 1190.55 841.89;" xml:space="preserve" viewBox="238.65 160 712.7 521">
2
+ <style type="text/css">
3
+ .st0{fill:#26367D;}
4
+ .st1{clip-path:url(#SVGID_00000165933728503162618370000013584018841918814628_);}
5
+ .st2{fill:#FFFFFF;}
6
+ .st3{fill:#FBBA2C;}
7
+ </style>
8
+ <path class="st0" d="M302,160h586c34.99,0,63.35,28.36,63.35,63.35v394.3c0,34.99-28.36,63.35-63.35,63.35H302 c-34.99,0-63.35-28.36-63.35-63.35v-394.3C238.65,188.36,267.01,160,302,160z"/>
9
+ <g>
10
+ <defs>
11
+ <rect id="SVGID_1_" x="341.04" y="162.96" width="507.9" height="506.81"/>
12
+ </defs>
13
+ <clipPath id="SVGID_00000137126528663880082090000017192719521453020821_">
14
+ <use xlink:href="#SVGID_1_" style="overflow:visible;"/>
15
+ </clipPath>
16
+ <g style="clip-path:url(#SVGID_00000137126528663880082090000017192719521453020821_);">
17
+ <path class="st0" d="M367.09,226.31h455.81c14.38,0,26.04,13.62,26.04,30.41v319.29c0,16.79-11.66,30.41-26.04,30.41H367.09 c-14.38,0-26.05-13.62-26.05-30.41V256.72C341.04,239.93,352.71,226.31,367.09,226.31z"/>
18
+ <path class="st2" d="M404.53,381.92c0,3.22,0,5.34,1.06,8.55c0,1.09,1.05,2.12,1.05,3.22c2.06,13.91,8.3,20.27,21.76,21.38 c9.3,1.08,19.64,2.12,29,3.22c5.18,0,8.3,3.22,8.3,9.58c0,6.38-2.06,10.69-8.3,10.69h-18.64c-6.24,0-8.3-3.22-9.3-12.81h-24.82 v9.66c0,1.08,1.06,3.22,1.06,4.24c1.05,9.6,7.24,16.03,16.58,18.15c6.22,1.09,12.4,1.09,18.64,2.12h18.64 c6.24-1.09,11.4-2.12,17.58-4.24c7.24-2.12,12.42-7.48,13.46-16.03c1.06-7.48,1.06-16.03,1.06-23.5 c-1.05-8.55-5.16-14.94-13.46-17.1l-18.64-3.23c-6.22-1.09-12.42-1.09-18.64-1.09c-7.24,0-9.3-3.22-9.3-9.58 c0-7.48,3.12-9.6,9.3-10.69h10.34c9.3,0,11.4,1.11,12.42,10.69v1.09h24.82v-6.43c0-13.91-6.22-22.47-20.7-24.6 c-14.46-2.12-28.98-2.12-43.46,1.09c-11.4,2.12-16.58,7.48-18.64,20.27C405.55,378.69,404.53,379.8,404.53,381.92z M631.19,458.83 v-27.8h35.18c13.46,0,22.76-8.55,24.82-21.38c1.06-8.55,2.06-17.1,1.06-25.61c-1.06-19.24-10.34-28.82-28.98-29.93 c-17.58-1.09-35.18-1.09-52.82-1.09c-4.12,0-5.18,1.11-5.18,5.35v101.5C613.61,459.92,622.91,459.92,631.19,458.83z M631.19,407.53V377.6c0,0,2.06-2.12,3.12-2.12h18.64c7.24,1.11,10.36,4.32,10.36,11.78v10.69c0,6.43-2.06,9.58-8.3,9.58H631.19z M708.83,459.92c1.06-5.34,2.06-11.77,4.18-17.1c0-1.11,2.06-2.14,3.12-2.14h33.1c1.06,0,2.06,2.12,3.12,3.22 c2.06,5.35,3.12,10.69,5.18,16.03h27.94c-1.06-2.12-2.06-5.34-3.12-7.46l-27.94-96.23c0-1.09-2.06-3.22-3.12-3.22h-39.34 c-4.18,12.88-7.24,25.67-11.4,38.49c-6.24,22.41-13.46,45.9-19.64,68.37C680.9,459.87,708.83,459.92,708.83,459.92z M747.12,421.43h-26.94c4.12-15,8.3-31.01,12.42-45.96h1.05c4.18,15,9.34,29.93,13.46,45.96H747.12z"/>
19
+ <path class="st3" d="M575.35,441.71c-16.6,0-29-7.46-34.18-19.24h37.28l4.12-11.77h-45.58v-8.55l46.59,2.12l5.16-13.86h-49.68 c5.16-14.94,18.64-22.46,36.22-22.46c7.44-0.05,14.81,1.44,21.76,4.26l4.12-14.94c-4.12-2.14-13.46-4.26-26.92-4.26 c-27.95,0-49.71,13.91-56.94,38.49h-12.35l-5.18,11.78h15.52v9.53h-9.3l-6.22,11.78h17.58c6.24,21.37,25.88,34.18,53.82,34.18 c13.46,0,23.82-2.12,27.94-4.26l-3.12-14.94C591.93,440.69,583.63,441.71,575.35,441.71z"/>
20
+ </g>
21
+ </g>
22
+ </svg>
@@ -56,7 +56,7 @@ module Spree
56
56
  render_error_payload(Spree.t('order_already_completed'))
57
57
  elsif order != spree_current_order
58
58
  raise ActiveRecord::RecordNotFound
59
- elsif stripe_payment_intent.status == 'succeeded'
59
+ elsif @payment_intent.accepted?
60
60
  spree_authorize! :update, spree_current_order, order_token
61
61
 
62
62
  SpreeStripe::CompleteOrder.new(payment_intent: @payment_intent).call
@@ -1,5 +1,5 @@
1
1
  module SpreeStripe
2
- class ApplePayDomainVerificationController < ::Spree::StoreController
2
+ class ApplePayDomainVerificationController < Spree::BaseController
3
3
  def show
4
4
  gateway = SpreeStripe::Gateway.last
5
5
 
@@ -1,8 +1,8 @@
1
1
  # this is the endpoint that Stripe JS SDK will redirect customer to after payment
2
2
  # it will handle the payment intent status and process the payment
3
3
  module SpreeStripe
4
- class PaymentIntentsController < Spree::StoreController
5
- include Spree::CheckoutAnalyticsHelper
4
+ class PaymentIntentsController < defined?(Spree::StoreController) ? Spree::StoreController : Spree::BaseController
5
+ include Spree::CheckoutAnalyticsHelper if defined?(Spree::CheckoutAnalyticsHelper)
6
6
 
7
7
  # GET /spree/payment_intents/:id
8
8
  def show
@@ -26,11 +26,11 @@ module SpreeStripe
26
26
  elsif @order.completed?
27
27
  redirect_to spree.checkout_complete_path(@order.token), status: :see_other
28
28
  # if the payment intent is successful, we need to process the payment and complete the order
29
- elsif @stripe_payment_intent.status == 'succeeded'
29
+ elsif @payment_intent_record.accepted?
30
30
  @order = SpreeStripe::CompleteOrder.new(payment_intent: @payment_intent_record).call
31
31
 
32
32
  # set the session flag to indicate that the order was placed now
33
- track_checkout_completed if @order.completed?
33
+ track_checkout_completed if @order.completed? && defined?(track_checkout_completed)
34
34
 
35
35
  # redirect the customer to the complete checkout page
36
36
  redirect_to spree.checkout_complete_path(@order.token), status: :see_other
@@ -6,4 +6,6 @@ module SpreeStripe
6
6
  end
7
7
  end
8
8
 
9
- Spree::StoreController.prepend(SpreeStripe::StoreControllerDecorator) if defined?(Spree::StoreController)
9
+ if defined?(Spree::StoreController)
10
+ Spree::StoreController.prepend(SpreeStripe::StoreControllerDecorator) if defined?(Spree::StoreController)
11
+ end
@@ -12,4 +12,6 @@ module SpreeStripe
12
12
  end
13
13
  end
14
14
 
15
- Spree::CheckoutHelper.prepend(SpreeStripe::CheckoutHelperDecorator)
15
+ if defined?(Spree::CheckoutHelper)
16
+ Spree::CheckoutHelper.prepend(SpreeStripe::CheckoutHelperDecorator)
17
+ end
@@ -0,0 +1,18 @@
1
+ module SpreeStripe
2
+ class AttachCustomerToCreditCardJob < BaseJob
3
+ def perform(order_id)
4
+ return if Spree.user_class.blank?
5
+
6
+ order = Spree::Order.find_by(id: order_id)
7
+ return if order.blank? || order.user_id.blank?
8
+
9
+ gateway = order.store.stripe_gateway
10
+ return if gateway.blank?
11
+
12
+ user = Spree.user_class.find_by(id: order.user_id)
13
+ return if user.blank?
14
+
15
+ gateway.attach_customer_to_credit_card(user)
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,9 @@ module SpreeStripe
2
2
  class Gateway < ::Spree::Gateway
3
3
  include SpreeStripe::Gateway::Tax if defined?(SpreeStripe::Gateway::Tax)
4
4
 
5
+ DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES = %w[sepa_debit us_bank_account].freeze
6
+ BANK_PAYMENT_METHOD_TYPES = %w[customer_balance us_bank_account].freeze
7
+
5
8
  preference :publishable_key, :password
6
9
  preference :secret_key, :password
7
10
 
@@ -23,6 +26,28 @@ module SpreeStripe
23
26
  self.class
24
27
  end
25
28
 
29
+ def payment_intent_accepted?(payment_intent)
30
+ payment_intent.status.in?(payment_intent_accepted_statuses(payment_intent))
31
+ end
32
+
33
+ def payment_intent_delayed_notification?(payment_intent)
34
+ payment_method = payment_intent.payment_method
35
+ return false unless payment_method.respond_to?(:type)
36
+
37
+ payment_intent.payment_method.type.in?(DELAYED_NOTIFICATION_PAYMENT_METHOD_TYPES)
38
+ end
39
+
40
+ def payment_intent_charge_not_required?(payment_intent)
41
+ payment_intent_bank_payment_method?(payment_intent)
42
+ end
43
+
44
+ def payment_intent_bank_payment_method?(payment_intent)
45
+ payment_method = payment_intent.payment_method
46
+ return false unless payment_method.respond_to?(:type)
47
+
48
+ payment_intent.payment_method.type.in?(BANK_PAYMENT_METHOD_TYPES)
49
+ end
50
+
26
51
  # @param amount_in_cents [Integer] the amount in cents to capture
27
52
  # @param payment_source [Spree::CreditCard | Spree::PaymentSource]
28
53
  # @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
@@ -52,7 +77,7 @@ module SpreeStripe
52
77
  payment = ensure_payment_intent_exists_for_payment(payment, amount_in_cents, payment_source)
53
78
  stripe_payment_intent = retrieve_payment_intent(payment.response_code)
54
79
 
55
- response = if stripe_payment_intent.status == 'succeeded'
80
+ response = if payment_intent_accepted?(stripe_payment_intent)
56
81
  # payment intent is already confirmed via Stripe JS SDK
57
82
  stripe_payment_intent
58
83
  else
@@ -70,7 +95,7 @@ module SpreeStripe
70
95
  payment_intent: payment_intent_id
71
96
  }
72
97
 
73
- response = send_request { Stripe::Refund.create(payload) }
98
+ response = send_request { |opts| Stripe::Refund.create(payload, opts) }
74
99
 
75
100
  success(response.id, response)
76
101
  end
@@ -92,8 +117,13 @@ module SpreeStripe
92
117
  end
93
118
  end
94
119
 
95
- def void(response_code, source, gateway_options)
96
- raise NotImplementedError
120
+ def void(response_code, _source, _gateway_options)
121
+ return failure('Response code is blank') if response_code.blank?
122
+
123
+ protect_from_error do
124
+ response = cancel_payment_intent(response_code)
125
+ success(response.id, response)
126
+ end
97
127
  end
98
128
 
99
129
  def cancel(payment_intent_id, payment = nil)
@@ -135,7 +165,7 @@ module SpreeStripe
135
165
  # @return [Stripe::Customer] the created Stripe customer
136
166
  def create_customer(order: nil, user: nil)
137
167
  payload = build_customer_payload(order: order, user: user)
138
- response = send_request { Stripe::Customer.create(payload) }
168
+ response = send_request { |opts| Stripe::Customer.create(payload, opts) }
139
169
 
140
170
  customer = gateway_customers.build(user: user, profile_id: response.id)
141
171
  customer.save! if user.present?
@@ -155,7 +185,7 @@ module SpreeStripe
155
185
  return if customer.blank?
156
186
 
157
187
  payload = build_customer_payload(order: order, user: user)
158
- send_request { Stripe::Customer.update(customer.profile_id, payload) }
188
+ send_request { |opts| Stripe::Customer.update(customer.profile_id, payload, opts) }
159
189
  end
160
190
 
161
191
  # Creates a Stripe payment intent for the order
@@ -176,7 +206,7 @@ module SpreeStripe
176
206
  ).call
177
207
 
178
208
  protect_from_error do
179
- response = send_request { Stripe::PaymentIntent.create(payload) }
209
+ response = send_request { |opts| Stripe::PaymentIntent.create(payload, opts) }
180
210
 
181
211
  success(response.id, response)
182
212
  end
@@ -198,29 +228,29 @@ module SpreeStripe
198
228
  payment_method_id: payment_method_id
199
229
  ).call.slice(:amount, :currency, :payment_method, :shipping, :customer)
200
230
 
201
- response = send_request { Stripe::PaymentIntent.update(payment_intent_id, payload) }
231
+ response = send_request { |opts| Stripe::PaymentIntent.update(payment_intent_id, payload, opts) }
202
232
 
203
233
  success(response.id, response)
204
234
  end
205
235
  end
206
236
 
207
237
  def retrieve_payment_intent(payment_intent_id)
208
- send_request { Stripe::PaymentIntent.retrieve(payment_intent_id) }
238
+ send_request { |opts| Stripe::PaymentIntent.retrieve({ id: payment_intent_id, expand: ['payment_method'] }, opts) }
209
239
  end
210
240
 
211
241
  def confirm_payment_intent(payment_intent_id)
212
- send_request { Stripe::PaymentIntent.confirm(payment_intent_id) }
242
+ send_request { |opts| Stripe::PaymentIntent.confirm(payment_intent_id, {}, opts) }
213
243
  end
214
244
 
215
245
  def capture_payment_intent(payment_intent_id, amount_in_cents)
216
- send_request { Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }) }
246
+ send_request { |opts| Stripe::PaymentIntent.capture(payment_intent_id, { amount_to_capture: amount_in_cents }, opts) }
217
247
  end
218
248
 
219
249
  # Cancels a Stripe payment intent
220
250
  #
221
251
  # @param payment_intent_id [String] Stripe payment intent ID, eg. pi_123
222
252
  def cancel_payment_intent(payment_intent_id)
223
- send_request { Stripe::PaymentIntent.cancel(payment_intent_id) }
253
+ send_request { |opts| Stripe::PaymentIntent.cancel(payment_intent_id, {}, opts) }
224
254
  end
225
255
 
226
256
  # Ensures a Stripe payment intent exists for Spree payment
@@ -252,12 +282,12 @@ module SpreeStripe
252
282
  end
253
283
 
254
284
  def retrieve_charge(charge_id)
255
- send_request { Stripe::Charge.retrieve(charge_id) }
285
+ send_request { |opts| Stripe::Charge.retrieve(charge_id, opts) }
256
286
  end
257
287
 
258
288
  def create_ephemeral_key(customer_id)
259
289
  protect_from_error do
260
- response = send_request { Stripe::EphemeralKey.create({ customer: customer_id }, { stripe_version: Stripe.api_version }) }
290
+ response = send_request { |opts| Stripe::EphemeralKey.create({ customer: customer_id }, opts.merge(stripe_version: Stripe.api_version)) }
261
291
 
262
292
  success(response.secret, response)
263
293
  end
@@ -265,7 +295,7 @@ module SpreeStripe
265
295
 
266
296
  def create_setup_intent(customer_id)
267
297
  protect_from_error do
268
- response = send_request { Stripe::SetupIntent.create({ customer: customer_id, automatic_payment_methods: { enabled: true } }) }
298
+ response = send_request { |opts| Stripe::SetupIntent.create({ customer: customer_id, automatic_payment_methods: { enabled: true } }, opts) }
269
299
 
270
300
  success(response.client_secret, response)
271
301
  end
@@ -273,9 +303,9 @@ module SpreeStripe
273
303
 
274
304
  def create_tax_calculation(order)
275
305
  protect_from_error do
276
- send_request do
306
+ send_request do |opts|
277
307
  Stripe::Tax::Calculation.create(
278
- SpreeStripe::TaxPresenter.new(order: order).call
308
+ SpreeStripe::TaxPresenter.new(order: order).call, opts
279
309
  )
280
310
  end
281
311
  end
@@ -289,7 +319,7 @@ module SpreeStripe
289
319
  expand: ['line_items']
290
320
  }
291
321
 
292
- send_request { Stripe::Tax::Transaction.create_from_calculation(payload) }
322
+ send_request { |opts| Stripe::Tax::Transaction.create_from_calculation(payload, opts) }
293
323
  end
294
324
  end
295
325
 
@@ -300,7 +330,7 @@ module SpreeStripe
300
330
  customer = fetch_or_create_customer(user: user)
301
331
  return if customer.blank?
302
332
 
303
- send_request { Stripe::PaymentMethod.attach(payment_method_id, { customer: customer.profile_id }) }
333
+ send_request { |opts| Stripe::PaymentMethod.attach(payment_method_id, { customer: customer.profile_id }, opts) }
304
334
 
305
335
  user.default_credit_card.update(gateway_customer_profile_id: customer.profile_id, gateway_customer_id: customer.id)
306
336
  rescue Stripe::StripeError => e
@@ -357,16 +387,11 @@ module SpreeStripe
357
387
  end
358
388
 
359
389
  def api_options
360
- { api_key: preferred_secret_key, api_version: Stripe.api_version }
390
+ { api_key: preferred_secret_key }
361
391
  end
362
392
 
363
- def send_request(&block)
364
- result, _response = client.request(&block)
365
- result
366
- end
367
-
368
- def client
369
- @client ||= Stripe::StripeClient.new(api_options)
393
+ def send_request
394
+ yield(api_options)
370
395
  end
371
396
 
372
397
  private
@@ -419,5 +444,12 @@ module SpreeStripe
419
444
 
420
445
  SpreeStripe::CustomerPresenter.new(name: name, email: email, address: address).call
421
446
  end
447
+
448
+ def payment_intent_accepted_statuses(payment_intent)
449
+ statuses = %w[succeeded]
450
+ statuses << 'processing' if payment_intent_delayed_notification?(payment_intent)
451
+ statuses << 'requires_action' if payment_intent_charge_not_required?(payment_intent)
452
+ statuses
453
+ end
422
454
  end
423
455
  end
@@ -26,14 +26,6 @@ module SpreeStripe
26
26
  payment_intents.update_all(customer_id: customer_id) if customer_id.present?
27
27
  end
28
28
 
29
- def persist_user_credit_card
30
- super
31
- stripe_gateway = store.stripe_gateway
32
- return if stripe_gateway.blank?
33
-
34
- stripe_gateway.attach_customer_to_credit_card(user)
35
- end
36
-
37
29
  def stripe_payment_intent
38
30
  @stripe_payment_intent ||= payment_intents.last.stripe_payment_intent
39
31
  end
@@ -14,13 +14,13 @@ module SpreeStripe
14
14
  'pass' => 'P',
15
15
  'unchecked' => 'I'
16
16
  }
17
- }.freeze
17
+ }.freeze unless defined?(AVS_CODES)
18
18
 
19
19
  CVV_CODES = {
20
20
  'pass' => 'M',
21
21
  'fail' => 'N',
22
22
  'unchecked' => 'P'
23
- }.freeze
23
+ }.freeze unless defined?(CVV_CODES)
24
24
 
25
25
  def self.prepended(base)
26
26
  base.store_accessor :private_metadata, :stripe_charge_id
@@ -26,12 +26,27 @@ module SpreeStripe
26
26
  delegate :api_options, to: :payment_method
27
27
  delegate :store, :currency, to: :order
28
28
 
29
+ def accepted?
30
+ payment_method.payment_intent_accepted?(stripe_payment_intent)
31
+ end
32
+
33
+ def successful?
34
+ stripe_payment_intent.status == 'succeeded'
35
+ end
36
+
37
+ def charge_not_required?
38
+ payment_method.payment_intent_charge_not_required?(stripe_payment_intent)
39
+ end
40
+
29
41
  def stripe_payment_intent
30
42
  @stripe_payment_intent ||= payment_method.retrieve_payment_intent(stripe_id)
31
43
  end
32
44
 
33
45
  def stripe_charge
34
- @stripe_charge ||= payment_method.retrieve_charge(stripe_payment_intent.latest_charge)
46
+ @stripe_charge ||= begin
47
+ latest_charge = stripe_payment_intent.latest_charge
48
+ latest_charge.present? ? payment_method.retrieve_charge(latest_charge) : nil
49
+ end
35
50
  end
36
51
 
37
52
  # here we create a payment if it doesn't exist
@@ -1,6 +1,6 @@
1
1
  module SpreeStripe
2
2
  module PaymentMethodDecorator
3
- STRIPE_TYPE = 'SpreeStripe::Gateway'.freeze
3
+ STRIPE_TYPE = 'SpreeStripe::Gateway'.freeze unless defined?(STRIPE_TYPE)
4
4
 
5
5
  def self.prepended(base)
6
6
  base.has_many :payment_methods_webhook_keys, class_name: 'SpreeStripe::PaymentMethodsWebhookKey'
@@ -0,0 +1,17 @@
1
+ module SpreeStripe
2
+ module PaymentSources
3
+ class BankTransfer < ::Spree::PaymentSource
4
+ def actions
5
+ %w[credit]
6
+ end
7
+
8
+ def self.display_name
9
+ Spree.t(:bank_transfer)
10
+ end
11
+
12
+ def name
13
+ Spree.t(:bank_transfer)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -8,6 +8,10 @@ module SpreeStripe
8
8
  def self.display_name
9
9
  'SEPA Debit'
10
10
  end
11
+
12
+ def name
13
+ 'SEPA Debit'
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -76,6 +76,9 @@ module SpreeStripe
76
76
  payment_method_options: {
77
77
  card: {
78
78
  setup_future_usage: SETUP_FUTURE_USAGE
79
+ },
80
+ sepa_debit: {
81
+ setup_future_usage: SETUP_FUTURE_USAGE
79
82
  }
80
83
  }
81
84
  }
@@ -9,7 +9,7 @@ module SpreeStripe
9
9
  def call
10
10
  order = payment_intent.order
11
11
 
12
- return order if order.completed? || order.canceled?
12
+ return order if (order.completed? && order.paid?) || order.canceled?
13
13
 
14
14
  charge = payment_intent.stripe_charge
15
15
 
@@ -21,10 +21,14 @@ module SpreeStripe
21
21
  payment = payment_intent.find_or_create_payment!
22
22
 
23
23
  # process the payment
24
- payment.process!
24
+ if payment_intent.successful?
25
+ payment.process!
26
+ else
27
+ payment.authorize!
28
+ end
25
29
 
26
30
  # complete the order
27
- Spree::Dependencies.checkout_complete_service.constantize.call(order: order)
31
+ Spree::Dependencies.checkout_complete_service.constantize.call(order: order) unless order.completed?
28
32
  end
29
33
 
30
34
  order.reload
@@ -34,6 +38,8 @@ module SpreeStripe
34
38
 
35
39
  # we need to perform this for quick checkout orders which do not have these fields filled
36
40
  def add_customer_information(order, charge)
41
+ return order if charge.blank?
42
+
37
43
  billing_details = charge.billing_details
38
44
  address = billing_details.address
39
45
 
@@ -19,6 +19,15 @@ module SpreeStripe
19
19
  stripe_billing_details: stripe_charge.billing_details,
20
20
  gateway: gateway
21
21
  ).call
22
+ elsif payment_intent.charge_not_required?
23
+ stripe_payment_intent = payment_intent.stripe_payment_intent
24
+ source = SpreeStripe::CreateSource.new(
25
+ order: order,
26
+ stripe_payment_method_details: stripe_payment_intent.payment_method,
27
+ stripe_payment_method_id: stripe_payment_intent.payment_method.id,
28
+ stripe_billing_details: nil,
29
+ gateway: gateway
30
+ ).call
22
31
  end
23
32
 
24
33
  # sometimes a job is re-tried and creates a double payment record so we need to avoid it!
@@ -34,6 +34,8 @@ module SpreeStripe
34
34
  SpreeStripe::PaymentSources::Link.create!(source_params)
35
35
  when 'affirm'
36
36
  SpreeStripe::PaymentSources::Affirm.create!(source_params)
37
+ when 'customer_balance', 'us_bank_account'
38
+ SpreeStripe::PaymentSources::BankTransfer.create!(source_params)
37
39
  else
38
40
  raise "[STRIPE] Unknown payment method #{stripe_payment_method_details.type}"
39
41
  end
@@ -3,14 +3,14 @@ module SpreeStripe
3
3
  def call(model:)
4
4
  gateway = model.is_a?(Spree::Store) ? model.stripe_gateway : model.store.stripe_gateway
5
5
 
6
- payment_method_domain = gateway.send_request { Stripe::PaymentMethodDomain.create({ domain_name: model.url }) }
6
+ payment_method_domain = gateway.send_request { |opts| Stripe::PaymentMethodDomain.create({ domain_name: model.url }, opts) }
7
7
 
8
8
  attributes_to_update = { stripe_apple_pay_domain_id: payment_method_domain.id }
9
9
 
10
10
  tld_length = model.url.split('.').length
11
11
  if tld_length > 2 && model.is_a?(Spree::CustomDomain)
12
12
  top_level_domain_name = model.url.split('.').last(tld_length - 1).join('.')
13
- top_level_domain = gateway.send_request { Stripe::PaymentMethodDomain.create({ domain_name: top_level_domain_name }) }
13
+ top_level_domain = gateway.send_request { |opts| Stripe::PaymentMethodDomain.create({ domain_name: top_level_domain_name }, opts) }
14
14
  attributes_to_update[:stripe_top_level_domain_id] = top_level_domain.id
15
15
  end
16
16
 
@@ -2,16 +2,13 @@ module SpreeStripe
2
2
  module WebhookHandlers
3
3
  class PaymentIntentPaymentFailed
4
4
  def call(event)
5
- return unless ['affirm', 'afterpay_clearpay'].include?(event.data.object&.last_payment_error&.payment_method&.type)
6
-
7
5
  payment_intent = SpreeStripe::PaymentIntent.find_by(stripe_id: event.data.object.id)
8
6
  return if payment_intent.nil?
9
7
 
10
8
  order = payment_intent.order
11
-
12
9
  return if order.canceled?
13
10
 
14
- order.cancel!
11
+ order.cancel! if order.can_cancel?
15
12
  end
16
13
  end
17
14
  end
@@ -0,0 +1,14 @@
1
+ module SpreeStripe
2
+ class OrderCompletedSubscriber < Spree::Subscriber
3
+ subscribes_to 'order.completed'
4
+
5
+ on 'order.completed', :attach_customer_to_credit_card
6
+
7
+ def attach_customer_to_credit_card(event)
8
+ order_id = event.payload['id']
9
+ return if order_id.blank?
10
+
11
+ SpreeStripe::AttachCustomerToCreditCardJob.perform_later(order_id)
12
+ end
13
+ end
14
+ end
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  en:
3
3
  spree:
4
+ bank_transfer: Bank Transfer
4
5
  stripe:
5
6
  payment_intent_errors:
6
7
  canceled: Payment intent canceled
@@ -4,6 +4,9 @@ module SpreeStripe
4
4
  isolate_namespace Spree
5
5
  engine_name 'spree_stripe'
6
6
 
7
+ # Add app/subscribers to autoload paths
8
+ config.paths.add 'app/subscribers', eager_load: true
9
+
7
10
  # use rspec for tests
8
11
  config.generators do |g|
9
12
  g.test_framework :rspec
@@ -22,9 +25,11 @@ module SpreeStripe
22
25
  end
23
26
 
24
27
  initializer 'spree_stripe.importmap', before: 'importmap' do |app|
25
- app.config.importmap.paths << root.join('config/importmap.rb')
26
- # https://github.com/rails/importmap-rails?tab=readme-ov-file#sweeping-the-cache-in-development-and-test
27
- app.config.importmap.cache_sweepers << root.join('app/javascript')
28
+ if app.config.respond_to?(:importmap)
29
+ app.config.importmap.paths << root.join('config/importmap.rb')
30
+ # https://github.com/rails/importmap-rails?tab=readme-ov-file#sweeping-the-cache-in-development-and-test
31
+ app.config.importmap.cache_sweepers << root.join('app/javascript')
32
+ end
28
33
  end
29
34
 
30
35
  def self.activate
@@ -33,6 +38,12 @@ module SpreeStripe
33
38
  end
34
39
  end
35
40
 
41
+ config.after_initialize do
42
+ Spree.subscribers.concat [
43
+ SpreeStripe::OrderCompletedSubscriber
44
+ ]
45
+ end
46
+
36
47
  config.to_prepare(&method(:activate).to_proc)
37
48
  end
38
49
  end
@@ -1,5 +1,5 @@
1
1
  module SpreeStripe
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.5.0'.freeze
3
3
 
4
4
  def gem_version
5
5
  Gem::Version.new(VERSION)
data/lib/spree_stripe.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'spree_core'
2
- require 'spree_extension'
3
2
  require 'spree_stripe/engine'
4
3
  require 'spree_stripe/version'
5
4
  require 'spree_stripe/configuration'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
- - Vendo Connect Inc.
8
- autorequire:
7
+ - Vendo Connect Inc., Vendo Sp. z o.o.
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-11-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: spree
@@ -16,100 +15,78 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 5.0.0.alpha
18
+ version: 5.3.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 5.0.0.alpha
27
- - !ruby/object:Gem::Dependency
28
- name: spree_storefront
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 5.0.0.alpha
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 5.0.0.alpha
25
+ version: 5.3.0
41
26
  - !ruby/object:Gem::Dependency
42
27
  name: spree_admin
43
28
  requirement: !ruby/object:Gem::Requirement
44
29
  requirements:
45
30
  - - ">="
46
31
  - !ruby/object:Gem::Version
47
- version: 5.0.0.alpha
32
+ version: 5.3.0
48
33
  type: :runtime
49
34
  prerelease: false
50
35
  version_requirements: !ruby/object:Gem::Requirement
51
36
  requirements:
52
37
  - - ">="
53
38
  - !ruby/object:Gem::Version
54
- version: 5.0.0.alpha
39
+ version: 5.3.0
55
40
  - !ruby/object:Gem::Dependency
56
- name: spree_extension
41
+ name: stripe
57
42
  requirement: !ruby/object:Gem::Requirement
58
43
  requirements:
59
44
  - - ">="
60
45
  - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: importmap-rails
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
46
+ version: '10.1'
47
+ - - "<"
74
48
  - !ruby/object:Gem::Version
75
- version: '0'
49
+ version: '19'
76
50
  type: :runtime
77
51
  prerelease: false
78
52
  version_requirements: !ruby/object:Gem::Requirement
79
53
  requirements:
80
54
  - - ">="
81
55
  - !ruby/object:Gem::Version
82
- version: '0'
56
+ version: '10.1'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '19'
83
60
  - !ruby/object:Gem::Dependency
84
- name: stripe
61
+ name: stripe_event
85
62
  requirement: !ruby/object:Gem::Requirement
86
63
  requirements:
87
64
  - - "~>"
88
65
  - !ruby/object:Gem::Version
89
- version: 10.1.0
66
+ version: '2.14'
90
67
  type: :runtime
91
68
  prerelease: false
92
69
  version_requirements: !ruby/object:Gem::Requirement
93
70
  requirements:
94
71
  - - "~>"
95
72
  - !ruby/object:Gem::Version
96
- version: 10.1.0
73
+ version: '2.14'
97
74
  - !ruby/object:Gem::Dependency
98
- name: stripe_event
75
+ name: dotenv
99
76
  requirement: !ruby/object:Gem::Requirement
100
77
  requirements:
101
- - - "~>"
78
+ - - ">="
102
79
  - !ruby/object:Gem::Version
103
- version: '2.11'
104
- type: :runtime
80
+ version: '0'
81
+ type: :development
105
82
  prerelease: false
106
83
  version_requirements: !ruby/object:Gem::Requirement
107
84
  requirements:
108
- - - "~>"
85
+ - - ">="
109
86
  - !ruby/object:Gem::Version
110
- version: '2.11'
87
+ version: '0'
111
88
  - !ruby/object:Gem::Dependency
112
- name: dotenv
89
+ name: jsonapi-rspec
113
90
  requirement: !ruby/object:Gem::Requirement
114
91
  requirements:
115
92
  - - ">="
@@ -178,17 +155,17 @@ dependencies:
178
155
  - - ">="
179
156
  - !ruby/object:Gem::Version
180
157
  version: '0'
181
- description:
182
158
  email: hello@spreecommerce.org
183
159
  executables: []
184
160
  extensions: []
185
161
  extra_rdoc_files: []
186
162
  files:
187
- - LICENSE.md
188
163
  - README.md
189
164
  - Rakefile
190
165
  - app/assets/config/spree_stripe_manifest.js
166
+ - app/assets/images/payment_icons/banktransfer.svg
191
167
  - app/assets/images/payment_icons/link.svg
168
+ - app/assets/images/payment_icons/sepadebit.svg
192
169
  - app/controllers/spree/api/v2/storefront/stripe/base_controller.rb
193
170
  - app/controllers/spree/api/v2/storefront/stripe/payment_intents_controller.rb
194
171
  - app/controllers/spree/api/v2/storefront/stripe/setup_intents_controller.rb
@@ -201,6 +178,7 @@ files:
201
178
  - app/javascript/spree_stripe/application.js
202
179
  - app/javascript/spree_stripe/controllers/stripe_button_controller.js
203
180
  - app/javascript/spree_stripe/controllers/stripe_controller.js
181
+ - app/jobs/spree_stripe/attach_customer_to_credit_card_job.rb
204
182
  - app/jobs/spree_stripe/base_job.rb
205
183
  - app/jobs/spree_stripe/complete_order_job.rb
206
184
  - app/jobs/spree_stripe/create_tax_transaction_job.rb
@@ -222,6 +200,7 @@ files:
222
200
  - app/models/spree_stripe/payment_sources/affirm.rb
223
201
  - app/models/spree_stripe/payment_sources/after_pay.rb
224
202
  - app/models/spree_stripe/payment_sources/alipay.rb
203
+ - app/models/spree_stripe/payment_sources/bank_transfer.rb
225
204
  - app/models/spree_stripe/payment_sources/ideal.rb
226
205
  - app/models/spree_stripe/payment_sources/klarna.rb
227
206
  - app/models/spree_stripe/payment_sources/link.rb
@@ -264,6 +243,7 @@ files:
264
243
  - app/services/spree_stripe/webhook_handlers/payment_intent_payment_failed.rb
265
244
  - app/services/spree_stripe/webhook_handlers/payment_intent_succeeded.rb
266
245
  - app/services/spree_stripe/webhook_handlers/setup_intent_succeeded.rb
246
+ - app/subscribers/spree_stripe/order_completed_subscriber.rb
267
247
  - app/views/spree/admin/payment_methods/configuration_guides/_spree_stripe.html.erb
268
248
  - app/views/spree/admin/payment_methods/custom_form_fields/_spree_stripe.html.erb
269
249
  - app/views/spree/admin/payment_methods/descriptions/_spree_stripe.html.erb
@@ -296,9 +276,12 @@ files:
296
276
  - vendor/javascript/@stripe--stripe-js--dist--pure.esm.js.js
297
277
  homepage: https://github.com/spree/spree_stripe
298
278
  licenses:
299
- - AGPL-3.0-or-later
300
- metadata: {}
301
- post_install_message:
279
+ - MIT
280
+ metadata:
281
+ bug_tracker_uri: https://github.com/spree/spree_stripe/issues
282
+ changelog_uri: https://github.com/spree/spree_stripe/releases/tag/v1.5.0
283
+ documentation_uri: https://docs.spreecommerce.org/
284
+ source_code_uri: https://github.com/spree/spree_stripe/tree/v1.5.0
302
285
  rdoc_options: []
303
286
  require_paths:
304
287
  - lib
@@ -314,8 +297,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
314
297
  version: '0'
315
298
  requirements:
316
299
  - none
317
- rubygems_version: 3.5.3
318
- signing_key:
300
+ rubygems_version: 4.0.2
319
301
  specification_version: 4
320
302
  summary: Official Spree Commerce Stripe payment gateway extension
321
303
  test_files: []
data/LICENSE.md DELETED
@@ -1,14 +0,0 @@
1
- # LICENSE
2
-
3
- Copyright (c) 2025 Vendo Connect Inc.
4
- All rights reserved.
5
-
6
- This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7
-
8
- This program is distributed in the hope that it will be useful,
9
- but WITHOUT ANY WARRANTY; without even the implied warranty of
10
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
- GNU Affero General Public License for more details.
12
-
13
- You should have received a copy of the GNU Affero General Public License
14
- along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).