Fix errors and bugs in the given code, without changing its intended functionality. ONLY return the fixed code and nothing else. Enclose the entire code in a single code block.



class Trigger < ApplicationRecord
  class Error < StandardError
    attr_reader :details, :trigger, :at

    def initialize(msg, trigger: nil, details: nil, at: nil)
      super(msg)

      @trigger = trigger
      @details = details
      @at = at
    end
  end

  acts_as_list scope: %i[type]

  scope :active, ->(active = true) { where(active:) }
  scope :order_by_position, -> { order(:position) }
  scope :ready_to_run, -> { where("#{Trigger.table_name}.next_run_at <= NOW()") }

  enum when: {
    save: "save",
    create: "create",
    update: "update",
    destroy: "destroy",
    cron: "cron",
    manual: "manual"
  }, _prefix: true

  belongs_to :author, class_name: "User"
  has_one :job_status, class_name: "JobStatus::Status", as: :reference

  validates :condition_script,
            :before_script,
            :after_script,
            :cron_script,
            :manual_script,
            :async_script,
            syntax: true
  validates :name,
            :when,
            presence: true
  validates :name,
            uniqueness: { scope: %i[type] },
            length: { maximum: 255 }
  validates :timezone,
            inclusion: { in: proc { ::ActiveSupport::TimeZone.all.map { _1.tzinfo.canonical_identifier } } },
            allow_blank: true,
            if: :when_cron?
  validates :async_delay,
            presence: true,
            numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_blank: true }

  validate :validate_cron,
           if: :when_cron?
  validate :validate_filters_json,
           if: :when_bulk?
  validate :validate_filters,
           if: :when_bulk?

  before_save :cleanup_changed_fields
  before_save :reschedule

  class << self
    def has_constraint?(name)
      constraint = "::Triggers::Constraints::#{name.to_s.camelize}Constraint".constantize
      included_modules.include?(constraint)
    end

    def registered_entity_classes
      @registered_entity_classes ||= {}
    end

    def register_entity_class(klass)
      registered_entity_classes[klass.name] = klass
    end

    def entity_class_registered?(klass)
      registered_entity_classes.include?(klass.to_s)
    end

    def entity_class
      @entity_class ||= name.delete_suffix("Trigger").delete_prefix("Triggers::").constantize
    end

    def entity_custom_field_class
      @entity_custom_field_class ||= "#{entity_class.name}CustomField".safe_constantize
    end

    def native_entity_fields
      raise NotImplementedError
    end

    def entity_fields
      RequestStore["#{name.underscore}_entity_fields"] ||=
        begin
          fields = native_entity_fields
          fields += entity_custom_field_class.all.map(&:attribute_name) if entity_custom_field_class
          fields
        end
    end

    def loading_scope(events: [], changes: nil, **)
      scope = active.order_by_position
      scope.where! when: events
      scope.where! "((changed_fields && ARRAY[?]::VARCHAR[]) OR changed_fields='{}')", changes.keys if changes
      scope
    end

    def for(events: [], changes: nil, **constraints)
      cache = RequestStore[:triggers_cache] ||= {}
      cache[[events, changes, constraints]] ||= loading_scope(events:, changes:, **constraints).to_a
    end

    def query_scope
      all
    end
  end

  def when_bulk?
    when_cron? || when_manual?
  end

  def satisfies?(entity)
    execute_script(entity, at: :condition)
  end

  def to_s
    "##{id} (#{name})"
  end

  def cron_parts
    cron.presence&.split || (["*"] * 5)
  end

  def cron_parts=(parts)
    parts = parts.values if parts.is_a?(Hash)
    parts = parts.slice(0, 5).map { _1.strip.presence || "*" }
    (5 - parts.size).times { parts << "*" } if parts.size < 5
    self.cron = parts.join(" ")
  end

  def filters_json
    @filters_json || filters&.to_json
  end

  def filters_json=(filters)
    self.filters =
      begin
        JSON.parse(filters)
      rescue JSON::ParserError
        nil
      end
    @filters_json = filters.presence
  end

  def trigger_logger(object = nil)
    if object
      logger = object.object_logger
    else
      logger = Rails.logger
      logger = ActiveSupport::TaggedLogging.new(logger) unless logger.is_a?(ActiveSupport::TaggedLogging)
      logger = logger.tagged("User ##{User.current.id}")
    end
    logger.tagged("Trigger ##{id}")
  end

  def get_script(at)
    try(:"#{at}_script")
  end

  def schedule_script(object, at: :async, wait: async_delay)
    script = get_script(at)
    return true if script.blank?

    Triggers::SingleRecordJob.set(wait:).perform_later(user: User.current,
                                                       trigger: self,
                                                       record: object,
                                                       at:)
  end

  def execute_script(object, at:, timeout: OpenProject::Configuration.triggers_timeout)
    script = get_script(at)
    return true if script.blank?

    logger = trigger_logger(object)
    logger.info "*** Execute #{at} script for trigger #{self} on \"#{object}\""

    time = Benchmark.realtime do
      Timeout.timeout(timeout) do
        RestClient.with_log(logger) do
          execute_internal(script, object:, logger:, at:)
        end
      end
    end

    logger.info "*** Finished #{at} script for trigger #{self} on \"#{object}\" (#{time.round(3)} sec.)"
  rescue StandardError => e
    cleanup_backtrace e
    message = e.full_message(order: :top, highlight: false)
    logger.warn "*** Can't evaluate #{at} script for trigger #{self} on \"#{object}\": #{message}"
    if e.is_a?(Error)
      raise
    else
      raise Error.new(reported_message(e), details: "#{e.message} (#{e.class})", trigger: self, at:)
    end
  end

  def records_scope
    call = build_filters_scope
    call.success? ? call.result : self.class.entity_class.none
  end

  def records
    return [] unless active?

    records_scope.select { |record| satisfies?(record) }
  end

  def reschedule!
    reschedule
    save
  end

  def timezone=(value)
    super(value.presence)
  end

  def running?
    job_status&.in_queue? || job_status&.in_process?
  end

  def project
    nil
  end

  private

  def execute_internal(script, object:, logger:, at:)
    current_trigger = self

    object.define_singleton_method(:show_error) do |message, details = nil|
      raise Trigger::Error.new(message, details:, trigger: current_trigger, at:)
    end

    if OpenProject::Configuration.triggers_safe_mode_enabled?
      object.to_jail.eval script, current_trigger:, logger:
    else
      object.instance_eval(script)
    end
  end

  def build_filters_scope
    Triggers::BuildFiltersScope
      .new(User.current).call(filters: filters&.to_json || "[]",
                              project:,
                              include_subprojects:,
                              model: self.class.entity_class)
  end

  def validate_cron
    Fugit::Cron.do_parse(cron)
  rescue StandardError
    errors.add :cron, :invalid
  end

  def validate_filters
    call = build_filters_scope
    return if call.success?

    errors.add :filters, call.errors.full_messages.join(" #{I18n.t('support.array.sentence_connector')} ")
  end

  def validate_filters_json
    return if filters_json.blank?

    JSON.parse(filters_json)
  rescue StandardError
    errors.add :filters, :invalid
  end

  def cron_with_timezone
    "#{cron} #{timezone || 'UTC'}"
  end

  def reschedule
    self.next_run_at =
      if active? && when_cron?
        Fugit::Cron.do_parse(cron_with_timezone).next_time.to_local_time
      end
  end

  def cleanup_changed_fields
    changed_fields.compact_blank!
    self.changed_fields = [] unless accepts_changed_fields?
  end

  def accepts_changed_fields?
    when_update? || when_save?
  end

  def cleanup_backtrace(exception)
    bt = exception.backtrace.take_while { |s| s.exclude?("in `execute_script'") || s.include?("(eval)") }
    exception.set_backtrace bt
    bt
  end

  def reported_message(exception)
    if debug?
      I18n::t(:error_trigger_debug_error, message: exception.full_message(order: :top, highlight: false), name: to_s)
    elsif exception.is_a?(Error)
      exception.message
    else
      I18n::t(:error_trigger_internal_error)
    end
  end
end