pay 12.1.1 → 12.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cc4265d73925a1e3134b5dafcdf452b131b8c478c2d9f04179148d313ac3275
4
- data.tar.gz: 02f84e17b0216f30d5d38d92455adc13a9fd14fff96a84f5ee045041340dc999
3
+ metadata.gz: f77372d0f6b47b272a614d14cd91183c86b56dd02d2ecea25fd99ebccde3ec7b
4
+ data.tar.gz: 7c6c0b2196445b486b0519a14c1fe1a1123c81316822af0cc2fea3c891c731dd
5
5
  SHA512:
6
- metadata.gz: fdf6618be0a7510e7cf015ee9e80a0e08b1550953c739fb978adfa1f3b74fe6ad58244deef09bdc52284bf92dcc1013e14daaa70d49d5516c85b0f63bcbbd701
7
- data.tar.gz: be11d81512c0dc629be7eaf13070c6382fa1c30f3606bc05b0ce2c882a8b9b3bf5760b5e52be48efe2353e6a0773225b2cc473c1b5e194153e515a57e459f701
6
+ metadata.gz: e30e10e229a3af4a2dea25d532c209831c9388222226d97b1c576e4ddf3aee7509da290a2fa6f762dd6a687bea07962e5c309c22ce1cca84a337d6edc979057c
7
+ data.tar.gz: 4878549cc6eb89db69734ae9bbec6d2e9fa4d6027322af4e266e89c5a026ebb19c946a202b63327f55c3b3da8b9384112ace9310effe1c9552a4ede2efef0516
@@ -27,7 +27,7 @@ module Pay
27
27
  update!(processor_id: pc.id)
28
28
  pc
29
29
  end
30
- rescue ::Paddle::Error => e
30
+ rescue ::Paddle::ErrorGenerator => e
31
31
  raise Pay::PaddleBilling::Error, e
32
32
  end
33
33
 
@@ -55,7 +55,7 @@ module Pay
55
55
  charge = charges.find_or_initialize_by(processor_id: transaction.id)
56
56
  charge.update(attrs)
57
57
  charge
58
- rescue ::Paddle::Error => e
58
+ rescue ::Paddle::ErrorGenerator => e
59
59
  raise Pay::PaddleBilling::Error, e
60
60
  end
61
61
 
@@ -39,8 +39,6 @@ module Pay
39
39
  pay_customer.payment_methods.destroy_all
40
40
  when "trialing"
41
41
  attributes[:trial_ends_at] = Time.parse(subscription.next_billed_at) if subscription.next_billed_at
42
- when "paused"
43
- attributes[:pause_starts_at] = Time.parse(subscription.paused_at) if subscription.paused_at
44
42
  when "active", "past_due"
45
43
  attributes[:trial_ends_at] = nil
46
44
  attributes[:pause_starts_at] = nil
@@ -76,10 +74,12 @@ module Pay
76
74
  end
77
75
 
78
76
  # If a subscription is paused, cancel immediately
79
- # Otherwise, cancel at period end
77
+ # Otherwise, cancel at period end. A scheduled pause is removed first
80
78
  def cancel(**options)
81
79
  return if canceled?
82
80
 
81
+ remove_scheduled_pause if will_pause?
82
+
83
83
  response = ::Paddle::Subscription.cancel(
84
84
  id: processor_id,
85
85
  effective_from: options.fetch(:effective_from, paused? ? "immediately" : "next_billing_period")
@@ -88,14 +88,12 @@ module Pay
88
88
  status: response.status,
89
89
  ends_at: response.scheduled_change&.effective_at || Time.current
90
90
  )
91
- rescue ::Paddle::Error => e
91
+ rescue ::Paddle::ErrorGenerator => e
92
92
  raise Pay::PaddleBilling::Error, e
93
93
  end
94
94
 
95
95
  def cancel_now!(**options)
96
96
  cancel(**options.merge(effective_from: "immediately"))
97
- rescue ::Paddle::Error => e
98
- raise Pay::PaddleBilling::Error, e
99
97
  end
100
98
 
101
99
  def change_quantity(quantity, **options)
@@ -110,24 +108,30 @@ module Pay
110
108
  proration_billing_mode: options.delete(:proration_billing_mode) || "prorated_immediately"
111
109
  )
112
110
  update(quantity: quantity)
113
- rescue ::Paddle::Error => e
111
+ rescue ::Paddle::ErrorGenerator => e
114
112
  raise Pay::PaddleBilling::Error, e
115
113
  end
116
114
 
117
115
  # A subscription could be set to cancel or pause in the future
118
116
  # It is considered on grace period until the cancel or pause time begins
119
117
  def on_grace_period?
120
- (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
118
+ (canceled? && ends_at.future?) || will_pause?
119
+ end
120
+
121
+ def will_pause?
122
+ pause_starts_at? && pause_starts_at.future?
121
123
  end
122
124
 
125
+ # Paddle keeps the status `active` until a scheduled pause starts, so check pause_starts_at as well
123
126
  def paused?
124
- status == "paused"
127
+ status == "paused" || pause_starts_at?
125
128
  end
126
129
 
130
+ # Pauses the subscription at the end of the current billing period
127
131
  def pause
128
132
  response = ::Paddle::Subscription.pause(id: processor_id)
129
- update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
130
- rescue ::Paddle::Error => e
133
+ update!(status: response.status, pause_starts_at: response.scheduled_change.effective_at)
134
+ rescue ::Paddle::ErrorGenerator => e
131
135
  raise Pay::PaddleBilling::Error, e
132
136
  end
133
137
 
@@ -141,15 +145,14 @@ module Pay
141
145
  end
142
146
 
143
147
  # Paddle Billing API only allows "resuming" subscriptions when they are paused
144
- # So cancel the scheduled change if it is in the future
145
- if paused? && pause_starts_at? && Time.current < pause_starts_at
146
- ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
148
+ if will_pause?
149
+ remove_scheduled_pause
147
150
  else
148
151
  ::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
149
152
  end
150
153
 
151
154
  update(ends_at: nil, status: :active, pause_starts_at: nil)
152
- rescue ::Paddle::Error => e
155
+ rescue ::Paddle::ErrorGenerator => e
153
156
  raise Pay::PaddleBilling::Error, e
154
157
  end
155
158
 
@@ -168,6 +171,14 @@ module Pay
168
171
  )
169
172
  update(processor_plan: plan, ends_at: nil, status: :active)
170
173
  end
174
+
175
+ private
176
+
177
+ # Paddle rejects changes to a subscription with a scheduled change
178
+ def remove_scheduled_pause
179
+ ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
180
+ update!(pause_starts_at: nil)
181
+ end
171
182
  end
172
183
  end
173
184
  end
@@ -30,7 +30,7 @@ module Pay
30
30
  charge = charges.find_or_initialize_by(processor_id: response[:invoice_id])
31
31
  charge.update(attributes)
32
32
  charge
33
- rescue ::Paddle::Error => e
33
+ rescue ::Paddle::Classic::Error => e
34
34
  raise Pay::PaddleClassic::Error, e
35
35
  end
36
36
 
@@ -13,7 +13,7 @@ module Pay
13
13
 
14
14
  payment_method.update!(attributes)
15
15
  payment_method
16
- rescue ::Paddle::Error => e
16
+ rescue ::Paddle::Classic::Error => e
17
17
  raise Pay::PaddleClassic::Error, e
18
18
  end
19
19
 
@@ -54,7 +54,7 @@ module Pay
54
54
 
55
55
  def api_record(**options)
56
56
  PaddleClassic.client.users.list(subscription_id: processor_id).data.try(:first)
57
- rescue ::Paddle::Error => e
57
+ rescue ::Paddle::Classic::Error => e
58
58
  raise Pay::PaddleClassic::Error, e
59
59
  end
60
60
 
@@ -78,7 +78,7 @@ module Pay
78
78
 
79
79
  # Remove payment methods since customer cannot be reused after cancelling
80
80
  Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
81
- rescue ::Paddle::Error => e
81
+ rescue ::Paddle::Classic::Error => e
82
82
  raise Pay::PaddleClassic::Error, e
83
83
  end
84
84
 
@@ -90,7 +90,7 @@ module Pay
90
90
 
91
91
  # Remove payment methods since customer cannot be reused after cancelling
92
92
  Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
93
- rescue ::Paddle::Error => e
93
+ rescue ::Paddle::Classic::Error => e
94
94
  raise Pay::PaddleClassic::Error, e
95
95
  end
96
96
 
@@ -111,7 +111,7 @@ module Pay
111
111
  def pause
112
112
  response = PaddleClassic.client.users.pause(subscription_id: processor_id)
113
113
  update(status: :paused, pause_starts_at: Time.zone.parse(response.dig(:next_payment, :date)))
114
- rescue ::Paddle::Error => e
114
+ rescue ::Paddle::Classic::Error => e
115
115
  raise Pay::PaddleClassic::Error, e
116
116
  end
117
117
 
@@ -126,7 +126,7 @@ module Pay
126
126
 
127
127
  PaddleClassic.client.users.unpause(subscription_id: processor_id)
128
128
  update(ends_at: nil, status: :active, pause_starts_at: nil)
129
- rescue ::Paddle::Error => e
129
+ rescue ::Paddle::Classic::Error => e
130
130
  raise Pay::PaddleClassic::Error, e
131
131
  end
132
132
 
@@ -138,7 +138,7 @@ module Pay
138
138
  PaddleClassic.client.users.update(subscription_id: processor_id, **attributes)
139
139
 
140
140
  update(processor_plan: plan, ends_at: nil, status: :active)
141
- rescue ::Paddle::Error => e
141
+ rescue ::Paddle::Classic::Error => e
142
142
  raise Pay::PaddleClassic::Error, e
143
143
  end
144
144
  end
@@ -133,7 +133,7 @@ module Pay
133
133
 
134
134
  # Used for creating Stripe Terminal charges
135
135
  def terminal_charge(amount, options = {})
136
- create_payment_intent(amount, options.merge(payment_method_types: ["card_present"], capture_method: "manual"))
136
+ create_payment_intent(amount, options.merge(allowed_payment_method_types: ["card_present"], capture_method: "manual"))
137
137
  end
138
138
 
139
139
  def create_setup_intent(options = {})
@@ -217,7 +217,7 @@ module Pay
217
217
  end
218
218
 
219
219
  def paused?
220
- pause_behavior == "void"
220
+ status_paused? || pause_behavior_void?
221
221
  end
222
222
 
223
223
  # Pauses a Stripe subscription
@@ -266,12 +266,22 @@ module Pay
266
266
  on_grace_period? || paused?
267
267
  end
268
268
 
269
- def resume
269
+ # Resumes a subscription that is on its grace period or paused
270
+ #
271
+ # Subscriptions with a `paused` status are billed immediately and stay paused until that invoice is paid
272
+ # Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if that payment needs a payment method or authentication
273
+ #
274
+ # resume(billing_cycle_anchor: "unchanged")
275
+ #
276
+ # https://docs.stripe.com/api/subscriptions/resume
277
+ def resume(**options)
270
278
  unless resumable?
271
- raise Error, "You can only resume subscriptions within their grace period."
279
+ raise Error, "You can only resume subscriptions that are paused or within their grace period."
272
280
  end
273
281
 
274
- if paused?
282
+ if status_paused?
283
+ @api_record = ::Stripe::Subscription.resume(processor_id, options.merge(expand_options), stripe_options)
284
+ elsif paused?
275
285
  unpause
276
286
  else
277
287
  @api_record = ::Stripe::Subscription.update(processor_id, {
@@ -281,6 +291,10 @@ module Pay
281
291
  stripe_options)
282
292
  end
283
293
  update(ends_at: nil, status: @api_record.status)
294
+
295
+ # Stripe resumes successfully even when the payment fails, leaving the subscription paused
296
+ validate_latest_payment if status_paused?
297
+ true
284
298
  rescue ::Stripe::StripeError => e
285
299
  raise Pay::Stripe::Error, e
286
300
  end
@@ -291,6 +305,11 @@ module Pay
291
305
  prorate = options.delete(:prorate) { true }
292
306
  proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
293
307
 
308
+ # Stripe 20 takes billing_cycle_anchor as {type: "now"} on updates, so convert the old "now" / "unchanged" form
309
+ if options[:billing_cycle_anchor].is_a?(String) || options[:billing_cycle_anchor].is_a?(Symbol)
310
+ options[:billing_cycle_anchor] = {type: options[:billing_cycle_anchor].to_s}
311
+ end
312
+
294
313
  @api_record = ::Stripe::Subscription.update(
295
314
  processor_id,
296
315
  {
@@ -303,9 +322,7 @@ module Pay
303
322
  )
304
323
 
305
324
  # Validate that swap was successful and handle SCA if needed
306
- if (payment_intent_id = @api_record.latest_invoice.payments.first&.payment&.payment_intent)
307
- Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
308
- end
325
+ validate_latest_payment
309
326
 
310
327
  sync!(object: @api_record)
311
328
  rescue ::Stripe::StripeError => e
@@ -369,6 +386,23 @@ module Pay
369
386
 
370
387
  private
371
388
 
389
+ # Stripe sets the status to `paused` when a trial ends without a payment method
390
+ def status_paused?
391
+ status == "paused"
392
+ end
393
+
394
+ # Pausing collection with `void` leaves the status unchanged
395
+ def pause_behavior_void?
396
+ pause_behavior == "void"
397
+ end
398
+
399
+ # Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if the latest invoice's payment is incomplete
400
+ def validate_latest_payment
401
+ if (payment_intent = latest_payment)
402
+ Pay::Payment.new(payment_intent, stripe_account: stripe_account).validate
403
+ end
404
+ end
405
+
372
406
  # Options for Stripe requests
373
407
  def stripe_options
374
408
  {stripe_account: stripe_account}.compact
@@ -1,12 +1,14 @@
1
+ <% scope = @payment.setup_intent? ? "pay.setup_intent" : "pay" %>
1
2
  <div
2
3
  data-controller="payment-intent"
3
4
  data-payment-intent-public-key-value="<%= Pay::Stripe.public_key %>"
4
5
  data-payment-intent-stripe-account-value="<%= @payment.stripe_account %>"
5
6
  data-payment-intent-client-secret-value="<%= @payment.client_secret %>"
6
7
  data-payment-intent-status-value="<%= @payment.status %>"
8
+ data-payment-intent-setup-intent-value="<%= @payment.setup_intent? %>"
7
9
  data-payment-intent-customer-value="<%= @payment.customer %>"
8
10
  data-payment-intent-name-missing-message-value="<%= t("pay.requires_action.name_missing") %>"
9
- data-payment-intent-confirmed-message-value="<%= t("pay.requires_action.success") %>"
11
+ data-payment-intent-confirmed-message-value="<%= t("requires_action.success", scope: scope) %>"
10
12
  class="h-screen md:flex md:justify-center md:items-center">
11
13
  <div class="w-full max-w-lg">
12
14
  <!-- Status Messages -->
@@ -28,18 +30,18 @@
28
30
 
29
31
  <div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
30
32
  <% if @payment.succeeded? %>
31
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.successful.header" %></h1>
32
- <p class="mb-6"><%=t "pay.successful.description" %></p>
33
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "successful.header", scope: scope %></h1>
34
+ <p class="mb-6"><%=t "successful.description", scope: scope %></p>
33
35
 
34
36
  <% elsif @payment.canceled? %>
35
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.cancelled.header" %></h1>
36
- <p class="mb-6"><%=t "pay.cancelled.description" %></p>
37
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "cancelled.header", scope: scope %></h1>
38
+ <p class="mb-6"><%=t "cancelled.description", scope: scope %></p>
37
39
 
38
40
  <% else %>
39
41
  <div data-payment-intent-target="form" id="payment-elements">
40
42
  <!-- Instructions -->
41
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.requires_action.header", amount: @payment.amount_with_currency %></h1>
42
- <p class="mb-6"><%=t "pay.requires_action.description" %></p>
43
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "requires_action.header", scope: scope, amount: @payment.amount_with_currency %></h1>
44
+ <p class="mb-6"><%=t "requires_action.description", scope: scope %></p>
43
45
 
44
46
  <div data-payment-intent-target="cardFields" class="hidden">
45
47
  <!-- Name -->
@@ -53,7 +55,7 @@
53
55
 
54
56
  <!-- Pay Button -->
55
57
  <button data-payment-intent-target="button" data-action="click->payment-intent#confirmPayment" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500">
56
- <%=t "pay.requires_action.button", amount: @payment.amount_with_currency %>
58
+ <%=t "requires_action.button", scope: scope, amount: @payment.amount_with_currency %>
57
59
  </button>
58
60
  </div>
59
61
  <% end %>
@@ -79,6 +81,7 @@
79
81
  clientSecret: String,
80
82
  customer: String,
81
83
  status: String,
84
+ setupIntent: Boolean,
82
85
  nameMissingMessage: String,
83
86
  confirmedMessage: String,
84
87
  errorMessage: String,
@@ -104,7 +107,7 @@
104
107
  switch(this.statusValue) {
105
108
  case "requires_action":
106
109
  this.processingValue = true
107
- this.stripe.confirmPayment({clientSecret: this.clientSecretValue, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
110
+ this.confirm({clientSecret: this.clientSecretValue})
108
111
  break
109
112
  case "requires_payment_method":
110
113
  this.cardFieldsTarget.classList.toggle("hidden", false)
@@ -114,7 +117,14 @@
114
117
 
115
118
  confirmPayment() {
116
119
  this.processingValue = true
117
- this.stripe.confirmPayment({elements: this.elements, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
120
+ this.confirm({elements: this.elements})
121
+ }
122
+
123
+ // SetupIntents (saving a payment method) and PaymentIntents confirm through different Stripe.js methods
124
+ confirm(options) {
125
+ const params = {...options, confirmParams: { return_url: document.location.href }}
126
+ const result = this.setupIntentValue ? this.stripe.confirmSetup(params) : this.stripe.confirmPayment(params)
127
+ result.then(this.handleConfirmResult.bind(this))
118
128
  }
119
129
 
120
130
  handleConfirmResult(result) {
@@ -126,7 +136,8 @@
126
136
  this.errorMessageValue = this.nameMissingMessageValue
127
137
  } else {
128
138
  this.errorMessageValue = result.error.message
129
- this.statusValue = result.error.payment_intent.status
139
+ const intent = result.error.payment_intent || result.error.setup_intent
140
+ if (intent) { this.statusValue = intent.status }
130
141
  }
131
142
  } else {
132
143
  this.completeValue = true
@@ -14,6 +14,18 @@ en:
14
14
  button: "Pay %{amount}"
15
15
  name_missing: "Please provide your name."
16
16
  success: "The payment was successful."
17
+ setup_intent:
18
+ successful:
19
+ header: "Payment Method Saved"
20
+ description: "Your payment method was successfully confirmed."
21
+ cancelled:
22
+ header: "Setup Cancelled"
23
+ description: "This payment method setup was cancelled."
24
+ requires_action:
25
+ header: "Confirm your payment method"
26
+ description: "Extra confirmation is needed to save your payment method. Please confirm it by filling out your payment details below."
27
+ button: "Save payment method"
28
+ success: "Your payment method was saved."
17
29
  all_rights_reserved: "All rights reserved."
18
30
  back: "Go back"
19
31
  refund: "Refund"
@@ -14,11 +14,13 @@ module Pay
14
14
 
15
15
  extend Env
16
16
 
17
+ REQUIRED_VERSION = "~> 3"
18
+
17
19
  def self.enabled?
18
20
  return false unless Pay.enabled_processors.include?(:paddle_billing) && defined?(::Paddle)
19
21
 
20
- Pay::Engine.version_matches?(required: "~> 2.5",
21
- current: ::Paddle::VERSION) || (raise "[Pay] paddle gem must be version ~> 2.5")
22
+ Pay::Engine.version_matches?(required: REQUIRED_VERSION,
23
+ current: ::Paddle::VERSION) || (raise "[Pay] paddle gem must be version #{REQUIRED_VERSION}")
22
24
  end
23
25
 
24
26
  def self.setup
@@ -18,11 +18,13 @@ module Pay
18
18
 
19
19
  extend Env
20
20
 
21
+ REQUIRED_VERSION = "~> 3"
22
+
21
23
  def self.enabled?
22
24
  return false unless Pay.enabled_processors.include?(:paddle_classic) && defined?(::Paddle)
23
25
 
24
- Pay::Engine.version_matches?(required: "~> 2.5",
25
- current: ::Paddle::VERSION) || (raise "[Pay] paddle gem must be version ~> 2.5")
26
+ Pay::Engine.version_matches?(required: REQUIRED_VERSION,
27
+ current: ::Paddle::VERSION) || (raise "[Pay] paddle gem must be version #{REQUIRED_VERSION}")
26
28
  end
27
29
 
28
30
  def self.client
data/lib/pay/payment.rb CHANGED
@@ -19,6 +19,10 @@ module Pay
19
19
  @stripe_account = stripe_account
20
20
  end
21
21
 
22
+ def setup_intent?
23
+ intent.is_a?(::Stripe::SetupIntent)
24
+ end
25
+
22
26
  def requires_payment_method?
23
27
  status == "requires_payment_method"
24
28
  end
@@ -35,7 +39,9 @@ module Pay
35
39
  status == "succeeded"
36
40
  end
37
41
 
42
+ # SetupIntents save a payment method for later and have no amount
38
43
  def amount_with_currency
44
+ return if setup_intent?
39
45
  Pay::Currency.format(amount, currency: currency)
40
46
  end
41
47
 
data/lib/pay/stripe.rb CHANGED
@@ -28,7 +28,7 @@ module Pay
28
28
 
29
29
  extend Env
30
30
 
31
- REQUIRED_VERSION = "~> 19"
31
+ REQUIRED_VERSION = "~> 20"
32
32
 
33
33
  # A list of database model names that include Pay
34
34
  # Used for safely looking up models with client_reference_id
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "12.1.1"
2
+ VERSION = "12.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.1.1
4
+ version: 12.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -191,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
191
  - !ruby/object:Gem::Version
192
192
  version: '0'
193
193
  requirements: []
194
- rubygems_version: 4.0.18
194
+ rubygems_version: 4.0.20
195
195
  specification_version: 4
196
196
  summary: Payments engine for Ruby on Rails
197
197
  test_files: []