class Form attr_reader :form_definition, :sections, :subsections, :pages, :questions, :start_date, :new_logs_end_date, :submission_deadline, :type, :name, :setup_definition, :setup_sections, :form_sections, :unresolved_log_redirect_page_id, :edit_end_date DEADLINES = { 2022 => { submission_deadline: Time.zone.local(2023, 6, 9), new_logs_end_date: Time.zone.local(2023, 11, 20), edit_end_date: Time.zone.local(2023, 11, 20), }, 2023 => { submission_deadline: Time.zone.local(2024, 6, 7), }, 2024 => { submission_deadline: Time.zone.local(2025, 6, 6), }, :default => { submission_deadline: ->(start_year) { Time.zone.local(start_year + 1, 6, 1) }, new_logs_end_date: ->(start_year) { Time.zone.local(start_year + 1, 12, 31) }, edit_end_date: ->(start_year) { Time.zone.local(start_year + 1, 12, 31) }, }, }.freeze def initialize(form_path, start_year = "", sections_in_form = [], type = "lettings") if sales_or_start_year_after_2022?(type, start_year) @start_date = Time.zone.local(start_year, 4, 1) @new_logs_end_date = DEADLINES.dig(start_year, :new_logs_end_date) || DEADLINES[:default][:new_logs_end_date].call(start_year) @submission_deadline = DEADLINES.dig(start_year, :submission_deadline) || DEADLINES[:default][:submission_deadline].call(start_year) @setup_sections = type == "sales" ? [Form::Sales::Sections::Setup.new(nil, nil, self)] : [Form::Lettings::Sections::Setup.new(nil, nil, self)] @form_sections = sections_in_form.map { |sec| sec.new(nil, nil, self) } @type = type @sections = setup_sections + form_sections @subsections = sections.flat_map(&:subsections) @pages = subsections.flat_map(&:pages) @questions = pages.flat_map(&:questions) @form_definition = { "form_type" => type, "start_date" => start_date, "end_date" => new_logs_end_date, "sections" => sections, } @unresolved_log_redirect_page_id = "tenancy_start_date" if type == "lettings" @edit_end_date = DEADLINES.dig(start_year, :edit_end_date) || DEADLINES[:default][:edit_end_date].call(start_year) else raise "No form definition file exists for given year".freeze unless File.exist?(form_path) @setup_sections = [Form::Lettings::Sections::Setup.new(nil, nil, self)] @form_definition = JSON.parse(File.open(form_path).read) @form_sections = form_definition["sections"].map { |id, s| Form::Section.new(id, s, self) } @type = form_definition["form_type"] @sections = setup_sections + form_sections @subsections = sections.flat_map(&:subsections) @pages = subsections.flat_map(&:pages) @questions = pages.flat_map(&:questions) @start_date = Time.iso8601(form_definition["start_date"]) @new_logs_end_date = Time.zone.local(@start_date.year + 1, 11, 20) @submission_deadline = Time.zone.local(@start_date.year + 1, 6, 9) @edit_end_date = Time.zone.local(@start_date.year + 1, 11, 20) @unresolved_log_redirect_page_id = form_definition["unresolved_log_redirect_page_id"] end @name = "#{start_date.year}_#{new_logs_end_date.year}_#{type}" end def get_subsection(id) subsections.find { |s| s.id == id.to_s.underscore } end def get_page(id) pages.find { |p| p.id == id.to_s.underscore } end def get_question(id, log, current_user = nil) all_questions = questions.select { |q| q.id == id.to_s.underscore } routed_question = all_questions.find { |q| q.page.routed_to?(log, current_user) } if log routed_question || all_questions[0] end def subsection_for_page(page) subsections.find { |s| s.pages.find { |p| p.id == page.id } } end def next_page_id(page, log, current_user, ignore_answered: false) return page.next_unresolved_page_id || :check_answers if log.unresolved page_ids = subsection_for_page(page).pages.map(&:id) page_index = page_ids.index(page.id) page_id = if page.interruption_screen? && log[page.questions[0].id] == 1 && page.routed_to?(log, current_user) previous_page_id(page, log, current_user) else page_ids[page_index + 1] end next_page = get_page(page_id) return :check_answers if next_page.nil? return next_page.id if next_page.routed_to?(log, current_user) && (!ignore_answered || next_page.has_unanswered_questions?(log)) next_page_id(next_page, log, current_user, ignore_answered:) end def next_page_redirect_path(page, log, current_user, ignore_answered: false) next_page_id = next_page_id(page, log, current_user, ignore_answered:) if next_page_id == :check_answers "#{type}_log_#{subsection_for_page(page).id}_check_answers_path" else "#{type}_log_#{next_page_id}_path" end end def previous_page_id(page, log, current_user) page_ids = subsection_for_page(page).pages.map(&:id) page_index = page_ids.index(page.id) return :tasklist if page_index.zero? page_id = page_ids[page_index - 1] previous_page = get_page(page_id) return previous_page.id if previous_page.routed_to?(log, current_user) previous_page_id(previous_page, log, current_user) end def previous_page_redirect_path(page, log, current_user, referrer) previous_page_id = previous_page_id(page, log, current_user) if referrer == "check_answers" "#{type}_log_#{subsection_for_page(page).id}_check_answers_path" elsif previous_page_id == :tasklist "#{type}_log_path" else "#{type}_log_#{previous_page_id}_path" end end def cancel_path(page, log) "#{log.class.name.underscore}_#{page.subsection.id}_check_answers_path" end def unresolved_log_path "#{type}_log_#{unresolved_log_redirect_page_id}_path" end def next_incomplete_section_redirect_path(subsection, log) subsection_ids = subsections.map(&:id) if log.status == "completed" || log.calculate_status == "completed" # if a log's status in in progress but then fields are made optional, all its subsections are complete, resulting in a stack error return first_question_in_last_subsection(subsection_ids) end next_subsection = next_subsection(subsection, log, subsection_ids) case next_subsection.status(log) when :completed next_incomplete_section_redirect_path(next_subsection, log) when :in_progress "#{next_subsection.id}/check_answers".dasherize when :not_started first_question_in_subsection = next_subsection.pages.find { |page| page.routed_to?(log, nil) } first_question_in_subsection ? first_question_in_subsection.id.to_s.dasherize : next_incomplete_section_redirect_path(next_subsection, log) else "error" end end def first_question_in_last_subsection(subsection_ids) next_subsection = get_subsection(subsection_ids[subsection_ids.length - 1]) first_question_in_subsection = next_subsection.pages.first.id first_question_in_subsection.to_s.dasherize end def next_subsection(subsection, log, subsection_ids) next_subsection_id_index = subsection_ids.index(subsection.id) + 1 next_subsection = get_subsection(subsection_ids[next_subsection_id_index]) if subsection_ids[subsection_ids.length - 1] == subsection.id && log.status != "completed" next_subsection = get_subsection(subsection_ids[0]) end next_subsection end def all_subsections_except_declaration_completed?(log) subsection_ids = subsections.map(&:id) subsection_ids.delete_at(subsection_ids.length - 1) return true if subsection_ids.all? { |subsection_id| get_subsection(subsection_id).status(log) == :completed } false end def conditional_question_conditions conditions = questions.map { |q| Hash(q.id => q.conditional_for) if q.conditional_for.present? }.compact conditions.map { |c| c.map { |k, v| v.keys.map { |key| Hash(from: k, to: key, cond: v[key]) } } }.flatten end def invalidated_pages(log, current_user = nil) pages.reject { |p| p.routed_to?(log, current_user) } end def reset_not_routed_questions_and_invalid_answers(log) reset_checkbox_questions_if_not_routed(log) reset_radio_questions_if_not_routed_or_invalid_answers(log) reset_free_user_input_questions_if_not_routed(log) end def reset_checkbox_questions_if_not_routed(log) checkbox_questions = routed_and_not_routed_questions_by_type(log, type: "checkbox") clear_checkbox_attributes(log, checkbox_questions[:routed], checkbox_questions[:not_routed]) checkbox_questions_recalculated = routed_and_not_routed_questions_by_type(log, type: "checkbox") newly_not_routed_checkbox_questions = checkbox_questions_recalculated[:not_routed].reject { |question| checkbox_questions[:not_routed].include?(question) } clear_checkbox_attributes(log, checkbox_questions_recalculated[:routed], newly_not_routed_checkbox_questions) end def reset_radio_questions_if_not_routed_or_invalid_answers(log) radio_questions = routed_and_not_routed_questions_by_type(log, type: "radio") clear_radio_attributes(log, radio_questions[:routed], radio_questions[:not_routed]) radio_questions_recalculated = routed_and_not_routed_questions_by_type(log, type: "radio") newly_not_routed_radio_questions = radio_questions_recalculated[:not_routed].reject { |question| radio_questions[:not_routed].include?(question) } clear_radio_attributes(log, radio_questions_recalculated[:routed], newly_not_routed_radio_questions) end def reset_free_user_input_questions_if_not_routed(log) non_radio_or_checkbox_questions = routed_and_not_routed_questions_by_type(log) clear_free_user_input_attributes(log, non_radio_or_checkbox_questions[:routed], non_radio_or_checkbox_questions[:not_routed]) non_radio_or_checkbox_questions_recalculated = routed_and_not_routed_questions_by_type(log) newly_not_routed_non_radio_or_checkbox_questions = non_radio_or_checkbox_questions_recalculated[:not_routed].reject { |question| non_radio_or_checkbox_questions[:not_routed].include?(question) } clear_free_user_input_attributes(log, non_radio_or_checkbox_questions_recalculated[:routed], newly_not_routed_non_radio_or_checkbox_questions) end def clear_checkbox_attributes(log, routed_questions, not_routed_questions) not_routed_questions.each do |not_routed_question| valid_options = routed_questions .select { |q| q.id == not_routed_question.id } .flat_map { |q| q.answer_options.keys } not_routed_question.answer_options.each_key do |invalid_option| clear_attribute(log, invalid_option) if log.respond_to?(invalid_option) && valid_options.exclude?(invalid_option) && log.public_send(invalid_option).present? end end end def clear_radio_attributes(log, routed_questions, not_routed_questions) valid_radio_options = routed_questions .group_by(&:id) .transform_values! { |q_array| q_array.flat_map { |q| q.answer_options.keys } } not_routed_questions.each do |not_routed_question| question_id = not_routed_question.id clear_attribute(log, question_id) if log.respond_to?(question_id) && log.public_send(question_id).present? && !valid_radio_options.key?(question_id) end valid_radio_options.each do |question_id, valid_options| clear_attribute(log, question_id) if log.respond_to?(question_id) && valid_options.exclude?(log.public_send(question_id).to_s) end end def clear_free_user_input_attributes(log, routed_questions, not_routed_questions) enabled_question_ids = routed_questions.map(&:id) not_routed_questions.each do |not_routed_question| question_id = not_routed_question.id clear_attribute(log, question_id) if log.public_send(question_id).present? && enabled_question_ids.exclude?(question_id) end end def routed_and_not_routed_questions_by_type(log, type: nil, current_user: nil) questions_by_type = if type questions.reject { |q| q.type != type || q.disable_clearing_if_not_routed_or_dynamic_answer_options } else questions.reject { |q| %w[radio checkbox].include?(q.type) || q.disable_clearing_if_not_routed_or_dynamic_answer_options } end routed, not_routed = questions_by_type.partition { |q| q.page.routed_to?(log, current_user) || q.derived?(log) } { routed:, not_routed: } end def clear_attribute(log, attribute) Rails.logger.debug("Cleared #{attribute} value") log.public_send("#{attribute}=", nil) end def readonly_questions questions.select(&:read_only?) end def numeric_questions questions.select { |q| q.type == "numeric" } end def previous_page(page_ids, page_index, log, current_user) prev_page = get_page(page_ids[page_index - 1]) return prev_page.id if prev_page.routed_to?(log, current_user) previous_page(page_ids, page_index - 1, log, current_user) end def send_chain(arr, log) Array(arr).inject(log) { |o, a| o.public_send(*a) } end def depends_on_met(depends_on, log) return true unless depends_on depends_on.any? do |conditions_set| return false unless conditions_set conditions_set.all? do |question, value| if value.is_a?(Hash) && value.key?("operator") operator = value["operator"] operand = value["operand"] log[question]&.send(operator, operand) else parts = question.split(".") log_value = send_chain(parts, log) value.nil? ? log_value == value : !log_value.nil? && log_value == value end end end end def inspect "#<#{self.class} @type=#{type} @name=#{name}>" end def valid_start_date_for_form?(start_date) start_date >= self.start_date && start_date <= new_logs_end_date end def sales_or_start_year_after_2022?(type, start_year) type == "sales" || (start_year && start_year.to_i > 2022) end def start_year_after_2024? start_date && start_date.year >= 2024 end end