diff --git a/spec/booking_spec.cr b/spec/booking_spec.cr index 08de6302..53d3f56b 100644 --- a/spec/booking_spec.cr +++ b/spec/booking_spec.cr @@ -743,6 +743,103 @@ module PlaceOS::Model deleted_booking.not_nil!.deleted.should be_true end + it "expands a cancelled series as cancelled occurrences only when include_deleted is set" do + tenant_id = Generator.tenant.id.not_nil! + user_email = "steve@place.tech" + + parent_booking = Booking.new( + booking_type: "desk", + asset_ids: ["desk1"], + booking_start: 1.day.from_now.at_beginning_of_day.to_unix + 9.hours.total_seconds.to_i64, + booking_end: 1.day.from_now.at_beginning_of_day.to_unix + 10.hours.total_seconds.to_i64, + timezone: "UTC", + user_email: PlaceOS::Model::Email.new(user_email), + user_name: "Steve", + booked_by_email: PlaceOS::Model::Email.new(user_email), + booked_by_name: "Steve", + tenant_id: tenant_id, + booked_by_id: "user-1234", + history: [] of Booking::History, + recurrence_type: Booking::Recurrence::DAILY, + recurrence_days: 0b1111111, + recurrence_interval: 1 + ).save! + + start_time = 1.day.from_now.at_beginning_of_day + end_time = start_time + 5.days + occurrences = parent_booking.calculate_daily(start_time, end_time).instances.map(&.to_unix) + occurrences.size.should eq 5 + + # an occurrence with its own override, checked in before the series was cancelled + checked_in_at = occurrences[1] + BookingInstance.new( + id: parent_booking.id.not_nil!, + instance_start: checked_in_at, + tenant_id: tenant_id, + booking_start: checked_in_at, + booking_end: checked_in_at + 1.hour.total_seconds.to_i64, + checked_in: true, + checked_in_at: checked_in_at, + ).save! + + parent_booking.deleted = true + parent_booking.deleted_at = Time.utc.to_unix + parent_booking.save! + + # without include_deleted the cancelled series stays a single, unexpanded row + hidden = Booking.expand_bookings!(start_time, end_time, [parent_booking], include_deleted: false).bookings + hidden.map(&.instance).should eq [nil] + + expanded = Booking.expand_bookings!(start_time, end_time, [parent_booking], include_deleted: true).bookings + expanded.compact_map(&.instance).sort!.should eq occurrences + expanded.all?(&.deleted).should be_true + expanded.all? { |booking| booking.deleted_at == parent_booking.deleted_at }.should be_true + + checked_in = expanded.find! { |booking| booking.instance == checked_in_at } + checked_in.checked_in.should be_true + end + + it "hydrates an occurrence of a cancelled series as cancelled" do + tenant_id = Generator.tenant.id.not_nil! + user_email = "steve@place.tech" + + parent_booking = Booking.new( + booking_type: "desk", + asset_ids: ["desk1"], + booking_start: 1.day.from_now.at_beginning_of_day.to_unix + 9.hours.total_seconds.to_i64, + booking_end: 1.day.from_now.at_beginning_of_day.to_unix + 10.hours.total_seconds.to_i64, + timezone: "UTC", + user_email: PlaceOS::Model::Email.new(user_email), + user_name: "Steve", + booked_by_email: PlaceOS::Model::Email.new(user_email), + booked_by_name: "Steve", + tenant_id: tenant_id, + booked_by_id: "user-1234", + history: [] of Booking::History, + recurrence_type: Booking::Recurrence::DAILY, + recurrence_days: 0b1111111, + recurrence_interval: 1 + ).save! + + starting = parent_booking.booking_start.not_nil! + override = BookingInstance.new( + id: parent_booking.id.not_nil!, + instance_start: starting, + tenant_id: tenant_id, + booking_start: starting, + booking_end: starting + 1.hour.total_seconds.to_i64, + ).save! + override.hydrate_booking(parent_booking).deleted.should be_false + + parent_booking.deleted = true + parent_booking.deleted_at = Time.utc.to_unix + parent_booking.save! + + hydrated = override.hydrate_booking(parent_booking) + hydrated.deleted.should be_true + hydrated.deleted_at.should eq parent_booking.deleted_at + end + it "sanitizes extension_data string values before saving", tags: "extension_data_sanitization" do tenant_id = Generator.tenant.id user_email = "test@place.tech" diff --git a/src/placeos-models/booking.cr b/src/placeos-models/booking.cr index f88127b6..9ea6eaef 100644 --- a/src/placeos-models/booking.cr +++ b/src/placeos-models/booking.cr @@ -849,8 +849,11 @@ module PlaceOS::Model # Recurring booking expansion # === - def recurring_booking? : Bool - !recurrence_type.none? && !deleted && !rejected && instance.nil? + # a series whose occurrences `expand_bookings!` generates. With + # `include_deleted`, a cancelled series counts too, and every one of its + # occurrences comes back cancelled. + def recurring_booking?(include_deleted : Bool = false) : Bool + !recurrence_type.none? && (include_deleted || !deleted) && !rejected && instance.nil? end def recurring_instance? : Bool @@ -874,7 +877,7 @@ module PlaceOS::Model is_checked_out : Bool? = nil, include_deleted : Bool = false, ) : ExpansionDetails - recurring = parents.select(&.recurring_booking?) + recurring = parents.select(&.recurring_booking?(include_deleted)) return ExpansionDetails.new(parents, 0, 0) if recurring.empty? parent_ids = recurring.compact_map(&.id) recurring.each { |booking| parents.delete booking } diff --git a/src/placeos-models/booking_instance.cr b/src/placeos-models/booking_instance.cr index 674d5afd..6fcaed8d 100644 --- a/src/placeos-models/booking_instance.cr +++ b/src/placeos-models/booking_instance.cr @@ -133,8 +133,11 @@ module PlaceOS::Model instance.checked_in = self.checked_in instance.checked_in_at = self.checked_in_at instance.checked_out_at = self.checked_out_at - instance.deleted = self.deleted - instance.deleted_at = self.deleted_at + # an occurrence of a cancelled series is cancelled, whatever its override says + if self.deleted || !main.deleted + instance.deleted = self.deleted + instance.deleted_at = self.deleted_at + end instance.process_state = self.process_state # A non-empty extension data object is a complete snapshot for this # occurrence and replaces the parent booking's extension data wholesale.