caffeinate 0.2.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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