caffeinate 0.2.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +142 -70
- data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +3 -3
- data/app/models/caffeinate/application_record.rb +0 -1
- data/app/models/caffeinate/campaign.rb +25 -7
- data/app/models/caffeinate/campaign_subscription.rb +44 -14
- data/app/models/caffeinate/mailing.rb +14 -6
- data/app/views/layouts/{caffeinate.html.erb → _caffeinate.html.erb} +0 -0
- data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
- data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +5 -3
- data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
- data/lib/caffeinate.rb +4 -18
- data/lib/caffeinate/action_mailer/extension.rb +1 -1
- data/lib/caffeinate/action_mailer/interceptor.rb +2 -2
- data/lib/caffeinate/action_mailer/observer.rb +4 -3
- data/lib/caffeinate/active_record/extension.rb +15 -10
- data/lib/caffeinate/configuration.rb +9 -2
- data/lib/caffeinate/drip.rb +22 -2
- data/lib/caffeinate/drip_evaluator.rb +1 -0
- data/lib/caffeinate/dripper/base.rb +4 -0
- data/lib/caffeinate/dripper/batching.rb +13 -11
- data/lib/caffeinate/dripper/callbacks.rb +46 -18
- data/lib/caffeinate/dripper/campaign.rb +18 -4
- data/lib/caffeinate/dripper/defaults.rb +1 -0
- data/lib/caffeinate/dripper/delivery.rb +7 -7
- data/lib/caffeinate/dripper/drip.rb +2 -41
- data/lib/caffeinate/dripper/drip_collection.rb +62 -0
- data/lib/caffeinate/dripper/inferences.rb +3 -1
- data/lib/caffeinate/dripper/perform.rb +12 -10
- data/lib/caffeinate/dripper/periodical.rb +26 -0
- data/lib/caffeinate/dripper/subscriber.rb +14 -2
- data/lib/caffeinate/dripper_collection.rb +17 -0
- data/lib/caffeinate/engine.rb +9 -2
- data/lib/caffeinate/mail_ext.rb +12 -0
- data/lib/caffeinate/version.rb +1 -1
- data/lib/generators/caffeinate/install_generator.rb +1 -1
- data/lib/generators/caffeinate/templates/caffeinate.rb +10 -0
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93135cbbdb419e6d7eccd587c9b10447c3c7247cc425ff86057a328b27761baa
|
4
|
+
data.tar.gz: 5b7993dff3ccb1e3e468438c5b50f73f525d2a03418e0ceaaa03f751c097f010
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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: "
|
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.
|
47
|
+
return if user.unsubscribed_from_onboarding_campaign?
|
48
|
+
return if user.onboarding_completed?
|
28
49
|
|
29
|
-
mail(to: user.email, subject: "
|
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
|
-
|
55
|
+
### What's wrong with this?
|
35
56
|
|
36
|
-
|
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
|
-
|
61
|
+
## Caffeinate to the rescue
|
39
62
|
|
40
|
-
|
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
|
-
|
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
|
-
|
72
|
+
## Onboarding in Caffeinate
|
47
73
|
|
48
|
-
|
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
|
-
###
|
76
|
+
### Install it
|
62
77
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
###
|
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
|
-
|
90
|
+
The only other change you need to make is the argument that the mailer action receives:
|
74
91
|
|
75
92
|
```ruby
|
76
|
-
|
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
|
-
|
111
|
+
While we're there, let's add an unsubscribe link to the views or layout:
|
80
112
|
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
117
|
+
### Create a Dripper
|
87
118
|
|
88
|
-
|
119
|
+
A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
|
89
120
|
|
90
|
-
|
121
|
+
In `app/drippers/onboarding_dripper.rb`:
|
91
122
|
|
92
123
|
```ruby
|
93
|
-
|
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
|
-
|
131
|
+
The `drip` syntax is `def drip(mailer_action, options = {})`.
|
97
132
|
|
98
|
-
|
99
|
-
$ bundle
|
100
|
-
```
|
133
|
+
### Add a subscriber to the Campaign
|
101
134
|
|
102
|
-
|
135
|
+
Call `OnboardingDripper.subscribe` to subscribe a polymorphic `subscriber` to the Campaign, which creates
|
136
|
+
a `Caffeinate::CampaignSubscription`.
|
103
137
|
|
104
|
-
```
|
105
|
-
|
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
|
-
|
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
|
-
|
111
|
-
|
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/
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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 '
|
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.
|
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
|
|
@@ -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.
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
77
|
+
def end!(reason = nil)
|
78
|
+
raise ::Caffeinate::InvalidState, 'CampaignSubscription is already unsubscribed.' if unsubscribed?
|
70
79
|
|
71
|
-
|
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
|
-
|
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
|
-
|
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
|