{"record":{"id":"cfcf7a6f645e30eb","repo":"instructure/canvas-lms","slug":"resource-is-not-closed","errorCode":null,"errorMessage":"Resource is not closed","messagePattern":"Resource is not closed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/services/accessibility/bulk_close_issues_service.rb","lineNumber":45,"sourceCode":"\n  def call\n    if close\n      close_issues\n    else\n      reopen_issues\n    end\n  end\n\n  private\n\n  attr_reader :scan, :user_id, :close\n\n  def close_issues\n    scan.bulk_close_issues!(user_id:)\n  end\n\n  def reopen_issues\n    raise \"Resource is not closed\" if scan.open?\n\n    # Reset closed status\n    scan.update!(closed_at: nil)\n\n    # Trigger a fresh re-scan\n    # This will:\n    # - Delete all rescannable issues (active + closed)\n    # - Scan the resource for current issues\n    # - Create new active issues\n    # - Update issue_count\n    # - Reset closed_at to nil\n    Accessibility::ResourceScannerService.call(resource: scan.context)\n  end\nend\n","sourceCodeStart":27,"sourceCodeEnd":60,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/accessibility/bulk_close_issues_service.rb#L27-L60","documentation":"Accessibility::BulkCloseIssuesService#reopen_issues can only reopen a scan that is currently closed. This guard fires when scan.open? is true — the issues were never bulk-closed — so reopening them is a no-op that would incorrectly reset state. It protects the closed_at lifecycle invariant of the accessibility scan.","triggerScenarios":"Calling reopen_issues on a scan that was never closed (open_at/closed_at state indicates open); calling reopen twice — the second call hits the already-reopened scan; UI/API double-submit racing with the first reopen that already succeeded.","commonSituations":"Users clicking an 'undo close' action after the state has already changed; background jobs re-delivering a reopen request; tests or scripts assuming reopen is idempotent.","solutions":["Check scan.open? before scheduling reopen and skip/no-op when it is already open","Track closed_at: only call reopen when scan.closed? (closed_at present)","Make the caller idempotent: catch this error and treat an already-open scan as success"],"exampleFix":"// before\nservice.reopen_issues\n// after\nservice.reopen_issues unless scan.open?","handlingStrategy":"try-catch","validationCode":"service.reopen_issues unless scan.open?","typeGuard":"scan.respond_to?(:open?) && scan.open? == false","tryCatchPattern":"begin\n  service.reopen_issues\nrescue RuntimeError => e\n  Rails.logger.info('scan already open') if e.message == 'Resource is not closed'\nend","preventionTips":["Treat reopen as conditional on scan.closed?","Make UI actions hide 'reopen' for open scans","De-duplicate background reopen jobs"],"tags":["accessibility","state","ruby"],"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"}