From 5ece7f98e67970edfc0538c9297e0b299293ce44 Mon Sep 17 00:00:00 2001 From: Kat Date: Thu, 25 Aug 2022 15:55:48 +0100 Subject: [PATCH] Add abstract log class and sales log class Created a parent log class for sales log and lettings log. Any bits common to both sales and lettings can live in the parent class. As the sales log functionality is built up any commonalities with lettings log can be extracted into the parent log class. The sales log model is set up without a json form and instead the form is defined in code - like the setup section of the lettings log. --- app/controllers/form_controller.rb | 95 ++++++++++--------- app/controllers/lettings_logs_controller.rb | 60 ++---------- app/controllers/logs_controller.rb | 64 +++++++++++++ app/controllers/sales_logs_controller.rb | 28 ++++++ app/helpers/tasklist_helper.rb | 10 +- app/models/form.rb | 66 ++++++++----- .../form/sales/setup/pages/sale_date.rb | 15 +++ .../form/sales/setup/questions/sale_date.rb | 10 ++ app/models/form/sales/setup/sections/setup.rb | 10 ++ .../form/sales/setup/subsections/setup.rb | 14 +++ app/models/form_handler.rb | 15 ++- app/models/lettings_log.rb | 5 +- app/models/log.rb | 7 ++ app/models/sales_log.rb | 72 ++++++++++++++ app/models/user.rb | 10 ++ .../form/_check_answers_summary_list.html.erb | 12 +-- app/views/form/_checkbox_question.html.erb | 4 +- app/views/form/_numeric_question.html.erb | 2 +- app/views/form/_radio_question.html.erb | 2 +- app/views/form/_select_question.html.erb | 6 +- app/views/form/check_answers.html.erb | 22 ++--- app/views/form/page.html.erb | 12 +-- app/views/form/review.html.erb | 8 +- app/views/layouts/application.html.erb | 2 +- app/views/lettings_logs/index.html.erb | 1 + app/views/sales_logs/_tasklist.html.erb | 25 +++++ app/views/sales_logs/edit.html.erb | 37 ++++++++ config/routes.rb | 16 +++- db/migrate/20220826093411_add_sales_log.rb | 12 +++ db/schema.rb | 16 ++++ spec/factories/sales_log.rb | 12 +++ spec/features/form/validations_spec.rb | 2 +- spec/features/log_spec.rb | 34 ++++++- .../form/sales/setup/pages/sale_date_spec.rb | 29 ++++++ .../sales/setup/questions/sale_date_spec.rb | 33 +++++++ .../form/sales/setup/sections/setup_spec.rb | 29 ++++++ .../sales/setup/subsections/setup_spec.rb | 27 ++++++ spec/models/form_handler_spec.rb | 5 + spec/models/form_spec.rb | 21 ++++ spec/models/lettings_log_spec.rb | 5 + spec/models/log_spec.rb | 8 ++ spec/models/sales_log_spec.rb | 85 +++++++++++++++++ spec/requests/sales_logs_controller_spec.rb | 87 +++++++++++++++++ spec/views/form/page_view_spec.rb | 2 +- 44 files changed, 867 insertions(+), 170 deletions(-) create mode 100644 app/controllers/logs_controller.rb create mode 100644 app/controllers/sales_logs_controller.rb create mode 100644 app/models/form/sales/setup/pages/sale_date.rb create mode 100644 app/models/form/sales/setup/questions/sale_date.rb create mode 100644 app/models/form/sales/setup/sections/setup.rb create mode 100644 app/models/form/sales/setup/subsections/setup.rb create mode 100644 app/models/log.rb create mode 100644 app/models/sales_log.rb create mode 100644 app/views/sales_logs/_tasklist.html.erb create mode 100644 app/views/sales_logs/edit.html.erb create mode 100644 db/migrate/20220826093411_add_sales_log.rb create mode 100644 spec/factories/sales_log.rb create mode 100644 spec/models/form/sales/setup/pages/sale_date_spec.rb create mode 100644 spec/models/form/sales/setup/questions/sale_date_spec.rb create mode 100644 spec/models/form/sales/setup/sections/setup_spec.rb create mode 100644 spec/models/form/sales/setup/subsections/setup_spec.rb create mode 100644 spec/models/log_spec.rb create mode 100644 spec/models/sales_log_spec.rb create mode 100644 spec/requests/sales_logs_controller_spec.rb diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index a7ee469b8..4da903999 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -4,22 +4,22 @@ class FormController < ApplicationController before_action :find_resource_by_named_id, except: %i[submit_form review] def submit_form - if @lettings_log - @page = @lettings_log.form.get_page(params[:lettings_log][:page]) + if @log + @page = @log.form.get_page(params[@log.model_name.param_key][:page]) responses_for_page = responses_for_page(@page) mandatory_questions_with_no_response = mandatory_questions_with_no_response(responses_for_page) - if mandatory_questions_with_no_response.empty? && @lettings_log.update(responses_for_page) + if mandatory_questions_with_no_response.empty? && @log.update(responses_for_page) session[:errors] = session[:fields] = nil redirect_to(successful_redirect_path) else - redirect_path = "lettings_log_#{@page.id}_path" + redirect_path = "#{@log.class.name.underscore}_#{@page.id}_path" mandatory_questions_with_no_response.map do |question| - @lettings_log.errors.add question.id.to_sym, question.unanswered_error_message + @log.errors.add question.id.to_sym, question.unanswered_error_message end - session[:errors] = @lettings_log.errors.to_json - Rails.logger.info "User triggered validation(s) on: #{@lettings_log.errors.map(&:attribute).join(', ')}" - redirect_to(send(redirect_path, @lettings_log)) + session[:errors] = @log.errors.to_json + Rails.logger.info "User triggered validation(s) on: #{@log.errors.map(&:attribute).join(', ')}" + redirect_to(send(redirect_path, @log)) end else render_not_found @@ -27,9 +27,9 @@ class FormController < ApplicationController end def check_answers - if @lettings_log + if @log current_url = request.env["PATH_INFO"] - subsection = @lettings_log.form.get_subsection(current_url.split("/")[-2]) + subsection = @log.form.get_subsection(current_url.split("/")[-2]) render "form/check_answers", locals: { subsection:, current_user: } else render_not_found @@ -37,29 +37,26 @@ class FormController < ApplicationController end def review - if @lettings_log + if @log render "form/review" else render_not_found end end - FormHandler.instance.forms.each do |_key, form| - form.pages.map do |page| - define_method(page.id) do |_errors = {}| - if @lettings_log - restore_error_field_values - @subsection = @lettings_log.form.subsection_for_page(page) - @page = @lettings_log.form.get_page(page.id) - if @page.routed_to?(@lettings_log, current_user) - render "form/page" - else - redirect_to lettings_log_path(@lettings_log) - end - else - render_not_found - end + def show_page + if @log + restore_error_field_values + page_id = request.path.split("/")[-1].underscore + @page = @log.form.get_page(page_id) + @subsection = @log.form.subsection_for_page(@page) + if @page.routed_to?(@log, current_user) + render "form/page" + else + redirect_to lettings_log_path(@log) end + else + render_not_found end end @@ -68,13 +65,13 @@ private def restore_error_field_values if session["errors"] JSON(session["errors"]).each do |field, messages| - messages.each { |message| @lettings_log.errors.add field.to_sym, message } + messages.each { |message| @log.errors.add field.to_sym, message } end end if session["fields"] session["fields"].each do |field, value| - unless @lettings_log.form.get_question(field, @lettings_log)&.type == "date" - @lettings_log[field] = value + unless @log.form.get_question(field, @log)&.type == "date" + @log[field] = value end end end @@ -82,11 +79,11 @@ private def responses_for_page(page) page.questions.each_with_object({}) do |question, result| - question_params = params["lettings_log"][question.id] + question_params = params[@log.class.name.underscore][question.id] if question.type == "date" - day = params["lettings_log"]["#{question.id}(3i)"] - month = params["lettings_log"]["#{question.id}(2i)"] - year = params["lettings_log"]["#{question.id}(1i)"] + day = params[@log.class.name.underscore]["#{question.id}(3i)"] + month = params[@log.class.name.underscore]["#{question.id}(2i)"] + year = params[@log.class.name.underscore]["#{question.id}(1i)"] next unless [day, month, year].any?(&:present?) result[question.id] = if Date.valid_date?(year.to_i, month.to_i, day.to_i) && year.to_i.between?(2000, 2200) @@ -109,11 +106,19 @@ private end def find_resource - @lettings_log = current_user.lettings_logs.find_by(id: params[:id]) + @log = if params.key?("sales_log") + current_user.sales_logs.find_by(id: params[:id]) + else + current_user.lettings_logs.find_by(id: params[:id]) + end end def find_resource_by_named_id - @lettings_log = current_user.lettings_logs.find_by(id: params[:lettings_log_id]) + @log = if params["sales_log_id"].present? + current_user.sales_logs.find_by(id: params[:sales_log_id]) + else + current_user.lettings_logs.find_by(id: params[:lettings_log_id]) + end end def is_referrer_check_answers? @@ -123,18 +128,18 @@ private def successful_redirect_path if is_referrer_check_answers? - page_ids = @lettings_log.form.subsection_for_page(@page).pages.map(&:id) + page_ids = @log.form.subsection_for_page(@page).pages.map(&:id) page_index = page_ids.index(@page.id) - next_page = @lettings_log.form.next_page(@page, @lettings_log, current_user) - previous_page = @lettings_log.form.previous_page(page_ids, page_index, @lettings_log, current_user) + next_page = @log.form.next_page(@page, @log, current_user) + previous_page = @log.form.previous_page(page_ids, page_index, @log, current_user) if next_page.to_s.include?("value_check") || next_page == previous_page - return "/logs/#{@lettings_log.id}/#{next_page.dasherize}?referrer=check_answers" + return "/logs/#{@log.id}/#{next_page.dasherize}?referrer=check_answers" else - return send("lettings_log_#{@lettings_log.form.subsection_for_page(@page).id}_check_answers_path", @lettings_log) + return send("#{@log.class.name.underscore}_#{@log.form.subsection_for_page(@page).id}_check_answers_path", @log) end end - redirect_path = @lettings_log.form.next_page_redirect_path(@page, @lettings_log, current_user) - send(redirect_path, @lettings_log) + redirect_path = @log.form.next_page_redirect_path(@page, @log, current_user) + send(redirect_path, @log) end def mandatory_questions_with_no_response(responses_for_page) @@ -153,7 +158,7 @@ private def required_questions @required_questions ||= begin - log = @lettings_log + log = @log log.assign_attributes(responses_for_page(@page)) @page.subsection.applicable_questions(log).select { |q| q.enabled?(log) }.map(&:id) end @@ -162,12 +167,12 @@ private def question_missing_response?(responses_for_page, question) if %w[checkbox validation_override].include?(question.type) answered = question.answer_options.keys.reject { |x| x.match(/divider/) }.map do |option| - session["fields"][option] = @lettings_log[option] = params["lettings_log"][question.id].include?(option) ? 1 : 0 + session["fields"][option] = @log[option] = params["lettings_log"][question.id].include?(option) ? 1 : 0 params["lettings_log"][question.id].exclude?(option) end answered.all? else - session["fields"][question.id] = @lettings_log[question.id] = responses_for_page[question.id] + session["fields"][question.id] = @log[question.id] = responses_for_page[question.id] responses_for_page[question.id].nil? || responses_for_page[question.id].blank? end end diff --git a/app/controllers/lettings_logs_controller.rb b/app/controllers/lettings_logs_controller.rb index a19fdbe6d..641a71214 100644 --- a/app/controllers/lettings_logs_controller.rb +++ b/app/controllers/lettings_logs_controller.rb @@ -1,10 +1,8 @@ -class LettingsLogsController < ApplicationController +class LettingsLogsController < LogsController include Pagy::Backend include Modules::LettingsLogsFilter include Modules::SearchFilter - skip_before_action :verify_authenticity_token, if: :json_api_request? - before_action :authenticate, if: :json_api_request? before_action :authenticate_user!, unless: :json_api_request? before_action :find_resource, except: %i[create index edit] @@ -28,25 +26,12 @@ class LettingsLogsController < ApplicationController end def create - lettings_log = LettingsLog.new(lettings_log_params) - respond_to do |format| - format.html do - lettings_log.save! - redirect_to lettings_log_url(lettings_log) - end - format.json do - if lettings_log.save - render json: lettings_log, status: :created - else - render json: { errors: lettings_log.errors.messages }, status: :unprocessable_entity - end - end - end + super { LettingsLog.new(log_params) } end def update if @lettings_log - if @lettings_log.update(api_lettings_log_params) + if @lettings_log.update(api_log_params) render json: @lettings_log, status: :ok else render json: { errors: @lettings_log.errors.messages }, status: :unprocessable_entity @@ -99,40 +84,15 @@ private params["search"] end - def json_api_request? - API_ACTIONS.include?(request["action"]) && request.format.json? - end - - def authenticate - http_basic_authenticate_or_request_with name: ENV["API_USER"], password: ENV["API_KEY"] - end - - def lettings_log_params - if current_user && !current_user.support? - org_params.merge(api_lettings_log_params) - else - api_lettings_log_params - end - end - - def org_params - { - "owning_organisation_id" => current_user.organisation.id, - "managing_organisation_id" => current_user.organisation.id, - "created_by_id" => current_user.id, - } - end - - def api_lettings_log_params - return {} unless params[:lettings_log] - - permitted = params.require(:lettings_log).permit(LettingsLog.editable_fields) - owning_id = permitted["owning_organisation_id"] - permitted["owning_organisation"] = Organisation.find(owning_id) if owning_id - permitted - end + def permitted_log_params + params.require(:lettings_log).permit(LettingsLog.editable_fields) + end def find_resource @lettings_log = LettingsLog.find_by(id: params[:id]) end + + def post_create_redirect_url(log) + lettings_log_url(log) + end end diff --git a/app/controllers/logs_controller.rb b/app/controllers/logs_controller.rb new file mode 100644 index 000000000..fb274993b --- /dev/null +++ b/app/controllers/logs_controller.rb @@ -0,0 +1,64 @@ +class LogsController < ApplicationController + skip_before_action :verify_authenticity_token, if: :json_api_request? + before_action :authenticate, if: :json_api_request? + +private + + def create + log = yield + raise "Caller must pass a block that implements model creation" if log.blank? + + respond_to do |format| + format.html do + log.save! + redirect_to post_create_redirect_url(log) + end + format.json do + if log.save + render json: log, status: :created + else + render json: { errors: log.errors.messages }, status: :unprocessable_entity + end + end + end + end + + def post_create_redirect_url + raise "implement in sub class" + end + + API_ACTIONS = %w[create show update destroy].freeze + + def json_api_request? + API_ACTIONS.include?(request["action"]) && request.format.json? + end + + def authenticate + http_basic_authenticate_or_request_with name: ENV["API_USER"], password: ENV["API_KEY"] + end + + def log_params + if current_user && !current_user.support? + org_params.merge(api_log_params) + else + api_log_params + end + end + + def api_log_params + return {} unless params[:lettings_log] || params[:sales_log] + + permitted = permitted_log_params + owning_id = permitted["owning_organisation_id"] + permitted["owning_organisation"] = Organisation.find(owning_id) if owning_id + permitted + end + + def org_params + { + "owning_organisation_id" => current_user.organisation.id, + "managing_organisation_id" => current_user.organisation.id, + "created_by_id" => current_user.id, + } + end +end diff --git a/app/controllers/sales_logs_controller.rb b/app/controllers/sales_logs_controller.rb new file mode 100644 index 000000000..84cf0d275 --- /dev/null +++ b/app/controllers/sales_logs_controller.rb @@ -0,0 +1,28 @@ +class SalesLogsController < LogsController + def create + super { SalesLog.new(log_params) } + end + + def show + respond_to do |format| + format.html { edit } + end + end + + def edit + @sales_log = current_user.sales_logs.find_by(id: params[:id]) + if @sales_log + render :edit, locals: { current_user: } + else + render_not_found + end + end + + def post_create_redirect_url(log) + sales_log_url(log) + end + + def permitted_log_params + params.require(:sales_log).permit(SalesLog.editable_fields) + end +end diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index e4308d97e..4256979e0 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -11,13 +11,13 @@ module TasklistHelper lettings_log.form.subsections.count { |subsection| subsection.status(lettings_log) == status && subsection.applicable_questions(lettings_log).count.positive? } end - def next_page_or_check_answers(subsection, lettings_log, current_user) - path = if subsection.is_started?(lettings_log) - "lettings_log_#{subsection.id}_check_answers_path" + def next_page_or_check_answers(subsection, log, current_user) + path = if subsection.is_started?(log) + "#{log.class.name.underscore}_#{subsection.id}_check_answers_path" else - "lettings_log_#{next_question_page(subsection, lettings_log, current_user)}_path" + "#{log.class.name.underscore}_#{next_question_page(subsection, log, current_user)}_path" end - send(path, lettings_log) + send(path, log) end def next_question_page(subsection, lettings_log, current_user) diff --git a/app/models/form.rb b/app/models/form.rb index 6fa92bba0..6e683ed5a 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -5,20 +5,38 @@ class Form include Form::Setup - def initialize(form_path, name) - raise "No form definition file exists for given year".freeze unless File.exist?(form_path) - - @name = name - @setup_sections = [Form::Setup::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"]) - @end_date = Time.iso8601(form_definition["end_date"]) + def initialize(form_path, name, sections_in_form = [], type = "lettings") + if type == "sales" + # @form_definition = sections.map do |s| + # s.new(nil, nil, self) + # end + @name = name + # @setup_sections = [Form::Setup::Sections::Setup.new(nil, nil, self)] + # @form_definition = JSON.parse(File.open(form_path).read) + @form_sections = sections_in_form.map { |sec| sec.new(nil, nil, self) } + @type = "sales" + @sections = form_sections + @subsections = sections.flat_map(&:subsections) + @pages = subsections.flat_map(&:pages) + @questions = pages.flat_map(&:questions) + @start_date = Time.zone.local(name[0..3], 4, 1) + @end_date = Time.zone.local(start_date.year + 1, 4, 1) + + else + raise "No form definition file exists for given year".freeze unless File.exist?(form_path) + + @name = name + @setup_sections = [Form::Setup::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"]) + @end_date = Time.iso8601(form_definition["end_date"]) + end end def get_subsection(id) @@ -58,28 +76,28 @@ class Form def next_page_redirect_path(page, lettings_log, current_user) nxt_page = next_page(page, lettings_log, current_user) if nxt_page == :check_answers - "lettings_log_#{subsection_for_page(page).id}_check_answers_path" + "#{type}_log_#{subsection_for_page(page).id}_check_answers_path" else - "lettings_log_#{nxt_page}_path" + "#{type}_log_#{nxt_page}_path" end end - def next_incomplete_section_redirect_path(subsection, lettings_log) + def next_incomplete_section_redirect_path(subsection, log) subsection_ids = subsections.map(&:id) - if lettings_log.status == "completed" + if log.status == "completed" return first_question_in_last_subsection(subsection_ids) end - next_subsection = next_subsection(subsection, lettings_log, subsection_ids) + next_subsection = next_subsection(subsection, log, subsection_ids) - case next_subsection.status(lettings_log) + case next_subsection.status(log) when :completed - next_incomplete_section_redirect_path(next_subsection, lettings_log) + 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?(lettings_log, nil) }.id + first_question_in_subsection = next_subsection.pages.find { |page| page.routed_to?(log, nil) }.id first_question_in_subsection.to_s.dasherize else "error" @@ -92,11 +110,11 @@ class Form first_question_in_subsection.to_s.dasherize end - def next_subsection(subsection, lettings_log, subsection_ids) + 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 && lettings_log.status != "completed" + if subsection_ids[subsection_ids.length - 1] == subsection.id && log.status != "completed" next_subsection = get_subsection(subsection_ids[0]) end diff --git a/app/models/form/sales/setup/pages/sale_date.rb b/app/models/form/sales/setup/pages/sale_date.rb new file mode 100644 index 000000000..4cdfe5a71 --- /dev/null +++ b/app/models/form/sales/setup/pages/sale_date.rb @@ -0,0 +1,15 @@ +class Form::Sales::Setup::Pages::SaleDate < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "sale_date" + @header = "" + @description = "" + @subsection = subsection + end + + def questions + @questions ||= [ + Form::Sales::Setup::Questions::SaleDate.new(nil, nil, self), + ] + end +end diff --git a/app/models/form/sales/setup/questions/sale_date.rb b/app/models/form/sales/setup/questions/sale_date.rb new file mode 100644 index 000000000..4f9088aaf --- /dev/null +++ b/app/models/form/sales/setup/questions/sale_date.rb @@ -0,0 +1,10 @@ +class Form::Sales::Setup::Questions::SaleDate < ::Form::Question + def initialize(id, hsh, page) + super + @id = "saledate" + @check_answer_label = "Sale completion date" + @header = "What is the sale completion date?" + @type = "date" + @page = page + end +end diff --git a/app/models/form/sales/setup/sections/setup.rb b/app/models/form/sales/setup/sections/setup.rb new file mode 100644 index 000000000..42835ec39 --- /dev/null +++ b/app/models/form/sales/setup/sections/setup.rb @@ -0,0 +1,10 @@ +class Form::Sales::Setup::Sections::Setup < ::Form::Section + def initialize(id, hsh, form) + super + @id = "setup" + @label = "Before you start" + @description = "" + @form = form + @subsections = [Form::Sales::Setup::Subsections::Setup.new(nil, nil, self)] || [] + end +end diff --git a/app/models/form/sales/setup/subsections/setup.rb b/app/models/form/sales/setup/subsections/setup.rb new file mode 100644 index 000000000..ee0af6350 --- /dev/null +++ b/app/models/form/sales/setup/subsections/setup.rb @@ -0,0 +1,14 @@ +class Form::Sales::Setup::Subsections::Setup < ::Form::Subsection + def initialize(id, hsh, section) + super + @id = "setup" + @label = "Set up this sales log" + @section = section + end + + def pages + @pages ||= [ + Form::Sales::Setup::Pages::SaleDate.new(nil, nil, self), + ] + end +end diff --git a/app/models/form_handler.rb b/app/models/form_handler.rb index 13efcb9ca..41414eb7d 100644 --- a/app/models/form_handler.rb +++ b/app/models/form_handler.rb @@ -14,9 +14,12 @@ class FormHandler forms[forms.keys.max_by(&:to_i)] end -private + def sales_forms + sales_sections = [Form::Sales::Setup::Sections::Setup] + { "2022_2023_sales" => Form.new(nil, "2022_2023_sales", sales_sections, "sales") } + end - def get_all_forms + def lettings_forms forms = {} directories.each do |directory| Dir.glob("#{directory}/*.json").each do |form_path| @@ -27,7 +30,15 @@ private forms end +private + + def get_all_forms + lettings_forms.merge(sales_forms) + end + def directories Rails.env.test? ? ["spec/fixtures/forms"] : ["config/forms"] end + + # SALES_FORM = [Form::Sales::Sections::Setup, Form::Sales::Sections::Property] end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 142c48e52..81c69f1f5 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -14,7 +14,7 @@ class LettingsLogValidator < ActiveModel::Validator end end -class LettingsLog < ApplicationRecord +class LettingsLog < Log include Validations::SoftValidations include DerivedVariables::LettingsLogVariables @@ -31,9 +31,6 @@ class LettingsLog < ApplicationRecord before_validation :set_derived_fields! before_save :update_status! - belongs_to :owning_organisation, class_name: "Organisation", optional: true - belongs_to :managing_organisation, class_name: "Organisation", optional: true - belongs_to :created_by, class_name: "User", optional: true belongs_to :scheme, optional: true belongs_to :location, optional: true diff --git a/app/models/log.rb b/app/models/log.rb new file mode 100644 index 000000000..316af6d06 --- /dev/null +++ b/app/models/log.rb @@ -0,0 +1,7 @@ +class Log < ApplicationRecord + self.abstract_class = true + + belongs_to :owning_organisation, class_name: "Organisation", optional: true + belongs_to :managing_organisation, class_name: "Organisation", optional: true + belongs_to :created_by, class_name: "User", optional: true +end diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb new file mode 100644 index 000000000..cdeaf0695 --- /dev/null +++ b/app/models/sales_log.rb @@ -0,0 +1,72 @@ +class SalesLogValidator < ActiveModel::Validator + def validate(record); end +end + +class SalesLog < Log + has_paper_trail + + validates_with SalesLogValidator + before_save :update_status! + + scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org).or(where(managing_organisation: org)) } + + STATUS = { "not_started" => 0, "in_progress" => 1, "completed" => 2 }.freeze + enum status: STATUS + + def self.editable_fields + attribute_names + end + + def form_name + return unless saledate + + "#{collection_start_year}_#{collection_start_year + 1}_sales" + end + + def collection_start_year + return @sale_year if @sale_year + return unless saledate + + window_end_date = Time.zone.local(saledate.year, 4, 1) + @sale_year = saledate < window_end_date ? saledate.year - 1 : saledate.year + end + + def form + FormHandler.instance.get_form(form_name) || FormHandler.instance.get_form("2022_2023_sales") + end + + def optional_fields + [] + end + + def not_started? + status == "not_started" + end + + def completed? + status == "completed" + end + + private + + def update_status! + self.status = if all_fields_completed? && errors.empty? + "completed" + elsif all_fields_nil? + "not_started" + else + "in_progress" + end + end + + def all_fields_completed? + subsection_statuses = form.subsections.map { |subsection| subsection.status(self) }.uniq + subsection_statuses == [:completed] + end + + def all_fields_nil? + not_started_statuses = %i[not_started cannot_start_yet] + subsection_statuses = form.subsections.map { |subsection| subsection.status(self) }.uniq + subsection_statuses.all? { |status| not_started_statuses.include?(status) } + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 22cc14b77..4384bc495 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,8 @@ class User < ApplicationRecord belongs_to :organisation, optional: true has_many :owned_lettings_logs, through: :organisation, dependent: :delete_all has_many :managed_lettings_logs, through: :organisation + has_many :owned_sales_logs, through: :organisation, dependent: :delete_all + has_many :managed_sales_logs, through: :organisation validates :name, presence: true validates :email, presence: true @@ -58,6 +60,14 @@ class User < ApplicationRecord end end + def sales_logs + if support? + SalesLog.all + else + SalesLog.filter_by_organisation(organisation) + end + end + def completed_lettings_logs lettings_logs.completed end diff --git a/app/views/form/_check_answers_summary_list.html.erb b/app/views/form/_check_answers_summary_list.html.erb index 85c29beca..68a9cf00a 100644 --- a/app/views/form/_check_answers_summary_list.html.erb +++ b/app/views/form/_check_answers_summary_list.html.erb @@ -1,21 +1,21 @@ <%= govuk_summary_list do |summary_list| %> - <% total_applicable_questions(subsection, @lettings_log, current_user).each do |question| %> + <% total_applicable_questions(subsection, @log, current_user).each do |question| %> <% summary_list.row do |row| %> <% row.key { question.check_answer_label.to_s.presence || question.header.to_s } %> <% row.value do %> - <%= get_answer_label(question, @lettings_log) %> - <% extra_value = question.get_extra_check_answer_value(@lettings_log) %> + <%= get_answer_label(question, @log) %> + <% extra_value = question.get_extra_check_answer_value(@log) %> <% if extra_value %> <%= extra_value %> <% end %>
- <% question.get_inferred_answers(@lettings_log).each do |inferred_answer| %> + <% question.get_inferred_answers(@log).each do |inferred_answer| %> <%= inferred_answer %> <% end %> <% end %> <% row.action( - text: question.action_text(@lettings_log), - href: question.action_href(@lettings_log, question.page.id), + text: question.action_text(@log), + href: question.action_href(@log, question.page.id), visually_hidden_text: question.check_answer_label.to_s.downcase, ) %> <% end %> diff --git a/app/views/form/_checkbox_question.html.erb b/app/views/form/_checkbox_question.html.erb index 245056c39..c855e2d88 100644 --- a/app/views/form/_checkbox_question.html.erb +++ b/app/views/form/_checkbox_question.html.erb @@ -6,7 +6,7 @@ hint: { text: question.hint_text&.html_safe } do %> <% after_divider = false %> - <% question.displayed_answer_options(@lettings_log).map do |key, options| %> + <% question.displayed_answer_options(@log).map do |key, options| %> <% if key.starts_with?("divider") %> <% after_divider = true %> <%= f.govuk_check_box_divider %> @@ -14,7 +14,7 @@ <%= f.govuk_check_box question.id, key, label: { text: options["value"] }, hint: { text: options["hint"] }, - checked: @lettings_log[key] == 1, + checked: @log[key] == 1, exclusive: after_divider, **stimulus_html_attributes(question) %> <% end %> diff --git a/app/views/form/_numeric_question.html.erb b/app/views/form/_numeric_question.html.erb index fc6aa274a..0aeb63801 100644 --- a/app/views/form/_numeric_question.html.erb +++ b/app/views/form/_numeric_question.html.erb @@ -8,7 +8,7 @@ width: question.width, readonly: question.read_only?, prefix_text: question.prefix.to_s, - suffix_text: question.suffix_label(@lettings_log), + suffix_text: question.suffix_label(@log), **stimulus_html_attributes(question) %> <%= render partial: "form/guidance/#{question.guidance_partial}" if question.bottom_guidance? %> diff --git a/app/views/form/_radio_question.html.erb b/app/views/form/_radio_question.html.erb index f153a838b..ed5e71a0b 100644 --- a/app/views/form/_radio_question.html.erb +++ b/app/views/form/_radio_question.html.erb @@ -5,7 +5,7 @@ legend: legend(question, page_header, conditional), hint: { text: question.hint_text&.html_safe } do %> - <% question.displayed_answer_options(@lettings_log).map do |key, options| %> + <% question.displayed_answer_options(@log).map do |key, options| %> <% if key.starts_with?("divider") %> <%= f.govuk_radio_divider %> <% else %> diff --git a/app/views/form/_select_question.html.erb b/app/views/form/_select_question.html.erb index fbdb20e9a..63f50fb94 100644 --- a/app/views/form/_select_question.html.erb +++ b/app/views/form/_select_question.html.erb @@ -1,7 +1,7 @@ <%= render partial: "form/guidance/#{question.guidance_partial}" if question.top_guidance? %> -<% selected = @lettings_log.public_send(question.id) || "" %> -<% answers = question.displayed_answer_options(@lettings_log).map { |key, value| OpenStruct.new(id: key, name: value.respond_to?(:service_name) ? value.service_name : nil, resource: value) } %> +<% selected = @log.public_send(question.id) || "" %> +<% answers = question.displayed_answer_options(@log).map { |key, value| OpenStruct.new(id: key, name: value.respond_to?(:service_name) ? value.service_name : nil, resource: value) } %> <%= f.govuk_select(question.id.to_sym, label: legend(question, page_header, conditional), "data-controller": "accessible-autocomplete", @@ -12,7 +12,7 @@ data-synonyms="<%= question.answer_option_synonyms(answer.resource) %>" data-append="<%= question.answer_option_append(answer.resource) %>" data-hint="<%= question.answer_option_hint(answer.resource) %>" - <%= question.answer_selected?(@lettings_log, answer) ? "selected" : "" %> + <%= question.answer_selected?(@log, answer) ? "selected" : "" %> <%= answer.id == "" ? "disabled" : "" %>><%= answer.name || answer.resource %> <% end %> <% end %> diff --git a/app/views/form/check_answers.html.erb b/app/views/form/check_answers.html.erb index a2bcb09bc..ec85104c5 100644 --- a/app/views/form/check_answers.html.erb +++ b/app/views/form/check_answers.html.erb @@ -1,7 +1,7 @@ <% content_for :title, "#{subsection.id.humanize} - Check your answers" %> <% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { "Logs" => "/logs", - "Log #{@lettings_log.id}" => "/logs/#{@lettings_log.id}", + "Log #{@log.id}" => "/logs/#{@log.id}", subsection.label => "", }) %> @@ -12,27 +12,27 @@ Check your answers - <% if subsection.id == "setup" && subsection.status(@lettings_log) == :completed %> + <% if subsection.id == "setup" && subsection.status(@log) == :completed %> <%= govuk_inset_text(text: "Changing these answers might remove answers you’ve already given in other sections.") %> <% end %> - <%= display_answered_questions_summary(subsection, @lettings_log, current_user) %> + <%= display_answered_questions_summary(subsection, @log, current_user) %> - <% if any_questions_have_summary_card_number?(subsection, @lettings_log) %> - <% subsection.applicable_questions(@lettings_log).group_by(&:check_answers_card_number).values.each do |question_group| %> - <%= render CheckAnswersSummaryListCardComponent.new(questions: question_group, lettings_log: @lettings_log, user: current_user) %> + <% if any_questions_have_summary_card_number?(subsection, @log) %> + <% subsection.applicable_questions(@log).group_by(&:check_answers_card_number).values.each do |question_group| %> + <%= render CheckAnswersSummaryListCardComponent.new(questions: question_group, lettings_log: @log, user: current_user) %> <% end %> <% else %> <%= render partial: "form/check_answers_summary_list", locals: { subsection:, - lettings_log: @lettings_log, + lettings_log: @log, } %> <% end %> - <%= form_with model: @lettings_log, method: "get" do |f| %> + <%= form_with model: @log, method: "get" do |f| %> <%= f.govuk_submit "Save and return to log" do %> - <% next_incomplete_section_redirect_path = @lettings_log.form.next_incomplete_section_redirect_path(subsection, @lettings_log) %> - <% if @lettings_log.status == "in_progress" && next_incomplete_section_redirect_path != "error" %> - <%= govuk_button_link_to "Save and go to next incomplete section", "/logs/#{@lettings_log.id}/#{next_incomplete_section_redirect_path}", secondary: true %> + <% next_incomplete_section_redirect_path = @log.form.next_incomplete_section_redirect_path(subsection, @log) %> + <% if @log.status == "in_progress" && next_incomplete_section_redirect_path != "error" %> + <%= govuk_button_link_to "Save and go to next incomplete section", "/logs/#{@log.id}/#{next_incomplete_section_redirect_path}", secondary: true %> <% end %> <% end %> <% end %> diff --git a/app/views/form/page.html.erb b/app/views/form/page.html.erb index 0fe72f647..542f5fd51 100644 --- a/app/views/form/page.html.erb +++ b/app/views/form/page.html.erb @@ -5,10 +5,10 @@ <% end %>
-<%= form_with model: @lettings_log, url: form_lettings_log_path(@lettings_log), method: "post", local: true do |f| %> +<%= form_with model: @log, url: form_lettings_log_path(@log), method: "post", local: true do |f| %>
"> - <% remove_other_page_errors(@lettings_log, @page) %> + <% remove_other_page_errors(@log, @page) %> <%= f.govuk_error_summary %> <% if @page.header.present? %> @@ -30,9 +30,9 @@ <%= govuk_section_break(visible: true, size: "m") %> <% end %> <% if question.type == "interruption_screen" %> - <%= render partial: "form/#{question.type}_question", locals: { question:, caption_text: @subsection.label, page_header: @page.header, lettings_log: @lettings_log, title_text: @page.title_text, informative_text: @page.informative_text, form: @form, f:, conditional: false } %> + <%= render partial: "form/#{question.type}_question", locals: { question:, caption_text: @subsection.label, page_header: @page.header, lettings_log: @log, title_text: @page.title_text, informative_text: @page.informative_text, form: @form, f:, conditional: false } %> <% else %> - <%= render partial: "form/#{question.type}_question", locals: { question:, caption_text: @subsection.label, page_header: @page.header, lettings_log: @lettings_log, f:, conditional: false } %> + <%= render partial: "form/#{question.type}_question", locals: { question:, caption_text: @subsection.label, page_header: @page.header, lettings_log: @log, f:, conditional: false } %> <% end %>
<% end %> @@ -42,10 +42,10 @@
<% if !@page.id.include?("value_check") && if request.query_parameters["referrer"] != "check_answers" %> <%= f.govuk_submit "Save and continue" %> - <%= govuk_link_to "Skip for now", send(@lettings_log.form.next_page_redirect_path(@page, @lettings_log, current_user), @lettings_log) %> + <%= govuk_link_to "Skip for now", send(@log.form.next_page_redirect_path(@page, @log, current_user), @log) %> <% else %> <%= f.govuk_submit "Save changes" %> - <%= govuk_link_to "Cancel", "/logs/#{@lettings_log.id}/setup/check-answers" %> + <%= govuk_link_to "Cancel", "/logs/#{@log.id}/setup/check-answers" %> <% end %> <% end %>
diff --git a/app/views/form/review.html.erb b/app/views/form/review.html.erb index 55a693913..59e0bde40 100644 --- a/app/views/form/review.html.erb +++ b/app/views/form/review.html.erb @@ -1,7 +1,7 @@ <% content_for :title, "Review lettings log" %> <% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { "Logs" => "/logs", - "Log #{@lettings_log.id}" => "/logs/#{@lettings_log.id}", + "Log #{@log.id}" => "/logs/#{@log.id}", "Review lettings log" => "", }) %> @@ -11,9 +11,9 @@ <%= content_for(:title) %>

- You can review and make changes to this log until 2nd June <%= @lettings_log.collection_start_year.present? ? @lettings_log.collection_start_year + 1 : "" %>. + You can review and make changes to this log until 2nd June <%= @log.collection_start_year.present? ? @log.collection_start_year + 1 : "" %>.

- <% @lettings_log.form.sections.map do |section| %> + <% @log.form.sections.map do |section| %>

<%= section.label %>

<% section.subsections.map do |subsection| %>
@@ -23,7 +23,7 @@
<%= render partial: "form/check_answers_summary_list", locals: { subsection:, - lettings_log: @lettings_log, + lettings_log: @log, } %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5dc643e4e..c868337ff 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ - <%= browser_title(yield(:title), @pagy, @admin_user, @user, @organisation, @lettings_log, @resource) %> + <%= browser_title(yield(:title), @pagy, @admin_user, @user, @organisation, @log, @resource) %> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= tag.meta name: "viewport", content: "width=device-width, initial-scale=1" %> diff --git a/app/views/lettings_logs/index.html.erb b/app/views/lettings_logs/index.html.erb index 9378aee09..d1c7511f3 100644 --- a/app/views/lettings_logs/index.html.erb +++ b/app/views/lettings_logs/index.html.erb @@ -8,6 +8,7 @@
<%= govuk_button_to "Create a new lettings log", lettings_logs_path %> + <%= govuk_button_to "Create a new sales log", sales_logs_path %> <%#= govuk_link_to "Upload logs", bulk_upload_lettings_logs_path %>
diff --git a/app/views/sales_logs/_tasklist.html.erb b/app/views/sales_logs/_tasklist.html.erb new file mode 100644 index 000000000..e635d8241 --- /dev/null +++ b/app/views/sales_logs/_tasklist.html.erb @@ -0,0 +1,25 @@ +
    + <% @sales_log.form.sections.map do |section| %> +
  1. +

    + <%= section.label %> +

    + <% if section.description %> +

    <%= section.description.html_safe %>

    + <% end %> +
      + <% section.subsections.map do |subsection| %> + <% if subsection.applicable_questions(@sales_log).count > 0 || !subsection.enabled?(@sales_log) %> + <% subsection_status = subsection.status(@sales_log) %> +
    • + + <%= subsection_link(subsection, @sales_log, current_user) %> + + <%= status_tag(subsection_status, "app-task-list__tag") %> +
    • + <% end %> + <% end %> +
    +
  2. + <% end %> +
diff --git a/app/views/sales_logs/edit.html.erb b/app/views/sales_logs/edit.html.erb new file mode 100644 index 000000000..e444dd0e8 --- /dev/null +++ b/app/views/sales_logs/edit.html.erb @@ -0,0 +1,37 @@ +<% content_for :title, "Log #{@sales_log.id}" %> +<% content_for :breadcrumbs, govuk_breadcrumbs(breadcrumbs: { + "Logs" => "/logs", + content_for(:title) => "", +}) %> + +
+
+

+ <%= content_for(:title) %> +

+ + <% if @sales_log.status == "in_progress" %> +

<%= get_subsections_count(@sales_log, :completed) %> of <%= get_subsections_count(@sales_log, :all) %> sections completed.

+

+ <% next_incomplete_section = get_next_incomplete_section(@sales_log) %> +

+

+ <% if next_incomplete_section.present? %> + + Skip to next incomplete section: <%= next_incomplete_section.label %> + + <% end %> +

+ <% elsif @sales_log.status == "not_started" %> +

This log has not been started.

+ <% elsif @sales_log.status == "completed" %> +

+ <%= status_tag(@sales_log.status) %> +

+

+ You can <%= govuk_link_to "review and make changes to this log", "/logs/#{@sales_log.id}/review" %> until 2nd June <%= @sales_log.collection_start_year.present? ? @sales_log.collection_start_year + 1 : "" %>. +

+ <% end %> + <%= render "tasklist" %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 5a2fda475..5a3cdb43f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -84,9 +84,21 @@ Rails.application.routes.draw do get "review", to: "form#review" end - FormHandler.instance.forms.each do |_key, form| + FormHandler.instance.lettings_forms.each do |_key, form| form.pages.map do |page| - get page.id.to_s.dasherize, to: "form##{page.id}" + get page.id.to_s.dasherize, to: "form#show_page" + end + + form.subsections.map do |subsection| + get "#{subsection.id.to_s.dasherize}/check-answers", to: "form#check_answers" + end + end + end + + resources :sales_logs, path: "/sales-logs" do + FormHandler.instance.sales_forms.each do |_key, form| + form.pages.map do |page| + get page.id.to_s.dasherize, to: "form#show_page" end form.subsections.map do |subsection| diff --git a/db/migrate/20220826093411_add_sales_log.rb b/db/migrate/20220826093411_add_sales_log.rb new file mode 100644 index 000000000..549f1b6cd --- /dev/null +++ b/db/migrate/20220826093411_add_sales_log.rb @@ -0,0 +1,12 @@ +class AddSalesLog < ActiveRecord::Migration[7.0] + def change + create_table :sales_logs do |t| + t.integer :status, default: 0 + t.datetime :saledate + t.timestamps + t.references :owning_organisation, class_name: "Organisation", foreign_key: { to_table: :organisations, on_delete: :cascade } + t.references :managing_organisation, class_name: "Organisation", foreign_key: { to_table: :organisations } + t.references :created_by, class_name: "User", foreign_key: { to_table: :users } + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 23db463fc..dbdad5c31 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -313,6 +313,19 @@ ActiveRecord::Schema[7.0].define(version: 2022_09_02_082245) do t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true end + create_table "sales_logs", force: :cascade do |t| + t.integer "status", default: 0 + t.datetime "saledate" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "owning_organisation_id" + t.bigint "managing_organisation_id" + t.bigint "created_by_id" + t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" + t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id" + t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id" + end + create_table "schemes", force: :cascade do |t| t.string "service_name" t.bigint "owning_organisation_id", null: false @@ -395,6 +408,9 @@ ActiveRecord::Schema[7.0].define(version: 2022_09_02_082245) do add_foreign_key "lettings_logs", "organisations", column: "owning_organisation_id", on_delete: :cascade add_foreign_key "lettings_logs", "schemes" add_foreign_key "locations", "schemes" + add_foreign_key "sales_logs", "organisations", column: "managing_organisation_id" + add_foreign_key "sales_logs", "organisations", column: "owning_organisation_id", on_delete: :cascade + add_foreign_key "sales_logs", "users", column: "created_by_id" add_foreign_key "schemes", "organisations", column: "managing_organisation_id" add_foreign_key "schemes", "organisations", column: "owning_organisation_id", on_delete: :cascade add_foreign_key "users", "organisations", on_delete: :cascade diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb new file mode 100644 index 000000000..bacb8e3e6 --- /dev/null +++ b/spec/factories/sales_log.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :sales_log do + created_by { FactoryBot.create(:user) } + owning_organisation { created_by.organisation } + managing_organisation { created_by.organisation } + created_at { Time.utc(2022, 2, 8, 16, 52, 15) } + updated_at { Time.utc(2022, 2, 8, 16, 52, 15) } + trait :completed do + saledate { Time.utc(2022, 2, 2, 10, 36, 49) } + end + end +end diff --git a/spec/features/form/validations_spec.rb b/spec/features/form/validations_spec.rb index d944f5161..30a69b989 100644 --- a/spec/features/form/validations_spec.rb +++ b/spec/features/form/validations_spec.rb @@ -37,7 +37,7 @@ RSpec.describe "validations" do describe "Question validation" do context "when the tenant age is invalid" do - it "shows validation for under 0" do + it "shows validation for under 0", js: true do visit("/logs/#{id}/person-1-age") fill_in_number_question(empty_lettings_log.id, "age1", -5, "person-1-age") expect(page).to have_selector("#error-summary-title") diff --git a/spec/features/log_spec.rb b/spec/features/log_spec.rb index cef344145..2ae3d74d7 100644 --- a/spec/features/log_spec.rb +++ b/spec/features/log_spec.rb @@ -77,7 +77,7 @@ RSpec.describe "Log Features" do click_button("Submit") end - context "when completing the setup log section" do + context "when completing the setup lettings log section" do it "includes the organisation and created by questions" do visit("/logs") click_button("Create a new lettings log") @@ -93,6 +93,22 @@ RSpec.describe "Log Features" do expect(page).to have_content("You have answered 2 of 8 questions") end end + + context "when completing the setup sales log section" do + it "includes the sale completion date question", js: true do + visit("/logs") + click_button("Create a new sales log") + click_link("Set up this sales log") + fill_in("sales_log[saledate(1i)]", with: "2022") + fill_in("sales_log[saledate(2i)]", with: "08") + fill_in("sales_log[saledate(3i)]", with: "10") + click_button("Save and continue") + log_id = page.current_path.scan(/\d/).join + visit("sales-logs/#{log_id}/setup/check-answers") + expect(page).to have_content("Sale completion date") + expect(page).to have_content("2022") + end + end end context "when the signed is user is not a Support user" do @@ -123,5 +139,21 @@ RSpec.describe "Log Features" do expect(page).to have_content("You have answered 0 of 6 questions") end end + + context "when completing the setup sales log section" do + it "includes the sale completion date question", js: true do + visit("/logs") + click_button("Create a new sales log") + click_link("Set up this sales log") + fill_in("sales_log[saledate(1i)]", with: "2022") + fill_in("sales_log[saledate(2i)]", with: "08") + fill_in("sales_log[saledate(3i)]", with: "10") + click_button("Save and continue") + log_id = page.current_path.scan(/\d/).join + visit("sales-logs/#{log_id}/setup/check-answers") + expect(page).to have_content("Sale completion date") + expect(page).to have_content("2022") + end + end end end diff --git a/spec/models/form/sales/setup/pages/sale_date_spec.rb b/spec/models/form/sales/setup/pages/sale_date_spec.rb new file mode 100644 index 000000000..56f6a6953 --- /dev/null +++ b/spec/models/form/sales/setup/pages/sale_date_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Setup::Pages::SaleDate, type: :model do + subject(:page) { described_class.new(page_id, page_definition, subsection) } + + let(:page_id) { nil } + let(:page_definition) { nil } + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[saledate]) + end + + it "has the correct id" do + expect(page.id).to eq("sale_date") + end + + it "has the correct header" do + expect(page.header).to eq("") + end + + it "has the correct description" do + expect(page.description).to eq("") + end +end diff --git a/spec/models/form/sales/setup/questions/sale_date_spec.rb b/spec/models/form/sales/setup/questions/sale_date_spec.rb new file mode 100644 index 000000000..c340ea0c7 --- /dev/null +++ b/spec/models/form/sales/setup/questions/sale_date_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Setup::Questions::SaleDate, type: :model do + subject(:question) { described_class.new(question_id, question_definition, page) } + + let(:question_id) { nil } + let(:question_definition) { nil } + let(:page) { instance_double(Form::Page) } + + it "has correct page" do + expect(question.page).to eq(page) + end + + it "has the correct id" do + expect(question.id).to eq("saledate") + end + + it "has the correct header" do + expect(question.header).to eq("What is the sale completion date?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Sale completion date") + end + + it "has the correct type" do + expect(question.type).to eq("date") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end +end diff --git a/spec/models/form/sales/setup/sections/setup_spec.rb b/spec/models/form/sales/setup/sections/setup_spec.rb new file mode 100644 index 000000000..7bfab8cd1 --- /dev/null +++ b/spec/models/form/sales/setup/sections/setup_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Setup::Sections::Setup, type: :model do + subject(:setup) { described_class.new(section_id, section_definition, form) } + + let(:section_id) { nil } + let(:section_definition) { nil } + let(:form) { instance_double(Form) } + + it "has correct form" do + expect(setup.form).to eq(form) + end + + it "has correct subsections" do + expect(setup.subsections.map(&:id)).to eq(%w[setup]) + end + + it "has the correct id" do + expect(setup.id).to eq("setup") + end + + it "has the correct label" do + expect(setup.label).to eq("Before you start") + end + + it "has the correct description" do + expect(setup.description).to eq("") + end +end diff --git a/spec/models/form/sales/setup/subsections/setup_spec.rb b/spec/models/form/sales/setup/subsections/setup_spec.rb new file mode 100644 index 000000000..d2e1bedce --- /dev/null +++ b/spec/models/form/sales/setup/subsections/setup_spec.rb @@ -0,0 +1,27 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Setup::Subsections::Setup, type: :model do + subject(:setup) { described_class.new(subsection_id, subsection_definition, section) } + + let(:subsection_id) { nil } + let(:subsection_definition) { nil } + let(:section) { instance_double(Form::Sales::Setup::Sections::Setup) } + + it "has correct section" do + expect(setup.section).to eq(section) + end + + it "has correct pages" do + expect(setup.pages.map(&:id)).to eq( + %w[sale_date], + ) + end + + it "has the correct id" do + expect(setup.id).to eq("setup") + end + + it "has the correct label" do + expect(setup.label).to eq("Set up this sales log") + end +end diff --git a/spec/models/form_handler_spec.rb b/spec/models/form_handler_spec.rb index 94a3af2fe..4e6a61913 100644 --- a/spec/models/form_handler_spec.rb +++ b/spec/models/form_handler_spec.rb @@ -35,4 +35,9 @@ RSpec.describe FormHandler do expect(Form).not_to receive(:new).with(:any, test_form_name) expect(form_handler.get_form(test_form_name)).to be_a(Form) end + + it "can get a saleslog form" do + form_handler = described_class.instance + expect(form_handler.get_form("2022_2023_sales")).to be_a(Form) + end end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index 7f1d4cb81..14297fcd4 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -205,4 +205,25 @@ RSpec.describe Form, type: :model do end end end + + describe "when creating a sales log" do + it "creates a valid sales form" do + sections = [Form::Sales::Setup::Sections::Setup] + form = described_class.new(nil, "2022_23_sales", sections, "sales") + expect(form.type).to eq("sales") + expect(form.name).to eq("2022_23_sales") + expect(form.form_sections.count).to eq(1) + expect(form.form_sections[0].class).to eq(sections[0]) + expect(form.sections.count).to eq(1) + expect(form.sections[0].class).to eq(sections[0]) + expect(form.subsections.count).to eq(1) + expect(form.subsections.first.id).to eq("setup") + expect(form.pages.count).to eq(1) + expect(form.pages.first.id).to eq("sale_date") + expect(form.questions.count).to eq(1) + expect(form.questions.first.id).to eq("saledate") + expect(form.start_date).to eq(Time.zone.parse("2022-04-01")) + expect(form.end_date).to eq(Time.zone.parse("2023-04-01")) + end + end end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index 772255b19..e2d78e60c 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -5,6 +5,11 @@ RSpec.describe LettingsLog do let(:different_managing_organisation) { FactoryBot.create(:organisation) } let(:created_by_user) { FactoryBot.create(:user) } + it "inherits from log" do + expect(described_class).to be < Log + expect(described_class).to be < ApplicationRecord + end + describe "#form" do let(:lettings_log) { FactoryBot.build(:lettings_log, created_by: created_by_user) } let(:lettings_log_2) { FactoryBot.build(:lettings_log, startdate: Time.zone.local(2022, 1, 1), created_by: created_by_user) } diff --git a/spec/models/log_spec.rb b/spec/models/log_spec.rb new file mode 100644 index 000000000..0cba24086 --- /dev/null +++ b/spec/models/log_spec.rb @@ -0,0 +1,8 @@ +require "rails_helper" + +RSpec.describe Log, type: :model do + it "has two child log classes" do + expect(SalesLog).to be < described_class + expect(LettingsLog).to be < described_class + end +end diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb new file mode 100644 index 000000000..39f694cd8 --- /dev/null +++ b/spec/models/sales_log_spec.rb @@ -0,0 +1,85 @@ +require "rails_helper" + +RSpec.describe SalesLog, type: :model do + let(:owning_organisation) { FactoryBot.create(:organisation) } + let(:created_by_user) { FactoryBot.create(:user) } + + it "inherits from log" do + expect(described_class).to be < Log + expect(described_class).to be < ApplicationRecord + end + + describe "#new" do + context "when creating a record" do + let(:sales_log) do + described_class.create + end + + it "attaches the correct custom validator" do + expect(sales_log._validators.values.flatten.map(&:class)) + .to include(SalesLogValidator) + end + end + end + + describe "#form" do + let(:sales_log) { FactoryBot.build(:sales_log, created_by: created_by_user) } + let(:sales_log_2) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2022, 5, 1), created_by: created_by_user) } + + it "has returns the correct form based on the start date" do + expect(sales_log.form_name).to be_nil + expect(sales_log.form).to be_a(Form) + expect(sales_log_2.form_name).to eq("2022_2023_sales") + expect(sales_log_2.form).to be_a(Form) + end + end + + describe "status" do + let!(:empty_sales_log) { FactoryBot.create(:sales_log) } + # let!(:in_progress_sales_log) { FactoryBot.create(:sales_log, :in_progress) } + let!(:completed_sales_log) { FactoryBot.create(:sales_log, :completed) } + + it "is set to not started for an empty sales log" do + expect(empty_sales_log.not_started?).to be(true) + # expect(empty_sales_log.in_progress?).to be(false) + expect(empty_sales_log.completed?).to be(false) + end + + # it "is set to in progress for a started sales log" do + # expect(in_progress_sales_log.in_progress?).to be(true) + # expect(in_progress_sales_log.not_started?).to be(false) + # expect(in_progress_sales_log.completed?).to be(false) + # end + + it "is set to completed for a completed sales log" do + # expect(completed_sales_log.in_progress?).to be(false) + expect(completed_sales_log.not_started?).to be(false) + expect(completed_sales_log.completed?).to be(true) + end + end + + context "when filtering by organisation" do + let(:organisation_1) { FactoryBot.create(:organisation) } + let(:organisation_2) { FactoryBot.create(:organisation) } + let(:organisation_3) { FactoryBot.create(:organisation) } + + before do + FactoryBot.create(:sales_log, :in_progress, owning_organisation: organisation_1, managing_organisation: organisation_1) + FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_1, managing_organisation: organisation_2) + FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_2, managing_organisation: organisation_1) + FactoryBot.create(:sales_log, :completed, owning_organisation: organisation_2, managing_organisation: organisation_2) + end + + it "filters by given organisation id" do + expect(described_class.filter_by_organisation([organisation_1.id]).count).to eq(3) + expect(described_class.filter_by_organisation([organisation_1.id, organisation_2.id]).count).to eq(4) + expect(described_class.filter_by_organisation([organisation_3.id]).count).to eq(0) + end + + it "filters by given organisation" do + expect(described_class.filter_by_organisation([organisation_1]).count).to eq(3) + expect(described_class.filter_by_organisation([organisation_1, organisation_2]).count).to eq(4) + expect(described_class.filter_by_organisation([organisation_3]).count).to eq(0) + end + end +end diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb new file mode 100644 index 000000000..6fc6337ab --- /dev/null +++ b/spec/requests/sales_logs_controller_spec.rb @@ -0,0 +1,87 @@ +require "rails_helper" + +RSpec.describe SalesLogsController, type: :request do + let(:user) { FactoryBot.create(:user) } + let(:owning_organisation) { user.organisation } + let(:managing_organisation) { owning_organisation } + let(:api_username) { "test_user" } + let(:api_password) { "test_password" } + let(:basic_credentials) do + ActionController::HttpAuthentication::Basic + .encode_credentials(api_username, api_password) + end + + let(:params) do + { + "owning_organisation_id": owning_organisation.id, + "managing_organisation_id": managing_organisation.id, + "created_by_id": user.id, + } + end + + let(:headers) do + { + "Content-Type" => "application/json", + "Accept" => "application/json", + "Authorization" => basic_credentials, + } + end + + before do + allow(ENV).to receive(:[]) + allow(ENV).to receive(:[]).with("API_USER").and_return(api_username) + allow(ENV).to receive(:[]).with("API_KEY").and_return(api_password) + end + + describe "POST #create" do + context "when API" do + before do + post "/sales-logs", headers:, params: params.to_json + end + + it "returns http success" do + expect(response).to have_http_status(:success) + end + + it "returns a serialized sales log" do + json_response = JSON.parse(response.body) + expect(json_response.keys).to match_array(SalesLog.new.attributes.keys) + end + + it "creates a sales log with the values passed" do + json_response = JSON.parse(response.body) + expect(json_response["owning_organisation_id"]).to eq(owning_organisation.id) + expect(json_response["managing_organisation_id"]).to eq(managing_organisation.id) + expect(json_response["created_by_id"]).to eq(user.id) + end + + context "with a request containing invalid credentials" do + let(:basic_credentials) do + ActionController::HttpAuthentication::Basic.encode_credentials(api_username, "Oops") + end + + it "returns 401" do + expect(response).to have_http_status(:unauthorized) + end + end + end + + context "when UI" do + let(:user) { FactoryBot.create(:user) } + let(:headers) { { "Accept" => "text/html" } } + + before do + RequestHelper.stub_http_requests + sign_in user + post "/sales-logs", headers: + end + + it "tracks who created the record" do + created_id = response.location.match(/[0-9]+/)[0] + whodunnit_actor = SalesLog.find_by(id: created_id).versions.last.actor + expect(whodunnit_actor).to be_a(User) + expect(whodunnit_actor.id).to eq(user.id) + end + end + end +end diff --git a/spec/views/form/page_view_spec.rb b/spec/views/form/page_view_spec.rb index 2cedf6eba..a7039a9a9 100644 --- a/spec/views/form/page_view_spec.rb +++ b/spec/views/form/page_view_spec.rb @@ -18,7 +18,7 @@ RSpec.describe "form/page" do end before do - assign(:lettings_log, lettings_log) + assign(:log, lettings_log) assign(:page, page) assign(:subsection, subsection) assign_attributes(page, page_attributes)