email_digest 1.0.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 (32) hide show
  1. checksums.yaml +7 -0
  2. data/.vscode/settings.json +3 -0
  3. data/ANSWERS_TO_YOUR_QUESTIONS.md +361 -0
  4. data/CHANGELOG.md +14 -0
  5. data/COMPLETE_SETUP_INSTRUCTIONS.md +298 -0
  6. data/CUSTOMIZATION_GUIDE.md +264 -0
  7. data/FAQ.md +133 -0
  8. data/LICENSE.txt +21 -0
  9. data/QUICK_START.md +62 -0
  10. data/README.md +253 -0
  11. data/REPOSITORY_READY.md +146 -0
  12. data/RUBYGEMS_PUBLISHING.md +272 -0
  13. data/SETUP_SEPARATE_GEM_REPO.md +131 -0
  14. data/email_digest.gemspec +52 -0
  15. data/lib/email_digest/concerns/digestable.rb +57 -0
  16. data/lib/email_digest/configuration.rb +40 -0
  17. data/lib/email_digest/generators/email_digest/install/install_generator.rb +50 -0
  18. data/lib/email_digest/generators/email_digest/install/templates/README +16 -0
  19. data/lib/email_digest/generators/email_digest/install/templates/create_email_digest_tables.rb +77 -0
  20. data/lib/email_digest/generators/email_digest/install/templates/email_digest.rb +25 -0
  21. data/lib/email_digest/models/digest_item.rb +49 -0
  22. data/lib/email_digest/models/digest_preference.rb +106 -0
  23. data/lib/email_digest/railtie.rb +9 -0
  24. data/lib/email_digest/services/digest_collector.rb +38 -0
  25. data/lib/email_digest/services/digest_scheduler.rb +55 -0
  26. data/lib/email_digest/services/digest_summarizer.rb +135 -0
  27. data/lib/email_digest/version.rb +5 -0
  28. data/lib/email_digest/workers/digest_processor_worker.rb +64 -0
  29. data/lib/email_digest/workers/digest_scheduler_worker.rb +26 -0
  30. data/lib/email_digest/workers/digest_sender_worker.rb +48 -0
  31. data/lib/email_digest.rb +37 -0
  32. metadata +241 -0
@@ -0,0 +1,131 @@
1
+ # Setup Separate EmailDigest Gem Repository
2
+
3
+ This guide will help you create a completely separate gem repository that you can publish to RubyGems.
4
+
5
+ ## Step 1: Create New Repository Directory
6
+
7
+ ```bash
8
+ # Navigate to where you want the gem repo (outside your main app)
9
+ cd ~/workspace # or any location you prefer
10
+ mkdir email_digest
11
+ cd email_digest
12
+ git init
13
+ ```
14
+
15
+ ## Step 2: Copy All Files from gem_package
16
+
17
+ ```bash
18
+ # Copy everything from gem_package to your new repo
19
+ cp -r /Users/rosharma/workspace/sll-la/eportfolio/gem_package/* ~/workspace/email_digest/
20
+ ```
21
+
22
+ ## Step 3: Update Gemspec Metadata
23
+
24
+ Edit `email_digest.gemspec` and update:
25
+ - `spec.authors` - Your name
26
+ - `spec.email` - Your email
27
+ - `spec.homepage` - Your GitHub repo URL
28
+ - `spec.metadata['source_code_uri']` - Your GitHub repo
29
+ - `spec.metadata['changelog_uri']` - Your CHANGELOG URL
30
+
31
+ ## Step 4: Create GitHub Repository
32
+
33
+ ```bash
34
+ # Create a new repo on GitHub (via web or CLI)
35
+ # Then connect it:
36
+ git remote add origin https://github.com/rohitsharma170801/email_digest.git
37
+ git add .
38
+ git commit -m "Initial commit"
39
+ git branch -M main
40
+ git push -u origin main
41
+ ```
42
+
43
+ ## Step 5: Test the Gem Locally
44
+
45
+ ```bash
46
+ # Build the gem
47
+ gem build email_digest.gemspec
48
+
49
+ # Test installation
50
+ gem install ./email_digest-1.0.0.gem
51
+
52
+ # Or test in your app
53
+ # In your app's Gemfile:
54
+ # gem 'email_digest', path: '../email_digest'
55
+ ```
56
+
57
+ ## Step 6: Publish to RubyGems
58
+
59
+ ```bash
60
+ # 1. Create account at https://rubygems.org
61
+ # 2. Get your API key from https://rubygems.org/profile/edit
62
+
63
+ # 3. Build and push
64
+ gem build email_digest.gemspec
65
+ gem push email_digest-1.0.0.gem
66
+
67
+ # Or use your API key
68
+ gem push email_digest-1.0.0.gem --key your_api_key_name
69
+ ```
70
+
71
+ ## Step 7: Use in Your Application
72
+
73
+ In your main app's `Gemfile`:
74
+
75
+ ```ruby
76
+ # From RubyGems (after publishing)
77
+ gem 'email_digest'
78
+
79
+ # Or from GitHub (before publishing)
80
+ gem 'email_digest', git: 'https://github.com/yourusername/email_digest.git'
81
+
82
+ # Or local development
83
+ gem 'email_digest', path: '../email_digest'
84
+ ```
85
+
86
+ Then:
87
+
88
+ ```bash
89
+ bundle install
90
+ rails generate email_digest:install
91
+ rails db:migrate
92
+ ```
93
+
94
+ ## File Checklist
95
+
96
+ Make sure your gem repo has:
97
+ - ✅ `email_digest.gemspec`
98
+ - ✅ `lib/email_digest.rb`
99
+ - ✅ `lib/email_digest/version.rb`
100
+ - ✅ `lib/email_digest/configuration.rb`
101
+ - ✅ `lib/email_digest/railtie.rb`
102
+ - ✅ All models, services, workers, concerns
103
+ - ✅ Generator files
104
+ - ✅ `README.md`
105
+ - ✅ `LICENSE.txt`
106
+ - ✅ `CHANGELOG.md`
107
+ - ✅ `.gitignore`
108
+
109
+ ## Publishing Checklist
110
+
111
+ Before publishing:
112
+ - [ ] Update version in `lib/email_digest/version.rb`
113
+ - [ ] Update `CHANGELOG.md`
114
+ - [ ] Update gemspec metadata
115
+ - [ ] Test gem build: `gem build email_digest.gemspec`
116
+ - [ ] Test installation locally
117
+ - [ ] Test in a test Rails app
118
+ - [ ] Push to GitHub
119
+ - [ ] Publish to RubyGems
120
+
121
+ ## Version Updates
122
+
123
+ When updating the gem:
124
+
125
+ 1. Update version in `lib/email_digest/version.rb`
126
+ 2. Update `CHANGELOG.md`
127
+ 3. Commit changes
128
+ 4. Create git tag: `git tag -a v1.0.1 -m "Version 1.0.1"`
129
+ 5. Push tag: `git push origin v1.0.1`
130
+ 6. Build: `gem build email_digest.gemspec`
131
+ 7. Push: `gem push email_digest-1.0.1.gem`
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/email_digest/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'email_digest'
7
+ spec.version = EmailDigest::VERSION
8
+ spec.authors = ['Rohit sharma']
9
+ spec.email = ['rohitsharma170801@gmail.com']
10
+
11
+ spec.summary = 'A flexible, production-ready email digest system for Rails applications'
12
+ spec.description = <<~DESC
13
+ EmailDigest is a comprehensive email digest system that allows you to collect,
14
+ summarize, and send batched email notifications to users. It supports multiple
15
+ digest types, customizable schedules, priority-based processing, and multi-tenant
16
+ architectures. Built with PostgreSQL and Sidekiq for reliability and scalability.
17
+ DESC
18
+ spec.homepage = 'https://github.com/rohitsharma170801/email_digest'
19
+ spec.license = 'MIT'
20
+
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['source_code_uri'] = 'https://github.com/rohitsharma170801/email_digest'
23
+ spec.metadata['changelog_uri'] = 'https://github.com/rohitsharma170801/email_digest/blob/main/CHANGELOG.md'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (File.expand_path(f) == __FILE__) ||
30
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
31
+ end
32
+ end
33
+
34
+ spec.bindir = 'exe'
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ['lib']
37
+
38
+ # Dependencies
39
+ spec.add_dependency 'rails', '>= 6.0', '< 8.0'
40
+ spec.add_dependency 'sidekiq', '>= 5.0'
41
+ spec.add_dependency 'sidekiq-cron', '>= 1.0'
42
+ spec.add_dependency 'pg', '>= 1.0'
43
+
44
+ # Development dependencies
45
+ spec.add_development_dependency 'bundler', '~> 2.0'
46
+ spec.add_development_dependency 'rake', '~> 13.0'
47
+ spec.add_development_dependency 'rspec', '~> 3.0'
48
+ spec.add_development_dependency 'rspec-rails', '~> 6.0'
49
+ spec.add_development_dependency 'rubocop', '~> 1.0'
50
+ spec.add_development_dependency 'rubocop-rails', '~> 2.0'
51
+ spec.add_development_dependency 'simplecov', '~> 0.21'
52
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ module Concerns
5
+ module Digestable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # This concern can be included in models that want to support digest notifications
10
+ end
11
+
12
+ module ClassMethods
13
+ def digestable(digest_type, options = {})
14
+ @digest_type = digest_type
15
+ @digest_options = options
16
+ end
17
+
18
+ def digest_type
19
+ @digest_type
20
+ end
21
+
22
+ def digest_options
23
+ @digest_options || {}
24
+ end
25
+ end
26
+
27
+ def add_to_digest(user, notification_type, options = {})
28
+ digest_type = self.class.digest_type || options[:digest_type]
29
+ return unless digest_type
30
+
31
+ # Get organization from user or options
32
+ organization = options[:organization] || user.organization
33
+
34
+ EmailDigest::Models::DigestItem.create_for_user(
35
+ user,
36
+ digest_type,
37
+ notification_type,
38
+ {
39
+ subject: options[:subject],
40
+ body: options[:body],
41
+ metadata: options[:metadata] || {},
42
+ source_id: id.to_s,
43
+ source_type: self.class.name,
44
+ priority: options[:priority] || 0,
45
+ organization: organization
46
+ }.merge(options)
47
+ )
48
+ end
49
+
50
+ def add_to_digest_for_users(users, notification_type, options = {})
51
+ users.each do |user|
52
+ add_to_digest(user, notification_type, options)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ class Configuration
5
+ attr_accessor :default_frequency,
6
+ :default_time,
7
+ :default_timezone,
8
+ :queue_name,
9
+ :max_items_per_digest,
10
+ :enable_summarization,
11
+ :mailer_class,
12
+ :mailer_method,
13
+ :organization_class,
14
+ :user_class,
15
+ :digest_types
16
+
17
+ def initialize
18
+ @default_frequency = :daily
19
+ @default_time = '09:00'
20
+ @default_timezone = 'UTC'
21
+ @queue_name = :email_digest
22
+ @max_items_per_digest = 50
23
+ @enable_summarization = true
24
+ @mailer_class = 'UserMailer'
25
+ @mailer_method = 'digest_email'
26
+ @organization_class = 'Organization'
27
+ @user_class = 'User'
28
+ @digest_types = {}
29
+ end
30
+
31
+ def register_digest_type(type_name, options = {})
32
+ @digest_types[type_name.to_sym] = {
33
+ collector: options[:collector] || "EmailDigest::Collectors::#{type_name.to_s.classify}Collector",
34
+ summarizer: options[:summarizer] || "EmailDigest::Summarizers::#{type_name.to_s.classify}Summarizer",
35
+ mailer_method: options[:mailer_method] || "digest_#{type_name}_email"
36
+ }
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ desc 'Installs EmailDigest and generates the necessary configuration files'
9
+
10
+ def create_initializer
11
+ template 'email_digest.rb', 'config/initializers/email_digest.rb'
12
+ end
13
+
14
+ def create_migration
15
+ migration_template 'create_email_digest_tables.rb',
16
+ 'db/migrate/create_email_digest_tables.rb'
17
+ end
18
+
19
+ def add_sidekiq_queue
20
+ if File.exist?('config/sidekiq.yml')
21
+ inject_into_file 'config/sidekiq.yml', after: ":queues:\n" do
22
+ " - email_digest\n"
23
+ end
24
+ else
25
+ say_status 'warning', 'config/sidekiq.yml not found. Please add email_digest to your Sidekiq queues manually.', :yellow
26
+ end
27
+ end
28
+
29
+ def add_sidekiq_cron_job
30
+ if File.exist?('config/sidekiq_cron_schedule.yml')
31
+ inject_into_file 'config/sidekiq_cron_schedule.yml', after: "# sidekiq-cron will make sure only one cron job will be inserted into queue\n" do
32
+ <<~YAML
33
+
34
+ email_digest_scheduler_worker:
35
+ cron: "*/15 * * * *" # every 15 minutes
36
+ class: "EmailDigest::Workers::DigestSchedulerWorker"
37
+ queue: email_digest
38
+ YAML
39
+ end
40
+ else
41
+ say_status 'warning', 'config/sidekiq_cron_schedule.yml not found. Please add the scheduler job manually.', :yellow
42
+ end
43
+ end
44
+
45
+ def show_readme
46
+ readme 'README' if behavior == :invoke
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ EmailDigest has been successfully installed!
2
+
3
+ Next steps:
4
+
5
+ 1. Run the migration:
6
+ rails db:migrate
7
+
8
+ 2. Configure your digest types in config/initializers/email_digest.rb
9
+
10
+ 3. Add digest_email method to your mailer (e.g., app/mailers/user_mailer.rb)
11
+
12
+ 4. Include EmailDigest::Concerns::Digestable in models that should create digest items
13
+
14
+ 5. Start Sidekiq to process digest jobs
15
+
16
+ For more information, visit: https://github.com/yourusername/email_digest
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateEmailDigestTables < ActiveRecord::Migration[7.2]
4
+ def change
5
+ create_table :email_digest_items do |t|
6
+ t.string :user_id, null: false
7
+ t.string :organization_id, null: false
8
+ t.string :digest_type, null: false
9
+ t.string :notification_type, null: false
10
+ t.string :subject
11
+ t.text :body
12
+ t.jsonb :metadata, default: {}
13
+ t.string :source_id # ID of the source object (e.g., time_log_id)
14
+ t.string :source_type # Type of source object
15
+ t.integer :priority, default: 0, null: false
16
+ t.boolean :processed, default: false, null: false
17
+ t.datetime :processed_at
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ create_table :email_digest_preferences do |t|
23
+ t.string :user_id, null: false
24
+ t.string :organization_id, null: false
25
+ t.string :digest_type, null: false
26
+ t.boolean :enabled, default: true, null: false
27
+ t.string :frequency, default: 'daily', null: false # daily, weekly, custom
28
+ t.integer :day_of_week # 0-6 (Sunday-Saturday) for weekly
29
+ t.string :time, default: '09:00', null: false # HH:MM format
30
+ t.string :timezone, default: 'UTC', null: false
31
+ t.jsonb :custom_schedule # For complex schedules
32
+ t.datetime :last_sent_at
33
+ t.datetime :next_send_at
34
+
35
+ t.timestamps
36
+ end
37
+
38
+ # Indexes for email_digest_items
39
+ add_index :email_digest_items, [:user_id, :digest_type, :processed, :organization_id],
40
+ name: 'index_digest_items_on_user_digest_processed_org'
41
+ add_index :email_digest_items, [:organization_id, :processed, :created_at],
42
+ name: 'index_digest_items_on_org_processed_created'
43
+ add_index :email_digest_items, [:source_id, :source_type],
44
+ name: 'index_digest_items_on_source'
45
+ # Optimized index for DigestCollector queries (pending items sorted by priority and created_at)
46
+ # Partial index for better performance (only indexes unprocessed items)
47
+ execute <<-SQL
48
+ CREATE INDEX index_digest_items_collector_query
49
+ ON email_digest_items (user_id, digest_type, organization_id, created_at DESC, priority DESC)
50
+ WHERE processed = false;
51
+ SQL
52
+ add_index :email_digest_items, :organization_id
53
+ add_index :email_digest_items, :user_id
54
+
55
+ # Indexes for email_digest_preferences
56
+ add_index :email_digest_preferences, [:user_id, :digest_type, :organization_id],
57
+ unique: true,
58
+ name: 'index_digest_preferences_on_user_digest_org_unique'
59
+ add_index :email_digest_preferences, [:organization_id, :next_send_at],
60
+ name: 'index_digest_preferences_on_org_next_send'
61
+ add_index :email_digest_preferences, [:organization_id, :enabled],
62
+ name: 'index_digest_preferences_on_org_enabled'
63
+ add_index :email_digest_preferences, :user_id
64
+ add_index :email_digest_preferences, :organization_id
65
+
66
+ # Add check constraints
67
+ add_check_constraint :email_digest_preferences,
68
+ "frequency IN ('daily', 'weekly', 'custom')",
69
+ name: 'check_digest_preferences_frequency'
70
+ add_check_constraint :email_digest_preferences,
71
+ "time ~ '^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'",
72
+ name: 'check_digest_preferences_time_format'
73
+ add_check_constraint :email_digest_preferences,
74
+ "day_of_week IS NULL OR (day_of_week >= 0 AND day_of_week <= 6)",
75
+ name: 'check_digest_preferences_day_of_week'
76
+ end
77
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ EmailDigest.configure do |config|
4
+ # Default settings
5
+ config.default_frequency = :daily
6
+ config.default_time = '09:00'
7
+ config.default_timezone = 'UTC'
8
+ config.queue_name = :email_digest
9
+ config.max_items_per_digest = 50
10
+ config.enable_summarization = true
11
+ config.mailer_class = 'UserMailer'
12
+ config.mailer_method = 'digest_email'
13
+ config.organization_class = 'Organization'
14
+ config.user_class = 'User'
15
+
16
+ # Register digest types
17
+ # Example:
18
+ # config.register_digest_type(:time_log_notifications, {
19
+ # mailer_method: 'time_log_digest_email'
20
+ # })
21
+ #
22
+ # config.register_digest_type(:mentor_supervisor_notifications, {
23
+ # mailer_method: 'mentor_supervisor_digest_email'
24
+ # })
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ module Models
5
+ class DigestItem < ActiveRecord::Base
6
+ self.table_name = 'email_digest_items'
7
+
8
+ # Associations
9
+ # Note: User and Organization are in MongoDB, so we use string IDs
10
+ # belongs_to :user would require the User model to be in PostgreSQL
11
+ # For now, we'll keep it as a string reference
12
+
13
+ # Validations
14
+ validates :user_id, presence: true
15
+ validates :organization_id, presence: true
16
+ validates :digest_type, presence: true
17
+ validates :notification_type, presence: true
18
+
19
+ # Scopes
20
+ scope :pending, -> { where(processed: false) }
21
+ scope :for_user, ->(user) { where(user_id: user.id.to_s) }
22
+ scope :for_type, ->(type) { where(digest_type: type.to_s) }
23
+ scope :recent, -> { order(created_at: :desc) }
24
+ scope :for_organization, ->(org) { where(organization_id: org.id.to_s) }
25
+
26
+ # Class methods
27
+ def self.create_for_user(user, digest_type, notification_type, options = {})
28
+ organization = options[:organization] || user.organization
29
+ create(
30
+ user_id: user.id.to_s,
31
+ organization_id: organization.id.to_s,
32
+ digest_type: digest_type.to_s,
33
+ notification_type: notification_type.to_s,
34
+ subject: options[:subject],
35
+ body: options[:body],
36
+ metadata: options[:metadata] || {},
37
+ source_id: options[:source_id],
38
+ source_type: options[:source_type],
39
+ priority: options[:priority] || 0
40
+ )
41
+ end
42
+
43
+ # Instance methods
44
+ def mark_processed
45
+ update(processed: true, processed_at: Time.current)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ module Models
5
+ class DigestPreference < ActiveRecord::Base
6
+ self.table_name = 'email_digest_preferences'
7
+
8
+ # Validations
9
+ validates :user_id, presence: true
10
+ validates :organization_id, presence: true
11
+ validates :digest_type, presence: true
12
+ validates :frequency, inclusion: { in: %w[daily weekly custom] }
13
+ validates :time, format: { with: /\A([0-1]?[0-9]|2[0-3]):[0-5][0-9]\z/ }
14
+ validates :day_of_week, presence: true, if: -> { frequency == 'weekly' }
15
+ validates :timezone, inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }, allow_blank: true
16
+ validates :user_id, uniqueness: { scope: [:digest_type, :organization_id],
17
+ message: 'already has a preference for this digest type in this organization' }
18
+
19
+ # Callbacks
20
+ before_save :calculate_next_send_at
21
+
22
+ # Scopes
23
+ scope :for_organization, ->(org) { where(organization_id: org.id.to_s) }
24
+ scope :enabled, -> { where(enabled: true) }
25
+
26
+ # Class methods
27
+ def self.for_user_and_type(user, digest_type, organization)
28
+ find_or_create_by(
29
+ user_id: user.id.to_s,
30
+ digest_type: digest_type.to_s,
31
+ organization_id: organization.id.to_s
32
+ ) do |pref|
33
+ pref.frequency = EmailDigest.configuration.default_frequency.to_s
34
+ pref.time = EmailDigest.configuration.default_time
35
+ pref.timezone = EmailDigest.configuration.default_timezone
36
+ end
37
+ end
38
+
39
+ def self.ready_to_send(organization = nil)
40
+ now = Time.current
41
+ query = where(enabled: true).where('next_send_at <= ?', now)
42
+ query = query.where(organization_id: organization.id.to_s) if organization
43
+ query
44
+ end
45
+
46
+ # Instance methods
47
+ def calculate_next_send_at
48
+ return unless enabled
49
+
50
+ time_parts = time.split(':')
51
+ hour = time_parts[0].to_i
52
+ minute = time_parts[1].to_i
53
+
54
+ # Safely handle timezone, fallback to UTC if invalid
55
+ begin
56
+ base_time = Time.current.in_time_zone(timezone)
57
+ rescue ArgumentError, TZInfo::InvalidTimezoneIdentifier
58
+ base_time = Time.current.in_time_zone('UTC')
59
+ end
60
+ target_time = base_time.change(hour: hour, min: minute, sec: 0)
61
+
62
+ case frequency
63
+ when 'daily'
64
+ self.next_send_at = target_time < base_time ? target_time + 1.day : target_time
65
+ when 'weekly'
66
+ days_until_target = (day_of_week - base_time.wday) % 7
67
+ days_until_target = 7 if days_until_target == 0 && target_time < base_time
68
+ self.next_send_at = (base_time.beginning_of_week + days_until_target.days).change(hour: hour, min: minute)
69
+ when 'custom'
70
+ calculate_custom_schedule
71
+ end
72
+ end
73
+
74
+ def calculate_custom_schedule
75
+ return unless custom_schedule.present?
76
+
77
+ # Custom schedule can be: { days: [1,3,5], time: "14:00" }
78
+ # Or cron-like: { cron: "0 9 * * 1,3,5" }
79
+ # For now, simple implementation
80
+ if custom_schedule['days'].present?
81
+ begin
82
+ base_time = Time.current.in_time_zone(timezone)
83
+ rescue ArgumentError, TZInfo::InvalidTimezoneIdentifier
84
+ base_time = Time.current.in_time_zone('UTC')
85
+ end
86
+ time_parts = (custom_schedule['time'] || time).split(':')
87
+ hour = time_parts[0].to_i
88
+ minute = time_parts[1].to_i
89
+
90
+ target_days = custom_schedule['days'].map(&:to_i)
91
+ current_day = base_time.wday
92
+ next_day = target_days.find { |d| d > current_day } || target_days.first
93
+ days_ahead = next_day > current_day ? next_day - current_day : (7 - current_day) + next_day
94
+
95
+ self.next_send_at = (base_time + days_ahead.days).change(hour: hour, min: minute)
96
+ end
97
+ end
98
+
99
+ def update_last_sent
100
+ self.last_sent_at = Time.current
101
+ calculate_next_send_at
102
+ save
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ class Railtie < Rails::Railtie
5
+ generators do
6
+ require 'email_digest/generators/email_digest/install/install_generator'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailDigest
4
+ module Services
5
+ class DigestCollector
6
+ attr_reader :user, :digest_type, :organization
7
+
8
+ def initialize(user, digest_type, organization = nil)
9
+ @user = user
10
+ @digest_type = digest_type.to_sym
11
+ @organization = organization || user.organization
12
+ end
13
+
14
+ def collect_items(since: nil)
15
+ since ||= last_digest_sent_at || 7.days.ago
16
+
17
+ items = Models::DigestItem.pending
18
+ .for_user(user)
19
+ .for_type(digest_type)
20
+ .for_organization(organization)
21
+ .where('created_at >= ?', since)
22
+ .order(priority: :desc, created_at: :desc)
23
+ .limit(EmailDigest.configuration.max_items_per_digest)
24
+
25
+ items
26
+ end
27
+
28
+ def last_digest_sent_at
29
+ preference = Models::DigestPreference.find_by(
30
+ user_id: user.id.to_s,
31
+ digest_type: digest_type.to_s,
32
+ organization_id: organization.id.to_s
33
+ )
34
+ preference&.last_sent_at
35
+ end
36
+ end
37
+ end
38
+ end