caffeinate 0.2.1 → 0.7.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +142 -70
  3. data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +3 -3
  4. data/app/models/caffeinate/application_record.rb +0 -1
  5. data/app/models/caffeinate/campaign.rb +25 -7
  6. data/app/models/caffeinate/campaign_subscription.rb +44 -14
  7. data/app/models/caffeinate/mailing.rb +14 -6
  8. data/app/views/layouts/{caffeinate.html.erb → _caffeinate.html.erb} +0 -0
  9. data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
  10. data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +5 -3
  11. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
  12. data/lib/caffeinate.rb +4 -18
  13. data/lib/caffeinate/action_mailer/extension.rb +1 -1
  14. data/lib/caffeinate/action_mailer/interceptor.rb +2 -2
  15. data/lib/caffeinate/action_mailer/observer.rb +4 -3
  16. data/lib/caffeinate/active_record/extension.rb +15 -10
  17. data/lib/caffeinate/configuration.rb +9 -2
  18. data/lib/caffeinate/drip.rb +22 -2
  19. data/lib/caffeinate/drip_evaluator.rb +1 -0
  20. data/lib/caffeinate/dripper/base.rb +4 -0
  21. data/lib/caffeinate/dripper/batching.rb +13 -11
  22. data/lib/caffeinate/dripper/callbacks.rb +46 -18
  23. data/lib/caffeinate/dripper/campaign.rb +18 -4
  24. data/lib/caffeinate/dripper/defaults.rb +1 -0
  25. data/lib/caffeinate/dripper/delivery.rb +7 -7
  26. data/lib/caffeinate/dripper/drip.rb +2 -41
  27. data/lib/caffeinate/dripper/drip_collection.rb +62 -0
  28. data/lib/caffeinate/dripper/inferences.rb +3 -1
  29. data/lib/caffeinate/dripper/perform.rb +12 -10
  30. data/lib/caffeinate/dripper/periodical.rb +26 -0
  31. data/lib/caffeinate/dripper/subscriber.rb +14 -2
  32. data/lib/caffeinate/dripper_collection.rb +17 -0
  33. data/lib/caffeinate/engine.rb +9 -2
  34. data/lib/caffeinate/mail_ext.rb +12 -0
  35. data/lib/caffeinate/version.rb +1 -1
  36. data/lib/generators/caffeinate/install_generator.rb +1 -1
  37. data/lib/generators/caffeinate/templates/caffeinate.rb +10 -0
  38. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3241ca7ca0c6e31220511ad04172105d97058fa84f1de1e6ac442052823a91e0
4
- data.tar.gz: 5b0bda6f08ae89f79cbd64df4c75f48fcb0dacf7471155aae779290b6a1b2e41
3
+ metadata.gz: 93135cbbdb419e6d7eccd587c9b10447c3c7247cc425ff86057a328b27761baa
4
+ data.tar.gz: 5b7993dff3ccb1e3e468438c5b50f73f525d2a03418e0ceaaa03f751c097f010
5
5
  SHA512:
6
- metadata.gz: d43a0030851c7fa107fed83fcc05c9cfa8596be068a8327d01f597a0a45fc84aa403d1a95990e8a4c45013fb44bc899215778d36ce8a8f5cc770db50b33cd0df
7
- data.tar.gz: 6f9e399cc7e0a00bbb544b668d80e0438f4482013f3163a819b9f2ab8ea502f3c8535aacb91d57eb80efc5f12c22bd03a2bdfd36c2216544bb9aa1fd6928e797
6
+ metadata.gz: 504b0123a7628ff97fda131c0ddb55ccc76d1ddc63b9a75afb799170ad3e10bbdc4598860848045eb6ac5976a44575bbb0287bf0bcb33744c29e12c551feb8e0
7
+ data.tar.gz: 48f9293fdbe4f34ed6a0d46bdfce4278783a8e49fde0c740a5f988572790570209e6e06db559bdfc7acaf0a15ab00718af14e8e43d69d30332b9a8a578d3410b
data/README.md CHANGED
@@ -1,129 +1,201 @@
1
+ <div align="center">
2
+ <img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
3
+ </div>
4
+
5
+ ---
6
+
1
7
  # Caffeinate
2
8
 
3
9
  Caffeinate is a drip campaign engine for Ruby on Rails applications.
4
10
 
5
- Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
11
+ Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
6
12
  and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
7
13
  already familiar with: Ruby on Rails.
8
14
 
9
- ## Usage
15
+ There's a cool demo with all the things included at [caffeinate.email](https://caffeinate.email). You can view the [marketing site source code here](https://github.com/joshmn/caffeinate-marketing).
16
+
17
+ ## Do you suffer from ActionMailer tragedies?
18
+
19
+ If you have _anything_ like this is your codebase, **you need Caffeinate**:
10
20
 
11
- You can probably imagine seeing a Mailer like this:
21
+ ```ruby
22
+ class User < ApplicationRecord
23
+ after_commit on: :create do
24
+ OnboardingMailer.welcome_to_my_cool_app(self).deliver_later
25
+ OnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)
26
+ OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)
27
+ end
28
+ end
29
+ ```
12
30
 
13
- ```ruby
14
- class OnboardingMailer < ActionMailer::Base
31
+ ```ruby
32
+ class OnboardingMailer < ActionMailer::Base
15
33
  # Send on account creation
16
34
  def welcome_to_my_cool_app(user)
17
- mail(to: user.email, subject: "You forgot something!")
35
+ mail(to: user.email, subject: "Welcome to CoolApp!")
18
36
  end
19
37
 
20
38
  # Send 2 days after the user signs up
21
39
  def some_cool_tips(user)
40
+ return if user.unsubscribed_from_onboarding_campaign?
41
+
22
42
  mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
23
- end
43
+ end
24
44
 
25
45
  # Sends 3 days after the user signs up and hasn't added a company profile yet
26
46
  def help_getting_started(user)
27
- return if user.company.present?
47
+ return if user.unsubscribed_from_onboarding_campaign?
48
+ return if user.onboarding_completed?
28
49
 
29
- mail(to: user.email, subject: "Did you know...")
30
- end
31
- end
50
+ mail(to: user.email, subject: "Do you need help getting started?")
51
+ end
52
+ end
32
53
  ```
33
54
 
34
- With background jobs running, checking, and everything else. That's messy. Why are we checking state in the Mailer? Ugh.
55
+ ### What's wrong with this?
35
56
 
36
- We can clean this up with Caffeinate. Here's how we'd do it.
57
+ * You're checking state in a mailer
58
+ * The unsubscribe feature is, most likely, tied to a `User`, which means...
59
+ * It's going to be _so fun_ to scale horizontally
37
60
 
38
- ### Create a Campaign
61
+ ## Caffeinate to the rescue
39
62
 
40
- ```ruby
41
- Caffeinate::Campaign.create!(name: "Onboarding Campaign", slug: "onboarding")
42
- ```
63
+ Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.
43
64
 
44
- ### Create a Caffeinate::Dripper
65
+ What can you do with drip campaigns?
66
+ * Onboard new customers with cool tips and tricks
67
+ * Remind customers to use your product
68
+ * Nag customers about using your product
69
+ * Reach their spam folder after you fail to handle their unsubscribe request
70
+ * And more!
45
71
 
46
- Place the contents below in `app/drippers/onboarding_dripper.rb`:
72
+ ## Onboarding in Caffeinate
47
73
 
48
- ```ruby
49
- class OnboardingDripper < ApplicationDripper
50
- drip :welcome_to_my_cool_app, delay: 0.hours
51
- drip :some_cool_tips, delay: 2.days
52
- drip :help_getting_started, delay: 3.days do
53
- if mailing.user.company.present?
54
- mailing.unsubscribe!
55
- return false
56
- end
57
- end
58
- end
59
- ```
74
+ In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!
60
75
 
61
- ### Add a subscriber to the Campaign
76
+ ### Install it
62
77
 
63
- ```ruby
64
- class User < ApplicationRecord
65
- after_create_commit do
66
- Caffeinate::Campaign.find_by(slug: "onboarding").subscribe(self)
67
- end
68
- end
78
+ Add to Gemfile, run the installer, migrate:
79
+
80
+ ```bash
81
+ $ bundle add caffeinate
82
+ $ rails g caffeinate:install
83
+ $ rake db:migrate
69
84
  ```
70
85
 
71
- ### Run the Dripper
86
+ ### Remove that ActionMailer logic
87
+
88
+ Just delete it. Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
72
89
 
73
- You'd normally want to do this in a cron/whenever/scheduled Sidekiq/etc job.
90
+ The only other change you need to make is the argument that the mailer action receives:
74
91
 
75
92
  ```ruby
76
- OnboardingDripper.perform!
93
+ class OnboardingMailer < ActionMailer::Base
94
+ def welcome_to_my_cool_app(mailing)
95
+ @user = mailing.subscriber
96
+ mail(to: @user.email, subject: "Welcome to CoolApp!")
97
+ end
98
+
99
+ def some_cool_tips(mailing)
100
+ @user = mailing.subscriber
101
+ mail(to: @user.email, subject: "Here are some cool tips for MyCoolApp")
102
+ end
103
+
104
+ def help_getting_started(mailing)
105
+ @user = mailing.subscriber
106
+ mail(to: @user.email, subject: "Do you need help getting started?")
107
+ end
108
+ end
77
109
  ```
78
110
 
79
- ### Spend more time building
111
+ While we're there, let's add an unsubscribe link to the views or layout:
80
112
 
81
- Now you can spend more time building your app and less time managing your marketing campaigns.
82
- * Centralized logic makes it easy to understand the flow
83
- * Subscription management, timings, send history all built-in
84
- * Built on the stack you're already familiar with
113
+ ```erb
114
+ <%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>
115
+ ```
85
116
 
86
- There's a lot more than what you just saw, too! Caffeinate almost makes managing timed email sequences fun.
117
+ ### Create a Dripper
87
118
 
88
- ## Installation
119
+ A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
89
120
 
90
- Add this line to your application's Gemfile:
121
+ In `app/drippers/onboarding_dripper.rb`:
91
122
 
92
123
  ```ruby
93
- gem 'caffeinate'
124
+ class OnboardingDripper < ApplicationDripper
125
+ drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
126
+ drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
127
+ drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
128
+ end
94
129
  ```
95
130
 
96
- And then do the bundle:
131
+ The `drip` syntax is `def drip(mailer_action, options = {})`.
97
132
 
98
- ```bash
99
- $ bundle
100
- ```
133
+ ### Add a subscriber to the Campaign
101
134
 
102
- Add do some housekeeping:
135
+ Call `OnboardingDripper.subscribe` to subscribe a polymorphic `subscriber` to the Campaign, which creates
136
+ a `Caffeinate::CampaignSubscription`.
103
137
 
104
- ```bash
105
- $ rails g caffeinate:install
138
+ ```ruby
139
+ class User < ApplicationRecord
140
+ after_commit on: :create do
141
+ OnboardingDripper.subscribe(self)
142
+ end
143
+
144
+ after_commit on: :update do
145
+ if onboarding_completed? && onboarding_completed_changed?
146
+ if OnboardingDripper.subscribed?(self)
147
+ OnboardingDripper.unsubscribe(self)
148
+ end
149
+ end
150
+ end
151
+ end
106
152
  ```
107
153
 
108
- Followed by a migrate:
154
+ When a `Caffeinate::CampaignSubscription` is created, the relevant Dripper is parsed and `Caffeinate::Mailing` records
155
+ are created from the `drip` DSL. A `Caffeinate::Mailing` record has a `send_at` attribute which tells Caffeinate when we
156
+ can send the mail, which we get from `Caffeiate::Mailing#mailer_class` and `Caffeinate::Mailing#mailer_action`.
109
157
 
110
- ```bash
111
- $ rails db:migrate
158
+ ### Run the Dripper
159
+
160
+ Running `OnboardingDripper.perform!` every `x` minutes will call `Caffeinate::Mailing#process!` on `Caffeinate::Mailing`
161
+ records that have `send_at < Time.now`.
162
+
163
+ ```ruby
164
+ OnboardingDripper.perform!
112
165
  ```
113
166
 
167
+ ### Done. But wait, there's more fun if you want
168
+
169
+ * Automatic subscriptions
170
+ * Campaign-specific unsubscribe links
171
+ * Reasons for unsubscribing so you can have some sort of analytics
172
+ * Periodical emails (daily, weekly, monthly digests, anyone?)
173
+ * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
174
+
175
+ ### Done. But wait, there's more fun if you want
176
+
177
+ * Automatic subscriptions
178
+ * Campaign-specific unsubscribe links
179
+ * Reasons for unsubscribing so you can have some sort of analytics
180
+ * Periodical emails (daily, weekly, monthly digests, anyone?)
181
+ * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
182
+
114
183
  ## Documentation
115
184
 
116
- * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
117
- * [Better-than-average code documentation](https://rubydoc.info/github/joshmn/caffeinate)
185
+ * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
186
+ * [Better-than-average code documentation](https://rubydoc.info/gems/caffeinate)
118
187
 
119
188
  ## Upcoming features/todo
120
189
 
121
- * Ability to optionally use relative start time when creating a step
122
- * Logo
123
- * Conversion tracking
124
- * Custom field support on CampaignSubscription
125
- * GUI (?)
126
- * REST API (?)
190
+ [Handy dandy roadmap](https://github.com/joshmn/caffeinate/projects/1).
191
+
192
+ ## Alternatives
193
+
194
+ Not a fan? There are some alternatives!
195
+
196
+ * https://github.com/honeybadger-io/heya
197
+ * https://github.com/tarr11/dripper
198
+ * https://github.com/Sology/maily_herald
127
199
 
128
200
  ## Contributing
129
201
 
@@ -131,8 +203,8 @@ Just do it.
131
203
 
132
204
  ## Contributors & thanks
133
205
 
134
- * Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
135
-
206
+ * Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
207
+
136
208
  ## License
137
209
 
138
210
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  class CampaignSubscriptionsController < ApplicationController
5
- layout 'caffeinate'
5
+ layout '_caffeinate'
6
6
 
7
7
  helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
8
8
 
@@ -13,7 +13,7 @@ module Caffeinate
13
13
  end
14
14
 
15
15
  def subscribe
16
- @campaign_subscription.subscribe!
16
+ @campaign_subscription.resubscribe!
17
17
  end
18
18
 
19
19
  private
@@ -22,7 +22,7 @@ module Caffeinate
22
22
  Caffeinate::UrlHelpers.caffeinate_subscribe_url(@campaign_subscription, options)
23
23
  end
24
24
 
25
- def caffeinate_unsubscribe_url
25
+ def caffeinate_unsubscribe_url(**options)
26
26
  Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(@campaign_subscription, options)
27
27
  end
28
28
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- # :nodoc:
5
4
  class ApplicationRecord < ::ActiveRecord::Base
6
5
  self.abstract_class = true
7
6
  end
@@ -14,12 +14,15 @@ module Caffeinate
14
14
  # Campaign ties together subscribers and mailings, and provides one core model for handling your Drippers.
15
15
  class Campaign < ApplicationRecord
16
16
  self.table_name = 'caffeinate_campaigns'
17
+
17
18
  has_many :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
19
+ has_many :subscriptions, class_name: 'Caffeinate::CampaignSubscription', foreign_key: :caffeinate_campaign_id
18
20
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
21
+ has_many :mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
19
22
 
20
23
  # Poorly-named Campaign class resolver
21
24
  def to_dripper
22
- Caffeinate.dripper_to_campaign_class[slug.to_sym].constantize
25
+ ::Caffeinate.dripper_collection.resolve(self)
23
26
  end
24
27
 
25
28
  # Convenience method for find_by!(slug: value)
@@ -43,17 +46,32 @@ module Caffeinate
43
46
  subscriber(record, **args).present?
44
47
  end
45
48
 
46
- # Subscribes an object to a campaign.
49
+ # Unsubscribes an object from a campaign.
50
+ #
51
+ # Campaign[:onboarding].unsubscribe(Company.first, user: Company.first.admin, reason: "Because I said so")
52
+ #
53
+ # is the same as
54
+ #
55
+ # Campaign.find_by(slug: "onboarding").caffeinate_campaign_subscriptions.find_by(subscriber: Company.first, user: Company.first.admin).unsubscribe!("Because I said so")
56
+ #
57
+ # Just... mintier.
58
+ def unsubscribe(subscriber, **args)
59
+ reason = args.delete(:reason)
60
+ subscription = subscriber(subscriber, **args)
61
+ raise ActiveRecord::RecordInvalid, subscription if subscription.nil?
62
+
63
+ subscription.unsubscribe!(reason)
64
+ end
65
+
66
+ # Creates a `CampaignSubscription` object for the present Campaign. Allows passing `**args` to
67
+ # delegate additional arguments to the record. Uses `find_or_create_by`.
47
68
  def subscribe(subscriber, **args)
48
69
  caffeinate_campaign_subscriptions.find_or_create_by(subscriber: subscriber, **args)
49
70
  end
50
71
 
51
- # Subscribes an object to a campaign.
72
+ # Subscribes an object to a campaign. Raises `ActiveRecord::RecordInvalid` if the record was invalid.
52
73
  def subscribe!(subscriber, **args)
53
- subscription = subscribe(subscriber, **args)
54
- return subscription if subscribe.persisted?
55
-
56
- raise ActiveRecord::RecordInvalid, subscription
74
+ subscribe(subscriber, **args)
57
75
  end
58
76
  end
59
77
  end
@@ -17,23 +17,30 @@
17
17
  # updated_at :datetime not null
18
18
  #
19
19
  module Caffeinate
20
+ # If a record tries to be `unsubscribed!` or `ended!` or `resubscribe!` and it's in a state that is not
21
+ # correct, raise this
22
+ class InvalidState < ::ActiveRecord::RecordInvalid; end
23
+
20
24
  # CampaignSubscription associates an object and its optional user to a Campaign
21
25
  # and its relevant Mailings.
22
26
  class CampaignSubscription < ApplicationRecord
23
27
  self.table_name = 'caffeinate_campaign_subscriptions'
24
28
 
25
29
  has_many :caffeinate_mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
26
- has_one :next_caffeinate_mailing, -> { upcoming.unsent }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
30
+ has_many :mailings, class_name: 'Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id, dependent: :destroy
31
+
32
+ has_one :next_caffeinate_mailing, -> { upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
33
+ has_one :next_mailing, -> { upcoming.unsent.order(send_at: :asc) }, class_name: '::Caffeinate::Mailing', foreign_key: :caffeinate_campaign_subscription_id
34
+
27
35
  belongs_to :caffeinate_campaign, class_name: 'Caffeinate::Campaign', foreign_key: :caffeinate_campaign_id
36
+ alias_attribute :campaign, :caffeinate_campaign
37
+
28
38
  belongs_to :subscriber, polymorphic: true
29
39
  belongs_to :user, polymorphic: true, optional: true
30
40
 
31
41
  # All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
32
42
  scope :active, -> { where(unsubscribed_at: nil, ended_at: nil) }
33
-
34
- # All CampaignSubscriptions that where `unsubscribed_at` is nil and `ended_at` is nil
35
43
  scope :subscribed, -> { active }
36
-
37
44
  scope :unsubscribed, -> { where.not(unsubscribed_at: nil) }
38
45
 
39
46
  # All CampaignSubscriptions that where `ended_at` is not nil
@@ -44,49 +51,70 @@ module Caffeinate
44
51
 
45
52
  after_commit :create_mailings!, on: :create
46
53
 
54
+ after_commit :on_complete, if: :completed?
55
+
47
56
  # Actually deliver and process the mail
48
57
  def deliver!(mailing)
49
58
  caffeinate_campaign.to_dripper.deliver!(mailing)
50
59
  end
51
60
 
52
- # Checks if the subscription is not ended and not unsubscribed
61
+ # Checks if the `CampaignSubscription` is not ended and not unsubscribed
53
62
  def subscribed?
54
63
  !ended? && !unsubscribed?
55
64
  end
56
65
 
57
- # Checks if the CampaignSubscription is not subscribed by checking the presence of `unsubscribed_at`
66
+ # Checks if the `CampaignSubscription` is not subscribed by checking the presence of `unsubscribed_at`
58
67
  def unsubscribed?
59
68
  unsubscribed_at.present?
60
69
  end
61
70
 
62
- # Checks if the CampaignSubscription is ended by checking the presence of `ended_at`
71
+ # Checks if the `CampaignSubscription` is ended by checking the presence of `ended_at`
63
72
  def ended?
64
73
  ended_at.present?
65
74
  end
66
75
 
67
76
  # Updates `ended_at` and runs `on_complete` callbacks
68
- def end!
69
- update!(ended_at: ::Caffeinate.config.time_now)
77
+ def end!(reason = nil)
78
+ raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
70
79
 
71
- caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
80
+ update!(ended_at: ::Caffeinate.config.time_now, ended_reason: reason)
81
+
82
+ caffeinate_campaign.to_dripper.run_callbacks(:on_end, self)
83
+ true
72
84
  end
73
85
 
74
86
  # Updates `unsubscribed_at` and runs `on_subscribe` callbacks
75
- def unsubscribe!
76
- update!(unsubscribed_at: ::Caffeinate.config.time_now)
87
+ def unsubscribe!(reason = nil)
88
+ raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended?
89
+
90
+ update!(unsubscribed_at: ::Caffeinate.config.time_now, unsubscribe_reason: reason)
77
91
 
78
92
  caffeinate_campaign.to_dripper.run_callbacks(:on_unsubscribe, self)
93
+ true
79
94
  end
80
95
 
81
- # Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks
82
- def resubscribe!
96
+ # Updates `unsubscribed_at` to nil and runs `on_subscribe` callbacks.
97
+ # Use `force` to forcefully reset. Does not create the mailings.
98
+ def resubscribe!(force = false)
99
+ raise ::Caffeinate::InvalidState, 'CampaignSubscription is already ended.' if ended? && !force
100
+ raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed? && !force
101
+
83
102
  update!(unsubscribed_at: nil, resubscribed_at: ::Caffeinate.config.time_now)
84
103
 
85
104
  caffeinate_campaign.to_dripper.run_callbacks(:on_resubscribe, self)
105
+ true
106
+ end
107
+
108
+ def completed?
109
+ caffeinate_mailings.unsent.count.zero?
86
110
  end
87
111
 
88
112
  private
89
113
 
114
+ def on_complete
115
+ caffeinate_campaign.to_dripper.run_callbacks(:on_complete, self)
116
+ end
117
+
90
118
  # Create mailings according to the drips registered in the Campaign
91
119
  def create_mailings!
92
120
  caffeinate_campaign.to_dripper.drips.each do |drip|
@@ -94,8 +122,10 @@ module Caffeinate
94
122
  mailing.save!
95
123
  end
96
124
  caffeinate_campaign.to_dripper.run_callbacks(:on_subscribe, self)
125
+ true
97
126
  end
98
127
 
128
+ # Sets a unique token
99
129
  def set_token!
100
130
  loop do
101
131
  self.token = SecureRandom.uuid