Class: Pay::Braintree::Subscription

Inherits:
Subscription show all
Defined in:
app/models/pay/braintree/subscription.rb

Constant Summary

Constants inherited from Subscription

Subscription::STATUSES

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Subscription

#active?, #canceled?, #cancelled?, #ended?, find_by_processor_and_id, #generic_trial?, #has_incomplete_payment?, #has_trial?, #incomplete?, #no_prorate, #on_grace_period?, #on_trial?, #past_due?, #skip_trial, #swap_and_invoice, #sync!, #trial_ended?, #unpaid?

Class Method Details

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



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
# File 'app/models/pay/braintree/subscription.rb', line 4

def self.sync(subscription_id, object: nil, name: nil, try: 0, retries: 1)
  object ||= Pay.braintree_gateway.subscription.find(subscription_id)

  # Retrieve Pay::Customer
  payment_method = Pay.braintree_gateway.payment_method.find(object.payment_method_token)
  pay_customer = Pay::Customer.find_by(processor: :braintree, processor_id: payment_method.customer_id)
  return unless pay_customer

  # Sync the PaymentMethod since we've got it
  pay_customer.save_payment_method(payment_method, default: payment_method.default?)

  attributes = {
    created_at: object.created_at,
    current_period_end: object.billing_period_end_date,
    current_period_start: object.billing_period_start_date,
    payment_method_id: object.payment_method_token,
    processor_plan: object.plan_id,
    status: object.status.parameterize(separator: "_"),
    trial_ends_at: (object.created_at + object.trial_duration.send(object.trial_duration_unit) if object.trial_period)
  }

  # Canceled subscriptions should have access through the paid_through_date or updated_at
  if object.status == "Canceled"
    attributes[:ends_at] = object.updated_at

  # Set grace period for subscriptions that are marked to be canceled
  elsif object.status == "Active" && object.number_of_billing_cycles
    attributes[:ends_at] = object.paid_through_date.end_of_day
  end

  pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
  if pay_subscription
    pay_subscription.with_lock { pay_subscription.update!(attributes) }
  else
    name ||= Pay.default_product_name
    pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
  end

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

Instance Method Details

#api_record(**options) ⇒ Object



53
54
55
# File 'app/models/pay/braintree/subscription.rb', line 53

def api_record(**options)
  gateway.subscription.find(processor_id)
end

#cancel(**options) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/models/pay/braintree/subscription.rb', line 57

def cancel(**options)
  return if canceled?

  # Braintree doesn't allow canceling at period end while on trial, so trials are canceled immediately
  result = if on_trial?
    gateway.subscription.cancel(processor_id)
  else
    gateway.subscription.update(processor_id, {number_of_billing_cycles: api_record.current_billing_cycle})
  end
  sync!(object: result.subscription)
rescue ::Braintree::BraintreeError => e
  raise Pay::Braintree::Error, e
end

#cancel_now!(**options) ⇒ Object



71
72
73
74
75
76
77
78
# File 'app/models/pay/braintree/subscription.rb', line 71

def cancel_now!(**options)
  return if canceled?

  result = gateway.subscription.cancel(processor_id)
  sync!(object: result.subscription)
rescue ::Braintree::BraintreeError => e
  raise Pay::Braintree::Error, e
end

#change_quantity(quantity, **options) ⇒ Object

Raises:

  • (NotImplementedError)


80
81
82
# File 'app/models/pay/braintree/subscription.rb', line 80

def change_quantity(quantity, **options)
  raise NotImplementedError, "Braintree does not support setting quantity on subscriptions"
end

#pauseObject

Raises:

  • (NotImplementedError)


88
89
90
# File 'app/models/pay/braintree/subscription.rb', line 88

def pause
  raise NotImplementedError, "Braintree does not support pausing subscriptions"
end

#paused?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'app/models/pay/braintree/subscription.rb', line 84

def paused?
  false
end

#resumable?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'app/models/pay/braintree/subscription.rb', line 92

def resumable?
  on_grace_period?
end

#resumeObject



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'app/models/pay/braintree/subscription.rb', line 96

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

  if canceled? && on_trial?
    duration = trial_ends_at.to_date - Date.today

    customer.subscribe(
      name: name,
      plan: processor_plan,
      trial_period: true,
      trial_duration: duration.to_i,
      trial_duration_unit: :day
    )
  else
    gateway.subscription.update(processor_id, {
      never_expires: true,
      number_of_billing_cycles: nil
    })
  end

  update(ends_at: nil, status: :active)
rescue ::Braintree::BraintreeError => e
  raise Pay::Braintree::Error, e
end

#retry_failed_paymentObject

Retries the latest invoice for a Past Due subscription



160
161
162
163
164
165
166
167
168
169
170
# File 'app/models/pay/braintree/subscription.rb', line 160

def retry_failed_payment
  result = gateway.subscription.retry_charge(
    processor_id,
    nil, # amount if different
    true # submit for settlement
  )

  if result.success?
    update(status: :active)
  end
end

#swap(plan, **options) ⇒ Object



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/models/pay/braintree/subscription.rb', line 123

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

  if on_grace_period? && processor_plan == plan
    resume
    return
  end

  unless active?
    customer.subscribe(name: name, plan: plan, trial_period: false)
    return
  end

  braintree_plan = find_braintree_plan(plan)

  if would_change_billing_frequency?(braintree_plan) && prorate?
    swap_across_frequencies(braintree_plan)
    return
  end

  result = gateway.subscription.update(processor_id, {
    plan_id: braintree_plan.id,
    price: braintree_plan.price,
    never_expires: true,
    number_of_billing_cycles: nil,
    options: {
      prorate_charges: prorate?
    }
  })
  raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?

  update(processor_plan: plan, ends_at: nil, status: :active)
rescue ::Braintree::BraintreeError => e
  raise Pay::Braintree::Error, e
end