caffeinate 0.2.0 → 0.6.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -77
  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 +49 -2
  6. data/app/models/caffeinate/campaign_subscription.rb +50 -13
  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 +6 -3
  11. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
  12. data/lib/caffeinate.rb +4 -8
  13. data/lib/caffeinate/action_mailer.rb +4 -4
  14. data/lib/caffeinate/action_mailer/extension.rb +11 -5
  15. data/lib/caffeinate/action_mailer/interceptor.rb +4 -2
  16. data/lib/caffeinate/action_mailer/observer.rb +4 -3
  17. data/lib/caffeinate/active_record/extension.rb +17 -11
  18. data/lib/caffeinate/configuration.rb +11 -2
  19. data/lib/caffeinate/drip.rb +15 -2
  20. data/lib/caffeinate/drip_evaluator.rb +3 -0
  21. data/lib/caffeinate/dripper/base.rb +12 -5
  22. data/lib/caffeinate/dripper/batching.rb +22 -0
  23. data/lib/caffeinate/dripper/callbacks.rb +89 -6
  24. data/lib/caffeinate/dripper/campaign.rb +20 -8
  25. data/lib/caffeinate/dripper/defaults.rb +4 -2
  26. data/lib/caffeinate/dripper/delivery.rb +8 -8
  27. data/lib/caffeinate/dripper/drip.rb +3 -42
  28. data/lib/caffeinate/dripper/drip_collection.rb +62 -0
  29. data/lib/caffeinate/dripper/inferences.rb +7 -2
  30. data/lib/caffeinate/dripper/perform.rb +14 -7
  31. data/lib/caffeinate/dripper/periodical.rb +26 -0
  32. data/lib/caffeinate/dripper/subscriber.rb +14 -2
  33. data/lib/caffeinate/dripper_collection.rb +17 -0
  34. data/lib/caffeinate/engine.rb +6 -4
  35. data/lib/caffeinate/helpers.rb +3 -0
  36. data/lib/caffeinate/mail_ext.rb +12 -0
  37. data/lib/caffeinate/url_helpers.rb +3 -0
  38. data/lib/caffeinate/version.rb +1 -1
  39. data/lib/generators/caffeinate/install_generator.rb +5 -1
  40. data/lib/generators/caffeinate/templates/caffeinate.rb +21 -1
  41. metadata +22 -4
  42. data/lib/caffeinate/action_mailer/helpers.rb +0 -12
@@ -17,12 +17,12 @@
17
17
  module Caffeinate
18
18
  # Records of the mails sent and to be sent for a given `::Caffeinate::CampaignSubscriber`
19
19
  class Mailing < ApplicationRecord
20
- CURRENT_THREAD_KEY = :current_caffeinate_mailing.freeze
21
-
22
20
  self.table_name = 'caffeinate_mailings'
23
21
 
24
22
  belongs_to :caffeinate_campaign_subscription, class_name: 'Caffeinate::CampaignSubscription'
23
+ alias_attribute :subscription, :caffeinate_campaign_subscription
25
24
  has_one :caffeinate_campaign, through: :caffeinate_campaign_subscription
25
+ alias_attribute :campaign, :caffeinate_campaign
26
26
 
27
27
  scope :upcoming, -> { unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
28
28
  scope :unsent, -> { unskipped.where(sent_at: nil) }
@@ -30,6 +30,13 @@ module Caffeinate
30
30
  scope :skipped, -> { where.not(skipped_at: nil) }
31
31
  scope :unskipped, -> { where(skipped_at: nil) }
32
32
 
33
+ def initialize_dup(args)
34
+ super
35
+ self.send_at = nil
36
+ self.sent_at = nil
37
+ self.skipped_at = nil
38
+ end
39
+
33
40
  # Checks if the Mailing is not skipped and not sent
34
41
  def pending?
35
42
  unskipped? && unsent?
@@ -59,13 +66,12 @@ module Caffeinate
59
66
  def skip!
60
67
  update!(skipped_at: Caffeinate.config.time_now)
61
68
 
62
- caffeinate_campaign.to_dripper.run_callbacks(:on_skip, caffeinate_campaign_subscription, self)
69
+ caffeinate_campaign.to_dripper.run_callbacks(:on_skip, self)
63
70
  end
64
71
 
65
72
  # The associated drip
66
- # @todo This can be optimized with a better cache
67
73
  def drip
68
- @drip ||= caffeinate_campaign.to_dripper.drip_collection[mailer_action]
74
+ @drip ||= caffeinate_campaign.to_dripper.drip_collection.for(mailer_action)
69
75
  end
70
76
 
71
77
  # The associated Subscriber from `::Caffeinate::CampaignSubscription`
@@ -80,7 +86,7 @@ module Caffeinate
80
86
 
81
87
  # Assigns attributes to the Mailing from the Drip
82
88
  def from_drip(drip)
83
- self.send_at = drip.send_at
89
+ self.send_at = drip.send_at(self)
84
90
  self.mailer_class = drip.options[:mailer_class]
85
91
  self.mailer_action = drip.action
86
92
  self
@@ -93,6 +99,8 @@ module Caffeinate
93
99
  else
94
100
  deliver!
95
101
  end
102
+
103
+ caffeinate_campaign_subscription.touch
96
104
  end
97
105
 
98
106
  # Delivers the Mailing in the foreground
@@ -6,6 +6,7 @@ class CreateCaffeinateCampaigns < ActiveRecord::Migration[6.0]
6
6
  create_table :caffeinate_campaigns do |t|
7
7
  t.string :name, null: false
8
8
  t.string :slug, null: false
9
+ t.boolean :active, default: true, null: false
9
10
 
10
11
  t.timestamps
11
12
  end
@@ -7,16 +7,19 @@ class CreateCaffeinateCampaignSubscriptions < ActiveRecord::Migration[6.0]
7
7
  create_table :caffeinate_campaign_subscriptions do |t|
8
8
  t.references :caffeinate_campaign, null: false, index: { name: :caffeineate_campaign_subscriptions_on_campaign }, foreign_key: true
9
9
  t.string :subscriber_type, null: false
10
- t.string :subscriber_id, null: false
10
+ t.integer :subscriber_id, null: false
11
11
  t.string :user_type
12
- t.string :user_id
12
+ t.integer :user_id
13
13
  t.string :token, null: false
14
14
  t.datetime :ended_at
15
+ t.string :ended_reason
16
+ t.datetime :resubscribed_at
15
17
  t.datetime :unsubscribed_at
18
+ t.string :unsubscribe_reason
16
19
 
17
20
  t.timestamps
18
21
  end
19
22
  add_index :caffeinate_campaign_subscriptions, :token, unique: true
20
- add_index :caffeinate_campaign_subscriptions, %i[subscriber_id subscriber_type user_id user_type], name: :index_caffeinate_campaign_subscriptions
23
+ add_index :caffeinate_campaign_subscriptions, %i[caffeinate_campaign_id subscriber_id subscriber_type user_id user_type ended_at resubscribed_at unsubscribed_at], name: :index_caffeinate_campaign_subscriptions
21
24
  end
22
25
  end
@@ -14,6 +14,7 @@ class CreateCaffeinateMailings < ActiveRecord::Migration[6.0]
14
14
 
15
15
  t.timestamps
16
16
  end
17
- add_index :caffeinate_mailings, %i[campaign_subscription_id mailer_class mailer_action sent_at send_at skipped_at], name: :index_caffeinate_mailings
17
+
18
+ add_index :caffeinate_mailings, %i[caffeinate_campaign_subscription_id send_at sent_at skipped_at], name: :index_caffeinate_mailings
18
19
  end
19
20
  end
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/all'
4
+ require 'caffeinate/mail_ext'
4
5
  require 'caffeinate/engine'
5
6
  require 'caffeinate/drip'
6
7
  require 'caffeinate/url_helpers'
7
8
  require 'caffeinate/configuration'
8
9
  require 'caffeinate/dripper/base'
9
10
  require 'caffeinate/deliver_async'
11
+ require 'caffeinate/dripper_collection'
10
12
 
11
13
  module Caffeinate
12
- # Caches the campaign to the campaign class
13
- def self.dripper_to_campaign_class
14
- @dripper_to_campaign_class ||= {}
15
- end
16
-
17
- # Convenience method for `dripper_to_campaign_class`
18
- def self.register_dripper(name, klass)
19
- dripper_to_campaign_class[name.to_sym] = klass
14
+ def self.dripper_collection
15
+ @drippers ||= DripperCollection.new
20
16
  end
21
17
 
22
18
  # Global configuration
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/action_mailer/extension'
4
- require 'caffeinate/action_mailer/helpers'
5
- require 'caffeinate/action_mailer/interceptor'
6
- require 'caffeinate/action_mailer/observer'
3
+ require 'mail'
4
+
5
+ # Includes all files in `caffeinate/action_mailer`
6
+ Dir["#{__dir__}/action_mailer/*"].each { |path| require "caffeinate/action_mailer/#{File.basename(path)}" }
@@ -2,21 +2,27 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Convenience for setting `@mailing`, and convenience methods for inferred `caffeinate_unsubscribe_url` and
6
+ # `caffeinate_subscribe_url`.
5
7
  module Extension
6
8
  def self.included(klass)
7
9
  klass.before_action do
8
- @mailing = Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] if Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
10
+ @mailing = params[:mailing] if params
9
11
  end
10
12
 
11
13
  klass.helper_method :caffeinate_unsubscribe_url, :caffeinate_subscribe_url
12
14
  end
13
15
 
14
- def caffeinate_unsubscribe_url(**options)
15
- Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(@mailing.caffeinate_campaign_subscription, **options)
16
+ # Assumes `@mailing` is set
17
+ def caffeinate_unsubscribe_url(mailing: nil, **options)
18
+ mailing ||= @mailing
19
+ Caffeinate::UrlHelpers.caffeinate_unsubscribe_url(mailing.caffeinate_campaign_subscription, **options)
16
20
  end
17
21
 
18
- def caffeinate_subscribe_url
19
- Caffeinate::UrlHelpers.caffeinate_subscribe_url(@mailing.caffeinate_campaign_subscription, **options)
22
+ # Assumes `@mailing` is set
23
+ def caffeinate_subscribe_url(mailing: nil, **options)
24
+ mailing ||= @mailing
25
+ Caffeinate::UrlHelpers.caffeinate_subscribe_url(mailing.caffeinate_campaign_subscription, **options)
20
26
  end
21
27
  end
22
28
  end
@@ -2,13 +2,15 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
+ # Handles the evaluation of a drip against a mailing to determine if it ultimately gets delivered.
6
+ # Also invokes the `before_send` callbacks.
5
7
  class Interceptor
6
8
  # Handles `before_send` callbacks for a `Caffeinate::Dripper`
7
9
  def self.delivering_email(message)
8
- mailing = Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
10
+ mailing = message.caffeinate_mailing
9
11
  return unless mailing
10
12
 
11
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing.caffeinate_campaign_subscription, mailing, message)
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:before_send, mailing, message)
12
14
  drip = mailing.drip
13
15
  message.perform_deliveries = drip.enabled?(mailing)
14
16
  end
@@ -2,15 +2,16 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActionMailer
5
- # Handles updating the Caffeinate::Message if it's available in Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
5
+ # Handles updating the Caffeinate::Message if it's available in Mail::Message.caffeinate_mailing
6
6
  # and runs any associated callbacks
7
7
  class Observer
8
8
  def self.delivered_email(message)
9
- mailing = Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY]
9
+ mailing = message.caffeinate_mailing
10
10
  return unless mailing
11
11
 
12
12
  mailing.update!(sent_at: Caffeinate.config.time_now, skipped_at: nil) if message.perform_deliveries
13
- mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing.caffeinate_campaign_subscription, mailing, message)
13
+ mailing.caffeinate_campaign.to_dripper.run_callbacks(:after_send, mailing, message)
14
+ true
14
15
  end
15
16
  end
16
17
  end
@@ -2,32 +2,38 @@
2
2
 
3
3
  module Caffeinate
4
4
  module ActiveRecord
5
+ # Includes the ActiveRecord association and relevant scopes for an ActiveRecord-backed model
5
6
  module Extension
6
7
  # Adds the associations for a subscriber
7
- def caffeinate_subscriber
8
- has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription'
8
+ def acts_as_caffeinate_subscriber
9
+ has_many :caffeinate_campaign_subscriptions, as: :subscriber, class_name: '::Caffeinate::CampaignSubscription', dependent: :destroy
9
10
  has_many :caffeinate_campaigns, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Campaign'
10
11
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: '::Caffeinate::Mailing'
11
12
 
12
13
  scope :not_subscribed_to_campaign, lambda { |list|
13
- subscribed = ::Caffeinate::CampaignSubscription.select(:subscriber_id).joins(:caffeinate_campaign).where(caffeinate_campaigns: { slug: list }, subscriber_type: name)
14
- where.not(id: subscribed)
14
+ where.not(id: ::Caffeinate::CampaignSubscription
15
+ .select(:subscriber_id)
16
+ .joins(:caffeinate_campaign)
17
+ .where(subscriber_type: name, caffeinate_campaigns: { slug: list }))
15
18
  }
16
19
 
17
20
  scope :unsubscribed_from_campaign, lambda { |list|
18
- unsubscribed = ::Caffeinate::CampaignSubscription
19
- .unsubscribed
20
- .select(:subscriber_id)
21
- .joins(:caffeinate_campaign)
22
- .where(caffeinate_campaigns: { slug: list }, subscriber_type: name)
23
- where(id: unsubscribed)
21
+ where(id: ::Caffeinate::CampaignSubscription
22
+ .unsubscribed
23
+ .select(:subscriber_id)
24
+ .joins(:caffeinate_campaign)
25
+ .where(subscriber_type: name, caffeinate_campaigns: { slug: list }))
24
26
  }
25
27
  end
28
+ alias caffeinate_subscriber acts_as_caffeinate_subscriber
26
29
 
27
30
  # Adds the associations for a user
28
- def caffeinate_user
31
+ def acts_as_caffeinate_user
29
32
  has_many :caffeinate_campaign_subscriptions_as_user, as: :user, class_name: '::Caffeinate::CampaignSubscription'
33
+ has_many :caffeinate_campaigns_as_user, through: :caffeinate_campaign_subscriptions_as_user, class_name: '::Caffeinate::Campaign'
34
+ has_many :caffeinate_mailings_as_user, through: :caffeinate_campaign_subscriptions_as_user, class_name: '::Caffeinate::Campaign'
30
35
  end
36
+ alias caffeinate_user acts_as_caffeinate_user
31
37
  end
32
38
  end
33
39
  end
@@ -1,19 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
+ # Global configuration
4
5
  class Configuration
5
- attr_accessor :now, :async_delivery, :mailing_job
6
+ attr_accessor :now, :async_delivery, :mailing_job, :batch_size, :drippers_path, :implicit_campaigns
6
7
 
7
8
  def initialize
8
9
  @now = -> { Time.current }
9
10
  @async_delivery = false
10
11
  @mailing_job = nil
12
+ @batch_size = 1_000
13
+ @drippers_path = 'app/drippers'
14
+ @implicit_campaigns = true
11
15
  end
12
16
 
13
17
  def now=(val)
14
18
  raise ArgumentError, '#now must be a proc' unless val.respond_to?(:call)
15
19
 
16
- super
20
+ @now = val
21
+ end
22
+
23
+ # Automatically create a `::Caffeinate::Campaign` object if not found per `Dripper.inferred_campaign_slug`
24
+ def implicit_campaigns?
25
+ @implicit_campaigns == true
17
26
  end
18
27
 
19
28
  # The current time, for database calls
@@ -7,6 +7,7 @@ module Caffeinate
7
7
  # Handles the block and provides convenience methods for the drip
8
8
  class Drip
9
9
  attr_reader :dripper, :action, :options, :block
10
+
10
11
  def initialize(dripper, action, options, &block)
11
12
  @dripper = dripper
12
13
  @action = action
@@ -19,12 +20,24 @@ module Caffeinate
19
20
  options[:using] == :parameterized
20
21
  end
21
22
 
22
- def send_at
23
- options[:delay].from_now
23
+ def send_at(mailing = nil)
24
+ if periodical?
25
+ start = mailing.instance_exec(&options[:start])
26
+ start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
27
+ start.from_now
28
+ else
29
+ options[:delay].from_now
30
+ end
31
+ end
32
+
33
+ def periodical?
34
+ options[:every].present?
24
35
  end
25
36
 
26
37
  # Checks if the drip is enabled
27
38
  def enabled?(mailing)
39
+ dripper.run_callbacks(:before_drip, self, mailing)
40
+
28
41
  DripEvaluator.new(mailing).call(&@block)
29
42
  end
30
43
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
2
4
  # Handles evaluating the `drip` block and provides convenience methods for handling the mailing or its campaign.
3
5
  class DripEvaluator
4
6
  attr_reader :mailing
7
+
5
8
  def initialize(mailing)
6
9
  @mailing = mailing
7
10
  end
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'caffeinate/dripper/drip'
4
- require 'caffeinate/dripper/inferences'
3
+ require 'caffeinate/dripper/batching'
5
4
  require 'caffeinate/dripper/callbacks'
6
- require 'caffeinate/dripper/defaults'
7
- require 'caffeinate/dripper/subscriber'
8
5
  require 'caffeinate/dripper/campaign'
9
- require 'caffeinate/dripper/perform'
6
+ require 'caffeinate/dripper/defaults'
10
7
  require 'caffeinate/dripper/delivery'
8
+ require 'caffeinate/dripper/drip'
9
+ require 'caffeinate/dripper/inferences'
10
+ require 'caffeinate/dripper/perform'
11
+ require 'caffeinate/dripper/periodical'
12
+ require 'caffeinate/dripper/subscriber'
11
13
 
12
14
  module Caffeinate
13
15
  module Dripper
16
+ # Base class
14
17
  class Base
18
+ include Batching
15
19
  include Callbacks
16
20
  include Campaign
17
21
  include Defaults
@@ -19,7 +23,10 @@ module Caffeinate
19
23
  include Drip
20
24
  include Inferences
21
25
  include Perform
26
+ include Periodical
22
27
  include Subscriber
23
28
  end
24
29
  end
25
30
  end
31
+
32
+ ActiveSupport.run_load_hooks :caffeinate, Caffeinate::Dripper::Base
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Includes batch support for setting the batch size for Perform
6
+ module Batching
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def batch_size=(num)
13
+ @batch_size = num
14
+ end
15
+
16
+ def batch_size
17
+ @batch_size || ::Caffeinate.config.batch_size
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,12 +2,17 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Callbacks for a Dripper.
5
6
  module Callbacks
6
7
  # :nodoc:
7
8
  def self.included(klass)
8
9
  klass.extend ClassMethods
9
10
  end
10
11
 
12
+ def run_callbacks(name, *args)
13
+ self.class.run_callbacks(name, *args)
14
+ end
15
+
11
16
  module ClassMethods
12
17
  # :nodoc:
13
18
  def run_callbacks(name, *args)
@@ -33,15 +38,80 @@ module Caffeinate
33
38
  @on_subscribe_blocks ||= []
34
39
  end
35
40
 
41
+ # Callback after a Caffeinate::CampaignSubscription is `#resubscribed!`
42
+ #
43
+ # on_resubscribe do |campaign_subscription|
44
+ # Slack.notify(:caffeinate, "Someone resubscribed to #{campaign_subscription.campaign.name}!")
45
+ # end
46
+ #
47
+ # @yield Caffeinate::CampaignSubscription
48
+ def on_resubscribe(&block)
49
+ on_resubscribe_blocks << block
50
+ end
51
+
52
+ # :nodoc:
53
+ def on_resubscribe_blocks
54
+ @on_resubscribe_blocks ||= []
55
+ end
56
+
57
+ # Callback before the mailings get processed.
58
+ #
59
+ # before_perform do |dripper|
60
+ # Slack.notify(:caffeinate, "Dripper is getting ready for mailing! #{dripper.caffeinate_campaign.name}!")
61
+ # end
62
+ #
63
+ # @yield Caffeinate::Dripper
64
+ def before_perform(&block)
65
+ before_perform_blocks << block
66
+ end
67
+
68
+ # :nodoc:
69
+ def before_perform_blocks
70
+ @before_perform_blocks ||= []
71
+ end
72
+
73
+ # Callback before the mailings get processed in a batch.
74
+ #
75
+ # after_process do |dripper, mailings|
76
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
77
+ # end
78
+ #
79
+ # @yield Caffeinate::Dripper
80
+ # @yield Caffeinate::Mailing [Array]
81
+ def on_perform(&block)
82
+ on_perform_blocks << block
83
+ end
84
+
85
+ # :nodoc:
86
+ def on_perform_blocks
87
+ @on_perform_blocks ||= []
88
+ end
89
+
90
+ # Callback after the all the mailings have been sent.
91
+ #
92
+ # after_process do |dripper|
93
+ # Slack.notify(:caffeinate, "Dripper #{dripper.name} sent #{mailings.size} mailings! Whoa!")
94
+ # end
95
+ #
96
+ # @yield Caffeinate::Dripper
97
+ # @yield Caffeinate::Mailing [Array]
98
+ def after_perform(&block)
99
+ after_perform_blocks << block
100
+ end
101
+
102
+ # :nodoc:
103
+ def after_perform_blocks
104
+ @after_perform_blocks ||= []
105
+ end
106
+
36
107
  # Callback before a Drip has called the mailer.
37
108
  #
38
109
  # before_drip do |campaign_subscription, mailing, drip|
39
110
  # Slack.notify(:caffeinate, "#{drip.action_name} is starting")
40
111
  # end
41
112
  #
42
- # @yield Caffeinate::CampaignSubscription
43
- # @yield Caffeinate::Mailing
44
113
  # @yield Caffeinate::Drip current drip
114
+ # @yield Caffeinate::Mailing
45
115
  def before_drip(&block)
46
116
  before_drip_blocks << block
47
117
  end
@@ -57,7 +127,6 @@ module Caffeinate
57
127
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
58
128
  # end
59
129
  #
60
- # @yield Caffeinate::CampaignSubscription
61
130
  # @yield Caffeinate::Mailing
62
131
  # @yield Mail::Message
63
132
  def before_send(&block)
@@ -75,7 +144,6 @@ module Caffeinate
75
144
  # Slack.notify(:caffeinate, "A new subscriber to #{campaign_subscription.campaign.name}!")
76
145
  # end
77
146
  #
78
- # @yield Caffeinate::CampaignSubscription
79
147
  # @yield Caffeinate::Mailing
80
148
  # @yield Mail::Message
81
149
  def after_send(&block)
@@ -119,14 +187,29 @@ module Caffeinate
119
187
  @on_unsubscribe_blocks ||= []
120
188
  end
121
189
 
190
+ # Callback after a CampaignSubscriber has ended.
191
+ #
192
+ # on_end do |campaign_sub|
193
+ # Slack.notify(:caffeinate, "#{campaign_sub.id} has ended... sad day.")
194
+ # end
195
+ #
196
+ # @yield Caffeinate::CampaignSubscription
197
+ def on_end(&block)
198
+ on_end_blocks << block
199
+ end
200
+
201
+ # :nodoc:
202
+ def on_end_blocks
203
+ @on_end_blocks ||= []
204
+ end
205
+
122
206
  # Callback after a `Caffeinate::Mailing` is skipped.
123
207
  #
124
208
  # on_skip do |campaign_subscription, mailing, message|
125
209
  # Slack.notify(:caffeinate, "#{campaign_sub.id} has unsubscribed... sad day.")
126
210
  # end
127
211
  #
128
- # @yield Caffeinate::CampaignSubscription
129
- # @yield Caffeinate::Mailing
212
+ # @yield `Caffeinate::Mailing`
130
213
  def on_skip(&block)
131
214
  on_skip_blocks << block
132
215
  end