diff --git a/app/models/notification/bundle.rb b/app/models/notification/bundle.rb index 84e1058cb..306b9083f 100644 --- a/app/models/notification/bundle.rb +++ b/app/models/notification/bundle.rb @@ -3,10 +3,20 @@ class Notification::Bundle < ApplicationRecord enum :status, %i[ pending processing delivered ] - before_create :set_default_window - scope :due, -> { pending.where("ends_at <= ?", Time.current) } scope :containing, -> (notification) { where("starts_at <= ? AND ends_at >= ?", notification.created_at, notification.created_at) } + scope :overlapping_with, -> (other_bundle) { + where( + "(starts_at <= ? AND ends_at >= ?) OR (starts_at <= ? AND ends_at >= ?) OR (starts_at >= ? AND ends_at <= ?)", + other_bundle.starts_at, other_bundle.starts_at, + other_bundle.ends_at, other_bundle.ends_at, + other_bundle.starts_at, other_bundle.ends_at + ) + } + + before_create :set_default_window + + validate :validate_no_overlapping class << self def deliver_all @@ -51,4 +61,14 @@ class Notification::Bundle < ApplicationRecord def has_unread_notifications? notifications.unread.any? end + + def validate_no_overlapping + if overlapping_bundles.exists? + errors.add(:base, "Bundle window overlaps with an existing pending bundle") + end + end + + def overlapping_bundles + user.notification_bundles.where.not(id: id).overlapping_with(self) + end end diff --git a/test/models/notification/bundle_test.rb b/test/models/notification/bundle_test.rb index 1033a46a7..f9bf4abca 100644 --- a/test/models/notification/bundle_test.rb +++ b/test/models/notification/bundle_test.rb @@ -30,6 +30,47 @@ class Notification::BundleTest < ActiveSupport::TestCase end bundle_1, bundle_2 = @user.notification_bundles.last(2) + assert_includes bundle_1.notifications, notification_1 + assert_includes bundle_1.notifications, notification_2 + assert_includes bundle_2.notifications, notification_3 + end + test "overlapping bundles are invalid" do + bundle_1 = @user.notification_bundles.create!( + starts_at: Time.current, + ends_at: 4.hours.from_now, + status: :pending + ) + + bundle_2 = @user.notification_bundles.build( + starts_at: 2.hours.from_now, + ends_at: 6.hours.from_now, + status: :pending + ) + assert_not bundle_2.valid? + + # Bundle with overlapping end time should be invalid + bundle_3 = @user.notification_bundles.build( + starts_at: 2.hours.ago, + ends_at: 2.hours.from_now, + status: :pending + ) + assert_not bundle_3.valid? + + # Bundle completely within another bundle should be invalid + bundle_4 = @user.notification_bundles.build( + starts_at: 1.hour.from_now, + ends_at: 3.hours.from_now, + status: :pending + ) + assert_not bundle_4.valid? + + # Non-overlapping bundle should be valid + bundle_5 = @user.notification_bundles.build( + starts_at: 5.hours.from_now, + ends_at: 9.hours.from_now, + status: :pending + ) + assert bundle_5.valid? end end