diff --git a/app/components/bulk_upload_summary_component.html.erb b/app/components/bulk_upload_summary_component.html.erb new file mode 100644 index 000000000..abbff4493 --- /dev/null +++ b/app/components/bulk_upload_summary_component.html.erb @@ -0,0 +1,37 @@ +
+
+
+
+

+ <%= bulk_upload.filename %> + +

+
+
+

Uploaded by: <%= bulk_upload.user.name %> (<%= bulk_upload.user.email %>)

+

Uploading organisation: <%= bulk_upload.organisation.name %>

+

Time of upload: <%= bulk_upload.created_at.to_formatted_s(:govuk_date_and_time) %>

+
+

+ <%= download_file_link(bulk_upload) %> + <%= view_error_report_link(bulk_upload) %> + <%= view_logs_link(bulk_upload) %> +

+
+ +
+
diff --git a/app/components/bulk_upload_summary_component.rb b/app/components/bulk_upload_summary_component.rb new file mode 100644 index 000000000..fa4cad414 --- /dev/null +++ b/app/components/bulk_upload_summary_component.rb @@ -0,0 +1,72 @@ +class BulkUploadSummaryComponent < ViewComponent::Base + attr_reader :bulk_upload + + def initialize(bulk_upload:) + @bulk_upload = bulk_upload + @bulk_upload_errors = bulk_upload.bulk_upload_errors + super + end + + def upload_status + helpers.status_tag(bulk_upload.status, ["app-tag--small govuk-!-font-weight-regular no-max-width"]) + end + + def setup_errors_count + @bulk_upload_errors.where(category: "setup").count + end + + def critical_errors_count + @bulk_upload_errors.where(category: [nil, "", "not_answered"]).count + end + + def potential_errors_count + @bulk_upload_errors.where(category: "soft_validation").count + end + + def formatted_count_text(count, singular_text, plural_text = nil) + return if count.nil? || count <= 0 + + text = count > 1 ? (plural_text || singular_text.pluralize(count)) : singular_text + content_tag(:p, class: "govuk-!-font-size-16 govuk-!-margin-bottom-1") do + concat(content_tag(:strong, count)) + concat(" #{text}") + end + end + + def counts(*counts_with_texts) + counts_with_texts.map { |count, singular_text, plural_text| + formatted_count_text(count, singular_text, plural_text) if count.present? + }.compact.join("").html_safe + end + + def download_file_link(bulk_upload) + send("download_#{bulk_upload.log_type}_file_link", bulk_upload) + end + + def download_lettings_file_link(bulk_upload) + govuk_link_to "Download file", download_lettings_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2" + end + + def download_sales_file_link(bulk_upload) + govuk_link_to "Download file", download_sales_bulk_upload_path(bulk_upload), class: "govuk-link govuk-!-margin-right-2" + end + + def view_error_report_link(bulk_upload) + status = bulk_upload.status.to_s + return unless %w[important_errors critical_errors potential_errors].include?(status) + + path = if status == "important_errors" + "summary_bulk_upload_#{bulk_upload.log_type}_result_url" + else + "bulk_upload_#{bulk_upload.log_type}_result_path" + end + + govuk_link_to "View error report", send(path, bulk_upload), class: "govuk-link" + end + + def view_logs_link(bulk_upload) + return unless bulk_upload.status.to_s == "logs_uploaded_with_errors" + + govuk_link_to "View logs with errors", send("#{bulk_upload.log_type}_logs_path", bulk_upload_id: [bulk_upload.id]), class: "govuk-link" + end +end diff --git a/app/components/create_log_actions_component.html.erb b/app/components/create_log_actions_component.html.erb index 5a90587ed..8af78b169 100644 --- a/app/components/create_log_actions_component.html.erb +++ b/app/components/create_log_actions_component.html.erb @@ -1,10 +1,11 @@ <% if display_actions? %>
- <% if create_button_href.present? %> - <%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-6" %> + <%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-6" %> + <% unless user.support? %> + <%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %> <% end %> - <% if upload_button_href.present? && !user.support? %> - <%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %> + <% if user.support? %> + <%= govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %> <% end %>
<% end %> diff --git a/app/components/create_log_actions_component.rb b/app/components/create_log_actions_component.rb index 4b451c2cc..4395c48a9 100644 --- a/app/components/create_log_actions_component.rb +++ b/app/components/create_log_actions_component.rb @@ -18,39 +18,27 @@ class CreateLogActionsComponent < ViewComponent::Base user.organisation.data_protection_confirmed? && user.organisation.organisation_or_stock_owner_signed_dsa_and_holds_own_stock? end - def create_button_href - case log_type - when "lettings" - lettings_logs_path - when "sales" - sales_logs_path - end + def create_button_copy + "Create a new #{log_type} log" end - def create_button_copy - case log_type - when "lettings" - "Create a new lettings log" - when "sales" - "Create a new sales log" - end + def create_button_href + send("#{log_type}_logs_path") end def upload_button_copy - case log_type - when "lettings" - "Upload lettings logs in bulk" - when "sales" - "Upload sales logs in bulk" - end + "Upload #{log_type} logs in bulk" end def upload_button_href - case log_type - when "lettings" - bulk_upload_lettings_log_path(id: "start") - when "sales" - bulk_upload_sales_log_path(id: "start") - end + send("bulk_upload_#{log_type}_log_path", id: "start") + end + + def view_uploads_button_copy + "View #{log_type} bulk uploads" + end + + def view_uploads_button_href + send("bulk_uploads_#{log_type}_logs_path") end end diff --git a/app/components/search_component.rb b/app/components/search_component.rb index 36d621240..6c77eb3c4 100644 --- a/app/components/search_component.rb +++ b/app/components/search_component.rb @@ -9,17 +9,9 @@ class SearchComponent < ViewComponent::Base end def path(current_user) - if request.path.include?("organisations") && request.path.include?("users") - request.path - elsif request.path.include?("organisations") && request.path.include?("logs") - request.path - elsif request.path.include?("organisations") && request.path.include?("schemes") - request.path - elsif request.path.include?("organisations") && request.path.include?("stock-owners") - request.path - elsif request.path.include?("organisations") && request.path.include?("managing-agents") - request.path - elsif request.path.include?("users") + return request.path if matching_path_conditions? + + if request.path.include?("users") user_path(current_user) elsif request.path.include?("organisations") organisations_path @@ -35,4 +27,17 @@ private def user_path(current_user) current_user.support? ? users_path : users_organisation_path(current_user.organisation) end + + def matching_path_conditions? + [ + %r{organisations/\d+/users}, + %r{organisations/\d+/lettings-logs}, + %r{organisations/\d+/sales-logs}, + %r{organisations/\d+/schemes}, + %r{organisations/\d+/stock-owners}, + %r{organisations/\d+/managing-agents}, + %r{sales-logs/bulk-uploads}, + %r{lettings-logs/bulk-uploads}, + ].any? { |pattern| request.path.match?(pattern) } + end end diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 8a95464a7..70b6f892b 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -409,6 +409,7 @@ private next if question.subsection.id == "setup" question.page.questions.map(&:id).each { |id| @log[id] = nil } + @log.previous_la_known = nil if question.id == "ppostcode_full" end @log.save! @questions = params[@log.model_name.param_key].keys.reject { |id| %w[clear_question_ids page].include?(id) }.map { |id| @log.form.get_question(id, @log) } diff --git a/app/controllers/lettings_logs_controller.rb b/app/controllers/lettings_logs_controller.rb index 491d92372..0194946d4 100644 --- a/app/controllers/lettings_logs_controller.rb +++ b/app/controllers/lettings_logs_controller.rb @@ -5,8 +5,8 @@ class LettingsLogsController < LogsController before_action :find_resource, only: %i[update show] - before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] - before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index email_csv download_csv] + before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv bulk_uploads] + before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index email_csv download_csv bulk_uploads] before_action :authenticate_scope!, only: %i[download_csv email_csv] before_action :extract_bulk_upload_from_session_filters, only: [:index] @@ -115,6 +115,40 @@ class LettingsLogsController < LogsController end end + def bulk_uploads + return render_not_authorized unless current_user.support? + + @filter_type = "lettings_bulk_uploads" + + if params[:organisation_id].present? && params[:clear_old_filters].present? + redirect_to clear_filters_path(filter_type: @filter_type, organisation_id: params[:organisation_id]) and return + end + + uploads = BulkUpload.lettings.where("created_at >= ?", 30.days.ago) + unpaginated_filtered_uploads = filter_manager.filtered_uploads(uploads, search_term, filter_manager.session_filters) + + @pagy, @bulk_uploads = pagy(unpaginated_filtered_uploads) + @search_term = search_term + @total_count = uploads.size + @searched = search_term.presence + render "bulk_upload_shared/uploads" + end + + def download_bulk_upload + return render_not_authorized unless current_user.support? + + bulk_upload = BulkUpload.find(params[:id]) + downloader = BulkUpload::Downloader.new(bulk_upload:) + + if Rails.env.development? + downloader.call + send_file downloader.path, filename: bulk_upload.filename, type: "text/csv" + else + presigned_url = downloader.presigned_url + redirect_to presigned_url, allow_other_host: true + end + end + private def session_filters @@ -122,7 +156,11 @@ private end def filter_manager - FilterManager.new(current_user:, session:, params:, filter_type: "lettings_logs") + if request.path.include?("bulk-uploads") + FilterManager.new(current_user:, session:, params:, filter_type: "lettings_bulk_uploads") + else + FilterManager.new(current_user:, session:, params:, filter_type: "lettings_logs") + end end def authenticate_scope! diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 044bab74d..61cd43674 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -44,6 +44,12 @@ class OrganisationsController < ApplicationController redirect_to schemes_csv_confirmation_organisation_path end + def duplicate_schemes + authorize @organisation + + get_duplicate_schemes_and_locations + end + def show redirect_to details_organisation_path(@organisation) end @@ -295,6 +301,22 @@ class OrganisationsController < ApplicationController render json: org_data.to_json end + def confirm_duplicate_schemes + authorize @organisation + + if scheme_duplicates_checked_params[:scheme_duplicates_checked] == "true" + @organisation.schemes_deduplicated_at = Time.zone.now + if @organisation.save + flash[:notice] = I18n.t("organisation.duplicate_schemes_confirmed") + redirect_to schemes_organisation_path(@organisation) + end + else + @organisation.errors.add(:scheme_duplicates_checked, I18n.t("validations.organisation.scheme_duplicates_not_resolved")) + get_duplicate_schemes_and_locations + render :duplicate_schemes, status: :unprocessable_entity + end + end + private def filter_type @@ -325,6 +347,10 @@ private params.require(:organisation).permit(rent_periods: [], all_rent_periods: []) end + def scheme_duplicates_checked_params + params.require(:organisation).permit(:scheme_duplicates_checked) + end + def codes_only_export? params.require(:codes_only) == "true" end @@ -344,4 +370,18 @@ private def find_resource @organisation = Organisation.find(params[:id]) end + + def get_duplicate_schemes_and_locations + duplicate_scheme_sets = @organisation.owned_schemes.duplicate_sets + @duplicate_schemes = duplicate_scheme_sets.map { |set| set.map { |id| @organisation.owned_schemes.find(id) } } + @duplicate_locations = [] + @organisation.owned_schemes.each do |scheme| + duplicate_location_sets = scheme.locations.duplicate_sets + next unless duplicate_location_sets.any? + + duplicate_location_sets.each do |duplicate_set| + @duplicate_locations << { scheme: scheme, locations: duplicate_set.map { |id| scheme.locations.find(id) } } + end + end + end end diff --git a/app/controllers/sales_logs_controller.rb b/app/controllers/sales_logs_controller.rb index 7c4d8aa60..d1bbe3bc2 100644 --- a/app/controllers/sales_logs_controller.rb +++ b/app/controllers/sales_logs_controller.rb @@ -3,8 +3,8 @@ class SalesLogsController < LogsController rescue_from ActiveRecord::RecordNotFound, with: :render_not_found - before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] - before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index email_csv download_csv] + before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv bulk_uploads] + before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index email_csv download_csv bulk_uploads] before_action :authenticate_scope!, only: %i[download_csv email_csv] before_action :extract_bulk_upload_from_session_filters, only: [:index] @@ -85,6 +85,40 @@ class SalesLogsController < LogsController params.require(:sales_log).permit(SalesLog.editable_fields) end + def bulk_uploads + return render_not_authorized unless current_user.support? + + @filter_type = "sales_bulk_uploads" + + if params[:organisation_id].present? && params[:clear_old_filters].present? + redirect_to clear_filters_path(filter_type: @filter_type, organisation_id: params[:organisation_id]) and return + end + + uploads = BulkUpload.sales.where("created_at >= ?", 30.days.ago) + unpaginated_filtered_uploads = filter_manager.filtered_uploads(uploads, search_term, session_filters) + + @pagy, @bulk_uploads = pagy(unpaginated_filtered_uploads) + @search_term = search_term + @total_count = uploads.size + @searched = search_term.presence + render "bulk_upload_shared/uploads" + end + + def download_bulk_upload + return render_not_authorized unless current_user.support? + + bulk_upload = BulkUpload.find(params[:id]) + downloader = BulkUpload::Downloader.new(bulk_upload:) + + if Rails.env.development? + downloader.call + send_file downloader.path, filename: bulk_upload.filename, type: "text/csv" + else + presigned_url = downloader.presigned_url + redirect_to presigned_url, allow_other_host: true + end + end + private def session_filters @@ -92,7 +126,11 @@ private end def filter_manager - FilterManager.new(current_user:, session:, params:, filter_type: "sales_logs") + if request.path.include?("bulk-uploads") + FilterManager.new(current_user:, session:, params:, filter_type: "sales_bulk_uploads") + else + FilterManager.new(current_user:, session:, params:, filter_type: "sales_logs") + end end def extract_bulk_upload_from_session_filters diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 868b4936d..70a538900 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -5,6 +5,14 @@ class SessionsController < ApplicationController if path_params[:organisation_id].present? redirect_to send("#{params[:filter_type]}_organisation_path", id: path_params[:organisation_id], scheme_id: path_params[:scheme_id], search: path_params[:search]) + elsif params[:filter_type].include?("bulk_uploads") + bulk_upload_type = params[:filter_type].split("_").first + uploading_organisation = params[:organisation_id].presence + if uploading_organisation.present? + redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: path_params[:search], uploading_organisation:) + else + redirect_to send("bulk_uploads_#{bulk_upload_type}_logs_path", search: path_params[:search]) + end else redirect_to send("#{params[:filter_type]}_path", scheme_id: path_params[:scheme_id], search: path_params[:search]) end diff --git a/app/controllers/start_controller.rb b/app/controllers/start_controller.rb index f3f793a17..dd4232b7b 100644 --- a/app/controllers/start_controller.rb +++ b/app/controllers/start_controller.rb @@ -1,4 +1,6 @@ class StartController < ApplicationController + include CollectionResourcesHelper + def index if current_user @homepage_presenter = HomepagePresenter.new(current_user) @@ -7,114 +9,67 @@ class StartController < ApplicationController end def download_24_25_sales_form - send_file( - Rails.root.join("public/files/2024_25_sales_paper_form.pdf"), - filename: "2024-25 Sales paper form.pdf", - type: "application/pdf", - ) + download_resource("2024_25_sales_paper_form.pdf", "2024-25 Sales paper form.pdf") end def download_23_24_sales_form - send_file( - Rails.root.join("public/files/2023_24_sales_paper_form.pdf"), - filename: "2023-24 Sales paper form.pdf", - type: "application/pdf", - ) + download_resource("2023_24_sales_paper_form.pdf", "2023-24 Sales paper form.pdf") end def download_24_25_lettings_form - send_file( - Rails.root.join("public/files/2024_25_lettings_paper_form.pdf"), - filename: "2024-25 Lettings paper form.pdf", - type: "application/pdf", - ) + download_resource("2024_25_lettings_paper_form.pdf", "2024-25 Lettings paper form.pdf") end def download_23_24_lettings_form - send_file( - Rails.root.join("public/files/2023_24_lettings_paper_form.pdf"), - filename: "2023-24 Lettings paper form.pdf", - type: "application/pdf", - ) + download_resource("2023_24_lettings_paper_form.pdf", "2023-24 Lettings paper form.pdf") end def download_24_25_lettings_bulk_upload_template - send_file( - Rails.root.join("public/files/bulk-upload-lettings-template-2024-25.xlsx"), - filename: "2024-25-lettings-bulk-upload-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-lettings-template-2024-25.xlsx", "2024-25-lettings-bulk-upload-template.xlsx") end def download_24_25_lettings_bulk_upload_specification - send_file( - Rails.root.join("public/files/bulk-upload-lettings-specification-2024-25.xlsx"), - filename: "2024-25-lettings-bulk-upload-specification.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-lettings-specification-2024-25.xlsx", "2024-25-lettings-bulk-upload-specification.xlsx") end def download_24_25_sales_bulk_upload_template - send_file( - Rails.root.join("public/files/bulk-upload-sales-template-2024-25.xlsx"), - filename: "2024-25-sales-bulk-upload-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-sales-template-2024-25.xlsx", "2024-25-sales-bulk-upload-template.xlsx") end def download_24_25_sales_bulk_upload_specification - send_file( - Rails.root.join("public/files/bulk-upload-sales-specification-2024-25.xlsx"), - filename: "2024-25-sales-bulk-upload-specification.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-sales-specification-2024-25.xlsx", "2024-25-sales-bulk-upload-specification.xlsx") end def download_23_24_lettings_bulk_upload_template - send_file( - Rails.root.join("public/files/bulk-upload-lettings-template-2023-24.xlsx"), - filename: "2023-24-lettings-bulk-upload-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-lettings-template-2023-24.xlsx", "2023-24-lettings-bulk-upload-template.xlsx") end def download_23_24_lettings_bulk_upload_legacy_template - send_file( - Rails.root.join("public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx"), - filename: "2023-24-lettings-bulk-upload-legacy-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-lettings-legacy-template-2023-24.xlsx", "2023-24-lettings-bulk-upload-legacy-template.xlsx") end def download_23_24_lettings_bulk_upload_specification - send_file( - Rails.root.join("public/files/bulk-upload-lettings-specification-2023-24.xlsx"), - filename: "2023-24-lettings-bulk-upload-specification.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-lettings-specification-2023-24.xlsx", "2023-24-lettings-bulk-upload-specification.xlsx") end def download_23_24_sales_bulk_upload_template - send_file( - Rails.root.join("public/files/bulk-upload-sales-template-2023-24.xlsx"), - filename: "2023-24-sales-bulk-upload-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-sales-template-2023-24.xlsx", "2023-24-sales-bulk-upload-template.xlsx") end def download_23_24_sales_bulk_upload_legacy_template - send_file( - Rails.root.join("public/files/bulk-upload-sales-legacy-template-2023-24.xlsx"), - filename: "2023-24-sales-bulk-upload-legacy-template.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-sales-legacy-template-2023-24.xlsx", "2023-24-sales-bulk-upload-legacy-template.xlsx") end def download_23_24_sales_bulk_upload_specification - send_file( - Rails.root.join("public/files/bulk-upload-sales-specification-2023-24.xlsx"), - filename: "2023-24-sales-bulk-upload-specification.xlsx", - type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ) + download_resource("bulk-upload-sales-specification-2023-24.xlsx", "2023-24-sales-bulk-upload-specification.xlsx") + end + +private + + def download_resource(filename, download_filename) + file = CollectionResourcesService.new.get_file(filename) + return render_not_found unless file + + send_data(file, disposition: "attachment", filename: download_filename) end end diff --git a/app/frontend/styles/_bulk-uploads.scss b/app/frontend/styles/_bulk-uploads.scss new file mode 100644 index 000000000..eceae6565 --- /dev/null +++ b/app/frontend/styles/_bulk-uploads.scss @@ -0,0 +1,18 @@ +.grouped-rows td { + border-top: none; + border-bottom: none; +} + +.grouped-rows.first-row td { + border-top: 1px solid #b1b4b6; +} + +.grouped-rows.last-row td, +.grouped-rows .grouped-multirow-cell { + border-bottom: 1px solid #b1b4b6; +} + +.text-normal-break { + white-space: normal; + word-break: break-all; +} diff --git a/app/frontend/styles/_tag.scss b/app/frontend/styles/_tag.scss index dc6c366fa..20a5d51c5 100644 --- a/app/frontend/styles/_tag.scss +++ b/app/frontend/styles/_tag.scss @@ -5,3 +5,7 @@ padding-bottom: 2px; padding-left: 6px; } + +.no-max-width { + max-width: none; +} diff --git a/app/frontend/styles/application.scss b/app/frontend/styles/application.scss index f37d9eb05..837b0db6d 100644 --- a/app/frontend/styles/application.scss +++ b/app/frontend/styles/application.scss @@ -23,6 +23,7 @@ $govuk-breakpoints: ( @import "govuk-prototype-styles"; @import "accessible-autocomplete"; +@import "bulk-uploads"; @import "button"; @import "card"; @import "data_box"; @@ -82,20 +83,6 @@ $govuk-breakpoints: ( border-top: govuk-spacing(2) solid $govuk-brand-colour; } -.grouped-rows td { - border-top: none; - border-bottom: none; -} - -.grouped-rows.first-row td { - border-top: 1px solid #b1b4b6; -} - -.grouped-rows.last-row td, -.grouped-rows .grouped-multirow-cell { - border-bottom: 1px solid #b1b4b6; -} - .govuk-notification-banner__content > * { max-width: fit-content; } diff --git a/app/helpers/bulk_upload_helper.rb b/app/helpers/bulk_upload_helper.rb new file mode 100644 index 000000000..7c2fe6972 --- /dev/null +++ b/app/helpers/bulk_upload_helper.rb @@ -0,0 +1,12 @@ +module BulkUploadHelper + def bulk_upload_title(controller_name) + case controller_name + when "lettings_logs" + "Lettings bulk uploads" + when "sales_logs" + "Sales bulk uploads" + else + "Bulk uploads" + end + end +end diff --git a/app/helpers/collection_resources_helper.rb b/app/helpers/collection_resources_helper.rb index a85670bda..5ab539cde 100644 --- a/app/helpers/collection_resources_helper.rb +++ b/app/helpers/collection_resources_helper.rb @@ -1,15 +1,22 @@ module CollectionResourcesHelper + HUMAN_READABLE_CONTENT_TYPE = { "application/pdf": "PDF", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Microsoft Excel", + "application/vnd.ms-excel": "Microsoft Excel (Old Format)", + "application/msword": "Microsoft Word", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Microsoft Word (DOCX)", + "image/jpeg": "JPEG Image", + "image/png": "PNG Image", + "text/plain": "Text Document", + "text/html": "HTML Document" }.freeze + def file_type_size_and_pages(file, number_of_pages: nil) - extension_mapping = { - "xlsx" => "Microsoft Excel", - "pdf" => "PDF", - } - extension = File.extname(file)[1..] + file_pages = number_of_pages ? pluralize(number_of_pages, "page") : nil + metadata = CollectionResourcesService.new.get_file_metadata(file) - file_type = extension_mapping.fetch(extension, extension) + return [file_pages].compact.join(", ") unless metadata - file_size = number_to_human_size(File.size("public/files/#{file}"), precision: 0, significant: false) - file_pages = number_of_pages ? pluralize(number_of_pages, "page") : nil + file_size = number_to_human_size(metadata["content_length"].to_i) + file_type = HUMAN_READABLE_CONTENT_TYPE[metadata["content_type"].to_sym] || "Unknown File Type" [file_type, file_size, file_pages].compact.join(", ") end end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index c090a4a41..e43614d45 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -5,18 +5,21 @@ module FiltersHelper return false unless session[session_name_for(filter_type)] selected_filters = JSON.parse(session[session_name_for(filter_type)]) - return true if !selected_filters.key?("user") && filter == "assigned_to" && value == :all - return true if selected_filters["assigned_to"] == "specific_user" && filter == "assigned_to" && value == :specific_user - return true if !selected_filters.key?("owning_organisation") && filter == "owning_organisation_select" && value == :all - return true if !selected_filters.key?("managing_organisation") && filter == "managing_organisation_select" && value == :all - - return true if (selected_filters["owning_organisation"].present? || selected_filters["owning_organisation_text_search"].present?) && filter == "owning_organisation_select" && value == :specific_org - return true if (selected_filters["managing_organisation"].present? || selected_filters["managing_organisation_text_search"].present?) && filter == "managing_organisation_select" && value == :specific_org - - return false if selected_filters[filter].blank? - - selected_filters[filter].include?(value.to_s) + case filter + when "assigned_to" + assigned_to_filter_selected?(selected_filters, value) + when "owning_organisation_select" + owning_organisation_filter_selected?(selected_filters, value) + when "managing_organisation_select" + managing_organisation_filter_selected?(selected_filters, value) + when "uploaded_by" + uploaded_by_filter_selected?(selected_filters, value) + when "uploading_organisation_select" + uploading_organisation_filter_selected?(selected_filters, value) + else + selected_filters[filter]&.include?(value.to_s) || false + end end def any_filter_selected?(filter_type) @@ -119,6 +122,11 @@ module FiltersHelper [OpenStruct.new(id: "", name: "Select an option", hint: "")] end + def uploaded_by_filter_options + user_options = User.all + [OpenStruct.new(id: "", name: "Select an option", hint: "")] + user_options.map { |user_option| OpenStruct.new(id: user_option.id, name: user_option.name, hint: user_option.email) } + end + def filter_search_url(category) case category when :user @@ -270,7 +278,7 @@ private filters.each.sum do |category, category_filters| if %w[years status needstypes bulk_upload_id].include?(category) category_filters.count(&:present?) - elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search].include?(category) + elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category) 1 else 0 @@ -335,4 +343,34 @@ private def unanswered_filter_value "You didn’t answer this filter".html_safe end + + def assigned_to_filter_selected?(selected_filters, value) + return true if !selected_filters.key?("user") && value == :all + + selected_filters["assigned_to"] == value.to_s + end + + def owning_organisation_filter_selected?(selected_filters, value) + return true if !selected_filters.key?("owning_organisation") && value == :all + + (selected_filters["owning_organisation"].present? || selected_filters["owning_organisation_text_search"].present?) && value == :specific_org + end + + def managing_organisation_filter_selected?(selected_filters, value) + return true if !selected_filters.key?("managing_organisation") && value == :all + + (selected_filters["managing_organisation"].present? || selected_filters["managing_organisation_text_search"].present?) && value == :specific_org + end + + def uploaded_by_filter_selected?(selected_filters, value) + return true if !selected_filters.key?("user") && value == :all + + selected_filters["uploaded_by"] == value.to_s + end + + def uploading_organisation_filter_selected?(selected_filters, value) + return true if !selected_filters.key?("uploading_organisation") && value == :all + + (selected_filters["uploading_organisation"].present? || selected_filters["uploading_organisation_text_search"].present?) && value == :specific_org + end end diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index 0e318d283..7efb9fffd 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -85,6 +85,14 @@ module SchemesHelper end end + def display_duplicate_schemes_banner?(organisation, current_user) + return unless organisation.absorbed_organisations.merged_during_open_collection_period.any? + return unless current_user.data_coordinator? || current_user.support? + return if organisation.schemes_deduplicated_at.present? && organisation.schemes_deduplicated_at > organisation.absorbed_organisations.map(&:merge_date).max + + organisation.owned_schemes.duplicate_sets.any? || organisation.owned_schemes.any? { |scheme| scheme.locations.duplicate_sets.any? } + end + private ActivePeriod = Struct.new(:from, :to) diff --git a/app/helpers/tag_helper.rb b/app/helpers/tag_helper.rb index 3c2e332f6..bc0d8e06b 100644 --- a/app/helpers/tag_helper.rb +++ b/app/helpers/tag_helper.rb @@ -19,6 +19,14 @@ module TagHelper request_merged: "Merged", ready_to_merge: "Ready to merge", processing: "Processing", + blank_template: "Blank template", + wrong_template: "Wrong template used", + important_errors: "Errors on important questions in CSV", + critical_errors: "Critical errors in CSV", + potential_errors: "Potential errors in CSV", + logs_uploaded_with_errors: "Logs uploaded with errors", + errors_fixed_in_service: "Errors fixed on site", + logs_uploaded_no_errors: "Logs uploaded with no errors", }.freeze COLOUR = { @@ -39,6 +47,14 @@ module TagHelper request_merged: "green", ready_to_merge: "blue", processing: "yellow", + blank_template: "red", + wrong_template: "red", + important_errors: "red", + critical_errors: "red", + potential_errors: "red", + logs_uploaded_with_errors: "blue", + errors_fixed_in_service: "green", + logs_uploaded_no_errors: "green", }.freeze def status_tag(status, classes = []) diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb index 69ab42871..ad76c2192 100644 --- a/app/models/bulk_upload.rb +++ b/app/models/bulk_upload.rb @@ -1,6 +1,7 @@ class BulkUpload < ApplicationRecord enum log_type: { lettings: "lettings", sales: "sales" } enum rent_type_fix_status: { not_applied: "not_applied", applied: "applied", not_needed: "not_needed" } + enum failure_reason: { blank_template: "blank_template", wrong_template: "wrong_template" } belongs_to :user @@ -10,12 +11,53 @@ class BulkUpload < ApplicationRecord has_many :sales_logs after_initialize :generate_identifier, unless: :identifier + after_initialize :initialize_processing, if: :new_record? + + scope :search_by_filename, ->(filename) { where("filename ILIKE ?", "%#{filename}%") } + scope :search_by_user_name, ->(name) { where(user_id: User.where("name ILIKE ?", "%#{name}%").select(:id)) } + scope :search_by_user_email, ->(email) { where(user_id: User.where("email ILIKE ?", "%#{email}%").select(:id)) } + scope :search_by_organisation_name, ->(name) { where(organisation_id: Organisation.where("name ILIKE ?", "%#{name}%").select(:id)) } + + scope :search_by, lambda { |param| + search_by_filename(param) + .or(search_by_user_name(param)) + .or(search_by_user_email(param)) + .or(search_by_organisation_name(param)) + } + + scope :filter_by_id, ->(id) { where(id:) } + scope :filter_by_years, ->(years, _user = nil) { where(year: years) } + scope :filter_by_uploaded_by, ->(user_id, _user = nil) { where(user_id:) } + scope :filter_by_user_text_search, ->(param, _user = nil) { where(user_id: User.search_by(param).select(:id)) } + scope :filter_by_user, ->(user_id, _user = nil) { user_id.present? ? where(user_id:) : all } + scope :filter_by_uploading_organisation, ->(organisation_id, _user = nil) { where(organisation_id:) } def completed? incomplete_logs = logs.where.not(status: "completed") !incomplete_logs.exists? end + def status + return :processing if processing + return :blank_template if failure_reason == "blank_template" + return :wrong_template if failure_reason == "wrong_template" + + if logs.visible.exists? + return :errors_fixed_in_service if completed? && bulk_upload_errors.any? + return :logs_uploaded_with_errors if bulk_upload_errors.any? + end + + if bulk_upload_errors.important.any? + :important_errors + elsif bulk_upload_errors.critical.any? + :critical_errors + elsif bulk_upload_errors.potential.any? + :potential_errors + else + :logs_uploaded_no_errors + end + end + def year_combo "#{year}/#{year - 2000 + 1}" end @@ -105,9 +147,17 @@ class BulkUpload < ApplicationRecord User.find_by(id: moved_user_id)&.name end + def organisation + Organisation.find_by(id: organisation_id) + end + private def generate_identifier self.identifier ||= SecureRandom.uuid end + + def initialize_processing + self.processing = true if processing.nil? + end end diff --git a/app/models/bulk_upload_error.rb b/app/models/bulk_upload_error.rb index c9ca14b0f..154fca2fc 100644 --- a/app/models/bulk_upload_error.rb +++ b/app/models/bulk_upload_error.rb @@ -4,4 +4,8 @@ class BulkUploadError < ApplicationRecord scope :order_by_row, -> { order("row::integer ASC") } scope :order_by_cell, -> { order(Arel.sql("LPAD(cell, 10, '0')")) } scope :order_by_col, -> { order(Arel.sql("LPAD(col, 10, '0')")) } + scope :important, -> { where(category: "setup") } + scope :potential, -> { where(category: "soft_validation") } + scope :critical, -> { where(category: nil).or(where.not(category: %w[setup soft_validation])) } + scope :critical_or_important, -> { critical.or(important) } end diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb index 159436ce1..984451dbb 100644 --- a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -35,25 +35,25 @@ module Forms def legacy_template_path case year when 2023 - "/files/bulk-upload-lettings-legacy-template-2023-24.xlsx" + download_23_24_lettings_bulk_upload_legacy_template_path end end def template_path case year when 2023 - "/files/bulk-upload-lettings-template-2023-24.xlsx" + download_23_24_lettings_bulk_upload_template_path when 2024 - "/files/bulk-upload-lettings-template-2024-25.xlsx" + download_24_25_lettings_bulk_upload_template_path end end def specification_path case year when 2023 - "/files/bulk-upload-lettings-specification-2023-24.xlsx" + download_23_24_lettings_bulk_upload_specification_path when 2024 - "/files/bulk-upload-lettings-specification-2024-25.xlsx" + download_24_25_lettings_bulk_upload_specification_path end end diff --git a/app/models/forms/bulk_upload_sales/prepare_your_file.rb b/app/models/forms/bulk_upload_sales/prepare_your_file.rb index 4bf0797a8..d6d5276c2 100644 --- a/app/models/forms/bulk_upload_sales/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_sales/prepare_your_file.rb @@ -34,25 +34,25 @@ module Forms def legacy_template_path case year when 2023 - "/files/bulk-upload-sales-legacy-template-2023-24.xlsx" + download_23_24_sales_bulk_upload_legacy_template_path end end def template_path case year when 2023 - "/files/bulk-upload-sales-template-2023-24.xlsx" + download_23_24_sales_bulk_upload_template_path when 2024 - "/files/bulk-upload-sales-template-2024-25.xlsx" + download_24_25_sales_bulk_upload_template_path end end def specification_path case year when 2023 - "/files/bulk-upload-sales-specification-2023-24.xlsx" + download_23_24_sales_bulk_upload_specification_path when 2024 - "/files/bulk-upload-sales-specification-2024-25.xlsx" + download_24_25_sales_bulk_upload_specification_path end end diff --git a/app/models/log.rb b/app/models/log.rb index cb66074cf..aed375117 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -130,7 +130,10 @@ class Log < ApplicationRecord if [address_line1_input, postcode_full_input].all?(&:present?) service = AddressClient.new(address_string) service.call - return nil if service.result.blank? || service.error.present? + if service.result.blank? || service.error.present? + @address_options = [] + return @answer_options + end address_opts = [] service.result.first(10).each do |result| diff --git a/app/models/user.rb b/app/models/user.rb index a32748167..0d3bc4846 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -224,12 +224,26 @@ class User < ApplicationRecord def logs_filters(specific_org: false) if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners? - %w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id user_text_search owning_organisation_text_search managing_organisation_text_search] + %w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id user_text_search owning_organisation_text_search managing_organisation_text_search] else %w[years status needstypes assigned_to user bulk_upload_id user_text_search] end end + def scheme_filters(specific_org: false) + if (support? && !specific_org) || organisation.has_managing_agents? || organisation.has_stock_owners? + %w[status owning_organisation owning_organisation_text_search] + else + %w[status] + end + end + + def bulk_uploads_filters(specific_org: false) + return [] unless support? && !specific_org + + %w[user years uploaded_by uploading_organisation user_text_search uploading_organisation_text_search] + end + delegate :name, to: :organisation, prefix: true def self.download_attributes diff --git a/app/models/validations/financial_validations.rb b/app/models/validations/financial_validations.rb index 53e50a92f..724fa9b6e 100644 --- a/app/models/validations/financial_validations.rb +++ b/app/models/validations/financial_validations.rb @@ -121,6 +121,11 @@ module Validations::FinancialValidations def validate_rent_amount(record) if record.wtshortfall + if record.is_supported_housing? && record.wchchrg && (record.wtshortfall > record.wchchrg) + record.errors.add :tshortfall, message: I18n.t("validations.financial.tshortfall.more_than_carehome_charge") + record.errors.add :chcharge, I18n.t("validations.financial.carehome.less_than_shortfall") + end + if record.wtcharge && (record.wtshortfall > record.wtcharge) record.errors.add :tshortfall, :more_than_rent, message: I18n.t("validations.financial.tshortfall.more_than_total_charge") record.errors.add :tcharge, I18n.t("validations.financial.tcharge.less_than_shortfall") diff --git a/app/policies/organisation_policy.rb b/app/policies/organisation_policy.rb index 9c27d6e91..9c5fc4449 100644 --- a/app/policies/organisation_policy.rb +++ b/app/policies/organisation_policy.rb @@ -34,4 +34,12 @@ class OrganisationPolicy editable_sales_logs = organisation.sales_logs.visible.after_date(editable_from_date) organisation.sales_logs.visible.where(saledate: nil).any? || editable_sales_logs.any? end + + def duplicate_schemes? + user.support? || (user.data_coordinator? && user.organisation == organisation) + end + + def confirm_duplicate_schemes? + duplicate_schemes? + end end diff --git a/app/services/bulk_upload/downloader.rb b/app/services/bulk_upload/downloader.rb index a7dc9aad0..303b14070 100644 --- a/app/services/bulk_upload/downloader.rb +++ b/app/services/bulk_upload/downloader.rb @@ -15,6 +15,10 @@ class BulkUpload::Downloader file.unlink end + def presigned_url + s3_storage_service.get_presigned_url(bulk_upload.identifier, 60, response_content_disposition: "attachment; filename=#{bulk_upload.filename}") + end + private def download diff --git a/app/services/bulk_upload/processor.rb b/app/services/bulk_upload/processor.rb index 3fbb1e2d4..83a8d1ca0 100644 --- a/app/services/bulk_upload/processor.rb +++ b/app/services/bulk_upload/processor.rb @@ -1,6 +1,16 @@ class BulkUpload::Processor attr_reader :bulk_upload + BLANK_TEMPLATE_ERRORS = [ + I18n.t("activemodel.errors.models.bulk_upload/lettings/validator.attributes.base.blank_file"), + I18n.t("activemodel.errors.models.bulk_upload/sales/validator.attributes.base.blank_file"), + ].freeze + + WRONG_TEMPLATE_ERRORS = [ + *I18n.t("activemodel.errors.models.bulk_upload/lettings/validator.attributes.base", default: {}).values, + *I18n.t("activemodel.errors.models.bulk_upload/sales/validator.attributes.base", default: {}).values, + ].freeze + def initialize(bulk_upload:) @bulk_upload = bulk_upload end @@ -11,7 +21,7 @@ class BulkUpload::Processor download @bulk_upload.update!(total_logs_count: validator.total_logs_count) - return send_failure_mail(errors: validator.errors.full_messages) if validator.invalid? + return handle_invalid_validator if validator.invalid? validator.call @@ -37,6 +47,7 @@ class BulkUpload::Processor send_failure_mail ensure downloader.delete_local_file! + bulk_upload.update!(processing: false) end def approve @@ -144,4 +155,14 @@ private raise "Validator not found for #{bulk_upload.log_type}" end end + + def handle_invalid_validator + if BLANK_TEMPLATE_ERRORS.any? { |error| validator.errors.full_messages.include?(error) } + @bulk_upload.update!(failure_reason: "blank_template") + elsif WRONG_TEMPLATE_ERRORS.any? { |error| validator.errors.full_messages.include?(error) } + @bulk_upload.update!(failure_reason: "wrong_template") + end + + send_failure_mail(errors: validator.errors.full_messages) + end end diff --git a/app/services/bulk_upload/sales/validator.rb b/app/services/bulk_upload/sales/validator.rb index 777424349..a473a6461 100644 --- a/app/services/bulk_upload/sales/validator.rb +++ b/app/services/bulk_upload/sales/validator.rb @@ -43,12 +43,12 @@ class BulkUpload::Sales::Validator return false if any_setup_errors? if row_parsers.any?(&:block_log_creation?) - Sentry.capture_exception("Bulk upload log creation blocked: #{bulk_upload.id}.") + Sentry.capture_message("Bulk upload log creation blocked: #{bulk_upload.id}.") return false end if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled? - Sentry.capture_exception("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.") + Sentry.capture_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.") return false end @@ -57,7 +57,7 @@ class BulkUpload::Sales::Validator end if any_logs_invalid? - Sentry.capture_exception("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") + Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.") return false end diff --git a/app/services/collection_resources_service.rb b/app/services/collection_resources_service.rb new file mode 100644 index 000000000..50e1fc59a --- /dev/null +++ b/app/services/collection_resources_service.rb @@ -0,0 +1,21 @@ +class CollectionResourcesService + def initialize + @storage_service = if FeatureToggle.local_storage? + Storage::LocalDiskService.new + else + Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["COLLECTION_RESOURCES_BUCKET"]) + end + end + + def get_file(file) + @storage_service.get_file_io(file) + rescue StandardError + nil + end + + def get_file_metadata(file) + @storage_service.get_file_metadata(file) + rescue StandardError + nil + end +end diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index 91989ff86..f63eceaef 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -34,4 +34,8 @@ class FeatureToggle def self.delete_user_enabled? true end + + def self.local_storage? + Rails.env.development? + end end diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb index 9f68a097c..7757ad39e 100644 --- a/app/services/filter_manager.rb +++ b/app/services/filter_manager.rb @@ -76,6 +76,21 @@ class FilterManager locations.order(created_at: :desc) end + def self.filter_uploads(uploads, search_term, filters, all_orgs, user) + uploads = filter_by_search(uploads, search_term) + + filters.each do |category, values| + next if Array(values).reject(&:empty?).blank? + next if category == "uploading_organisation" && all_orgs + next if category == "uploading_organisation_text_search" && all_orgs + next if category == "uploaded_by" + next if category == "uploaded_by_text_search" && filters["uploaded_by"] != "specific_user" + + uploads = uploads.public_send("filter_by_#{category}", values, user) + end + uploads.order(created_at: :desc) + end + def serialize_filters_to_session(specific_org: false) session[session_name_for(filter_type)] = session_filters(specific_org:).to_json end @@ -91,7 +106,6 @@ class FilterManager else {} end - if filter_type.include?("logs") current_user.logs_filters(specific_org:).each do |filter| new_filters[filter] = params[filter] if params[filter].present? @@ -117,13 +131,21 @@ class FilterManager end if filter_type.include?("schemes") - current_user.logs_filters(specific_org:).each do |filter| + current_user.scheme_filters(specific_org:).each do |filter| new_filters[filter] = params[filter] if params[filter].present? end new_filters = new_filters.except("owning_organisation") if params["owning_organisation_select"] == "all" end + if filter_type.include?("bulk_uploads") + current_user.bulk_uploads_filters(specific_org:).each do |filter| + new_filters[filter] = params[filter] if params[filter].present? + end + new_filters = new_filters.except("uploading_organisation") if params["uploading_organisation_select"] == "all" + new_filters = new_filters.except("user") if params["uploaded_by"] == "all" + new_filters["user"] = current_user.id.to_s if params["uploaded_by"] == "you" + end new_filters end @@ -152,6 +174,12 @@ class FilterManager @bulk_upload ||= current_user.bulk_uploads.find_by(id:) end + def filtered_uploads(uploads, search_term, filters) + all_orgs = params["uploading_organisation_select"] == "all" + + FilterManager.filter_uploads(uploads, search_term, filters, all_orgs, current_user) + end + private def logs_filters diff --git a/app/services/storage/local_disk_service.rb b/app/services/storage/local_disk_service.rb index f0cc358d1..cd99d3d48 100644 --- a/app/services/storage/local_disk_service.rb +++ b/app/services/storage/local_disk_service.rb @@ -22,5 +22,14 @@ module Storage f.write data end end + + def get_file_metadata(filename) + path = Rails.root.join("tmp/storage", filename) + + { + "content_length" => File.size(path), + "content_type" => MiniMime.lookup_by_filename(path.to_s)&.content_type || "application/octet-stream", + } + end end end diff --git a/app/services/storage/s3_service.rb b/app/services/storage/s3_service.rb index 3592eaa67..de2f6ef65 100644 --- a/app/services/storage/s3_service.rb +++ b/app/services/storage/s3_service.rb @@ -20,10 +20,10 @@ module Storage response.key_count == 1 end - def get_presigned_url(file_name, duration) + def get_presigned_url(file_name, duration, response_content_disposition: nil) Aws::S3::Presigner .new({ client: @client }) - .presigned_url(:get_object, bucket: @configuration.bucket_name, key: file_name, expires_in: duration) + .presigned_url(:get_object, bucket: @configuration.bucket_name, key: file_name, expires_in: duration, response_content_disposition:) end def get_file_io(file_name) @@ -39,6 +39,10 @@ module Storage ) end + def get_file_metadata(file_name) + @client.head_object(bucket: @configuration.bucket_name, key: file_name) + end + private def create_configuration diff --git a/app/views/bulk_upload_lettings_results/show.html.erb b/app/views/bulk_upload_lettings_results/show.html.erb index 15c486b91..fd49c8f3e 100644 --- a/app/views/bulk_upload_lettings_results/show.html.erb +++ b/app/views/bulk_upload_lettings_results/show.html.erb @@ -13,7 +13,14 @@ Here’s a list of everything that you need to fix your spreadsheet. You can download the <%= govuk_link_to "specification", Forms::BulkUploadLettings::PrepareYourFile.new(year: @bulk_upload.year).specification_path, target: "_blank" %> to help you fix the cells in your CSV file. -

File: <%= @bulk_upload.filename %>

+

File name: <%= @bulk_upload.filename %>

+ + <% if current_user.support? %> +
+ <%= govuk_link_to "Download file", download_lettings_bulk_upload_path(@bulk_upload) %> +
+ <% end %> + diff --git a/app/views/bulk_upload_lettings_results/summary.html.erb b/app/views/bulk_upload_lettings_results/summary.html.erb index 8a59e8999..2e4fa91fc 100644 --- a/app/views/bulk_upload_lettings_results/summary.html.erb +++ b/app/views/bulk_upload_lettings_results/summary.html.erb @@ -5,13 +5,18 @@ Bulk upload for lettings (<%= @bulk_upload.year_combo %>)

Fix <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> and upload file again

-

+

We could not create logs from your bulk upload because of the following errors. Download the <%= govuk_link_to "specification", Forms::BulkUploadLettings::PrepareYourFile.new(year: @bulk_upload.year).specification_path, target: "_blank" %> to help you fix the cells in your CSV file.

-

- File: <%= @bulk_upload.filename %> -

+

File name: <%= @bulk_upload.filename %>

+ + <% if current_user.support? %> +
+ <%= govuk_link_to "Download file", download_lettings_bulk_upload_path(@bulk_upload) %> +
+ <% end %> + diff --git a/app/views/bulk_upload_sales_results/show.html.erb b/app/views/bulk_upload_sales_results/show.html.erb index 776fdfa2f..6d0863617 100644 --- a/app/views/bulk_upload_sales_results/show.html.erb +++ b/app/views/bulk_upload_sales_results/show.html.erb @@ -13,7 +13,14 @@ Here’s a list of everything that you need to fix your spreadsheet. You can download the <%= govuk_link_to "specification", Forms::BulkUploadSales::PrepareYourFile.new(year: @bulk_upload.year).specification_path, target: "_blank" %> to help you fix the cells in your CSV file. -

File: <%= @bulk_upload.filename %>

+

File name: <%= @bulk_upload.filename %>

+ + <% if current_user.support? %> +
+ <%= govuk_link_to "Download file", download_sales_bulk_upload_path(@bulk_upload) %> +
+ <% end %> + diff --git a/app/views/bulk_upload_sales_results/summary.html.erb b/app/views/bulk_upload_sales_results/summary.html.erb index 0e423621d..171cbf77f 100644 --- a/app/views/bulk_upload_sales_results/summary.html.erb +++ b/app/views/bulk_upload_sales_results/summary.html.erb @@ -5,13 +5,18 @@ Bulk upload for sales (<%= @bulk_upload.year_combo %>)

Fix <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> and upload file again

-

+

We could not create logs from your bulk upload because of the following errors. Download the <%= govuk_link_to "specification", Forms::BulkUploadSales::PrepareYourFile.new(year: @bulk_upload.year).specification_path, target: "_blank" %> to help you fix the cells in your CSV file.

-

- File: <%= @bulk_upload.filename %> -

+

File name: <%= @bulk_upload.filename %>

+ + <% if current_user.support? %> +
+ <%= govuk_link_to "Download file", download_sales_bulk_upload_path(@bulk_upload) %> +
+ <% end %> + diff --git a/app/views/bulk_upload_shared/_upload_filters.html.erb b/app/views/bulk_upload_shared/_upload_filters.html.erb new file mode 100644 index 000000000..39a51fa4b --- /dev/null +++ b/app/views/bulk_upload_shared/_upload_filters.html.erb @@ -0,0 +1,78 @@ +
+
+
+

Filters

+
+ +
+ <%= form_with html: { method: :get } do |f| %> + +
+

+ <%= filters_applied_text(@filter_type) %> +

+

+ <%= reset_filters_link(@filter_type, { search: request.params["search"] }.compact) %> +

+
+ + <%= render partial: "filters/checkbox_filter", + locals: { + f:, + options: collection_year_options, + label: "Collection year", + category: "years", + size: "s", + } %> + + <%= render partial: "filters/radio_filter", + locals: { + f:, + options: { + "all": { label: "Any user" }, + "you": { label: "You" }, + "specific_user": { + label: "Specific user", + conditional_filter: { + type: "text_select", + label: "User", + category: "user", + options: uploaded_by_filter_options, + caption_text: "User's name or email", + }, + }, + }, + label: "Uploaded by", + category: "uploaded_by", + size: "s", + } %> + + <%= render partial: "filters/radio_filter", locals: { + f:, + options: { + "all": { label: "Any organisation" }, + "specific_org": { + label: "Specific organisation", + conditional_filter: { + type: "select", + label: "Uploading Organisation", + category: "uploading_organisation", + options: all_owning_organisation_filter_options(current_user), + caption_text: "Organisation name", + }, + }, + }, + label: "Uploading organisation", + category: "uploading_organisation_select", + size: "s", + } %> + + <% if request.params["search"].present? %> + <%= f.hidden_field :search, value: request.params["search"] %> + <% end %> + + <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> + <% end %> +
+
+
diff --git a/app/views/bulk_upload_shared/_upload_list.html.erb b/app/views/bulk_upload_shared/_upload_list.html.erb new file mode 100644 index 000000000..7b6057228 --- /dev/null +++ b/app/views/bulk_upload_shared/_upload_list.html.erb @@ -0,0 +1,15 @@ +

+
+
+ <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "files uploaded in the last 30 days", filters_count: applied_filters_count(@filter_type))) %> +
+
+ <% if searched || applied_filters_count(@filter_type).positive? %> +
+ <% end %> +
+
+

+<% bulk_uploads.map do |bulk_upload| %> + <%= render BulkUploadSummaryComponent.new(bulk_upload:) %> +<% end %> diff --git a/app/views/bulk_upload_shared/uploads.html.erb b/app/views/bulk_upload_shared/uploads.html.erb new file mode 100644 index 000000000..958887453 --- /dev/null +++ b/app/views/bulk_upload_shared/uploads.html.erb @@ -0,0 +1,28 @@ +<% item_label = format_label(@pagy.count, "uploads") %> +<% title = format_title(@searched, bulk_upload_title(controller.controller_name), current_user, item_label, @pagy.count, nil) %> + +<% content_for :title, title %> + +

+ <%= bulk_upload_title(controller.controller_name) %> +

+ +
+ <%= render partial: "bulk_upload_shared/upload_filters" %> + +
+ <%= render SearchComponent.new(current_user:, search_label: "Search by file name, user's name or email, or organisation", value: @searched) %> + <%= govuk_section_break(visible: true, size: "m") %> + <%= render partial: "bulk_upload_shared/upload_list", + locals: { + bulk_uploads: @bulk_uploads, + title: "Bulk uploads", + pagy: @pagy, + searched: @searched, + item_label:, + total_count: @total_count, + filter_type: @filter_type, + } %> + <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "bulk uploads" } %> +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9e68fa7b4..d235491a1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -60,9 +60,6 @@ <% end %> - <% if Rails.env.production? && ENV["APP_HOST"].present? %> - - <% end %> diff --git a/app/views/logs/_create_for_org_actions.html.erb b/app/views/logs/_create_for_org_actions.html.erb index 0564e678a..9b5935cb0 100644 --- a/app/views/logs/_create_for_org_actions.html.erb +++ b/app/views/logs/_create_for_org_actions.html.erb @@ -1,12 +1,14 @@
<% if @organisation.data_protection_confirmed? %> <% if current_page?(controller: 'organisations', action: 'lettings_logs') %> - <%= govuk_button_to "Create a new lettings log for this organisation", lettings_logs_path(lettings_log: { owning_organisation_id: @organisation.id }, method: :post), class: "govuk-!-margin-right-6" %> - <%= govuk_button_link_to "Upload lettings logs in bulk", bulk_upload_lettings_log_path(id: "start", organisation_id: @organisation.id), secondary: true %> + <%= govuk_button_to "Create a new lettings log", lettings_logs_path(lettings_log: { owning_organisation_id: @organisation.id }, method: :post), class: "govuk-!-margin-right-6" %> + <%= govuk_button_link_to "Upload lettings logs in bulk", bulk_upload_lettings_log_path(id: "start", organisation_id: @organisation.id), secondary: true, class: "govuk-!-margin-right-6" %> + <%= govuk_button_link_to "View lettings bulk uploads", bulk_uploads_lettings_logs_path(organisation_id: @organisation.id, clear_old_filters: true), secondary: true %> <% end %> <% if current_page?(controller: 'organisations', action: 'sales_logs') %> - <%= govuk_button_to "Create a new sales log for this organisation", sales_logs_path(sales_log: { owning_organisation_id: @organisation.id }, method: :post), class: "govuk-!-margin-right-6" %> - <%= govuk_button_link_to "Upload sales logs in bulk", bulk_upload_sales_log_path(id: "start", organisation_id: @organisation.id), secondary: true %> + <%= govuk_button_to "Create a new sales log", sales_logs_path(sales_log: { owning_organisation_id: @organisation.id }, method: :post), class: "govuk-!-margin-right-6" %> + <%= govuk_button_link_to "Upload sales logs in bulk", bulk_upload_sales_log_path(id: "start", organisation_id: @organisation.id), secondary: true, class: "govuk-!-margin-right-6" %> + <%= govuk_button_link_to "View sales bulk uploads", bulk_uploads_sales_logs_path(organisation_id: @organisation.id, clear_old_filters: true), secondary: true %> <% end %> <% end %>
diff --git a/app/views/organisations/duplicate_schemes.html.erb b/app/views/organisations/duplicate_schemes.html.erb new file mode 100644 index 000000000..427cf427c --- /dev/null +++ b/app/views/organisations/duplicate_schemes.html.erb @@ -0,0 +1,138 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: schemes_organisation_path(@organisation) %> +<% end %> +<%= form_with model: @organisation, url: schemes_duplicates_organisation_path(@organisation), method: "post" do |f| %> + <%= f.govuk_error_summary %> + + <% if @duplicate_schemes.any? %> + <% if @duplicate_locations.any? %> + <% title = "Review these sets of schemes and locations" %> + <% else %> + <% title = "Review these sets of schemes" %> + <% end %> + <% else %> + <% title = "Review these sets of locations" %> + <% end %> + + <% content_for :title, title %> + + <% if current_user.support? %> + <%= render SubNavigationComponent.new( + items: secondary_items(request.path, @organisation.id), + ) %> + <% end %> + +
+
+

<%= title %>

+ +

Since your organisation recently merged, we’ve reviewed your schemes for possible duplicates.

+

These sets of schemes and locations might be duplicates because they have the same answers for certain fields.

+

What you need to do

+ +

If you need help with this, <%= govuk_link_to "contact the helpdesk (opens in a new tab)", GlobalConstants::HELPDESK_URL, target: "#" %>.

+ + <% if @duplicate_schemes.any? %> +

<%= @duplicate_schemes.count == 1 ? "This set" : "These #{@duplicate_schemes.count} sets" %> of schemes might have duplicates

+ + <%= govuk_details(summary_text: "Why are these schemes identified as duplicates?") do %> +

+ These schemes have the same answers for the following fields: +

+ + <% end %> + + <%= govuk_table do |table| %> + <%= table.with_head do |head| %> + <% head.with_row do |row| %> + <% row.with_cell(header: true, text: "Schemes") %> + <% end %> + + <%= table.with_body do |body| %> + <% @duplicate_schemes.each do |duplicate_set| %> + <% body.with_row do |row| %> + <% row.with_cell do %> +
    + <% duplicate_set.each do |scheme| %> +
  1. + <%= govuk_link_to scheme.service_name, scheme %> +
  2. + <% end %> +
+ <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + + <% if @duplicate_locations.any? %> +

<%= @duplicate_locations.count == 1 ? "This set" : "These #{@duplicate_locations.count} sets" %> of locations might have duplicates

+ <%= govuk_details(summary_text: "Why are these locations identified as duplicates?") do %> +

+ These locations belong to the same scheme and have the same answers for the following fields: +

+ + <% end %> + + <%= govuk_table do |table| %> + <%= table.with_head do |head| %> + <% head.with_row do |row| %> + <% row.with_cell(header: true, text: "Locations") %> + <% row.with_cell(header: true, text: "Scheme") %> + <% end %> + + <%= table.with_body do |body| %> + <% @duplicate_locations.each do |duplicate_set| %> + <% body.with_row do |row| %> + <% row.with_cell do %> +
    + <% duplicate_set[:locations].each do |location| %> +
  1. + <%= govuk_link_to location.name, scheme_location_path(location) %> +
  2. + <% end %> +
+ <% end %> + <% row.with_cell do %> + <%= govuk_link_to duplicate_set[:scheme].service_name, duplicate_set[:scheme] %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + + <%= f.govuk_check_boxes_fieldset :scheme_duplicates_checked, + legend: { text: "Have you resolved all duplicates?" } do %> + <%= f.govuk_check_box :scheme_duplicates_checked, + true, + false, + multiple: false, + checked: false, + label: { text: "Yes, none of the schemes and locations above are duplicates" } %> + <% end %> + + <%= f.govuk_submit "Confirm" %> +
+
+<% end %> diff --git a/app/views/organisations/index.html.erb b/app/views/organisations/index.html.erb index 3b96288f1..411d792c1 100644 --- a/app/views/organisations/index.html.erb +++ b/app/views/organisations/index.html.erb @@ -6,7 +6,7 @@ <%= render partial: "organisations/headings", locals: request.path == organisations_path ? { main: "Organisations", sub: nil } : { main: @organisation.name, sub: "Organisations" } %>
- <%= govuk_tabs(title: "Collection resources", classes: %w[app-tab__large-headers]) do |c| %> + <%= govuk_tabs(title: "Organisations", classes: %w[app-tab__large-headers]) do |c| %> <% c.with_tab(label: "All organisations") do %> <%= govuk_button_link_to "Create a new organisation", new_organisation_path, html: { method: :get } %> <%= render SearchComponent.new(current_user:, search_label: "Search by organisation name", value: @searched) %> diff --git a/app/views/organisations/schemes.html.erb b/app/views/organisations/schemes.html.erb index 58b16243a..b9706d4db 100644 --- a/app/views/organisations/schemes.html.erb +++ b/app/views/organisations/schemes.html.erb @@ -11,6 +11,16 @@ ) %>

Supported housing schemes

<% end %> + +<% if display_duplicate_schemes_banner?(@organisation, current_user) %> + <%= govuk_notification_banner(title_text: "Important") do %> +

+ Some schemes and locations might be duplicates. +

+ <%= govuk_link_to "Review possible duplicates", href: schemes_duplicates_organisation_path(@organisation) %> + <% end %> +<% end %> +

<% if SchemePolicy.new(current_user, nil).create? %> <%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %> diff --git a/config/locales/en.yml b/config/locales/en.yml index b788bcb1c..764b5d687 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -37,6 +37,7 @@ en: updated: "Organisation details updated." reactivated: "%{organisation} has been reactivated." deactivated: "%{organisation} has been deactivated." + duplicate_schemes_confirmed: "You’ve confirmed the remaining schemes and locations are not duplicates." user: create_password: "Create a password to finish setting up your account." reset_password: "Reset your password." @@ -229,6 +230,7 @@ en: blank: "You must choose a managing agent." already_added: "You have already added this managing agent." merged: "That organisation has already been merged. Select a different organisation." + scheme_duplicates_not_resolved: "You must resolve all duplicates or indicate that there are no duplicates" not_answered: "You must answer %{question}" invalid_option: "Enter a valid value for %{question}" invalid_number: "Enter a number for %{question}" @@ -382,6 +384,7 @@ en: tshortfall: outstanding_amount_not_expected: "You cannot answer the outstanding amount question if you don’t have outstanding rent or charges." more_than_total_charge: "Enter a value less than the total charge." + more_than_carehome_charge: "Enter a value less than the care home charge." must_be_positive: "Enter a value over £0.01 as you told us there is an outstanding amount." hbrentshortfall: outstanding_amount_not_expected: "Answer must be ‘yes’ as you have answered the outstanding amount question." @@ -454,6 +457,7 @@ en: carehome: out_of_range: "Household rent and other charges must be between %{min_chcharge} and %{max_chcharge} if paying %{period}." not_provided: "Enter how much rent and other charges the household pays %{period}." + less_than_shortfall: "The care home charge must be more than the outstanding amount." cash_discount_invalid: "Cash discount must be £0 - £999,999." staircasing: percentage_bought_must_be_greater_than_percentage_owned: "Total percentage %{buyer_now_owns} must be more than percentage bought in this transaction." diff --git a/config/routes.rb b/config/routes.rb index 77b862e5a..7ba2dabed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -178,6 +178,8 @@ Rails.application.routes.draw do get "schemes/csv-download", to: "organisations#download_schemes_csv" post "schemes/email-csv", to: "organisations#email_schemes_csv" get "schemes/csv-confirmation", to: "schemes#csv_confirmation" + get "schemes/duplicates", to: "organisations#duplicate_schemes" + post "schemes/duplicates", to: "organisations#confirm_duplicate_schemes" get "stock-owners", to: "organisation_relationships#stock_owners" get "stock-owners/add", to: "organisation_relationships#add_stock_owner" get "stock-owners/remove", to: "organisation_relationships#remove_stock_owner" @@ -239,6 +241,7 @@ Rails.application.routes.draw do get "csv-download", to: "lettings_logs#download_csv" post "email-csv", to: "lettings_logs#email_csv" get "csv-confirmation", to: "lettings_logs#csv_confirmation" + get "bulk-uploads", to: "lettings_logs#bulk_uploads" get "delete-logs", to: "delete_logs#delete_lettings_logs" post "delete-logs", to: "delete_logs#delete_lettings_logs_with_selected_ids" @@ -280,6 +283,12 @@ Rails.application.routes.draw do end end + resources :bulk_uploads, path: "bulk-uploads", only: [] do + member do + get "download", to: "lettings_logs#download_bulk_upload", as: "download_lettings" + end + end + get "update-logs", to: "lettings_logs#update_logs" end @@ -311,6 +320,7 @@ Rails.application.routes.draw do get "csv-download", to: "sales_logs#download_csv" post "email-csv", to: "sales_logs#email_csv" get "csv-confirmation", to: "sales_logs#csv_confirmation" + get "bulk-uploads", to: "sales_logs#bulk_uploads" get "delete-logs", to: "delete_logs#delete_sales_logs" post "delete-logs", to: "delete_logs#delete_sales_logs_with_selected_ids" @@ -351,6 +361,12 @@ Rails.application.routes.draw do patch "*page", to: "bulk_upload_sales_soft_validations_check#update" end end + + resources :bulk_uploads, path: "bulk-uploads", only: [] do + member do + get "download", to: "sales_logs#download_bulk_upload", as: "download-sales" + end + end end member do diff --git a/db/migrate/20240920144611_add_schemes_deduplicated_at.rb b/db/migrate/20240920144611_add_schemes_deduplicated_at.rb new file mode 100644 index 000000000..02c1a6e05 --- /dev/null +++ b/db/migrate/20240920144611_add_schemes_deduplicated_at.rb @@ -0,0 +1,5 @@ +class AddSchemesDeduplicatedAt < ActiveRecord::Migration[7.0] + def change + add_column :organisations, :schemes_deduplicated_at, :datetime + end +end diff --git a/db/migrate/20241002163937_add_failure_reason_and_processing_to_bulk_uploads.rb b/db/migrate/20241002163937_add_failure_reason_and_processing_to_bulk_uploads.rb new file mode 100644 index 000000000..ef242d0f0 --- /dev/null +++ b/db/migrate/20241002163937_add_failure_reason_and_processing_to_bulk_uploads.rb @@ -0,0 +1,8 @@ +class AddFailureReasonAndProcessingToBulkUploads < ActiveRecord::Migration[7.0] + def change + change_table :bulk_uploads, bulk: true do |t| + t.string :failure_reason + t.boolean :processing + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ff1f913df..684092f80 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_09_23_145326) do +ActiveRecord::Schema[7.0].define(version: 2024_10_02_163937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,6 +44,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_23_145326) do t.string "rent_type_fix_status", default: "not_applied" t.integer "organisation_id" t.integer "moved_user_id" + t.string "failure_reason" + t.boolean "processing" t.index ["identifier"], name: "index_bulk_uploads_on_identifier", unique: true t.index ["user_id"], name: "index_bulk_uploads_on_user_id" end @@ -514,6 +516,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_23_145326) do t.bigint "absorbing_organisation_id" t.datetime "available_from" t.datetime "discarded_at" + t.datetime "schemes_deduplicated_at" t.index ["absorbing_organisation_id"], name: "index_organisations_on_absorbing_organisation_id" t.index ["old_visible_id"], name: "index_organisations_on_old_visible_id", unique: true end diff --git a/lib/tasks/recalculate_status_when_la_missing.rake b/lib/tasks/recalculate_status_when_la_missing.rake index 16af29cc7..1e304c6fa 100644 --- a/lib/tasks/recalculate_status_when_la_missing.rake +++ b/lib/tasks/recalculate_status_when_la_missing.rake @@ -17,3 +17,22 @@ task recalculate_status_missing_la: :environment do end end end + +desc "Recalculates status for 2024 completed logs with missing LA" +task recalculate_status_missing_la_2024: :environment do + LettingsLog.filter_by_year(2024).where(needstype: 1, la: nil, status: "completed").find_each do |log| + log.status = log.calculate_status + + unless log.save + Rails.logger.info "Could not save changes to lettings log #{log.id}" + end + end + + SalesLog.filter_by_year(2024).where(la: nil, status: "completed").find_each do |log| + log.status = log.calculate_status + + unless log.save + Rails.logger.info "Could not save changes to sales log #{log.id}" + end + end +end diff --git a/public/files/2023_24_lettings_paper_form.pdf b/public/files/2023_24_lettings_paper_form.pdf deleted file mode 100644 index 9db94cc83..000000000 Binary files a/public/files/2023_24_lettings_paper_form.pdf and /dev/null differ diff --git a/public/files/2023_24_sales_paper_form.pdf b/public/files/2023_24_sales_paper_form.pdf deleted file mode 100644 index 1ad25cda0..000000000 Binary files a/public/files/2023_24_sales_paper_form.pdf and /dev/null differ diff --git a/public/files/2024_25_lettings_paper_form.pdf b/public/files/2024_25_lettings_paper_form.pdf deleted file mode 100644 index 910b28a19..000000000 Binary files a/public/files/2024_25_lettings_paper_form.pdf and /dev/null differ diff --git a/public/files/2024_25_sales_paper_form.pdf b/public/files/2024_25_sales_paper_form.pdf deleted file mode 100644 index 203757d9a..000000000 Binary files a/public/files/2024_25_sales_paper_form.pdf and /dev/null differ diff --git a/public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx b/public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx deleted file mode 100644 index 78050bd22..000000000 Binary files a/public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-lettings-specification-2023-24.xlsx b/public/files/bulk-upload-lettings-specification-2023-24.xlsx deleted file mode 100644 index e7e56b6e3..000000000 Binary files a/public/files/bulk-upload-lettings-specification-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-lettings-specification-2024-25.xlsx b/public/files/bulk-upload-lettings-specification-2024-25.xlsx deleted file mode 100644 index 577239804..000000000 Binary files a/public/files/bulk-upload-lettings-specification-2024-25.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-lettings-template-2023-24.xlsx b/public/files/bulk-upload-lettings-template-2023-24.xlsx deleted file mode 100644 index a0c46b73a..000000000 Binary files a/public/files/bulk-upload-lettings-template-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-lettings-template-2024-25.xlsx b/public/files/bulk-upload-lettings-template-2024-25.xlsx deleted file mode 100644 index d0e0bfecd..000000000 Binary files a/public/files/bulk-upload-lettings-template-2024-25.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-sales-legacy-template-2023-24.xlsx b/public/files/bulk-upload-sales-legacy-template-2023-24.xlsx deleted file mode 100644 index d52e722e5..000000000 Binary files a/public/files/bulk-upload-sales-legacy-template-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-sales-specification-2023-24.xlsx b/public/files/bulk-upload-sales-specification-2023-24.xlsx deleted file mode 100644 index 9d1d4be62..000000000 Binary files a/public/files/bulk-upload-sales-specification-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-sales-specification-2024-25.xlsx b/public/files/bulk-upload-sales-specification-2024-25.xlsx deleted file mode 100644 index 5eab435c3..000000000 Binary files a/public/files/bulk-upload-sales-specification-2024-25.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-sales-template-2023-24.xlsx b/public/files/bulk-upload-sales-template-2023-24.xlsx deleted file mode 100644 index 773054750..000000000 Binary files a/public/files/bulk-upload-sales-template-2023-24.xlsx and /dev/null differ diff --git a/public/files/bulk-upload-sales-template-2024-25.xlsx b/public/files/bulk-upload-sales-template-2024-25.xlsx deleted file mode 100644 index f63878aa5..000000000 Binary files a/public/files/bulk-upload-sales-template-2024-25.xlsx and /dev/null differ diff --git a/spec/components/bulk_upload_summary_component_spec.rb b/spec/components/bulk_upload_summary_component_spec.rb new file mode 100644 index 000000000..9c0c68731 --- /dev/null +++ b/spec/components/bulk_upload_summary_component_spec.rb @@ -0,0 +1,148 @@ +require "rails_helper" + +RSpec.describe BulkUploadSummaryComponent, type: :component do + let(:user) { create(:user) } + let(:support_user) { create(:user, :support) } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2024, total_logs_count: 10) } + + it "shows the file name" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content(bulk_upload.filename) + end + + it "shows the collection year" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("2024/2025") + end + + it "includes a download file link" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_link("Download file", href: "/lettings-logs/bulk-uploads/#{bulk_upload.id}/download") + end + + it "shows the total log count" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("10 total logs") + end + + it "shows the uploaded by user" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Uploaded by: #{bulk_upload.user.name}") + end + + it "shows the uploading organisation" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Uploading organisation: #{bulk_upload.user.organisation.name}") + end + + it "shows the time of upload" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Time of upload: #{bulk_upload.created_at.to_formatted_s(:govuk_date_and_time)}") + end + + context "when bulk upload has only critical errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: nil) } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:, total_logs_count: 10) } + + it "shows the critical errors status and error count" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Critical errors in CSV") + expect(result).to have_content("2 critical errors") + expect(result).to have_no_content("errors on important") + expect(result).to have_no_content("potential") + end + + it "includes a view error report link" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_link("View error report", href: "/lettings-logs/bulk-upload-results/#{bulk_upload.id}") + end + end + + context "when bulk upload has only potential errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "soft_validation") } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:, total_logs_count: 16) } + + it "shows the potential errors status and error count" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Potential errors in CSV") + expect(result).to have_content("2 potential errors") + expect(result).to have_content("16 total logs") + expect(result).to have_no_content("errors on important") + expect(result).to have_no_content("critical") + end + end + + context "when bulk upload has only errors on important questions" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "setup") } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:, total_logs_count: 16) } + + it "shows the errors on important questions status and error count" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Errors on important questions in CSV") + expect(result).to have_content("2 errors on important questions") + expect(result).to have_content("16 total logs") + expect(result).to have_no_content("potential") + expect(result).to have_no_content("critical") + end + + it "includes a view error report link to the summary page" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_link("View error report", href: %r{.*/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary}) + end + end + + context "when a bulk upload is uploaded with no errors" do + let(:bulk_upload) { create(:bulk_upload, :sales, user:, total_logs_count: 1) } + + it "shows the logs uploaded with no errors status and no error counts" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Logs uploaded with no errors") + expect(result).to have_content("1 total log") + expect(result).to have_no_content("important questions") + expect(result).to have_no_content("potential") + expect(result).to have_no_content("critical") + end + end + + context "when a bulk upload is uploaded with errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 1) } + let(:bulk_upload) { create(:bulk_upload, :sales, user:, bulk_upload_errors:, total_logs_count: 21) } + + before do + create_list(:sales_log, 21, bulk_upload:) + end + + it "shows the logs upload with errors status and error count" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Logs uploaded with errors") + expect(result).to have_content("21 total logs") + expect(result).to have_content("1 critical error") + expect(result).to have_no_content("important questions") + expect(result).to have_no_content("potential") + end + end + + context "when a bulk upload uses the wrong template" do + let(:bulk_upload) { create(:bulk_upload, :sales, user:, failure_reason: "wrong_template") } + + it "shows the wrong template status and no error counts" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Wrong template") + expect(result).to have_no_content("important questions") + expect(result).to have_no_content("potential") + expect(result).to have_no_content("critical") + end + end + + context "when a bulk upload uses a blank template" do + let(:bulk_upload) { create(:bulk_upload, :sales, user:, failure_reason: "blank_template") } + + it "shows the wrong template status and no error counts" do + result = render_inline(described_class.new(bulk_upload:)) + expect(result).to have_content("Blank template") + expect(result).to have_no_content("important questions") + expect(result).to have_no_content("potential") + expect(result).to have_no_content("critical") + end + end +end diff --git a/spec/components/create_log_actions_component_spec.rb b/spec/components/create_log_actions_component_spec.rb index b1caa1443..eda25b246 100644 --- a/spec/components/create_log_actions_component_spec.rb +++ b/spec/components/create_log_actions_component_spec.rb @@ -37,13 +37,23 @@ RSpec.describe CreateLogActionsComponent, type: :component do expect(component.create_button_href).to eq("/lettings-logs") end - it "returns upload button copy" do - expect(component.upload_button_copy).to eq("Upload lettings logs in bulk") + it "does not show the upload button" do + render_inline(component) + expect(rendered_content).not_to have_link("Upload lettings logs in bulk", href: "/lettings-logs/bulk-upload-logs/start") end - it "returns upload button href" do + it "returns view uploads button copy" do + expect(component.view_uploads_button_copy).to eq("View lettings bulk uploads") + end + + it "returns view uploads button href" do render - expect(component.upload_button_href).to eq("/lettings-logs/bulk-upload-logs/start") + expect(component.view_uploads_button_href).to eq("/lettings-logs/bulk-uploads") + end + + it "shows the view uploads button" do + render_inline(component) + expect(rendered_content).to have_link("View lettings bulk uploads", href: "/lettings-logs/bulk-uploads") end context "when sales log type" do @@ -61,6 +71,16 @@ RSpec.describe CreateLogActionsComponent, type: :component do render expect(component.create_button_href).to eq("/sales-logs") end + + it "does not show the upload button" do + render_inline(component) + expect(rendered_content).not_to have_link("Upload sales logs in bulk", href: "/sales-logs/bulk-upload-logs/start") + end + + it "shows the view uploads button" do + render_inline(component) + expect(rendered_content).to have_link("View sales bulk uploads", href: "/sales-logs/bulk-uploads") + end end end diff --git a/spec/factories/bulk_upload.rb b/spec/factories/bulk_upload.rb index cefe95c2b..3c345b97a 100644 --- a/spec/factories/bulk_upload.rb +++ b/spec/factories/bulk_upload.rb @@ -10,6 +10,8 @@ FactoryBot.define do needstype { 1 } rent_type_fix_status { BulkUpload.rent_type_fix_statuses.values.sample } organisation_id { user.organisation_id } + total_logs_count { Faker::Number.number(digits: 2) } + processing { false } trait(:sales) do log_type { BulkUpload.log_types[:sales] } diff --git a/spec/factories/bulk_upload_error.rb b/spec/factories/bulk_upload_error.rb index bd2b9038c..6e2497972 100644 --- a/spec/factories/bulk_upload_error.rb +++ b/spec/factories/bulk_upload_error.rb @@ -11,5 +11,6 @@ FactoryBot.define do purchaser_code { SecureRandom.hex(4) } field { "field_#{rand(1..134)}" } error { "some error" } + category { nil } end end diff --git a/spec/features/accessibility_spec.rb b/spec/features/accessibility_spec.rb index 97f632d92..15cd87ebb 100644 --- a/spec/features/accessibility_spec.rb +++ b/spec/features/accessibility_spec.rb @@ -3,6 +3,7 @@ require "rails_helper" RSpec.describe "Accessibility", js: true do let(:user) { create(:user, :support) } let!(:other_user) { create(:user, name: "new user", organisation: user.organisation, email: "new_user@example.com", confirmation_token: "abc") } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } def find_routes(type, resource, subresource) routes = Rails.application.routes.routes.select do |route| @@ -20,6 +21,8 @@ RSpec.describe "Accessibility", js: true do end before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) allow(user).to receive(:need_two_factor_authentication?).and_return(false) sign_in(user) end @@ -107,6 +110,7 @@ RSpec.describe "Accessibility", js: true do log.save(validate: false) end allow(FormHandler.instance).to receive(:in_crossover_period?).and_return(true) + allow(storage_service).to receive(:get_presigned_url).with(bulk_upload.identifier, 60, response_content_disposition: "attachment; filename=#{bulk_upload.filename}").and_return("http://example.com/lettings-logs/bulk-uploads/#{bulk_upload.id}/download") end it "is has accessible pages" do @@ -145,6 +149,10 @@ RSpec.describe "Accessibility", js: true do }.uniq end + before do + allow(storage_service).to receive(:get_presigned_url).with(bulk_upload.identifier, 60, response_content_disposition: "attachment; filename=#{bulk_upload.filename}").and_return("http://example.com/sales-logs/bulk-uploads/#{bulk_upload.id}/download") + end + it "is has accessible pages" do sales_log_paths.each do |path| path += "?original_log_id=#{sales_log.id}" if path.include?("duplicate") diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index ac9a1e4a8..218850992 100644 --- a/spec/features/lettings_log_spec.rb +++ b/spec/features/lettings_log_spec.rb @@ -1,6 +1,13 @@ require "rails_helper" RSpec.describe "Lettings Log Features" do + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end + context "when searching for specific logs" do context "when I am signed in and there are logs in the database" do let(:user) { create(:user, last_sign_in_at: Time.zone.now) } @@ -408,6 +415,37 @@ RSpec.describe "Lettings Log Features" do expect(deleted_log.status).to eq "deleted" expect(deleted_log.discarded_at).not_to be nil end + + context "when visiting the bulk uploads page" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: nil) } + let(:bulk_upload) { create(:bulk_upload, :lettings, user: support_user, bulk_upload_errors:, total_logs_count: 10) } + let(:mock_storage_service) { instance_double("S3Service") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + allow(mock_storage_service).to receive(:get_presigned_url).with(bulk_upload.identifier, 60, response_content_disposition: "attachment; filename=#{bulk_upload.filename}").and_return("/lettings-logs/bulk-uploads") + bulk_upload + visit("/lettings-logs/bulk-uploads") + end + + it "displays the right title" do + expect(page).to have_content("Lettings bulk uploads") + end + + it "shows the bulk upload file name" do + expect(page).to have_content(bulk_upload.filename) + end + + it "redirects to the error report page when clicking 'View error report'" do + click_link("View error report") + expect(page).to have_current_path("/lettings-logs/bulk-upload-results/#{bulk_upload.id}") + end + + it "allows the user to download the file" do + click_link("Download file", href: "/lettings-logs/bulk-uploads/#{bulk_upload.id}/download") + expect(page).to have_current_path("/lettings-logs/bulk-uploads") + end + end end context "when the signed is user is not a Support user" do diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb index e2bd4b151..a542cc97f 100644 --- a/spec/features/notifications_spec.rb +++ b/spec/features/notifications_spec.rb @@ -3,6 +3,12 @@ require_relative "form/helpers" RSpec.describe "Notifications Features" do include Helpers + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end context "when there are notifications" do let!(:user) { FactoryBot.create(:user) } diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb index 3d65cda87..6f90428be 100644 --- a/spec/features/organisation_spec.rb +++ b/spec/features/organisation_spec.rb @@ -10,8 +10,11 @@ RSpec.describe "User Features" do let(:notify_client) { instance_double(Notifications::Client) } let(:confirmation_token) { "MCDH5y6Km-U7CFPgAMVS" } let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(Devise).to receive(:friendly_token).and_return(confirmation_token) @@ -136,7 +139,7 @@ RSpec.describe "User Features" do end it "shows a create button for that organisation" do - expect(page).to have_button("Create a new lettings log for this organisation") + expect(page).to have_button("Create a new lettings log") end it "shows a upload lettings logs in bulk link" do @@ -145,7 +148,7 @@ RSpec.describe "User Features" do context "when creating a log for that organisation" do it "pre-fills the value for owning organisation for that log" do - click_button("Create a new lettings log for this organisation") + click_button("Create a new lettings log") click_link("Set up this lettings log") expect(page).to have_content(org_name) end @@ -231,7 +234,7 @@ RSpec.describe "User Features" do end it "shows a create button for that organisation" do - expect(page).to have_button("Create a new sales log for this organisation") + expect(page).to have_button("Create a new sales log") end it "shows a upload sales logs in bulk link" do @@ -240,7 +243,7 @@ RSpec.describe "User Features" do context "when creating a log for that organisation" do it "pre-fills the value for owning organisation for that log" do - click_button("Create a new sales log for this organisation") + click_button("Create a new sales log") click_link("Set up this sales log") expect(page).to have_content(org_name) end diff --git a/spec/features/sales_log_spec.rb b/spec/features/sales_log_spec.rb index 779d978bf..8e4ffba42 100644 --- a/spec/features/sales_log_spec.rb +++ b/spec/features/sales_log_spec.rb @@ -279,6 +279,37 @@ RSpec.describe "Sales Log Features" do expect(breadcrumbs[2][:href]).to eq sales_log_path(sales_log.id) end end + + context "when visiting the bulk uploads page" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: nil) } + let(:bulk_upload) { create(:bulk_upload, :sales, user:, bulk_upload_errors:, total_logs_count: 10) } + let(:mock_storage_service) { instance_double("S3Service") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + allow(mock_storage_service).to receive(:get_presigned_url).with(bulk_upload.identifier, 60, response_content_disposition: "attachment; filename=#{bulk_upload.filename}").and_return("/sales-logs/bulk-uploads") + bulk_upload + visit("/sales-logs/bulk-uploads") + end + + it "displays the right title" do + expect(page).to have_content("Sales bulk uploads") + end + + it "shows the bulk upload file name" do + expect(page).to have_content(bulk_upload.filename) + end + + it "redirects to the error report page when clicking 'View error report'" do + click_link("View error report") + expect(page).to have_current_path("/sales-logs/bulk-upload-results/#{bulk_upload.id}") + end + + it "allows the user to download the file" do + click_link("Download file", href: "/sales-logs/bulk-uploads/#{bulk_upload.id}/download") + expect(page).to have_current_path("/sales-logs/bulk-uploads") + end + end end context "when a log becomes a duplicate" do diff --git a/spec/features/start_page_spec.rb b/spec/features/start_page_spec.rb index ab09fd446..555db9238 100644 --- a/spec/features/start_page_spec.rb +++ b/spec/features/start_page_spec.rb @@ -4,6 +4,12 @@ require_relative "form/helpers" RSpec.describe "Start Page Features" do include Helpers let(:user) { FactoryBot.create(:user) } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end context "when the user is signed in" do before do diff --git a/spec/features/test_spec.rb b/spec/features/test_spec.rb index 6dc977a9b..a58867182 100644 --- a/spec/features/test_spec.rb +++ b/spec/features/test_spec.rb @@ -1,5 +1,12 @@ require "rails_helper" RSpec.describe "Test Features" do + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end + it "Displays the name of the app" do visit(root_path) expect(page).to have_content("Submit social housing lettings and sales data (CORE)") diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index 119dbfa52..bc562824c 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -6,12 +6,15 @@ RSpec.describe "User Features" do let(:notify_client) { instance_double(Notifications::Client) } let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" } let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(notify_client).to receive(:send_email).and_return(true) allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token) + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) end context "when the user navigates to lettings logs" do diff --git a/spec/helpers/collection_resources_helper_spec.rb b/spec/helpers/collection_resources_helper_spec.rb index c028177ce..4d39a0c2d 100644 --- a/spec/helpers/collection_resources_helper_spec.rb +++ b/spec/helpers/collection_resources_helper_spec.rb @@ -3,15 +3,29 @@ require "rails_helper" RSpec.describe CollectionResourcesHelper do let(:current_user) { create(:user, :data_coordinator) } let(:user) { create(:user, :data_coordinator) } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end describe "when displaying file metadata" do context "with pages" do + before do + allow(storage_service).to receive(:get_file_metadata).with("2023_24_lettings_paper_form.pdf").and_return("content_length" => 292_864, "content_type" => "application/pdf") + end + it "returns correct metadata" do expect(file_type_size_and_pages("2023_24_lettings_paper_form.pdf", number_of_pages: 8)).to eq("PDF, 286 KB, 8 pages") end end context "without pages" do + before do + allow(storage_service).to receive(:get_file_metadata).with("bulk-upload-lettings-template-2023-24.xlsx").and_return("content_length" => 19_456, "content_type" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + end + it "returns correct metadata" do expect(file_type_size_and_pages("bulk-upload-lettings-template-2023-24.xlsx")).to eq("Microsoft Excel, 19 KB") end diff --git a/spec/helpers/schemes_helper_spec.rb b/spec/helpers/schemes_helper_spec.rb index 0df032c3f..8ffde636a 100644 --- a/spec/helpers/schemes_helper_spec.rb +++ b/spec/helpers/schemes_helper_spec.rb @@ -118,4 +118,121 @@ RSpec.describe SchemesHelper do end end end + + describe "display_duplicate_schemes_banner?" do + let(:organisation) { create(:organisation) } + let(:current_user) { create(:user, :support) } + + context "when organisation has not absorbed other organisations" do + context "and it has duplicate schemes" do + before do + create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + end + + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + end + + context "when organisation has absorbed other organisations in open collection year" do + before do + build(:organisation, merge_date: Time.zone.yesterday, absorbing_organisation_id: organisation.id).save(validate: false) + end + + context "and it has duplicate schemes" do + before do + create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + end + + it "displays the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_truthy + end + + context "and organisation has confirmed duplicate schemes after the most recent merge" do + before do + organisation.update!(schemes_deduplicated_at: Time.zone.today) + end + + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + + context "and organisation has confirmed duplicate schemes before the most recent merge" do + before do + organisation.update!(schemes_deduplicated_at: Time.zone.today - 2.days) + end + + it "displays the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_truthy + end + end + end + + context "and it has duplicate locations" do + let(:scheme) { create(:scheme, owning_organisation: organisation) } + + before do + create_list(:location, 2, postcode: "AB1 2CD", mobility_type: "A", scheme:) + end + + it "displays the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_truthy + end + end + + context "and it has no duplicate schemes or locations" do + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + + context "and it is viewed by data provider" do + let(:current_user) { create(:user, :data_provider) } + + before do + create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + end + + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + end + + context "when organisation has absorbed other organisations in closed collection year" do + before do + build(:organisation, merge_date: Time.zone.today - 2.years, absorbing_organisation_id: organisation.id).save(validate: false) + end + + context "and it has duplicate schemes" do + before do + create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + end + + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + + context "and it has duplicate locations" do + let(:scheme) { create(:scheme, owning_organisation: organisation) } + + before do + create(:location, postcode: "AB1 2CD", mobility_type: "A", scheme:) + end + + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + + context "and it has no duplicate schemes or locations" do + it "does not display the banner" do + expect(display_duplicate_schemes_banner?(organisation, current_user)).to be_falsey + end + end + end + end end diff --git a/spec/models/bulk_upload_spec.rb b/spec/models/bulk_upload_spec.rb index e38ea0402..af2547d7a 100644 --- a/spec/models/bulk_upload_spec.rb +++ b/spec/models/bulk_upload_spec.rb @@ -46,9 +46,215 @@ RSpec.describe BulkUpload, type: :model do let(:bulk_upload) { build(:bulk_upload, year: test_case[:year]) } it "returns the expected year combination string" do - expect(bulk_upload.year_combo).to eql(test_case[:expected_value]) + expect(bulk_upload.year_combo).to eq(test_case[:expected_value]) end end end end + + describe "scopes" do + let!(:lettings_bulk_upload_1) { create(:bulk_upload, log_type: "lettings") } + let!(:lettings_bulk_upload_2) { create(:bulk_upload, log_type: "lettings") } + let!(:sales_bulk_upload_1) { create(:bulk_upload, log_type: "sales") } + let!(:sales_bulk_upload_2) { create(:bulk_upload, log_type: "sales") } + + describe ".lettings" do + it "returns only lettings bulk uploads" do + expect(described_class.lettings).to match_array([lettings_bulk_upload_1, lettings_bulk_upload_2]) + end + end + + describe ".sales" do + it "returns only sales bulk uploads" do + expect(described_class.sales).to match_array([sales_bulk_upload_1, sales_bulk_upload_2]) + end + end + + describe ".search_by_filename" do + it "returns the correct bulk upload" do + expect(described_class.search_by_filename(lettings_bulk_upload_1.filename).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.search_by_filename(lettings_bulk_upload_1.filename).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".search_by_user_name" do + it "returns the correct bulk upload" do + expect(described_class.search_by_user_name(lettings_bulk_upload_1.user.name).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.search_by_user_name(lettings_bulk_upload_1.user.name).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".search_by_user_email" do + it "returns the correct bulk upload" do + expect(described_class.search_by_user_email(sales_bulk_upload_1.user.email).first).to eq(sales_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.search_by_user_email(sales_bulk_upload_1.user.email).first).not_to eq(sales_bulk_upload_2) + end + end + + describe ".search_by_organisation_name" do + it "returns the correct bulk upload" do + expect(described_class.search_by_organisation_name(lettings_bulk_upload_1.user.organisation.name).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.search_by_organisation_name(lettings_bulk_upload_1.user.organisation.name).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".filter_by_id" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_id(lettings_bulk_upload_1.id).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_id(lettings_bulk_upload_1.id).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".filter_by_years" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_years([lettings_bulk_upload_1.year]).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_years([lettings_bulk_upload_1.year]).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".filter_by_uploaded_by" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_uploaded_by(sales_bulk_upload_1.user.id).first).to eq(sales_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_uploaded_by(sales_bulk_upload_1.user.id).first).not_to eq(sales_bulk_upload_2) + end + end + + describe ".filter_by_user_text_search" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_user_text_search(lettings_bulk_upload_1.user.name).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_user_text_search(lettings_bulk_upload_1.user.name).first).not_to eq(lettings_bulk_upload_2) + end + end + + describe ".filter_by_user" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_user(sales_bulk_upload_1.user.id).first).to eq(sales_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_user(sales_bulk_upload_1.user.id).first).not_to eq(sales_bulk_upload_2) + end + end + + describe ".filter_by_uploading_organisation" do + it "returns the correct bulk upload" do + expect(described_class.filter_by_uploading_organisation(lettings_bulk_upload_1.user.organisation.id).first).to eq(lettings_bulk_upload_1) + end + + it "does not return the incorrect bulk upload" do + expect(described_class.filter_by_uploading_organisation(lettings_bulk_upload_1.user.organisation.id).first).not_to eq(lettings_bulk_upload_2) + end + end + end + + describe "#status" do + context "when the bulk upload was uploaded with a blank template" do + let(:bulk_upload) { create(:bulk_upload, failure_reason: "blank_template") } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:blank_template) + end + end + + context "when the bulk upload was uploaded with the wrong template" do + let(:bulk_upload) { create(:bulk_upload, failure_reason: "wrong_template") } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:wrong_template) + end + end + + context "when the bulk upload is processing" do + let(:bulk_upload) { create(:bulk_upload, processing: true) } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:processing) + end + end + + context "when the bulk upload has potential errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "soft_validation") } + let(:bulk_upload) { create(:bulk_upload, bulk_upload_errors:) } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:potential_errors) + end + end + + context "when the bulk upload has critical errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: nil) } + let(:bulk_upload) { create(:bulk_upload, bulk_upload_errors:) } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:critical_errors) + end + end + + context "when the bulk upload has important errors" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "setup") } + let(:bulk_upload) { create(:bulk_upload, bulk_upload_errors:) } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:important_errors) + end + end + + context "when the bulk upload has no errors" do + let(:bulk_upload) { create(:bulk_upload) } + + it "returns the correct status" do + expect(bulk_upload.status).to eq(:logs_uploaded_no_errors) + end + end + + context "when the bulk upload has visible logs, errors and is not complete" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "soft_validation") } + let(:bulk_upload) { create(:bulk_upload, :lettings, bulk_upload_errors:) } + + before do + create(:lettings_log, :in_progress, bulk_upload:) + end + + it "returns logs_uploaded_with_errors" do + expect(bulk_upload.status).to eq(:logs_uploaded_with_errors) + end + end + + context "when the bulk upload has visible logs, errors and is complete" do + let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2, category: "soft_validation") } + let(:bulk_upload) { create(:bulk_upload, :lettings, bulk_upload_errors:) } + + before do + create(:lettings_log, :completed, bulk_upload:) + end + + it "returns errors_fixed_in_service" do + expect(bulk_upload.status).to eq(:errors_fixed_in_service) + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index beb3d589e..89f4f9dee 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -176,6 +176,10 @@ RSpec.describe User, type: :model do it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user managing_organisation owning_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search]) end + + it "can filter schemes by status and owning_organisation" do + expect(user.scheme_filters).to match_array(%w[status owning_organisation owning_organisation_text_search]) + end end end @@ -217,6 +221,14 @@ RSpec.describe User, type: :model do it "can filter lettings logs by user, year, status, managing_organisation and owning_organisation" do expect(user.logs_filters).to match_array(%w[years status needstypes assigned_to user owning_organisation managing_organisation bulk_upload_id managing_organisation_text_search owning_organisation_text_search user_text_search]) end + + it "can filter bulk uploads by year, uploaded_by and uploading_organisation " do + expect(user.bulk_uploads_filters).to match_array(%w[user years uploaded_by uploading_organisation user_text_search uploading_organisation_text_search]) + end + + it "can filter schemes by status and owning_organisation" do + expect(user.scheme_filters).to match_array(%w[status owning_organisation owning_organisation_text_search]) + end end context "when the user is in development environment" do diff --git a/spec/models/validations/financial_validations_spec.rb b/spec/models/validations/financial_validations_spec.rb index 4e1aa400f..f35cdd097 100644 --- a/spec/models/validations/financial_validations_spec.rb +++ b/spec/models/validations/financial_validations_spec.rb @@ -123,6 +123,22 @@ RSpec.describe Validations::FinancialValidations do .to include(match I18n.t("validations.financial.tshortfall.more_than_total_charge")) end + it "validates that carehome charge is no less than the shortfall" do + record.hb = 6 + record.hbrentshortfall = 1 + record.tshortfall_known = 0 + record.tshortfall = 299.50 + record.chcharge = 198 + record.needstype = 2 + record.period = 2 + record.set_derived_fields! + financial_validator.validate_rent_amount(record) + expect(record.errors["chcharge"]) + .to include(match I18n.t("validations.financial.carehome.less_than_shortfall")) + expect(record.errors["tshortfall"]) + .to include(match I18n.t("validations.financial.tshortfall.more_than_carehome_charge")) + end + it "expects that rent can be less than the shortfall if total charge is higher" do record.hb = 6 record.hbrentshortfall = 1 diff --git a/spec/requests/auth/passwords_controller_spec.rb b/spec/requests/auth/passwords_controller_spec.rb index 333985d9e..7f9bdfa30 100644 --- a/spec/requests/auth/passwords_controller_spec.rb +++ b/spec/requests/auth/passwords_controller_spec.rb @@ -5,11 +5,14 @@ RSpec.describe Auth::PasswordsController, type: :request do let(:page) { Capybara::Node::Simple.new(response.body) } let(:notify_client) { instance_double(Notifications::Client) } let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(notify_client).to receive(:send_email).and_return(true) + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) end context "when a regular user" do diff --git a/spec/requests/check_errors_controller_spec.rb b/spec/requests/check_errors_controller_spec.rb index c101e7959..29130f547 100644 --- a/spec/requests/check_errors_controller_spec.rb +++ b/spec/requests/check_errors_controller_spec.rb @@ -300,6 +300,33 @@ RSpec.describe CheckErrorsController, type: :request do end end + context "and clearing ppostcode_full when previous_la_known is yes" do + let(:params) do + { + id: lettings_log.id, + lettings_log: { + layear: "1", + clear_question_ids: "ppostcode_full", + page: "time_lived_in_local_authority", + }, + check_errors: "", + } + end + + before do + lettings_log.update!(previous_la_known: 1, ppcodenk: 0, ppostcode_full: "AA11AA") + sign_in user + post "/lettings-logs/#{lettings_log.id}/time-lived-in-local-authority", params: + end + + it "clears related previous location fields" do + expect(lettings_log.reload.prevloc).to eq(nil) + expect(lettings_log.reload.previous_la_known).to eq(nil) + expect(lettings_log.reload.ppostcode_full).to eq(nil) + expect(lettings_log.reload.ppcodenk).to eq(nil) + end + end + context "and clearing specific sales question" do let(:params) do { diff --git a/spec/requests/maintenance_controller_spec.rb b/spec/requests/maintenance_controller_spec.rb index 39e587302..28287dbc0 100644 --- a/spec/requests/maintenance_controller_spec.rb +++ b/spec/requests/maintenance_controller_spec.rb @@ -3,8 +3,11 @@ require "rails_helper" RSpec.describe MaintenanceController, type: :request do let(:page) { Capybara::Node::Simple.new(response.body) } let(:user) { FactoryBot.create(:user) } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) sign_in user end diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 91dc07094..223cc9e00 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -207,6 +207,24 @@ RSpec.describe OrganisationsController, type: :request do expect(page).to have_title("#{user.organisation.name} (1 scheme matching ‘#{search_param}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") end end + + context "when organisation has absorbed other organisations" do + before do + create(:organisation, merge_date: Time.zone.today, absorbing_organisation: organisation) + end + + context "and it has duplicate schemes or locations" do + before do + create_list(:scheme, 2, :duplicate, owning_organisation: organisation) + get "/organisations/#{organisation.id}/schemes", headers:, params: {} + end + + it "displays a banner with correct content" do + expect(page).to have_content("Some schemes and locations might be duplicates.") + expect(page).to have_link("Review possible duplicates", href: "/organisations/#{organisation.id}/schemes/duplicates") + end + end + end end context "when data coordinator user" do @@ -348,6 +366,77 @@ RSpec.describe OrganisationsController, type: :request do end end + describe "#duplicate_schemes" do + context "with support user" do + let(:user) { create(:user, :support) } + + before do + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + get "/organisations/#{organisation.id}/schemes/duplicates", headers: + end + + context "with duplicate schemes and locations" do + let(:schemes) { create_list(:scheme, 5, :duplicate, owning_organisation: organisation) } + + before do + create_list(:location, 2, scheme: schemes.first, postcode: "M1 1AA", mobility_type: "M") + create_list(:location, 2, scheme: schemes.first, postcode: "M1 1AA", mobility_type: "A") + get "/organisations/#{organisation.id}/schemes/duplicates", headers: + end + + it "displays the duplicate schemes" do + expect(page).to have_content("This set of schemes might have duplicates") + end + + it "displays the duplicate locations" do + expect(page).to have_content("These 2 sets of locations might have duplicates") + end + + it "has page heading" do + expect(page).to have_content("Review these sets of schemes and locations") + end + end + + context "without duplicate schemes and locations" do + it "does not display the schemes" do + expect(page).not_to have_content("schemes might have duplicates") + end + + it "does not display the locations" do + expect(page).not_to have_content("locations might have duplicates") + end + end + end + + context "with data coordinator user" do + let(:user) { create(:user, :data_coordinator) } + + before do + sign_in user + create_list(:scheme, 5, :duplicate, owning_organisation: organisation) + get "/organisations/#{organisation.id}/schemes/duplicates", headers: + end + + it "has page heading" do + expect(page).to have_content("Review these sets of schemes") + end + end + + context "with data provider user" do + let(:user) { create(:user, :data_provider) } + + before do + sign_in user + get "/organisations/#{organisation.id}/schemes/duplicates", headers: + end + + it "be unauthorised" do + expect(response).to have_http_status(:unauthorized) + end + end + end + describe "#show" do context "with an organisation that the user belongs to" do let(:set_time) {} @@ -2263,4 +2352,65 @@ RSpec.describe OrganisationsController, type: :request do end end end + + describe "POST #confirm_duplicate_schemes" do + let(:organisation) { create(:organisation) } + + context "when not signed in" do + it "redirects to sign in" do + post "/organisations/#{organisation.id}/schemes/duplicates", headers: headers + expect(response).to redirect_to("/account/sign-in") + end + end + + context "when signed in" do + before do + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + context "when user is data provider" do + let(:user) { create(:user, role: "data_provider", organisation:) } + + it "returns not found" do + post "/organisations/#{organisation.id}/schemes/duplicates", headers: headers + expect(response).to have_http_status(:unauthorized) + end + end + + context "when user is coordinator" do + let(:user) { create(:user, role: "data_coordinator", organisation:) } + + context "and the duplicate schemes have been confirmed" do + let(:params) { { "organisation": { scheme_duplicates_checked: "true" } } } + + it "redirects to schemes page" do + post "/organisations/#{organisation.id}/schemes/duplicates", headers: headers, params: params + + expect(response).to redirect_to("/organisations/#{organisation.id}/schemes") + expect(flash[:notice]).to eq("You’ve confirmed the remaining schemes and locations are not duplicates.") + end + + it "updates schemes_deduplicated_at" do + expect(organisation.reload.schemes_deduplicated_at).to be_nil + + post "/organisations/#{organisation.id}/schemes/duplicates", headers: headers, params: params + + expect(organisation.reload.schemes_deduplicated_at).not_to be_nil + end + end + + context "and the duplicate schemes have not been confirmed" do + let(:params) { { "organisation": { scheme_duplicates_checked: "" } } } + + it "displays an error" do + post "/organisations/#{organisation.id}/schemes/duplicates", headers: headers, params: params + + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("You must resolve all duplicates or indicate that there are no duplicates") + end + end + end + end + end end diff --git a/spec/requests/rails_admin_controller_spec.rb b/spec/requests/rails_admin_controller_spec.rb index 4d0bbb7c2..79f56d18f 100644 --- a/spec/requests/rails_admin_controller_spec.rb +++ b/spec/requests/rails_admin_controller_spec.rb @@ -4,9 +4,12 @@ RSpec.describe "RailsAdmin", type: :request do let(:user) { create(:user) } let(:support_user) { create(:user, :support) } let(:page) { Capybara::Node::Simple.new(response.body) } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do allow(support_user).to receive(:need_two_factor_authentication?).and_return(false) + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) end describe "GET /admin" do diff --git a/spec/requests/start_controller_spec.rb b/spec/requests/start_controller_spec.rb index 699bdfa9c..40c45a9f9 100644 --- a/spec/requests/start_controller_spec.rb +++ b/spec/requests/start_controller_spec.rb @@ -5,11 +5,14 @@ RSpec.describe StartController, type: :request do let(:page) { Capybara::Node::Simple.new(response.body) } let(:notify_client) { instance_double(Notifications::Client) } let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(notify_client).to receive(:send_email).and_return(true) + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) end describe "GET" do diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 1ab5aa9d4..05dc06d5b 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -10,11 +10,14 @@ RSpec.describe UsersController, type: :request do let(:params) { { id: user.id, user: { name: new_name } } } let(:notify_client) { instance_double(Notifications::Client) } let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } before do allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) allow(notify_client).to receive(:send_email).and_return(true) + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) end context "when user is not signed in" do diff --git a/spec/services/bulk_upload/downloader_spec.rb b/spec/services/bulk_upload/downloader_spec.rb index 48b046d8e..bae90aede 100644 --- a/spec/services/bulk_upload/downloader_spec.rb +++ b/spec/services/bulk_upload/downloader_spec.rb @@ -45,4 +45,16 @@ RSpec.describe BulkUpload::Downloader do expect(File).not_to exist(path) end end + + describe "#presigned_url" do + let(:mock_storage_service) { instance_double(Storage::S3Service, get_presigned_url: "https://example.com") } + + before do + allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service) + end + + it "returns a presigned URL" do + expect(downloader.presigned_url).to eql("https://example.com") + end + end end diff --git a/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb index 127cb9814..6d3feffa9 100644 --- a/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb @@ -215,7 +215,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::RowParser do field_131: "101.11", field_132: "1500.19", field_133: "1", - field_134: "234.56", + field_134: "34.56", field_27: "15", field_28: "0", diff --git a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb index c6d1a1a8b..68c1cc401 100644 --- a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb @@ -235,7 +235,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do field_127: "13.14", field_128: "101.11", field_129: "1", - field_130: "234.56", + field_130: "34.56", field_24: "15", field_30: now.day.to_s, diff --git a/spec/services/bulk_upload/processor_spec.rb b/spec/services/bulk_upload/processor_spec.rb index 08fe7d705..de0ed2dba 100644 --- a/spec/services/bulk_upload/processor_spec.rb +++ b/spec/services/bulk_upload/processor_spec.rb @@ -43,6 +43,13 @@ RSpec.describe BulkUpload::Processor do end describe "#call" do + it "changes processing from true to false" do + bulk_upload.update!(processing: true) + expect { + processor.call + }.to change { bulk_upload.reload.processing }.from(true).to(false) + end + context "when errors exist from prior job run" do let!(:existing_error) { create(:bulk_upload_error, bulk_upload:) } diff --git a/spec/views/bulk_upload_lettings_results/show.html.erb_spec.rb b/spec/views/bulk_upload_lettings_results/show.html.erb_spec.rb index 637dfa3a9..e7ffc68f8 100644 --- a/spec/views/bulk_upload_lettings_results/show.html.erb_spec.rb +++ b/spec/views/bulk_upload_lettings_results/show.html.erb_spec.rb @@ -1,10 +1,12 @@ require "rails_helper" RSpec.describe "bulk_upload_lettings_results/show.html.erb" do + let(:user) { create(:user) } let(:bulk_upload) { create(:bulk_upload, :lettings) } context "when mutiple rows in wrong order" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "C14", row: "14", col: "C") create(:bulk_upload_error, bulk_upload:, cell: "D10", row: "10", col: "D") end @@ -22,6 +24,7 @@ RSpec.describe "bulk_upload_lettings_results/show.html.erb" do context "when 1 row with 2 errors" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "AA100", row: "100", col: "AA") create(:bulk_upload_error, bulk_upload:, cell: "Z100", row: "100", col: "Z") end diff --git a/spec/views/bulk_upload_lettings_results/summary.html.erb_spec.rb b/spec/views/bulk_upload_lettings_results/summary.html.erb_spec.rb index ac0c1f82d..5ebb05dbe 100644 --- a/spec/views/bulk_upload_lettings_results/summary.html.erb_spec.rb +++ b/spec/views/bulk_upload_lettings_results/summary.html.erb_spec.rb @@ -1,10 +1,12 @@ require "rails_helper" RSpec.describe "bulk_upload_lettings_results/summary.html.erb" do + let(:user) { create(:user) } let(:bulk_upload) { create(:bulk_upload, :lettings) } context "when mutiple rows in wrong order" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "C14", row: "14", col: "C") create(:bulk_upload_error, bulk_upload:, cell: "D10", row: "10", col: "D") end @@ -22,6 +24,7 @@ RSpec.describe "bulk_upload_lettings_results/summary.html.erb" do context "when 1 row with 2 errors" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "AA100", row: "100", col: "AA") create(:bulk_upload_error, bulk_upload:, cell: "Z100", row: "100", col: "Z") end diff --git a/spec/views/bulk_upload_sales_results/show.html.erb_spec.rb b/spec/views/bulk_upload_sales_results/show.html.erb_spec.rb index dc6751dc8..0ad13fbb3 100644 --- a/spec/views/bulk_upload_sales_results/show.html.erb_spec.rb +++ b/spec/views/bulk_upload_sales_results/show.html.erb_spec.rb @@ -1,10 +1,12 @@ require "rails_helper" RSpec.describe "bulk_upload_sales_results/show.html.erb" do + let(:user) { create(:user) } let(:bulk_upload) { create(:bulk_upload, :sales) } context "when mutiple rows in wrong order" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "C14", row: "14", col: "C") create(:bulk_upload_error, bulk_upload:, cell: "D10", row: "10", col: "D") end @@ -22,6 +24,7 @@ RSpec.describe "bulk_upload_sales_results/show.html.erb" do context "when 1 row with 2 errors" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "AA100", row: "100", col: "AA") create(:bulk_upload_error, bulk_upload:, cell: "Z100", row: "100", col: "Z") end diff --git a/spec/views/bulk_upload_sales_results/summary.html.erb_spec.rb b/spec/views/bulk_upload_sales_results/summary.html.erb_spec.rb index b3d9aa006..d999ad9d4 100644 --- a/spec/views/bulk_upload_sales_results/summary.html.erb_spec.rb +++ b/spec/views/bulk_upload_sales_results/summary.html.erb_spec.rb @@ -1,10 +1,12 @@ require "rails_helper" RSpec.describe "bulk_upload_sales_results/summary.html.erb" do + let(:user) { create(:user) } let(:bulk_upload) { create(:bulk_upload, :sales) } context "when mutiple rows in wrong order" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "C14", row: "14", col: "C") create(:bulk_upload_error, bulk_upload:, cell: "D10", row: "10", col: "D") end @@ -22,6 +24,7 @@ RSpec.describe "bulk_upload_sales_results/summary.html.erb" do context "when 1 row with 2 errors" do before do + allow(view).to receive(:current_user).and_return(user) create(:bulk_upload_error, bulk_upload:, cell: "AA100", row: "100", col: "AA") create(:bulk_upload_error, bulk_upload:, cell: "Z100", row: "100", col: "Z") end diff --git a/spec/views/logs/_create_for_org_actions.html.erb_spec.rb b/spec/views/logs/_create_for_org_actions.html.erb_spec.rb index e82cb8d27..df448844d 100644 --- a/spec/views/logs/_create_for_org_actions.html.erb_spec.rb +++ b/spec/views/logs/_create_for_org_actions.html.erb_spec.rb @@ -14,8 +14,8 @@ RSpec.describe "logs/_create_for_org_actions.html.erb" do context "with data sharing agreement" do it "does include create log buttons" do render - expect(fragment).to have_button("Create a new lettings log for this organisation") - expect(fragment).to have_button("Create a new sales log for this organisation") + expect(fragment).to have_button("Create a new lettings log") + expect(fragment).to have_button("Create a new sales log") end end @@ -24,8 +24,8 @@ RSpec.describe "logs/_create_for_org_actions.html.erb" do it "does not include create log buttons" do render - expect(fragment).not_to have_button("Create a new lettings log for this organisation") - expect(fragment).not_to have_button("Create a new sales log for this organisation") + expect(fragment).not_to have_button("Create a new lettings log") + expect(fragment).not_to have_button("Create a new sales log") end end end