railsdrip-engine 0.0.1

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +4 -0
  4. data/README.md +80 -0
  5. data/app/controllers/concerns/rails_drips/admin_access.rb +28 -0
  6. data/app/controllers/rails_drips/application_controller.rb +7 -0
  7. data/app/controllers/rails_drips/campaign_archivals_controller.rb +11 -0
  8. data/app/controllers/rails_drips/campaign_pauses_controller.rb +11 -0
  9. data/app/controllers/rails_drips/campaign_resumptions_controller.rb +11 -0
  10. data/app/controllers/rails_drips/campaigns_controller.rb +14 -0
  11. data/app/controllers/rails_drips/deliveries_controller.rb +14 -0
  12. data/app/controllers/rails_drips/delivery_cancellations_controller.rb +11 -0
  13. data/app/controllers/rails_drips/delivery_retries_controller.rb +11 -0
  14. data/app/controllers/rails_drips/enrollments_controller.rb +11 -0
  15. data/app/jobs/rails_drips/delivery_job.rb +89 -0
  16. data/app/models/concerns/rails_drips/json_data.rb +29 -0
  17. data/app/models/rails_drips/application_record.rb +8 -0
  18. data/app/models/rails_drips/campaign.rb +62 -0
  19. data/app/models/rails_drips/campaign_version.rb +35 -0
  20. data/app/models/rails_drips/delivery.rb +90 -0
  21. data/app/models/rails_drips/enrollment.rb +77 -0
  22. data/app/models/rails_drips/enrollment_event.rb +28 -0
  23. data/app/models/rails_drips/step.rb +53 -0
  24. data/app/services/rails_drips/activate_campaign.rb +10 -0
  25. data/app/services/rails_drips/archive_campaign.rb +22 -0
  26. data/app/services/rails_drips/cancel_delivery.rb +17 -0
  27. data/app/services/rails_drips/compare_definition.rb +28 -0
  28. data/app/services/rails_drips/complete_enrollment.rb +18 -0
  29. data/app/services/rails_drips/enqueue_due_deliveries.rb +16 -0
  30. data/app/services/rails_drips/enroll.rb +72 -0
  31. data/app/services/rails_drips/import_definition.rb +68 -0
  32. data/app/services/rails_drips/integrity_audit.rb +43 -0
  33. data/app/services/rails_drips/mail_snapshot.rb +31 -0
  34. data/app/services/rails_drips/pause_campaign.rb +16 -0
  35. data/app/services/rails_drips/policy_decision.rb +21 -0
  36. data/app/services/rails_drips/record_event.rb +14 -0
  37. data/app/services/rails_drips/remove.rb +27 -0
  38. data/app/services/rails_drips/resume_campaign.rb +17 -0
  39. data/app/services/rails_drips/retry_delivery.rb +21 -0
  40. data/app/services/rails_drips/schedule_deliveries.rb +23 -0
  41. data/app/services/rails_drips/unarchive_campaign.rb +14 -0
  42. data/app/views/layouts/rails_drips/application.html.erb +5 -0
  43. data/app/views/rails_drips/campaigns/index.html.erb +7 -0
  44. data/app/views/rails_drips/campaigns/show.html.erb +7 -0
  45. data/app/views/rails_drips/deliveries/show.html.erb +9 -0
  46. data/app/views/rails_drips/enrollments/show.html.erb +6 -0
  47. data/config/routes.rb +14 -0
  48. data/db/migrate/20260829123000_create_rails_drips_tables.rb +142 -0
  49. data/docs/README.md +13 -0
  50. data/docs/architecture.md +95 -0
  51. data/docs/campaign-definitions.md +100 -0
  52. data/docs/getting-started.md +170 -0
  53. data/docs/host-integration.md +160 -0
  54. data/docs/operations.md +104 -0
  55. data/lib/generators/rails_drips/install/install_generator.rb +32 -0
  56. data/lib/generators/rails_drips/install/templates/initializer.rb +8 -0
  57. data/lib/generators/rails_drips/install/templates/sample.yml +13 -0
  58. data/lib/rails_drips/configuration.rb +65 -0
  59. data/lib/rails_drips/definition_loader.rb +42 -0
  60. data/lib/rails_drips/definition_schema.rb +40 -0
  61. data/lib/rails_drips/engine.rb +9 -0
  62. data/lib/rails_drips/errors.rb +17 -0
  63. data/lib/rails_drips/instrumentation.rb +11 -0
  64. data/lib/rails_drips/mail_registry.rb +62 -0
  65. data/lib/rails_drips/queue_inventory.rb +10 -0
  66. data/lib/rails_drips/result.rb +9 -0
  67. data/lib/rails_drips/version.rb +5 -0
  68. data/lib/rails_drips.rb +65 -0
  69. data/lib/tasks/rails_drips_tasks.rake +28 -0
  70. metadata +179 -0
@@ -0,0 +1,104 @@
1
+ # Operations
2
+
3
+ RailsDrips retains enough state to explain who was enrolled, which frozen campaign version they received, what was scheduled, and what happened to each delivery.
4
+
5
+ ## Secure the admin
6
+
7
+ Mounting the engine does not expose records by default. Access fails closed with `404` until all required adapters are configured:
8
+
9
+ ```ruby
10
+ # config/routes.rb
11
+ mount RailsDrips::Engine => "/drips"
12
+ ```
13
+
14
+ ```ruby
15
+ RailsDrips.configure do |config|
16
+ config.admin_enabled = true
17
+
18
+ config.admin_authenticator = ->(**) { Current.user.present? }
19
+
20
+ config.admin_authorizer = ->(action:, **context) { Current.user.admin? }
21
+
22
+ config.current_actor_resolver = ->(_controller) { Current.user }
23
+
24
+ config.snapshot_authorizer = ->(delivery:, **context) do
25
+ Current.user.can_view_email_content?
26
+ end
27
+ end
28
+ ```
29
+
30
+ Authentication establishes that someone is signed in. Authorization decides whether that identity may use the operational interface. Snapshot authorization is separate because stored email bodies may contain sensitive recipient data.
31
+
32
+ The engine controller currently inherits `ActionController::Base`, not the host's `ApplicationController`. Authentication methods mixed into all controllers may be available, but host-only controller methods are not guaranteed. A request-scoped object such as `Current.user` is the most portable adapter until configurable controller inheritance is added.
33
+
34
+ With a `/drips` mount, the prototype begins at `/drips/campaigns`. It supports inspection plus campaign pause/resume/archive and delivery retry/cancel. It is not yet the intended authoring experience; see the [admin UI TODO](todo/admin-ui-and-dummy-app.md).
35
+
36
+ ## Campaign operations
37
+
38
+ ```ruby
39
+ RailsDrips.pause_campaign(campaign: campaign, actor: operator)
40
+ RailsDrips.resume_campaign(campaign: campaign, actor: operator)
41
+ RailsDrips.archive_campaign(campaign: campaign, reason: "superseded", actor: operator)
42
+ RailsDrips.unarchive_campaign(campaign: campaign)
43
+ ```
44
+
45
+ Pause leaves scheduled rows intact and prevents sending. Resume returns the campaign to active and enqueues due rows. Archive cancels future scheduled work.
46
+
47
+ ## Retry and cancellation
48
+
49
+ Failed deliveries do not silently become scheduled again. Retry is explicit and permitted only while enrollment and campaign are active:
50
+
51
+ ```ruby
52
+ RailsDrips::RetryDelivery.call(delivery: delivery, actor: operator)
53
+ RailsDrips::CancelDelivery.call(delivery: delivery, reason: "operator_cancelled", actor: operator)
54
+ ```
55
+
56
+ Failures store a bounded error, attempt count, timestamp, and event before the job exception is re-raised to the host adapter.
57
+
58
+ ## Due-delivery repair
59
+
60
+ ```ruby
61
+ RailsDrips.enqueue_due_deliveries(dry_run: true).count
62
+ RailsDrips.enqueue_due_deliveries(campaign: campaign, dry_run: true).count
63
+ ```
64
+
65
+ Then enqueue due work with `bin/rails rails_drips:enqueue_due`. This is a repair tool, not a replacement for running the normal worker.
66
+
67
+ ## Integrity audit
68
+
69
+ ```ruby
70
+ RailsDrips::IntegrityAudit.call.each do |finding|
71
+ Rails.logger.warn(
72
+ code: finding.code,
73
+ severity: finding.severity,
74
+ record_type: finding.record_type,
75
+ record_id: finding.record_id,
76
+ details: finding.details
77
+ )
78
+ end
79
+ ```
80
+
81
+ The audit checks missing step deliveries, cross-version steps, stuck sending rows, scheduled work for removed enrollments, failed work on completed enrollments, and sent rows without snapshots.
82
+
83
+ Active Job has no portable queue-inspection API. A host can optionally add queue evidence:
84
+
85
+ ```ruby
86
+ config.queue_inventory = -> { MyQueueAudit.rails_drips_job_ids }
87
+ ```
88
+
89
+ ## The ambiguous transport window
90
+
91
+ RailsDrips marks a delivery `sending`, releases its lock, and performs transport I/O. If the provider accepts the email and the process dies before `sent` is persisted, database state alone cannot prove delivery.
92
+
93
+ Before retrying a stuck `sending` row:
94
+
95
+ 1. Search provider logs using the RailsDrips delivery ID or correlation header.
96
+ 2. Determine whether the provider accepted the message.
97
+ 3. Reconcile accepted work according to the host incident procedure.
98
+ 4. Retry only when evidence indicates transport did not accept it.
99
+
100
+ ## Monitoring
101
+
102
+ Watch scheduled deliveries past their processing window, stuck `sending` rows, failure rate, repeated attempts, due backlog by campaign, cancellation reasons, enrollment completion, and Active Job queue latency.
103
+
104
+ Pause an affected campaign if content, policy, preference, or transport behavior is wrong. Pausing is the fastest reversible way to stop new sends without deleting evidence.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+ require "rails/generators/active_record"
6
+
7
+ module RailsDrips
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def copy_initializer
15
+ template "initializer.rb", "config/initializers/rails_drips.rb"
16
+ end
17
+
18
+ def copy_sample_definition
19
+ template "sample.yml", "config/rails_drips/onboarding.yml"
20
+ end
21
+
22
+ def copy_migration
23
+ migration_template File.expand_path("../../../../db/migrate/20260829123000_create_rails_drips_tables.rb", __dir__),
24
+ "db/migrate/create_rails_drips_tables.rb"
25
+ end
26
+
27
+ def self.next_migration_number(dirname)
28
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ RailsDrips.configure do |config|
2
+ config.delivery_queue_name = :default
3
+
4
+ # Register Action Mailer messages returned for each delivery.
5
+ # config.mailers.register("welcome", label: "Welcome") do |delivery|
6
+ # DripMailer.welcome(delivery)
7
+ # end
8
+ end
@@ -0,0 +1,13 @@
1
+ schema_version: 1
2
+ campaign:
3
+ key: onboarding
4
+ name: Onboarding
5
+ description: A sample onboarding campaign
6
+ allow_reenrollment: false
7
+ steps:
8
+ - position: 1
9
+ name: Welcome
10
+ kind: email
11
+ delay_amount: 0
12
+ delay_unit: minutes
13
+ email_key: welcome
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class Configuration
5
+ attr_accessor :delivery_queue_name,
6
+ :delivery_job_class,
7
+ :eligibility_evaluator,
8
+ :logger,
9
+ :admin_enabled,
10
+ :admin_authenticator,
11
+ :admin_authorizer,
12
+ :preference_evaluator,
13
+ :queue_inventory,
14
+ :recipient_email_resolver,
15
+ :recipient_label_resolver,
16
+ :current_actor_resolver,
17
+ :snapshot_authorizer,
18
+ :time_source,
19
+ :transport_metadata_hook
20
+ attr_reader :mailers
21
+
22
+ def initialize
23
+ @delivery_queue_name = :default
24
+ @delivery_job_class = -> { RailsDrips::DeliveryJob }
25
+ @eligibility_evaluator = ->(**) { true }
26
+ @logger = defined?(Rails) ? Rails.logger : nil
27
+ @admin_enabled = false
28
+ @admin_authenticator = nil
29
+ @admin_authorizer = nil
30
+ @preference_evaluator = ->(**) { true }
31
+ @queue_inventory = nil
32
+ @recipient_email_resolver = method(:default_recipient_email)
33
+ @recipient_label_resolver = ->(recipient) { recipient.respond_to?(:name) ? recipient.name : "#{recipient.class.name} ##{recipient.id}" }
34
+ @current_actor_resolver = ->(_controller) { nil }
35
+ @snapshot_authorizer = ->(**) { false }
36
+ @time_source = -> { Time.current }
37
+ @transport_metadata_hook = ->(message:, **) { message }
38
+ @mailers = MailRegistry.new
39
+ end
40
+
41
+ def delivery_job
42
+ delivery_job_class.respond_to?(:call) ? delivery_job_class.call : delivery_job_class
43
+ end
44
+
45
+ def current_time
46
+ time_source.call
47
+ end
48
+
49
+ def recipient_email(recipient)
50
+ value = recipient_email_resolver.call(recipient).to_s.strip
51
+ return value unless value.empty?
52
+
53
+ raise RecipientEmailMissing, "No email address could be resolved for #{recipient.class.name}"
54
+ end
55
+
56
+ private
57
+
58
+ def default_recipient_email(recipient)
59
+ return recipient.email_address if recipient.respond_to?(:email_address)
60
+ return recipient.email if recipient.respond_to?(:email)
61
+
62
+ nil
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RailsDrips
6
+ class DefinitionLoader
7
+ def self.load_file(path)
8
+ load_string(File.read(path), source: path.to_s)
9
+ rescue Errno::ENOENT => error
10
+ raise DefinitionInvalid, [error.message]
11
+ end
12
+
13
+ def self.load_string(yaml, source: nil)
14
+ raw = YAML.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false)
15
+ definition = deep_stringify(raw)
16
+ normalize!(definition)
17
+ errors = DefinitionSchema.validate(definition)
18
+ raise DefinitionInvalid, errors.map { |error| source ? "#{source}: #{error}" : error } if errors.any?
19
+
20
+ definition
21
+ rescue Psych::Exception => error
22
+ raise DefinitionInvalid, [source ? "#{source}: #{error.message}" : error.message]
23
+ end
24
+
25
+ def self.deep_stringify(value)
26
+ case value
27
+ when Hash then value.to_h { |key, nested| [key.to_s, deep_stringify(nested)] }
28
+ when Array then value.map { |nested| deep_stringify(nested) }
29
+ else value
30
+ end
31
+ end
32
+
33
+ def self.normalize!(definition)
34
+ return definition unless definition.is_a?(Hash) && definition["campaign"].is_a?(Hash)
35
+
36
+ campaign = definition["campaign"]
37
+ campaign["steps"]&.each { |step| step["kind"] ||= "email" if step.is_a?(Hash) }
38
+ definition
39
+ end
40
+ private_class_method :deep_stringify, :normalize!
41
+ end
42
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class DefinitionSchema
5
+ SCHEMA_VERSION = 1
6
+
7
+ def self.validate(definition)
8
+ errors = []
9
+ return ["definition must be a mapping"] unless definition.is_a?(Hash)
10
+
11
+ errors << "schema_version must be #{SCHEMA_VERSION}" unless definition["schema_version"] == SCHEMA_VERSION
12
+ campaign = definition["campaign"]
13
+ return errors << "campaign must be a mapping" unless campaign.is_a?(Hash)
14
+
15
+ errors << "campaign.key cannot be blank" if campaign["key"].to_s.strip.empty?
16
+ errors << "campaign.name cannot be blank" if campaign["name"].to_s.strip.empty?
17
+ steps = campaign["steps"]
18
+ return errors << "campaign.steps must be a non-empty list" unless steps.is_a?(Array) && steps.any?
19
+
20
+ positions = steps.filter_map { |step| step["position"] if step.is_a?(Hash) }
21
+ errors << "campaign.steps positions must be unique" if positions.uniq.length != positions.length
22
+ steps.each_with_index { |step, index| validate_step(step, index, errors) }
23
+ errors
24
+ end
25
+
26
+ def self.validate_step(step, index, errors)
27
+ path = "campaign.steps[#{index}]"
28
+ return errors << "#{path} must be a mapping" unless step.is_a?(Hash)
29
+
30
+ errors << "#{path}.position must be a positive integer" unless step["position"].is_a?(Integer) && step["position"].positive?
31
+ errors << "#{path}.name cannot be blank" if step["name"].to_s.strip.empty?
32
+ errors << "#{path}.delay_amount must be a non-negative integer" unless step["delay_amount"].is_a?(Integer) && step["delay_amount"] >= 0
33
+ errors << "#{path}.delay_unit must be minutes, hours, or days" unless Step::DELAY_UNITS.include?(step["delay_unit"].to_s)
34
+ email_key = step["email_key"].to_s
35
+ errors << "#{path}.email_key #{email_key.inspect} is not registered" unless RailsDrips.configuration.mailers.registered?(email_key)
36
+ errors << "#{path}.kind must be email" unless step.fetch("kind", "email").to_s == "email"
37
+ end
38
+ private_class_method :validate_step
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsDrips
6
+ engine_name "rails_drips"
7
+ end
8
+ end
9
+
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class DuplicateMailerKey < ConfigurationError; end
7
+ class MailerNotRegistered < ConfigurationError; end
8
+ class RecipientEmailMissing < Error; end
9
+ class DefinitionInvalid < Error
10
+ attr_reader :errors
11
+
12
+ def initialize(errors)
13
+ @errors = Array(errors)
14
+ super(@errors.join("; "))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ module Instrumentation
5
+ module_function
6
+
7
+ def instrument(name, payload = {}, &block)
8
+ ActiveSupport::Notifications.instrument("#{name}.rails_drips", payload, &block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class MailRegistry
5
+ Definition = Struct.new(:key, :label, :callable, keyword_init: true) do
6
+ def call(delivery)
7
+ callable.call(delivery)
8
+ end
9
+ end
10
+
11
+ include Enumerable
12
+
13
+ def initialize
14
+ @definitions = {}
15
+ end
16
+
17
+ def register(key, label:, callable: nil, &block)
18
+ normalized_key = normalize_key(key)
19
+ delivery_callable = callable || block
20
+
21
+ raise ConfigurationError, "A mailer callable is required for #{normalized_key.inspect}" unless delivery_callable.respond_to?(:call)
22
+ raise ConfigurationError, "A mailer label is required for #{normalized_key.inspect}" if label.to_s.strip.empty?
23
+ raise DuplicateMailerKey, "Mailer key #{normalized_key.inspect} is already registered" if registered?(normalized_key)
24
+
25
+ @definitions[normalized_key] = Definition.new(
26
+ key: normalized_key,
27
+ label: label.to_s,
28
+ callable: delivery_callable
29
+ )
30
+ end
31
+
32
+ def fetch(key)
33
+ normalized_key = normalize_key(key)
34
+ @definitions.fetch(normalized_key) do
35
+ raise MailerNotRegistered, "No mailer is registered for #{normalized_key.inspect}"
36
+ end
37
+ end
38
+
39
+ def registered?(key)
40
+ @definitions.key?(key.to_s)
41
+ end
42
+
43
+ def each(&block)
44
+ return enum_for(:each) unless block
45
+
46
+ @definitions.values.each(&block)
47
+ end
48
+
49
+ def clear
50
+ @definitions.clear
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def normalize_key(key)
57
+ key.to_s.strip.tap do |normalized_key|
58
+ raise ConfigurationError, "Mailer key cannot be blank" if normalized_key.empty?
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ class QueueInventory
5
+ def self.call
6
+ adapter = RailsDrips.configuration.queue_inventory
7
+ adapter ? adapter.call : nil
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ Result = Struct.new(:status, :enrollment, :campaign, :delivery, :reason, :count, keyword_init: true) do
5
+ def success?
6
+ %i[enrolled removed activated paused resumed archived unarchived enqueued retried cancelled sent completed created version_created metadata_updated unchanged].include?(status)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsDrips
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "active_record/railtie"
5
+ require "active_job/railtie"
6
+ require "action_mailer/railtie"
7
+
8
+ require "rails_drips/version"
9
+ require "rails_drips/errors"
10
+ require "rails_drips/result"
11
+ require "rails_drips/mail_registry"
12
+ require "rails_drips/configuration"
13
+ require "rails_drips/instrumentation"
14
+ require "rails_drips/definition_schema"
15
+ require "rails_drips/definition_loader"
16
+ require "rails_drips/queue_inventory"
17
+ require "rails_drips/engine"
18
+
19
+ module RailsDrips
20
+ class << self
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield(configuration)
27
+ end
28
+
29
+ def reset_configuration!
30
+ @configuration = Configuration.new
31
+ end
32
+
33
+ def enroll(**attributes)
34
+ Enroll.call(**attributes)
35
+ end
36
+
37
+ def remove(**attributes)
38
+ Remove.call(**attributes)
39
+ end
40
+
41
+ def pause_campaign(**attributes)
42
+ PauseCampaign.call(**attributes)
43
+ end
44
+
45
+ def resume_campaign(**attributes)
46
+ ResumeCampaign.call(**attributes)
47
+ end
48
+
49
+ def archive_campaign(**attributes)
50
+ ArchiveCampaign.call(**attributes)
51
+ end
52
+
53
+ def unarchive_campaign(**attributes)
54
+ UnarchiveCampaign.call(**attributes)
55
+ end
56
+
57
+ def enqueue_due_deliveries(**attributes)
58
+ EnqueueDueDeliveries.call(**attributes)
59
+ end
60
+
61
+ def table_name_prefix
62
+ "rails_drips_"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_drips do
4
+ desc "Validate a campaign definition (DEFINITION=path)"
5
+ task validate: :environment do
6
+ definition = RailsDrips::DefinitionLoader.load_file(ENV.fetch("DEFINITION"))
7
+ puts "Valid: #{definition.dig("campaign", "key")}" # rubocop:disable Rails/Output
8
+ end
9
+
10
+ desc "Import a definition; dry-run unless APPLY=true (DEFINITION=path)"
11
+ task import: :environment do
12
+ definition = RailsDrips::DefinitionLoader.load_file(ENV.fetch("DEFINITION"))
13
+ result = RailsDrips::ImportDefinition.call(definition: definition, dry_run: ENV["APPLY"] != "true")
14
+ puts "#{result.status}: #{definition.dig("campaign", "key")}" # rubocop:disable Rails/Output
15
+ end
16
+
17
+ desc "Activate a draft campaign version (CAMPAIGN=key VERSION=number)"
18
+ task activate: :environment do
19
+ campaign = RailsDrips::Campaign.find_by!(key: ENV.fetch("CAMPAIGN"))
20
+ version = campaign.campaign_versions.find_by!(version_number: ENV.fetch("VERSION"))
21
+ puts RailsDrips::ActivateCampaign.call(campaign: campaign, version: version).status # rubocop:disable Rails/Output
22
+ end
23
+
24
+ desc "Enqueue due scheduled deliveries"
25
+ task enqueue_due: :environment do
26
+ puts RailsDrips.enqueue_due_deliveries.count # rubocop:disable Rails/Output
27
+ end
28
+ end