Browse Source

Merge branch 'main' into CLDC-3633++Full-stops-in-error-messages

pull/2674/head
Manny Dinssa 2 years ago committed by GitHub
parent
commit
49ef0b213f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 37
      app/components/bulk_upload_summary_component.html.erb
  2. 72
      app/components/bulk_upload_summary_component.rb
  3. 9
      app/components/create_log_actions_component.html.erb
  4. 40
      app/components/create_log_actions_component.rb
  5. 27
      app/components/search_component.rb
  6. 1
      app/controllers/form_controller.rb
  7. 44
      app/controllers/lettings_logs_controller.rb
  8. 40
      app/controllers/organisations_controller.rb
  9. 44
      app/controllers/sales_logs_controller.rb
  10. 8
      app/controllers/sessions_controller.rb
  11. 95
      app/controllers/start_controller.rb
  12. 18
      app/frontend/styles/_bulk-uploads.scss
  13. 4
      app/frontend/styles/_tag.scss
  14. 15
      app/frontend/styles/application.scss
  15. 12
      app/helpers/bulk_upload_helper.rb
  16. 23
      app/helpers/collection_resources_helper.rb
  17. 62
      app/helpers/filters_helper.rb
  18. 8
      app/helpers/schemes_helper.rb
  19. 16
      app/helpers/tag_helper.rb
  20. 50
      app/models/bulk_upload.rb
  21. 4
      app/models/bulk_upload_error.rb
  22. 10
      app/models/forms/bulk_upload_lettings/prepare_your_file.rb
  23. 10
      app/models/forms/bulk_upload_sales/prepare_your_file.rb
  24. 5
      app/models/log.rb
  25. 16
      app/models/user.rb
  26. 5
      app/models/validations/financial_validations.rb
  27. 8
      app/policies/organisation_policy.rb
  28. 4
      app/services/bulk_upload/downloader.rb
  29. 23
      app/services/bulk_upload/processor.rb
  30. 6
      app/services/bulk_upload/sales/validator.rb
  31. 21
      app/services/collection_resources_service.rb
  32. 4
      app/services/feature_toggle.rb
  33. 32
      app/services/filter_manager.rb
  34. 9
      app/services/storage/local_disk_service.rb
  35. 8
      app/services/storage/s3_service.rb
  36. 9
      app/views/bulk_upload_lettings_results/show.html.erb
  37. 13
      app/views/bulk_upload_lettings_results/summary.html.erb
  38. 9
      app/views/bulk_upload_sales_results/show.html.erb
  39. 13
      app/views/bulk_upload_sales_results/summary.html.erb
  40. 78
      app/views/bulk_upload_shared/_upload_filters.html.erb
  41. 15
      app/views/bulk_upload_shared/_upload_list.html.erb
  42. 28
      app/views/bulk_upload_shared/uploads.html.erb
  43. 3
      app/views/layouts/application.html.erb
  44. 10
      app/views/logs/_create_for_org_actions.html.erb
  45. 138
      app/views/organisations/duplicate_schemes.html.erb
  46. 2
      app/views/organisations/index.html.erb
  47. 10
      app/views/organisations/schemes.html.erb
  48. 4
      config/locales/en.yml
  49. 16
      config/routes.rb
  50. 5
      db/migrate/20240920144611_add_schemes_deduplicated_at.rb
  51. 8
      db/migrate/20241002163937_add_failure_reason_and_processing_to_bulk_uploads.rb
  52. 5
      db/schema.rb
  53. 19
      lib/tasks/recalculate_status_when_la_missing.rake
  54. BIN
      public/files/2023_24_lettings_paper_form.pdf
  55. BIN
      public/files/2023_24_sales_paper_form.pdf
  56. BIN
      public/files/2024_25_lettings_paper_form.pdf
  57. BIN
      public/files/2024_25_sales_paper_form.pdf
  58. BIN
      public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx
  59. BIN
      public/files/bulk-upload-lettings-specification-2023-24.xlsx
  60. BIN
      public/files/bulk-upload-lettings-specification-2024-25.xlsx
  61. BIN
      public/files/bulk-upload-lettings-template-2023-24.xlsx
  62. BIN
      public/files/bulk-upload-lettings-template-2024-25.xlsx
  63. BIN
      public/files/bulk-upload-sales-legacy-template-2023-24.xlsx
  64. BIN
      public/files/bulk-upload-sales-specification-2023-24.xlsx
  65. BIN
      public/files/bulk-upload-sales-specification-2024-25.xlsx
  66. BIN
      public/files/bulk-upload-sales-template-2023-24.xlsx
  67. BIN
      public/files/bulk-upload-sales-template-2024-25.xlsx
  68. 148
      spec/components/bulk_upload_summary_component_spec.rb
  69. 28
      spec/components/create_log_actions_component_spec.rb
  70. 2
      spec/factories/bulk_upload.rb
  71. 1
      spec/factories/bulk_upload_error.rb
  72. 8
      spec/features/accessibility_spec.rb
  73. 38
      spec/features/lettings_log_spec.rb
  74. 6
      spec/features/notifications_spec.rb
  75. 11
      spec/features/organisation_spec.rb
  76. 31
      spec/features/sales_log_spec.rb
  77. 6
      spec/features/start_page_spec.rb
  78. 7
      spec/features/test_spec.rb
  79. 3
      spec/features/user_spec.rb
  80. 14
      spec/helpers/collection_resources_helper_spec.rb
  81. 117
      spec/helpers/schemes_helper_spec.rb
  82. 208
      spec/models/bulk_upload_spec.rb
  83. 12
      spec/models/user_spec.rb
  84. 16
      spec/models/validations/financial_validations_spec.rb
  85. 3
      spec/requests/auth/passwords_controller_spec.rb
  86. 27
      spec/requests/check_errors_controller_spec.rb
  87. 3
      spec/requests/maintenance_controller_spec.rb
  88. 150
      spec/requests/organisations_controller_spec.rb
  89. 3
      spec/requests/rails_admin_controller_spec.rb
  90. 3
      spec/requests/start_controller_spec.rb
  91. 3
      spec/requests/users_controller_spec.rb
  92. 12
      spec/services/bulk_upload/downloader_spec.rb
  93. 2
      spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb
  94. 2
      spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb
  95. 7
      spec/services/bulk_upload/processor_spec.rb
  96. 3
      spec/views/bulk_upload_lettings_results/show.html.erb_spec.rb
  97. 3
      spec/views/bulk_upload_lettings_results/summary.html.erb_spec.rb
  98. 3
      spec/views/bulk_upload_sales_results/show.html.erb_spec.rb
  99. 3
      spec/views/bulk_upload_sales_results/summary.html.erb_spec.rb
  100. 8
      spec/views/logs/_create_for_org_actions.html.erb_spec.rb

37
app/components/bulk_upload_summary_component.html.erb

@ -0,0 +1,37 @@
<article class="app-log-summary">
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<header class="app-log-summary__header">
<h2 class="govuk-heading-m govuk-!-font-weight-regular govuk-!-margin-bottom-0 text-normal-break ">
<span class="govuk-!-margin-right-1"><%= bulk_upload.filename %></span>
<span class="app-metadata app-log-summary__details" style="white-space: nowrap;"><%= bulk_upload.year %>/<%= bulk_upload.year + 1 %></span>
</h2>
</header>
<div class="govuk-!-margin-bottom-2">
<p class="govuk-hint govuk-!-font-size-16 govuk-!-margin-bottom-1">Uploaded by: <%= bulk_upload.user.name %> (<%= bulk_upload.user.email %>)</p>
<p class="govuk-hint govuk-!-font-size-16 govuk-!-margin-bottom-1">Uploading organisation: <%= bulk_upload.organisation.name %></p>
<p class="govuk-hint govuk-!-font-size-16 govuk-!-margin-bottom-1">Time of upload: <%= bulk_upload.created_at.to_formatted_s(:govuk_date_and_time) %></p>
</div>
<p class="govuk-body govuk-!-margin-bottom-3">
<%= download_file_link(bulk_upload) %>
<%= view_error_report_link(bulk_upload) %>
<%= view_logs_link(bulk_upload) %>
</p>
</div>
<footer class="govuk-grid-column-one-third app-log-summary__footer">
<p class="govuk-body govuk-!-margin-bottom-3">
<%= upload_status %>
</p>
<% unless bulk_upload.processing %>
<div>
<%= counts(
[bulk_upload.total_logs_count, "total log"],
[setup_errors_count, "error on important questions", "errors on important questions"],
[critical_errors_count, "critical error"],
[potential_errors_count, "potential error"],
) %>
</div>
<% end %>
</footer>
</div>
</article>

72
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

9
app/components/create_log_actions_component.html.erb

@ -1,10 +1,11 @@
<% if display_actions? %>
<div class="govuk-button-group app-filter-toggle govuk-!-margin-bottom-6">
<% 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 %>
</div>
<% end %>

40
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

27
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

1
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) }

44
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!

40
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

44
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

8
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

95
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

18
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;
}

4
app/frontend/styles/_tag.scss

@ -5,3 +5,7 @@
padding-bottom: 2px;
padding-left: 6px;
}
.no-max-width {
max-width: none;
}

15
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;
}

12
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

23
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

62
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
"<span class=\"app-!-colour-muted\">You didn’t answer this filter</span>".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

8
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)

16
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 = [])

50
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

4
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

10
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

10
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

5
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|

16
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

5
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")

8
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

4
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

23
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

6
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

21
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

4
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

32
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

9
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

8
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

9
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.
</div>
<h2 class="govuk-heading-m">File: <%= @bulk_upload.filename %></h2>
<p class="govuk-!-font-size-19 govuk-!-margin-bottom-2"><strong>File name: </strong><%= @bulk_upload.filename %></p>
<% if current_user.support? %>
<div class="govuk-!-margin-bottom-7">
<%= govuk_link_to "Download file", download_lettings_bulk_upload_path(@bulk_upload) %>
</div>
<% end %>
</div>
</div>

13
app/views/bulk_upload_lettings_results/summary.html.erb

@ -5,13 +5,18 @@
<span class="govuk-caption-l">Bulk upload for lettings (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">Fix <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> and upload file again</h1>
<p class="govuk-body-l">
<p class="govuk-body">
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.
</p>
<p class="govuk-body-l">
File: <%= @bulk_upload.filename %>
</p>
<p class="govuk-!-font-size-19 govuk-!-margin-bottom-2"><strong>File name: </strong><%= @bulk_upload.filename %></p>
<% if current_user.support? %>
<div class="govuk-!-margin-bottom-7">
<%= govuk_link_to "Download file", download_lettings_bulk_upload_path(@bulk_upload) %>
</div>
<% end %>
</div>
</div>

9
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.
</div>
<h2 class="govuk-heading-m">File: <%= @bulk_upload.filename %></h2>
<p class="govuk-!-font-size-19 govuk-!-margin-bottom-2"><strong>File name: </strong><%= @bulk_upload.filename %></p>
<% if current_user.support? %>
<div class="govuk-!-margin-bottom-7">
<%= govuk_link_to "Download file", download_sales_bulk_upload_path(@bulk_upload) %>
</div>
<% end %>
</div>
</div>

13
app/views/bulk_upload_sales_results/summary.html.erb

@ -5,13 +5,18 @@
<span class="govuk-caption-l">Bulk upload for sales (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">Fix <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> and upload file again</h1>
<p class="govuk-body-l">
<p class="govuk-body">
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.
</p>
<p class="govuk-body-l">
File: <%= @bulk_upload.filename %>
</p>
<p class="govuk-!-font-size-19 govuk-!-margin-bottom-2"><strong>File name: </strong><%= @bulk_upload.filename %></p>
<% if current_user.support? %>
<div class="govuk-!-margin-bottom-7">
<%= govuk_link_to "Download file", download_sales_bulk_upload_path(@bulk_upload) %>
</div>
<% end %>
</div>
</div>

78
app/views/bulk_upload_shared/_upload_filters.html.erb

@ -0,0 +1,78 @@
<div class="app-filter-layout__filter">
<div class="app-filter">
<div class="app-filter__header">
<h2 class="govuk-heading-m">Filters</h2>
</div>
<div class="app-filter__content">
<%= form_with html: { method: :get } do |f| %>
<div class="govuk-grid-row" style="white-space: nowrap">
<p class="govuk-grid-column-one-half">
<%= filters_applied_text(@filter_type) %>
</p>
<p class="govuk-!-text-align-right govuk-grid-column-one-half">
<%= reset_filters_link(@filter_type, { search: request.params["search"] }.compact) %>
</p>
</div>
<%= 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 %>
</div>
</div>
</div>

15
app/views/bulk_upload_shared/_upload_list.html.erb

@ -0,0 +1,15 @@
<h2 class="govuk-body">
<div class="govuk-grid-row app-search__caption">
<div class="govuk-grid-column-three-quarters">
<%= 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))) %>
</div>
<div class="govuk-grid-column-one-quarter govuk-!-text-align-right">
<% if searched || applied_filters_count(@filter_type).positive? %>
<br>
<% end %>
</div>
</div>
</h2>
<% bulk_uploads.map do |bulk_upload| %>
<%= render BulkUploadSummaryComponent.new(bulk_upload:) %>
<% end %>

28
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 %>
<h1 class="govuk-heading-l govuk-!-margin-bottom-7">
<%= bulk_upload_title(controller.controller_name) %>
</h1>
<div class="app-filter-layout" data-controller="filter-layout">
<%= render partial: "bulk_upload_shared/upload_filters" %>
<div class="app-filter-layout__content">
<%= 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" } %>
</div>
</div>

3
app/views/layouts/application.html.erb

@ -60,9 +60,6 @@
</script>
<% end %>
<% if Rails.env.production? && ENV["APP_HOST"].present? %>
<script defer data-domain="<%= ENV["APP_HOST"].split("https://")[1] %>" src="https://plausible.io/js/plausible.js"></script>
<% end %>
</head>
<body class="govuk-template__body app-template--wide">

10
app/views/logs/_create_for_org_actions.html.erb

@ -1,12 +1,14 @@
<div class="govuk-button-group app-filter-toggle">
<% 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 %>
</div>

138
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 %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-xl"><%= title %></h1>
<p class="govuk-body">Since your organisation recently merged, we’ve reviewed your schemes for possible duplicates.</p>
<p class="govuk-body">These sets of schemes and locations might be duplicates because they have the same answers for certain fields.</p>
<h2 class="govuk-heading-m">What you need to do</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Review each set of schemes or locations and decide if they are duplicates.</li>
<li>If they are, choose one to keep and deactivate the others on the date your organisation merged.</li>
<li>When you have resolved all duplicates, confirm below.</li>
</ul>
<p class="govuk-body">If you need help with this, <%= govuk_link_to "contact the helpdesk (opens in a new tab)", GlobalConstants::HELPDESK_URL, target: "#" %>.</p>
<% if @duplicate_schemes.any? %>
<h2 class="govuk-heading-m"><%= @duplicate_schemes.count == 1 ? "This set" : "These #{@duplicate_schemes.count} sets" %> of schemes might have duplicates</h2>
<%= govuk_details(summary_text: "Why are these schemes identified as duplicates?") do %>
<p class="govuk-body">
These schemes have the same answers for the following fields:
</p>
<ul class="govuk-list govuk-list--bullet">
<li>Type of scheme</li>
<li>Registered under Care Standards Act 2000</li>
<li>Housing stock owned by</li>
<li>Support services provided by</li>
<li>Primary client group</li>
<li>Has another client group</li>
<li>Secondary client group</li>
<li>Level of support given</li>
<li>Intended length of stay</li>
</ul>
<% 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 %>
<ol class="govuk-list govuk-list--number">
<% duplicate_set.each do |scheme| %>
<li>
<%= govuk_link_to scheme.service_name, scheme %>
</li>
<% end %>
</ol>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% if @duplicate_locations.any? %>
<h2 class="govuk-heading-m"><%= @duplicate_locations.count == 1 ? "This set" : "These #{@duplicate_locations.count} sets" %> of locations might have duplicates</h2>
<%= govuk_details(summary_text: "Why are these locations identified as duplicates?") do %>
<p class="govuk-body">
These locations belong to the same scheme and have the same answers for the following fields:
</p>
<ul class="govuk-list govuk-list--bullet">
<li>Postcode</li>
<li>Mobility standards</li>
</ul>
<% 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 %>
<ol class="govuk-list govuk-list--number">
<% duplicate_set[:locations].each do |location| %>
<li>
<%= govuk_link_to location.name, scheme_location_path(location) %>
</li>
<% end %>
</ol>
<% 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" %>
</div>
</div>
<% end %>

2
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" } %>
<div class="app-tab__list-view" data-controller="tabs">
<%= 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) %>

10
app/views/organisations/schemes.html.erb

@ -11,6 +11,16 @@
) %>
<h2 class="govuk-visually-hidden">Supported housing schemes</h2>
<% end %>
<% if display_duplicate_schemes_banner?(@organisation, current_user) %>
<%= govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
Some schemes and locations might be duplicates.
<p>
<%= govuk_link_to "Review possible duplicates", href: schemes_duplicates_organisation_path(@organisation) %>
<% end %>
<% end %>
<div class="app-filter-layout" data-controller="filter-layout">
<% if SchemePolicy.new(current_user, nil).create? %>
<%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %>

4
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."

16
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

5
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

8
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

5
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

19
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

BIN
public/files/2023_24_lettings_paper_form.pdf

Binary file not shown.

BIN
public/files/2023_24_sales_paper_form.pdf

Binary file not shown.

BIN
public/files/2024_25_lettings_paper_form.pdf

Binary file not shown.

BIN
public/files/2024_25_sales_paper_form.pdf

Binary file not shown.

BIN
public/files/bulk-upload-lettings-legacy-template-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-lettings-specification-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-lettings-specification-2024-25.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-lettings-template-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-lettings-template-2024-25.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-legacy-template-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-specification-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-specification-2024-25.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-template-2023-24.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-template-2024-25.xlsx

Binary file not shown.

148
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

28
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

2
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] }

1
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

8
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")

38
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

6
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) }

11
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

31
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

6
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

7
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)")

3
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

14
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

117
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

208
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

12
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

16
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

3
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

27
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
{

3
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

150
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

3
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

3
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

3
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

12
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

2
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",

2
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,

7
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:) }

3
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

3
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

3
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

3
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

8
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

Loading…
Cancel
Save