{"record":{"id":"3482bfc3330041d5","repo":"instructure/canvas-lms","slug":"cannot-cancel-past-reservation","errorCode":null,"errorMessage":"cannot cancel past reservation","messagePattern":"cannot cancel past reservation","errorType":"exception","errorClass":"CalendarEvent::ReservationError","httpStatus":null,"severity":"error","filePath":"app/models/calendar_event.rb","lineNumber":601,"sourceCode":"  end\n\n  def account\n    (context_type == \"Account\") ? context : nil\n  end\n\n  class ReservationError < StandardError; end\n\n  def reserve_for(participant, user, options = {})\n    raise ReservationError, \"not an appointment\" unless context_type == \"AppointmentGroup\"\n    raise ReservationError, \"ineligible participant\" unless context.eligible_participant?(participant)\n\n    transaction do\n      lock! # in case two people two participants try to grab the same slot\n      participant.lock! # in case two people try to make a reservation for the same participant\n\n      if options[:cancel_existing]\n        context.reservations_for(participant).lock.each do |reservation|\n          raise ReservationError, \"cannot cancel past reservation\" if reservation.end_at < Time.now.utc\n\n          reservation.updating_user = user\n          reservation.destroy\n        end\n      end\n\n      raise ReservationError, \"participant has met per-participant limit\" if context.max_appointments_per_participant && context.reservations_for(participant).size >= context.max_appointments_per_participant\n      raise ReservationError, \"all slots filled\" if participants_per_appointment && child_events.size >= participants_per_appointment\n      raise ReservationError, \"participant has already reserved this appointment\" if child_events_for(participant).present?\n\n      event = child_events.build\n      event.updating_user = user\n      event.context = participant\n      event.workflow_state = :locked\n      event.comments = options[:comments]\n      event.save!\n      if active?\n        self.workflow_state = \"locked\"","sourceCodeStart":583,"sourceCodeEnd":619,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/calendar_event.rb#L583-L619","documentation":"CalendarEvent.reserve_for raises ReservationError when options[:cancel_existing] is set and one of the participant's existing reservations ends in the past. The code intentionally refuses to destroy reservations whose end_at is before Time.now.utc, since canceling a completed appointment would falsify history.","triggerScenarios":"Calling reserve_for on an appointment slot with options[:cancel_existing] => true while the participant holds a reservation whose end_at is earlier than the current UTC time.","commonSituations":"Rebooking a participant via the scheduler API after their old appointment already occurred; bulk-reschedule scripts that cancel existing reservations without filtering out past ones; UI flows letting users 'switch' into a new slot when their previous slot expired.","solutions":["Remove options[:cancel_existing], or first delete the past reservation via the reservations API (past reservations can be deleted directly) before calling reserve_for.","Only pass cancel_existing: true when the participant's existing reservations are all in the future.","Use the appointment group's update_participants or a manual destroy of the stale reservation in the same transaction."],"exampleFix":"// before\ncalendar_event.reserve_for(user, participant, cancel_existing: true)\n// after\nif participant.reservations.any? { |r| r.end_at < Time.now.utc }\n  participant.reservations.where('end_at < ?', Time.now.utc).each(&:destroy)\nend\ncalendar_event.reserve_for(user, participant, cancel_existing: true)","handlingStrategy":"validation","validationCode":"past = appointment_group.reservations_for(participant).any? { |r| r.end_at < Time.now.utc }\nraise UserFacingError, 'past reservation must be removed separately' if past && options[:cancel_existing]","typeGuard":"def cancellable?(reservation) = reservation.end_at >= Time.now.utc","tryCatchPattern":"begin\n  slot.reserve_for(user, participant, cancel_existing: true)\nrescue ReservationError => e\n  handle_past_reservation_conflict(e)\nend","preventionTips":["Filter out past reservations before requesting cancel_existing","Delete expired reservations in a background cleanup job","Never blind-pass cancel_existing when rebooking after an appointment date"],"tags":["calendar","appointments","scheduler","state-conflict"],"backgroundTag":"invalid-state-transition","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}