{"record":{"id":"9777c3f1a614a9d7","repo":"instructure/canvas-lms","slug":"due-date-cannot-be-after-until-date","errorCode":null,"errorMessage":"Due date cannot be after until date","messagePattern":"Due date cannot be after until date","errorType":"validation","errorClass":"PeerReview::InvalidDatesError","httpStatus":null,"severity":"error","filePath":"app/services/peer_review/validations.rb","lineNumber":95,"sourceCode":"          raise PeerReview::InvalidDatesError, I18n.t(\"Invalid datetime format for %{attribute}\", attribute: date_field)\n        end\n\n        parsed_dates[date_field.to_sym] = Time.zone.parse(date_value)\n      else\n        parsed_dates[date_field.to_sym] = date_value\n      end\n    end\n\n    due_at = parsed_dates[:due_at]\n    unlock_at = parsed_dates[:unlock_at]\n    lock_at = parsed_dates[:lock_at]\n\n    if due_at && unlock_at && due_at < unlock_at\n      raise PeerReview::InvalidDatesError, I18n.t(\"Due date cannot be before available from date\")\n    end\n\n    if due_at && lock_at && due_at > lock_at\n      raise PeerReview::InvalidDatesError, I18n.t(\"Due date cannot be after until date\")\n    end\n\n    if unlock_at && lock_at && unlock_at > lock_at\n      raise PeerReview::InvalidDatesError, I18n.t(\"Available from date cannot be after until date\")\n    end\n  end\n\n  # Validates that peer review override dates fall within parent assignment override dates\n  # assignment available from date <= assignment due date <= peer review available from date <= peer review due date <= peer review until date <= assignment until date\n  def validate_override_dates_against_parent_override(peer_review_override, parent_override)\n    parent_unlock_at = parent_override.unlock_at_overridden ? parent_override.unlock_at : nil\n    parent_due_at = parent_override.due_at_overridden ? parent_override.due_at : nil\n    parent_lock_at = parent_override.lock_at_overridden ? parent_override.lock_at : nil\n\n    validate_dates_within_parent_boundaries(\n      child_dates: peer_review_override,\n      parent_unlock_at:,\n      parent_due_at:,","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/peer_review/validations.rb#L77-L113","documentation":"PeerReview::InvalidDatesError raised by validate_peer_review_dates when a peer review sub-assignment's due_at is later than its lock_at (until date). Canvas enforces unlock_at <= due_at <= lock_at on peer review dates, so a due date after the until date is incoherent — submissions could still be due after reviews were locked. It is thrown during creation/update of peer review sub assignments via validate_peer_review_dates_against_parent_assignment.","triggerScenarios":"Calling the peer review creation/update API (e.g. POST/PUT that builds the peer_review_sub_assignment) with due_at set to a timestamp strictly greater than the supplied lock_at, while both are present. E.g. due_at: '2026-05-01T00:00:00Z', lock_at: '2026-04-30T00:00:00Z'.","commonSituations":"Admins copy an assignment's dates into a script and swap due/lock fields; clients that compute lock_at from due_at with an off-by-one-day offset; time zone mishandling making an intended-equal pair compare as due > lock.","solutions":["Compare the payload's due_at and lock_at; move lock_at to a timestamp >= due_at, or lower due_at","If dates were swapped by mistake, exchange the due_at and lock_at values","If only a due date is wanted, omit lock_at (nil lock_at skips this check)","Validate in the client before submitting: raise unless Time.parse(due_at) <= Time.parse(lock_at)"],"exampleFix":"// before\npeer_review_dates = { due_at: '2026-05-10T23:59:00Z', lock_at: '2026-05-05T23:59:00Z' }\n// after\npeer_review_dates = { due_at: '2026-05-05T23:59:00Z', lock_at: '2026-05-10T23:59:00Z' }","handlingStrategy":"validation","validationCode":"def valid_peer_review_dates?(dates)\n  due = dates[:due_at]&.then { |v| v.is_a?(String) ? Time.zone.parse(v) : v }\n  lock = dates[:lock_at]&.then { |v| v.is_a?(String) ? Time.zone.parse(v) : v }\n  !(due && lock && due > lock)\nend","typeGuard":"def as_time(v)\n  return v if v.is_a?(Time || ActiveSupport::TimeWithZone)\n  Time.zone.parse(v) if v.is_a?(String) && Api::ISO8601_REGEX.match?(v)\nend","tryCatchPattern":"begin\n  service.create_peer_review_with_dates(dates)\nrescue PeerReview::InvalidDatesError => e\n  render json: { error: e.message }, status: :bad_request\nend","preventionTips":["Always keep the ordering unlock_at <= due_at <= lock_at when setting peer review dates","Compute lock_at as due_at + buffer, never an independent earlier value","Use ISO8601 UTC strings consistently to avoid silent timezone comparisons","Add a client-side date picker constraint binding lock_at min to due_at"],"tags":["peer-review","date-validation","rails","canvas-lms"],"backgroundTag":"invalid-argument-value","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"}