slack-ruby-bot-server-stripe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +18 -0
  5. data/.rubocop_todo.yml +29 -0
  6. data/.travis.yml +29 -0
  7. data/CHANGELOG.md +5 -0
  8. data/CONTRIBUTING.md +125 -0
  9. data/Dangerfile +1 -0
  10. data/Gemfile +42 -0
  11. data/LICENSE +21 -0
  12. data/README.md +278 -0
  13. data/RELEASING.md +61 -0
  14. data/Rakefile +14 -0
  15. data/lib/slack-ruby-bot-server-stripe.rb +15 -0
  16. data/lib/slack-ruby-bot-server-stripe/api.rb +1 -0
  17. data/lib/slack-ruby-bot-server-stripe/api/endpoints.rb +1 -0
  18. data/lib/slack-ruby-bot-server-stripe/api/endpoints/subscriptions_endpoint.rb +44 -0
  19. data/lib/slack-ruby-bot-server-stripe/commands.rb +2 -0
  20. data/lib/slack-ruby-bot-server-stripe/commands/subscription.rb +14 -0
  21. data/lib/slack-ruby-bot-server-stripe/commands/unsubscribe.rb +34 -0
  22. data/lib/slack-ruby-bot-server-stripe/config.rb +41 -0
  23. data/lib/slack-ruby-bot-server-stripe/errors.rb +10 -0
  24. data/lib/slack-ruby-bot-server-stripe/lifecycle.rb +17 -0
  25. data/lib/slack-ruby-bot-server-stripe/models.rb +2 -0
  26. data/lib/slack-ruby-bot-server-stripe/models/activerecord.rb +18 -0
  27. data/lib/slack-ruby-bot-server-stripe/models/methods.rb +317 -0
  28. data/lib/slack-ruby-bot-server-stripe/models/mongoid.rb +27 -0
  29. data/lib/slack-ruby-bot-server-stripe/public/img/icon.png +0 -0
  30. data/lib/slack-ruby-bot-server-stripe/public/img/stripe.png +0 -0
  31. data/lib/slack-ruby-bot-server-stripe/public/subscribe.html.erb +99 -0
  32. data/lib/slack-ruby-bot-server-stripe/version.rb +5 -0
  33. data/lib/slack-ruby-bot-server/api.rb +2 -0
  34. data/lib/slack-ruby-bot-server/api/endpoints.rb +9 -0
  35. data/lib/slack-ruby-bot-server/api/presenters.rb +2 -0
  36. data/lib/slack-ruby-bot-server/api/presenters/root_presenter.rb +11 -0
  37. data/lib/slack-ruby-bot-server/api/presenters/team_presenter.rb +10 -0
  38. data/slack-ruby-bot-server-stripe.gemspec +19 -0
  39. metadata +107 -0
@@ -0,0 +1,61 @@
1
+ # Releasing Slack-Ruby-Bot-Server-Stripe
2
+
3
+ There're no hard rules about when to release slack-ruby-bot-server-stripe. Release bug fixes frequently, features not so frequently and breaking API changes rarely.
4
+
5
+ ### Release
6
+
7
+ Run tests, check that all tests succeed locally.
8
+
9
+ ```
10
+ bundle install
11
+ rake
12
+ ```
13
+
14
+ Check that the last build succeeded in [Travis CI](https://travis-ci.org/slack-ruby/slack-ruby-bot-server-stripe) for all supported platforms.
15
+
16
+ Change "Next" in [CHANGELOG.md](CHANGELOG.md) to the current date.
17
+
18
+ ```
19
+ ### 0.2.2 (7/10/2015)
20
+ ```
21
+
22
+ Remove the line with "Your contribution here.", since there will be no more contributions to this release.
23
+
24
+ Commit your changes.
25
+
26
+ ```
27
+ git add CHANGELOG.md
28
+ git commit -m "Preparing for release, 0.2.2."
29
+ git push origin master
30
+ ```
31
+
32
+ Release.
33
+
34
+ ```
35
+ $ rake release
36
+
37
+ slack-ruby-bot-server-stripe 0.2.2 built to pkg/slack-ruby-bot-server-stripe-0.2.2.gem.
38
+ Tagged v0.2.2.
39
+ Pushed git commits and tags.
40
+ Pushed slack-ruby-bot-server-stripe 0.2.2 to rubygems.org.
41
+ ```
42
+
43
+ ### Prepare for the Next Version
44
+
45
+ Add the next release to [CHANGELOG.md](CHANGELOG.md).
46
+
47
+ ```
48
+ ### 0.2.3 (Next)
49
+
50
+ * Your contribution here.
51
+ ```
52
+
53
+ Increment the third version number in [lib/slack-ruby-bot-server-stripe/version.rb](lib/slack-ruby-bot-server-stripe/version.rb).
54
+
55
+ Commit your changes.
56
+
57
+ ```
58
+ git add CHANGELOG.md lib/slack-ruby-bot-server-stripe/version.rb
59
+ git commit -m "Preparing for next development iteration, 0.2.3."
60
+ git push origin master
61
+ ```
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.pattern = FileList['spec/**/*_spec.rb'].exclude(%r{ext\/(?!#{ENV['DATABASE_ADAPTER']})})
9
+ end
10
+
11
+ require 'rubocop/rake_task'
12
+ RuboCop::RakeTask.new
13
+
14
+ task default: %i[rubocop spec]
@@ -0,0 +1,15 @@
1
+ require 'stripe'
2
+
3
+ require 'slack-ruby-bot-server'
4
+
5
+ require_relative 'slack-ruby-bot-server-stripe/version'
6
+ require_relative 'slack-ruby-bot-server-stripe/config'
7
+ require_relative 'slack-ruby-bot-server-stripe/errors'
8
+ require_relative 'slack-ruby-bot-server-stripe/models'
9
+ require_relative 'slack-ruby-bot-server-stripe/lifecycle'
10
+ require_relative 'slack-ruby-bot-server-stripe/api'
11
+ require_relative 'slack-ruby-bot-server-stripe/commands'
12
+
13
+ SlackRubyBotServer::Config.view_paths << File.expand_path(File.join(__dir__, 'slack-ruby-bot-server-stripe/public'))
14
+
15
+ require_relative 'slack-ruby-bot-server/api'
@@ -0,0 +1 @@
1
+ require_relative 'api/endpoints'
@@ -0,0 +1 @@
1
+ require_relative 'endpoints/subscriptions_endpoint'
@@ -0,0 +1,44 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Api
4
+ module Endpoints
5
+ class SubscriptionsEndpoint < Grape::API
6
+ format :json
7
+
8
+ namespace :subscriptions do
9
+ desc 'Create or update a subscription.'
10
+ params do
11
+ requires :stripe_token, type: String
12
+ optional :stripe_token_type, type: String
13
+ optional :stripe_email, type: String
14
+ requires :team_id, type: String
15
+ end
16
+ post do
17
+ begin
18
+ team = Team.where(team_id: params[:team_id]).first || error!('Team Not Found', 404)
19
+ if team.subscribed?
20
+ SlackRubyBotServer::Api::Middleware.logger.info "Updating a subscription for team #{team}."
21
+ stripe_customer = team.update_subscription!(params)
22
+ SlackRubyBotServer::Api::Middleware.logger.info "Updated subscription for team #{team}, stripe_customer_id=#{stripe_customer['id']}."
23
+ else
24
+ SlackRubyBotServer::Api::Middleware.logger.info "Creating a subscription for team #{team}."
25
+ stripe_customer = team.subscribe!(params)
26
+ SlackRubyBotServer::Api::Middleware.logger.info "Subscription for team #{team} created, stripe_customer_id=#{stripe_customer['id']}."
27
+ end
28
+ present team, with: SlackRubyBotServer::Api::Presenters::TeamPresenter
29
+ rescue Errors::AlreadySubscribedError
30
+ error! 'Already Subscribed', 400
31
+ rescue Errors::StripeCustomerExistsError
32
+ error! 'Customer Already Registered', 400
33
+ rescue Errors::NotSubscribedError
34
+ error! 'Not a Subscriber', 400
35
+ rescue Errors::MissingStripeCustomerError
36
+ error! 'Missing Stripe Customer', 400
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'commands/subscription'
2
+ require_relative 'commands/unsubscribe'
@@ -0,0 +1,14 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Commands
4
+ class Subscription < SlackRubyBot::Commands::Base
5
+ command 'subscription' do |client, data, _match|
6
+ team = ::Team.find(client.owner.id)
7
+ include_admin_info = (data.user == team.activated_user_id)
8
+ client.say(channel: data.channel, text: team.subscription_text(include_admin_info: include_admin_info))
9
+ logger.info "SUBSCRIPTION: #{client.owner} - #{data.user}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,34 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Commands
4
+ class Unsubscribe < SlackRubyBot::Commands::Base
5
+ command 'unsubscribe' do |client, data, match|
6
+ team = ::Team.find(client.owner.id)
7
+ if !team.active_stripe_subscription?
8
+ client.say(channel: data.channel, text: "You don't have a paid subscription, all set.")
9
+ logger.info "UNSUBSCRIBE: #{client.owner} - #{data.user} unsubscribe failed, no subscription"
10
+ elsif data.user == team.activated_user_id
11
+ subscription_info = []
12
+ subscription_id = match['expression']
13
+ active_subscription = team.active_stripe_subscription
14
+ if active_subscription && active_subscription.id == subscription_id
15
+ team.unsubscribe!
16
+ amount = ActiveSupport::NumberHelper.number_to_currency(active_subscription.plan.amount.to_f / 100)
17
+ subscription_info << "Successfully canceled auto-renew for #{active_subscription.plan.name} (#{amount})."
18
+ logger.info "UNSUBSCRIBE: #{client.owner} - #{data.user}, canceled #{subscription_id}"
19
+ elsif subscription_id
20
+ subscription_info << "Sorry, I cannot find a subscription with \"#{subscription_id}\"."
21
+ else
22
+ subscription_info << "Send \"unsubscribe #{active_subscription.id}\" to confirm."
23
+ end
24
+ client.say(channel: data.channel, text: subscription_info.compact.join("\n"))
25
+ logger.info "UNSUBSCRIBE: #{client.owner} - #{data.user}"
26
+ else
27
+ client.say(channel: data.channel, text: "Sorry, only <@#{team.activated_user_id}> can do that.")
28
+ logger.info "UNSUBSCRIBE: #{client.owner} - #{data.user} unsubscribe failed, not admin"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Config
4
+ extend self
5
+
6
+ attr_reader :stripe_api_key
7
+
8
+ def stripe_api_key=(value)
9
+ @stripe_api_key = value
10
+ ::Stripe.api_key = value
11
+ end
12
+
13
+ attr_accessor :stripe_api_publishable_key
14
+ attr_accessor :subscription_plan_id
15
+ attr_accessor :subscription_plan_amount
16
+ attr_accessor :trial_duration
17
+ attr_accessor :root_url
18
+
19
+ def reset!
20
+ self.stripe_api_publishable_key = ENV['STRIPE_API_PUBLISHABLE_KEY']
21
+ self.stripe_api_key = ENV['STRIPE_API_KEY']
22
+ self.subscription_plan_id = ENV['STRIPE_SUBSCRIPTION_PLAN_ID']
23
+ self.subscription_plan_amount = -1
24
+ self.root_url = ENV['URL']
25
+ self.trial_duration = 2.weeks
26
+ end
27
+
28
+ reset!
29
+ end
30
+
31
+ class << self
32
+ def configure
33
+ block_given? ? yield(Config) : Config
34
+ end
35
+
36
+ def config
37
+ Config
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Errors
4
+ class StripeCustomerExistsError < StandardError; end
5
+ class MissingStripeCustomerError < StandardError; end
6
+ class AlreadySubscribedError < StandardError; end
7
+ class NotSubscribedError < StandardError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ SlackRubyBotServer::Config.service_class.instance.on :starting do |team|
2
+ begin
3
+ team.check_stripe!
4
+ rescue StandardError => e
5
+ SlackRubyBotServer::Service.logger.error e
6
+ end
7
+ end
8
+
9
+ SlackRubyBotServer::Config.service_class.instance.every :day do
10
+ Team.each do |team|
11
+ begin
12
+ team.check_stripe!
13
+ rescue StandardError => e
14
+ SlackRubyBotServer::Service.logger.error e
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'models/methods'
2
+ require_relative "models/#{::SlackRubyBotServer::Config.database_adapter}.rb"
@@ -0,0 +1,18 @@
1
+ require_relative 'methods'
2
+
3
+ module SlackRubyBotServer
4
+ module Stripe
5
+ module Models
6
+ module ActiveRecord
7
+ extend ActiveSupport::Concern
8
+ include Methods
9
+
10
+ included do
11
+ # TODO
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Team.include SlackRubyBotServer::Stripe::Models::ActiveRecord
@@ -0,0 +1,317 @@
1
+ module SlackRubyBotServer
2
+ module Stripe
3
+ module Models
4
+ module Methods
5
+ extend ActiveSupport::Concern
6
+ extend ActiveModel::Callbacks
7
+
8
+ included do
9
+ define_model_callbacks :trial_expiring, :subscription_expired, :subscription_past_due, :unsubscribed
10
+ define_model_callbacks :subscribed, only: [:after]
11
+ before_validation :update_subscribed_at
12
+ before_validation :update_subscription_expired_at
13
+ after_update :subscribed!
14
+ end
15
+
16
+ # supports https://github.com/slack-ruby/slack-ruby-bot-server-mailchimp
17
+ def tags
18
+ [
19
+ subscribed? ? 'subscribed' : 'trial',
20
+ stripe_customer_id? ? 'paid' : nil
21
+ ].compact
22
+ end
23
+
24
+ def subscription_expired?
25
+ return false if subscribed?
26
+ return true if subscription_expired_at
27
+
28
+ time_limit = Time.now - trial_duration
29
+ created_at < time_limit
30
+ end
31
+
32
+ def trial_expired?
33
+ remaining_trial_days <= 0
34
+ end
35
+
36
+ def subscription_text(options = { include_admin_info: false })
37
+ subscription_text = []
38
+ if active_stripe_subscription?
39
+ subscription_text << stripe_customer_text
40
+ subscription_text.concat(stripe_customer_subscriptions_info)
41
+ if options[:include_admin_info]
42
+ subscription_text.concat(stripe_customer_invoices_info)
43
+ subscription_text.concat(stripe_customer_sources_info)
44
+ subscription_text << update_cc_text
45
+ end
46
+ elsif subscribed && subscribed_at
47
+ subscription_text << subscriber_text
48
+ else
49
+ subscription_text << trial_text
50
+ end
51
+ subscription_text.compact.join("\n")
52
+ end
53
+
54
+ # params:
55
+ # - stripe_token
56
+ # - stripe_email
57
+ # - subscription_plan_id
58
+ def subscribe!(params)
59
+ raise Errors::AlreadySubscribedError if subscribed?
60
+ raise Errors::StripeCustomerExistsError if stripe_customer_id
61
+
62
+ customer = ::Stripe::Customer.create(
63
+ source: params[:stripe_token],
64
+ plan: params[:subscription_plan_id] || SlackRubyBotServer::Stripe.config.subscription_plan_id,
65
+ email: params[:stripe_email],
66
+ metadata: {
67
+ id: id,
68
+ team_id: team_id,
69
+ name: name,
70
+ domain: domain
71
+ }
72
+ )
73
+
74
+ update_attributes!(
75
+ subscribed: true,
76
+ subscribed_at: Time.now.utc,
77
+ stripe_customer_id: customer['id'],
78
+ subscription_expired_at: nil,
79
+ subscription_past_due_at: nil,
80
+ subscription_past_due_informed_at: nil
81
+ )
82
+
83
+ customer
84
+ end
85
+
86
+ # params:
87
+ # - stripe_token
88
+ def update_subscription!(params)
89
+ raise Errors::NotSubscribedError unless subscribed?
90
+ raise Errors::MissingStripeCustomerError unless active_stripe_subscription?
91
+
92
+ stripe_customer.source = params[:stripe_token]
93
+ stripe_customer.save
94
+
95
+ stripe_customer
96
+ end
97
+
98
+ def unsubscribe!
99
+ raise Errors::NotSubscribedError unless subscribed?
100
+ raise Errors::MissingStripeCustomerError unless active_stripe_subscription?
101
+
102
+ run_callbacks :unsubscribed do
103
+ active_stripe_subscription.delete(at_period_end: true)
104
+ update_attributes!(subscribed: false, stripe_customer_id: nil)
105
+ end
106
+ end
107
+
108
+ def active_stripe_subscription?
109
+ !active_stripe_subscription.nil?
110
+ end
111
+
112
+ def active_stripe_subscription
113
+ return unless stripe_customer
114
+
115
+ stripe_customer.subscriptions.detect do |subscription|
116
+ subscription.status == 'active' && !subscription.cancel_at_period_end
117
+ end
118
+ end
119
+
120
+ def trial_ends_at
121
+ raise Errors::AlreadySubscribedError if subscribed?
122
+
123
+ created_at + trial_duration
124
+ end
125
+
126
+ def remaining_trial_days
127
+ raise Errors::AlreadySubscribedError if subscribed?
128
+
129
+ [0, (trial_ends_at.to_date - Time.now.utc.to_date).to_i].max
130
+ end
131
+
132
+ def trial_text
133
+ raise Errors::AlreadySubscribedError if subscribed?
134
+
135
+ [
136
+ remaining_trial_days.zero? ?
137
+ 'Your trial subscription has expired.' :
138
+ "Your trial subscription expires in #{remaining_trial_days} day#{remaining_trial_days == 1 ? '' : 's'}.",
139
+ subscribe_text
140
+ ].join(' ')
141
+ end
142
+
143
+ def unsubscribed_text
144
+ [
145
+ 'Your team has been unsubscribed.',
146
+ subscribe_text
147
+ ].join(' ')
148
+ end
149
+
150
+ def subscribed_text
151
+ 'Your team has been subscribed.'
152
+ end
153
+
154
+ def subscription_expired_text
155
+ [
156
+ 'Your subscription has expired.',
157
+ subscribe_text
158
+ ].join(' ')
159
+ end
160
+
161
+ def subscription_past_due_text
162
+ [
163
+ 'Your subscription is past due.',
164
+ update_cc_text
165
+ ].join(' ')
166
+ end
167
+
168
+ def check_trial!
169
+ raise Errors::AlreadySubscribedError if subscribed?
170
+ return if remaining_trial_days > 3
171
+
172
+ trial_expiring!
173
+ end
174
+
175
+ def check_subscription!
176
+ raise Errors::NotSubscribedError unless subscribed?
177
+ raise Errors::MissingStripeCustomerError unless stripe_customer
178
+
179
+ stripe_customer.subscriptions.each do |subscription|
180
+ case subscription.status
181
+ when 'past_due'
182
+ subscription_past_due!
183
+ when 'canceled', 'unpaid'
184
+ subscription_expired!
185
+ end
186
+ end
187
+ end
188
+
189
+ def check_stripe!
190
+ if subscribed? && active_stripe_subscription?
191
+ check_subscription!
192
+ elsif !subscribed?
193
+ check_trial!
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ def subscription_past_due!
200
+ return unless subscribed?
201
+ return if subscription_past_due_at && (Time.now.utc < subscription_past_due_informed_at + 3.days)
202
+
203
+ run_callbacks :subscription_past_due do
204
+ # use subscription_past_due_text to tell users to update their cc
205
+ update_attributes!(
206
+ subscription_past_due_at: subscription_past_due_at || Time.now.utc,
207
+ subscription_past_due_informed_at: Time.now.utc
208
+ )
209
+ end
210
+ end
211
+
212
+ def subscription_expired!
213
+ return if subscription_expired_at
214
+
215
+ run_callbacks :subscription_expired do
216
+ # use subscribe_text to tell users to (re)subscribe
217
+ update_attributes!(
218
+ subscribed: false,
219
+ subscription_expired_at: Time.now.utc
220
+ )
221
+ end
222
+ end
223
+
224
+ def update_cc_text
225
+ "Update your credit card info at #{root_url}/subscribe?team_id=#{team_id}."
226
+ end
227
+
228
+ def trial_expiring!
229
+ return false if subscribed? || subscription_expired?
230
+ return false if trial_informed_at && (Time.now.utc < trial_informed_at + 7.days)
231
+
232
+ run_callbacks :trial_expiring do
233
+ # use trial_text to inform users
234
+ update_attributes!(trial_informed_at: Time.now.utc)
235
+ end
236
+ end
237
+
238
+ def stripe_customer
239
+ return unless stripe_customer_id
240
+
241
+ @stripe_customer ||= ::Stripe::Customer.retrieve(stripe_customer_id)
242
+ end
243
+
244
+ def stripe_customer_text
245
+ "Customer since #{Time.at(stripe_customer.created).strftime('%B %d, %Y')}."
246
+ end
247
+
248
+ def subscriber_text
249
+ return unless subscribed_at
250
+
251
+ "Subscriber since #{subscribed_at.strftime('%B %d, %Y')}."
252
+ end
253
+
254
+ unless respond_to?(:subscribe_text)
255
+ def subscribe_text
256
+ "Subscribe your team at #{root_url}/subscribe?team_id=#{team_id}."
257
+ end
258
+ end
259
+
260
+ def trial_duration
261
+ SlackRubyBotServer::Stripe.config.trial_duration
262
+ end
263
+
264
+ def root_url
265
+ SlackRubyBotServer::Stripe.config.root_url
266
+ end
267
+
268
+ def stripe_customer_subscriptions_info
269
+ stripe_customer.subscriptions.map do |subscription|
270
+ amount = ActiveSupport::NumberHelper.number_to_currency(subscription.plan.amount.to_f / 100)
271
+ current_period_end = Time.at(subscription.current_period_end).strftime('%B %d, %Y')
272
+ "Subscribed to #{subscription.plan.name} (#{amount}), will#{subscription.cancel_at_period_end ? ' not' : ''} auto-renew on #{current_period_end}."
273
+ end
274
+ end
275
+
276
+ def stripe_auto_renew?
277
+ stripe_customer.subscriptions.any? do |subscription|
278
+ !subscription.cancel_at_period_end
279
+ end
280
+ end
281
+
282
+ def stripe_customer_invoices_info
283
+ stripe_customer.invoices.map do |invoice|
284
+ amount = ActiveSupport::NumberHelper.number_to_currency(invoice.amount_due.to_f / 100)
285
+ "Invoice for #{amount} on #{Time.at(invoice.date).strftime('%B %d, %Y')}, #{invoice.paid ? 'paid' : 'unpaid'}."
286
+ end
287
+ end
288
+
289
+ def stripe_customer_sources_info
290
+ stripe_customer.sources.map do |source|
291
+ "On file #{source.brand} #{source.object}, #{source.name} ending with #{source.last4}, expires #{source.exp_month}/#{source.exp_year}."
292
+ end
293
+ end
294
+
295
+ def subscribed!
296
+ return unless subscribed? && subscribed_changed?
297
+
298
+ run_callbacks :subscribed do
299
+ # use subscribed_text to inform users
300
+ end
301
+ end
302
+
303
+ def update_subscribed_at
304
+ return unless subscribed? && subscribed_changed?
305
+
306
+ self.subscribed_at = subscribed? ? DateTime.now.utc : nil
307
+ end
308
+
309
+ def update_subscription_expired_at
310
+ return unless subscribed? && subscription_expired_at?
311
+
312
+ self.subscription_expired_at = nil
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end