{"record":{"id":"10c34a342849e790","repo":"instructure/canvas-lms","slug":"enrollment-is-not-in-invited-state-reject-invitation","errorCode":null,"errorMessage":"Enrollment is not in invited state","messagePattern":"Enrollment is not in invited state","errorType":"exception","errorClass":"GraphQL::ExecutionError","httpStatus":null,"severity":"error","filePath":"app/graphql/mutations/reject_enrollment_invitation.rb","lineNumber":39,"sourceCode":"module Mutations\n  class RejectEnrollmentInvitation < BaseMutation\n    argument :enrollment_uuid, String, required: true\n\n    field :enrollment, Types::EnrollmentType, null: true\n    field :success, Boolean, null: false\n\n    def resolve(input:, **)\n      user = context[:current_user]\n      raise GraphQL::ExecutionError, I18n.t(\"Must be logged in\") unless user\n\n      enrollment = Enrollment.where(uuid: input[:enrollment_uuid]).first\n      raise GraphQL::ExecutionError, I18n.t(\"Enrollment invitation not found\") unless enrollment\n\n      # Verify the enrollment belongs to the current user\n      raise GraphQL::ExecutionError, I18n.t(\"Unauthorized\") unless enrollment.user == user\n\n      # Verify the enrollment is in invited state\n      raise GraphQL::ExecutionError, I18n.t(\"Enrollment is not in invited state\") unless enrollment.invited?\n\n      begin\n        if enrollment.reject\n          {\n            enrollment:,\n            success: true\n          }\n        else\n          {\n            enrollment: nil,\n            success: false,\n            errors: [{ message: I18n.t(\"Failed to reject enrollment invitation\") }]\n          }\n        end\n      rescue => e\n        {\n          enrollment: nil,\n          success: false,","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/graphql/mutations/reject_enrollment_invitation.rb#L21-L57","documentation":"Raised when the enrollment exists and belongs to the user but its workflow_state is not 'invited' (enrollment.invited? is false). Only pending invitations can be rejected; already-accepted, completed, rejected, or deleted enrollments are invalid targets.","triggerScenarios":"Double-clicking the reject action so the second call hits an already-rejected enrollment; rejecting after the user already accepted the course; enrollment auto-accepted due to a prior interaction; enrollment hard/soft deleted by an admin in the meantime.","commonSituations":"Users clicking a stale 'decline invitation' email link after previously accepting; UI not refreshing state after the first rejection; race between two browser tabs.","solutions":["Check enrollment.workflow_state == 'invited' before calling; surface a friendly message otherwise","Disable the reject action in the UI after the first successful call","If already accepted, instruct the user to leave the course instead of rejecting","Handle idempotency: treat 'already rejected' outcomes as success in client logic"],"exampleFix":"// before\n// second click after rejection\nrejectEnrollmentInvitation(uuid)\n// after\nif (enrollment.workflowState === 'invited') {\n  await rejectEnrollmentInvitation(uuid)\n} else {\n  showMessage('This invitation has already been processed.')\n}","handlingStrategy":"validation","validationCode":"if (enrollment.workflowState !== 'invited') {\n  show('This invitation has already been ' + enrollment.workflowState)\n  return\n}","typeGuard":"function isInvitable(e) { return e?.workflowState === 'invited' }","tryCatchPattern":"try {\n  await rejectEnrollmentInvitation({ enrollmentUuid })\n} catch (e) {\n  if (e.message.includes('not in invited state')) { refetchEnrollment(); show('Invitation already processed') }\n  else throw e\n}","preventionTips":["Disable the reject button immediately after a successful call","Check workflow_state === 'invited' before rendering the action","Handle stale email links gracefully with state re-fetch","Avoid concurrent mutations from multiple tabs"],"tags":["graphql","enrollments","state-machine"],"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"}