slack-ruby-bot-server-stripe 0.1.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.
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