{"record":{"id":"65d3e9eee6d85c13","repo":"instructure/canvas-lms","slug":"participant-has-already-reserved-this-appointment","errorCode":null,"errorMessage":"participant has already reserved this appointment","messagePattern":"participant has already reserved this appointment","errorType":"exception","errorClass":"CalendarEvent::ReservationError","httpStatus":null,"severity":"error","filePath":"app/models/calendar_event.rb","lineNumber":610,"sourceCode":"    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\"\n        save!\n      end\n      context.clear_cached_available_slots!\n      event\n    end\n  end\n\n  def child_events_for(participant)\n    if child_events.loaded?","sourceCodeStart":592,"sourceCodeEnd":628,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/calendar_event.rb#L592-L628","documentation":"CalendarEvent.reserve_for raises ReservationError when the participant already holds a reservation (child_events_for(participant) is non-empty) on this specific appointment slot. A participant can only reserve a given slot once.","triggerScenarios":"Calling reserve_for twice for the same participant on the same slot; double-click on the booking button issuing two requests; a retry after a timeout when the first request actually succeeded.","commonSituations":"Duplicate form submissions; idempotency-missing API clients; users refreshing a confirmation page that re-posts the booking.","solutions":["Treat this as success/idempotent: check child_events_for(participant) first and no-op if a reservation already exists.","Disable the booking button / debounce submissions in the client before re-posting.","If a different reservation is intended, cancel the existing one first, then call reserve_for with cancel_existing: true (only if it is in the future)."],"exampleFix":"// before\nslot.reserve_for(user, participant)\n// after\nunless slot.child_events_for(participant).present?\n  slot.reserve_for(user, participant)\nend","handlingStrategy":"validation","validationCode":"return if slot.child_events_for(participant).present? # already reserved, treat as idempotent success","typeGuard":null,"tryCatchPattern":"begin\n  slot.reserve_for(user, participant)\nrescue ReservationError => e\n  raise unless e.message.include?('already reserved')\n  # treat as success\nend","preventionTips":["Check child_events_for(participant) before booking","Debounce/disable submit buttons to prevent double posts","Make API clients idempotent: query before create"],"tags":["calendar","appointments","duplicate","idempotency"],"backgroundTag":"duplicate-record","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"}