Class: UsageCredits::CreditSubscriptionPlan

Inherits:
Object
  • Object
show all
Defined in:
lib/usage_credits/models/credit_subscription_plan.rb

Overview

A DSL to define subscription plans that give credits to users on a recurring basis.

The actual credit fulfillment is handled by the PaySubscriptionExtension, which monitors subscription events (creation, renewal, etc) and adds credits to the user's wallet accordingly.

Defined Under Namespace

Classes: CreditGiver

Constant Summary collapse

MIN_PERIOD =

Deprecated: Use UsageCredits.configuration.min_fulfillment_period instead

1.day

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ CreditSubscriptionPlan

Returns a new instance of CreditSubscriptionPlan.



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 24

def initialize(name)
  @name = name
  @processor_plan_ids = {}  # Store processor-specific plan IDs
  @fulfillment_period = nil
  @credits_per_period = 0
  @signup_bonus_credits = 0
  @trial_credits = 0
  @rollover_enabled = false
  @expire_credits_on_cancel = false
  @credit_expiration_period = nil
   = {}
end

Instance Attribute Details

#credit_expiration_periodObject (readonly)

Returns the value of attribute credit_expiration_period.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def credit_expiration_period
  @credit_expiration_period
end

#credits_per_periodObject (readonly)

Returns the value of attribute credits_per_period.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def credits_per_period
  @credits_per_period
end

#expire_credits_on_cancelObject (readonly)

Returns the value of attribute expire_credits_on_cancel.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def expire_credits_on_cancel
  @expire_credits_on_cancel
end

#fulfillment_periodObject

Returns the value of attribute fulfillment_period.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def fulfillment_period
  @fulfillment_period
end

#metadataObject (readonly)

Returns the value of attribute metadata.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def 
  
end

#nameObject (readonly)

Returns the value of attribute name.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def name
  @name
end

#processor_plan_idsObject (readonly)

Returns the value of attribute processor_plan_ids.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def processor_plan_ids
  @processor_plan_ids
end

#rollover_enabledObject (readonly)

Returns the value of attribute rollover_enabled.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def rollover_enabled
  @rollover_enabled
end

#signup_bonus_creditsObject (readonly)

Returns the value of attribute signup_bonus_credits.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def 
  @signup_bonus_credits
end

#trial_creditsObject (readonly)

Returns the value of attribute trial_credits.



12
13
14
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 12

def trial_credits
  @trial_credits
end

Instance Method Details

#base_metadataObject



266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 266

def 
  {
    purchase_type: "credit_subscription",
    subscription_name: name,
    fulfillment_period: fulfillment_period_display,
    credits_per_period: credits_per_period,
    signup_bonus_credits: ,
    trial_credits: trial_credits,
    rollover_enabled: rollover_enabled,
    expire_credits_on_cancel: expire_credits_on_cancel,
    credit_expiration_period: credit_expiration_period&.to_i,
    metadata: 
  }
end

#create_checkout_session(user, success_url:, cancel_url:, processor: :stripe, period: nil) ⇒ Object

Create a checkout session for this subscription plan

Examples:

Single-price plan (backward compatible)

plan.create_checkout_session(user, success_url: "/success", cancel_url: "/cancel")

Multi-period plan (must specify period)

plan.create_checkout_session(user, success_url: "/success", cancel_url: "/cancel", period: :month)
plan.create_checkout_session(user, success_url: "/success", cancel_url: "/cancel", period: :year)

Parameters:

  • user (Object)

    The user creating the checkout session

  • success_url (String)

    URL to redirect after successful checkout

  • cancel_url (String)

    URL to redirect if checkout is cancelled

  • processor (Symbol) (defaults to: :stripe)

    Payment processor to use (default: :stripe)

  • period (Symbol, nil) (defaults to: nil)

    Billing period for multi-period plans (e.g., :month, :year)

Raises:

  • (ArgumentError)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 203

def create_checkout_session(user, success_url:, cancel_url:, processor: :stripe, period: nil)
  raise ArgumentError, "User must respond to payment_processor" unless user.respond_to?(:payment_processor)
  raise ArgumentError, "No fulfillment period configured for plan: #{name}" unless fulfillment_period

  plan_ids = plan_id_for(processor)
  raise ArgumentError, "No #{processor.to_s.titleize} plan ID configured for plan: #{name}" unless plan_ids

  # Determine which price ID to use
  plan_id = if plan_ids.is_a?(Hash)
    # Multi-period plan: period is required
    raise ArgumentError, "This plan has multiple billing periods (#{plan_ids.keys.join(', ')}). Please specify period: parameter (e.g., period: :month)" if period.nil?
    plan_ids[period.to_sym] || raise(ArgumentError, "Period #{period.inspect} not found. Available periods: #{plan_ids.keys.inspect}")
  else
    # Single-price plan: use the ID directly
    plan_ids
  end

  case processor
  when :stripe
    create_stripe_checkout_session(user, plan_id, success_url, cancel_url)
  else
    raise ArgumentError, "Unsupported payment processor: #{processor}"
  end
end

#create_stripe_checkout_session(user, plan_id, success_url, cancel_url) ⇒ Object



253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 253

def create_stripe_checkout_session(user, plan_id, success_url, cancel_url)
  user.payment_processor.checkout(
    mode: "subscription",
    line_items: [{
      price: plan_id,
      quantity: 1
    }],
    success_url: success_url,
    cancel_url: cancel_url,
    subscription_data: { metadata:  }
  )
end

#expire_after(duration) ⇒ void

This method returns an undefined value.

Configure credit expiration after subscription cancellation

When a subscription is cancelled, you can control what happens to remaining credits:

  1. By default (if this is not called), users keep their credits forever
  2. If called with a duration, credits expire after that grace period
  3. If called with nil/0, credits expire immediately on cancellation

Parameters:

  • duration (ActiveSupport::Duration, nil)

    Grace period before credits expire



77
78
79
80
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 77

def expire_after(duration)
  @expire_credits_on_cancel = true
  @credit_expiration_period = duration
end

#fulfillment_period_displayObject

=========================================

Helper Methods



245
246
247
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 245

def fulfillment_period_display
  fulfillment_period.is_a?(ActiveSupport::Duration) ? fulfillment_period.inspect : fulfillment_period
end

#gives(amount) ⇒ Object

Set base credits given each period



42
43
44
45
46
47
48
49
50
51
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 42

def gives(amount)
  if amount.is_a?(UsageCredits::Cost::Fixed)
    @credits_per_period = amount.amount
    @fulfillment_period = UsageCredits::PeriodParser.normalize_period(amount.period || 1.month)
    self
  else
    @credits_per_period = amount.to_i
    CreditGiver.new(self)
  end
end

#matches_processor_id?(processor_id) ⇒ Boolean

Check if this plan matches a given processor price ID Works with both single-price and multi-period plans

Parameters:

  • processor_id (String)

    The price ID to match

Returns:

  • (Boolean)

    True if this plan includes the given price ID



180
181
182
183
184
185
186
187
188
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 180

def matches_processor_id?(processor_id)
  processor_plan_ids.values.any? do |ids|
    if ids.is_a?(Hash)
      ids.values.include?(processor_id)
    else
      ids == processor_id
    end
  end
end

#meta(hash) ⇒ Object

Add custom metadata



83
84
85
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 83

def meta(hash)
  .merge!(hash)
end

#parsed_fulfillment_periodObject



249
250
251
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 249

def parsed_fulfillment_period
  UsageCredits::PeriodParser.parse_period(@fulfillment_period)
end

#plan_id_for(processor, period: nil) ⇒ String, ...

Get the plan ID(s) for a specific processor

Parameters:

  • processor (Symbol)

    The payment processor

  • period (Symbol, nil) (defaults to: nil)

    Optional specific period to retrieve

Returns:

  • (String, Hash, nil)

    Single ID, hash of IDs, or nil



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 113

def plan_id_for(processor, period: nil)
  ids = processor_plan_ids[processor.to_sym]

  # If no period specified, return as-is (String or Hash)
  return ids if period.nil?

  # If ids is a Hash, return the specific period
  return ids[period.to_sym] if ids.is_a?(Hash)

  # If ids is a String and they asked for a period, return nil (not multi-period)
  nil
end

#processor_plan(processor, id) ⇒ Object

Set the processor-specific plan ID(s) Accepts either a single ID (String) or multiple period-specific IDs (Hash)

Examples:

Single ID (backward compatible)

processor_plan(:stripe, "price_123")

Multiple periods

processor_plan(:stripe, { month: "price_m", year: "price_y" })

Parameters:

  • processor (Symbol)

    The payment processor (e.g., :stripe)

  • id (String, Hash)

    Single ID or hash of period => ID pairs



100
101
102
103
104
105
106
107
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 100

def processor_plan(processor, id)
  if id.is_a?(Hash)
    raise ArgumentError, "Period hash cannot be empty" if id.empty?
    # Normalize all keys to symbols for consistent lookup
    id = id.transform_keys(&:to_sym)
  end
  processor_plan_ids[processor.to_sym] = id
end

#signup_bonus(amount) ⇒ Object

One-time signup bonus credits



54
55
56
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 54

def (amount)
  @signup_bonus_credits = amount.to_i
end

#stripe_priceString, Hash #stripe_price(id) ⇒ Object #stripe_price(prices) ⇒ Object #stripe_price(period) ⇒ String?

Shorthand for Stripe price ID(s) Supports both single price and multi-period prices

Examples:

Get all prices

plan.stripe_price # => { month: "price_m", year: "price_y" }

Get specific period

plan.stripe_price(:month) # => "price_m"

Set single price (backward compatible)

stripe_price "price_123"

Set multiple periods

stripe_price month: "price_m", year: "price_y"

Overloads:

  • #stripe_priceString, Hash

    Get all Stripe price IDs

    Returns:

    • (String, Hash)

      Single price ID or hash of period => price_id

  • #stripe_price(id) ⇒ Object

    Set a single Stripe price ID (backward compatible)

    Parameters:

    • id (String)

      The Stripe price ID

  • #stripe_price(prices) ⇒ Object

    Set multiple period-specific Stripe price IDs

    Parameters:

    • prices (Hash)

      Hash of period => price_id (e.g., { month: "price_m", year: "price_y" })

  • #stripe_price(period) ⇒ String?

    Get Stripe price ID for a specific period

    Parameters:

    • period (Symbol)

      The billing period (e.g., :month, :year)

    Returns:

    • (String, nil)

      Price ID for that period



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 151

def stripe_price(id_or_period = nil)
  if id_or_period.nil?
    # Getter: return all prices
    plan_id_for(:stripe)
  elsif id_or_period.is_a?(Hash)
    # Setter: hash of period => price_id
    processor_plan(:stripe, id_or_period)
  elsif id_or_period.is_a?(Symbol)
    # Getter: specific period
    plan_id_for(:stripe, period: id_or_period)
  else
    # Setter: single price ID (backward compatible)
    processor_plan(:stripe, id_or_period)
  end
end

#stripe_pricesHash

Get all Stripe price IDs as a hash (always returns hash format)

Returns:

  • (Hash)

    Hash of period => price_id, or { default: price_id } for single-price plans



169
170
171
172
173
174
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 169

def stripe_prices
  ids = plan_id_for(:stripe)
  return {} if ids.nil?
  return ids if ids.is_a?(Hash)
  { default: ids } # Wrap single ID in hash for consistency
end

#trial_includes(amount) ⇒ Object

Credits given during trial period



59
60
61
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 59

def trial_includes(amount)
  @trial_credits = amount.to_i
end

#unused_credits(behavior) ⇒ Object

Configure whether unused credits roll over between periods



64
65
66
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 64

def unused_credits(behavior)
  @rollover_enabled = (behavior == :rollover)
end

#validate!Object

=========================================

Validation

Raises:

  • (ArgumentError)


232
233
234
235
236
237
238
239
# File 'lib/usage_credits/models/credit_subscription_plan.rb', line 232

def validate!
  raise ArgumentError, "Name can't be blank" if name.blank?
  raise ArgumentError, "Credits per period must be greater than 0" unless credits_per_period.to_i.positive?
  raise ArgumentError, "Fulfillment period must be set" if fulfillment_period.nil?
  raise ArgumentError, "Signup bonus credits must be greater than or equal to 0" if .to_i.negative?
  raise ArgumentError, "Trial credits must be greater than or equal to 0" if trial_credits.to_i.negative?
  true
end