Class: Pay::Stripe::Subscription

Inherits:
Object
  • Object
show all
Defined in:
lib/pay/stripe/subscription.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pay_subscription) ⇒ Subscription

Returns a new instance of Subscription.



114
115
116
# File 'lib/pay/stripe/subscription.rb', line 114

def initialize(pay_subscription)
  @pay_subscription = pay_subscription
end

Instance Attribute Details

#pay_subscriptionObject (readonly)

Returns the value of attribute pay_subscription.



5
6
7
# File 'lib/pay/stripe/subscription.rb', line 5

def pay_subscription
  @pay_subscription
end

#stripe_subscriptionObject

Returns the value of attribute stripe_subscription.



4
5
6
# File 'lib/pay/stripe/subscription.rb', line 4

def stripe_subscription
  @stripe_subscription
end

Class Method Details

.expand_optionsObject

Common expand options for all requests that create, retrieve, or update a Stripe Subscription



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/pay/stripe/subscription.rb', line 102

def self.expand_options
  {
    expand: [
      "pending_setup_intent",
      "latest_invoice.payment_intent",
      "latest_invoice.charge",
      "latest_invoice.total_discount_amounts.discount",
      "latest_invoice.total_tax_amounts.tax_rate"
    ]
  }
end

.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/pay/stripe/subscription.rb', line 26

def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1)
  # Skip loading the latest subscription details from the API if we already have it
  object ||= ::Stripe::Subscription.retrieve({id: subscription_id}.merge(expand_options), {stripe_account: }.compact)

  pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
  return unless pay_customer

  attributes = {
    application_fee_percent: object.application_fee_percent,
    processor_plan: object.items.first.price.id,
    quantity: object.items.first.try(:quantity) || 0,
    status: object.status,
    stripe_account: pay_customer.,
    metadata: object.,
    subscription_items: [],
    metered: false,
    pause_behavior: object.pause_collection&.behavior,
    pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil)
  }

  # Subscriptions that have ended should have their trial ended at the same time
  if object.trial_end
    attributes[:trial_ends_at] = Time.at(object.ended_at || object.trial_end)
  end

  # Record subscription items to db
  object.items.auto_paging_each do |subscription_item|
    if !attributes[:metered] && (subscription_item.to_hash.dig(:price, :recurring, :usage_type) == "metered")
      attributes[:metered] = true
    end

    attributes[:subscription_items] << subscription_item.to_hash.slice(:id, :price, :metadata, :quantity)
  end

  attributes[:ends_at] = if object.ended_at
    # Fully cancelled subscription
    Time.at(object.ended_at)
  elsif object.cancel_at
    # subscription cancelling in the future
    Time.at(object.cancel_at)
  elsif object.cancel_at_period_end
    # Subscriptions cancelling in the future
    Time.at(object.current_period_end)
  end

  # Update or create the subscription
  pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
  if pay_subscription
    pay_subscription.with_lock { pay_subscription.update!(attributes) }
  else
    # Allow setting the subscription name in metadata, otherwise use the default
    name ||= object.["pay_name"] || Pay.default_product_name

    pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
  end

  # Cache the Stripe subscription on the Pay::Subscription that we return
  pay_subscription.stripe_subscription = object

  # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
  if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
    Pay::Stripe::Charge.sync(charge.id, stripe_account: pay_subscription.)
  end

  pay_subscription
rescue ActiveRecord::RecordInvalid
  try += 1
  if try <= retries
    sleep 0.1
    retry
  else
    raise
  end
end

Instance Method Details

#cancel(**options) ⇒ Object



133
134
135
136
137
138
# File 'lib/pay/stripe/subscription.rb', line 133

def cancel(**options)
  @stripe_subscription = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
  pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(@stripe_subscription.current_period_end)))
rescue ::Stripe::StripeError => e
  raise Pay::Stripe::Error, e
end

#cancel_now!(**options) ⇒ Object

Cancels a subscription immediately

cancel_now!(prorate: true) cancel_now!(invoice_now: true)



144
145
146
147
148
149
# File 'lib/pay/stripe/subscription.rb', line 144

def cancel_now!(**options)
  @stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
  pay_subscription.update(ends_at: Time.current, status: :canceled)
rescue ::Stripe::StripeError => e
  raise Pay::Stripe::Error, e
end

#change_quantity(quantity, **options) ⇒ Object

This updates a SubscriptionItem’s quantity in Stripe

For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update



155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pay/stripe/subscription.rb', line 155

def change_quantity(quantity, **options)
  subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
  if subscription_item_id
    ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
    @stripe_subscription = nil
  else
    @stripe_subscription = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
  end
  true
rescue ::Stripe::StripeError => e
  raise Pay::Stripe::Error, e
end

#client_secretObject

Returns a SetupIntent or PaymentIntent client secret for the subscription



128
129
130
131
# File 'lib/pay/stripe/subscription.rb', line 128

def client_secret
  stripe_sub = subscription
  stripe_sub&.pending_setup_intent&.client_secret || stripe_sub&.latest_invoice&.payment_intent&.client_secret
end

#create_usage_record(**options) ⇒ Object

Creates a metered billing usage record

Uses the first subscription_item ID unless ‘subscription_item_id: “si_1234”` is passed

create_usage_record(quantity: 4, action: :increment) create_usage_record(subscription_item_id: “si_1234”, quantity: 100, action: :set)



242
243
244
245
# File 'lib/pay/stripe/subscription.rb', line 242

def create_usage_record(**options)
  subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
  ::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
end

#on_grace_period?Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/pay/stripe/subscription.rb', line 168

def on_grace_period?
  canceled? && Time.current < ends_at
end

#pause(**options) ⇒ Object

Pauses a Stripe subscription

pause(behavior: “mark_uncollectible”) pause(behavior: “keep_as_draft”) pause(behavior: “void”) pause(behavior: “mark_uncollectible”, resumes_at: 1.month.from_now)



182
183
184
185
186
187
188
189
# File 'lib/pay/stripe/subscription.rb', line 182

def pause(**options)
  attributes = {pause_collection: options.reverse_merge(behavior: "mark_uncollectible")}
  @stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
  pay_subscription.update(
    pause_behavior: @stripe_subscription.pause_collection&.behavior,
    pause_resumes_at: (@stripe_subscription.pause_collection&.resumes_at ? Time.at(@stripe_subscription.pause_collection&.resumes_at) : nil)
  )
end

#paused?Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/pay/stripe/subscription.rb', line 172

def paused?
  pause_behavior.present?
end

#reload!Object



123
124
125
# File 'lib/pay/stripe/subscription.rb', line 123

def reload!
  @stripe_subscription = nil
end

#resumeObject



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/pay/stripe/subscription.rb', line 196

def resume
  unless on_grace_period? || paused?
    raise StandardError, "You can only resume subscriptions within their grace period."
  end

  if paused?
    unpause
  else
    @stripe_subscription = ::Stripe::Subscription.update(
      processor_id,
      {
        plan: processor_plan,
        trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
        cancel_at_period_end: false
      }.merge(expand_options),
      stripe_options
    )
  end
rescue ::Stripe::StripeError => e
  raise Pay::Stripe::Error, e
end

#subscription(**options) ⇒ Object



118
119
120
121
# File 'lib/pay/stripe/subscription.rb', line 118

def subscription(**options)
  options[:id] = processor_id
  @stripe_subscription ||= ::Stripe::Subscription.retrieve(options.merge(expand_options), {stripe_account: }.compact)
end

#swap(plan) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/pay/stripe/subscription.rb', line 218

def swap(plan)
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)

  @stripe_subscription = ::Stripe::Subscription.update(
    processor_id,
    {
      cancel_at_period_end: false,
      plan: plan,
      proration_behavior: (prorate ? "create_prorations" : "none"),
      trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
      quantity: quantity
    }.merge(expand_options),
    stripe_options
  )
rescue ::Stripe::StripeError => e
  raise Pay::Stripe::Error, e
end

#unpauseObject



191
192
193
194
# File 'lib/pay/stripe/subscription.rb', line 191

def unpause
  @stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: nil}.merge(expand_options), stripe_options)
  pay_subscription.update(pause_behavior: nil, pause_resumes_at: nil)
end

#upcoming_invoice(**options) ⇒ Object

Returns an upcoming invoice for a subscription



254
255
256
# File 'lib/pay/stripe/subscription.rb', line 254

def upcoming_invoice(**options)
  ::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
end

#usage_record_summaries(**options) ⇒ Object

Returns usage record summaries for a subscription item



248
249
250
251
# File 'lib/pay/stripe/subscription.rb', line 248

def usage_record_summaries(**options)
  subscription_item_id = options.fetch(:subscription_item_id, subscription_items.first["id"])
  ::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
end