Browse Source

Merge branch 'main' into CLDC-3626-downloading-a-csv-with-specific-user-filter-applied

pull/2646/head
Manny Dinssa 2 years ago committed by GitHub
parent
commit
ea4c01f00e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/review_pipeline.yml
  2. 37
      app/components/bulk_upload_error_row_component.html.erb
  3. 2
      app/components/create_log_actions_component.html.erb
  4. 6
      app/controllers/auth/sessions_controller.rb
  5. 6
      app/controllers/bulk_upload_lettings_logs_controller.rb
  6. 6
      app/controllers/bulk_upload_sales_logs_controller.rb
  7. 1
      app/helpers/application_helper.rb
  8. 10
      app/helpers/filters_helper.rb
  9. 19
      app/helpers/user_helper.rb
  10. 4
      app/jobs/data_export_xml_job.rb
  11. 2
      app/models/export.rb
  12. 25
      app/models/form/sales/pages/not_retired_value_check.rb
  13. 24
      app/models/form/sales/questions/not_retired_value_check.rb
  14. 21
      app/models/form/sales/subsections/household_characteristics.rb
  15. 7
      app/models/forms/bulk_upload_lettings/checking_file.rb
  16. 3
      app/models/forms/bulk_upload_lettings/guidance.rb
  17. 5
      app/models/forms/bulk_upload_lettings/needstype.rb
  18. 8
      app/models/forms/bulk_upload_lettings/prepare_your_file.rb
  19. 7
      app/models/forms/bulk_upload_lettings/upload_your_file.rb
  20. 9
      app/models/forms/bulk_upload_lettings/year.rb
  21. 7
      app/models/forms/bulk_upload_sales/checking_file.rb
  22. 3
      app/models/forms/bulk_upload_sales/guidance.rb
  23. 7
      app/models/forms/bulk_upload_sales/prepare_your_file.rb
  24. 6
      app/models/forms/bulk_upload_sales/upload_your_file.rb
  25. 9
      app/models/forms/bulk_upload_sales/year.rb
  26. 18
      app/models/lettings_log.rb
  27. 2
      app/models/logs_export.rb
  28. 13
      app/models/sales_log.rb
  29. 4
      app/models/user.rb
  30. 17
      app/services/bulk_upload/lettings/validator.rb
  31. 24
      app/services/bulk_upload/lettings/year2023/row_parser.rb
  32. 48
      app/services/bulk_upload/lettings/year2024/row_parser.rb
  33. 17
      app/services/bulk_upload/sales/validator.rb
  34. 20
      app/services/bulk_upload/sales/year2023/row_parser.rb
  35. 42
      app/services/bulk_upload/sales/year2024/row_parser.rb
  36. 78
      app/services/exports/export_service.rb
  37. 138
      app/services/exports/lettings_log_export_service.rb
  38. 27
      app/services/exports/organisation_export_constants.rb
  39. 72
      app/services/exports/organisation_export_service.rb
  40. 18
      app/services/exports/user_export_constants.rb
  41. 68
      app/services/exports/user_export_service.rb
  42. 97
      app/services/exports/xml_export_service.rb
  43. 1
      app/views/bulk_upload_lettings_logs/forms/checking_file.html.erb
  44. 1
      app/views/bulk_upload_lettings_logs/forms/needstype.erb
  45. 1
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb
  46. 1
      app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb
  47. 1
      app/views/bulk_upload_lettings_logs/forms/year.html.erb
  48. 2
      app/views/bulk_upload_lettings_results/show.html.erb
  49. 2
      app/views/bulk_upload_lettings_results/summary.html.erb
  50. 1
      app/views/bulk_upload_sales_logs/forms/checking_file.html.erb
  51. 1
      app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb
  52. 1
      app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb
  53. 1
      app/views/bulk_upload_sales_logs/forms/year.html.erb
  54. 2
      app/views/bulk_upload_sales_results/show.html.erb
  55. 2
      app/views/bulk_upload_sales_results/summary.html.erb
  56. 10
      app/views/logs/_create_for_org_actions.html.erb
  57. 4
      app/views/schemes/index.html.erb
  58. 4
      app/views/users/index.html.erb
  59. 9
      app/views/users/show.html.erb
  60. 2
      config/credentials.yml.enc
  61. 715
      config/locales/en.yml
  62. 5
      db/migrate/20240802093255_rename_export_table.rb
  63. 5
      db/migrate/20240905092332_add_organisation_id_to_bulk_uploads.rb
  64. 5
      db/migrate/20240923145326_add_validation_checked_field.rb
  65. 22
      db/schema.rb
  66. 2
      docs/Gemfile.lock
  67. 10
      lib/tasks/data_export.rake
  68. 20
      lib/tasks/recalculate_status_after_sales_over_retirement_age_validation.rake
  69. 1
      spec/factories/bulk_upload.rb
  70. 8
      spec/features/organisation_spec.rb
  71. 26
      spec/fixtures/exports/organisation.xml
  72. 17
      spec/fixtures/exports/user.xml
  73. 50
      spec/helpers/filters_helper_spec.rb
  74. 4
      spec/helpers/interruption_screen_helper_spec.rb
  75. 61
      spec/helpers/user_helper_spec.rb
  76. 20
      spec/jobs/data_export_xml_job_spec.rb
  77. 10
      spec/lib/tasks/data_export_spec.rb
  78. 55
      spec/lib/tasks/recalculate_status_after_sales_over_retirement_age_validation_spec.rb
  79. 2
      spec/lib/tasks/update_schemes_and_locations_from_csv_spec.rb
  80. 4
      spec/models/form/lettings/questions/declaration_spec.rb
  81. 2
      spec/models/form/lettings/questions/offered_spec.rb
  82. 2
      spec/models/form/lettings/questions/uprn_spec.rb
  83. 4
      spec/models/form/sales/questions/prevown_spec.rb
  84. 8
      spec/models/form/sales/questions/privacy_notice_spec.rb
  85. 4
      spec/models/form/sales/questions/staircase_owned_spec.rb
  86. 2
      spec/models/form/sales/questions/uprn_spec.rb
  87. 35
      spec/models/form/sales/subsections/household_characteristics_spec.rb
  88. 7
      spec/models/lettings_log_spec.rb
  89. 6
      spec/models/location_deactivation_period_spec.rb
  90. 4
      spec/models/notification_spec.rb
  91. 7
      spec/models/sales_log_spec.rb
  92. 4
      spec/models/scheme_deactivation_period_spec.rb
  93. 122
      spec/models/user_spec.rb
  94. 46
      spec/models/validations/financial_validations_spec.rb
  95. 4
      spec/models/validations/household_validations_spec.rb
  96. 4
      spec/models/validations/property_validations_spec.rb
  97. 12
      spec/models/validations/sales/financial_validations_spec.rb
  98. 8
      spec/models/validations/sales/household_validations_spec.rb
  99. 20
      spec/models/validations/sales/property_validations_spec.rb
  100. 12
      spec/models/validations/sales/sale_information_validations_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/review_pipeline.yml

@ -52,6 +52,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: "Created review app at https://review.submit-social-housing-data.levellingup.gov.uk/${{ github.event.pull_request.number }}"
msg: "Created review app at https://review.submit-social-housing-data.communities.gov.uk/${{ github.event.pull_request.number }}"
check_for_duplicate_msg: true
duplicate_msg_pattern: Created review app at*

37
app/components/bulk_upload_error_row_component.html.erb

@ -9,24 +9,27 @@
<div class="govuk-summary-card__content">
<% potential_errors, critical_errors = bulk_upload_errors.partition { |error| error.category == "soft_validation" } %>
<h2 class="govuk-heading-m">Critical errors</h2>
<p class="govuk-body">These errors must be fixed to complete your logs.</p>
<%= govuk_table do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>
<% row.with_cell(header: true, text: "Question") %>
<% row.with_cell(header: true, text: "Error") %>
<% row.with_cell(header: true, text: "Specification") %>
<% end %>
<%= table.with_body do |body| %>
<% critical_errors.each do |error| %>
<% body.with_row do |row| %>
<% row.with_cell(text: error.cell) %>
<% row.with_cell(text: question_for_field(error.field), html_attributes: { class: "govuk-!-width-one-half" }) %>
<% row.with_cell(text: error.error.html_safe, html_attributes: { class: "govuk-!-font-weight-bold govuk-!-width-one-half" }) %>
<% row.with_cell(text: error.field.humanize) %>
<% if critical_errors.any? %>
<h2 class="govuk-heading-m">Critical errors</h2>
<p class="govuk-body">These errors must be fixed to complete your logs.</p>
<%= govuk_table do |table| %>
<%= table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(header: true, text: "Cell") %>
<% row.with_cell(header: true, text: "Question") %>
<% row.with_cell(header: true, text: "Error") %>
<% row.with_cell(header: true, text: "Specification") %>
<% end %>
<%= table.with_body do |body| %>
<% critical_errors.each do |error| %>
<% body.with_row do |row| %>
<% row.with_cell(text: error.cell) %>
<% row.with_cell(text: question_for_field(error.field), html_attributes: { class: "govuk-!-width-one-half" }) %>
<% row.with_cell(text: error.error.html_safe, html_attributes: { class: "govuk-!-font-weight-bold govuk-!-width-one-half" }) %>
<% row.with_cell(text: error.field.humanize) %>
<% end %>
<% end %>
<% end %>
<% end %>

2
app/components/create_log_actions_component.html.erb

@ -3,7 +3,7 @@
<% if create_button_href.present? %>
<%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-6" %>
<% end %>
<% if upload_button_href.present? %>
<% if upload_button_href.present? && !user.support? %>
<%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
<% end %>
</div>

6
app/controllers/auth/sessions_controller.rb

@ -4,12 +4,12 @@ class Auth::SessionsController < Devise::SessionsController
def create
self.resource = User.new
if params.dig("user", "email").empty?
resource.errors.add :email, "Enter an email address"
resource.errors.add :email, "Enter an email address."
elsif !email_valid?(params.dig("user", "email"))
resource.errors.add :email, "Enter an email address in the correct format, like name@example.com"
resource.errors.add :email, "Enter an email address in the correct format, like name@example.com."
end
if params.dig("user", "password").empty?
resource.errors.add :password, "Enter a password"
resource.errors.add :password, "Enter a password."
end
if resource.errors.present?
render :new, status: :unprocessable_entity

6
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -4,9 +4,9 @@ class BulkUploadLettingsLogsController < ApplicationController
def start
if have_choice_of_year?
redirect_to bulk_upload_lettings_log_path(id: "year")
redirect_to bulk_upload_lettings_log_path(id: "year", form: { organisation_id: params[:organisation_id] }.compact)
else
redirect_to bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: current_year })
redirect_to bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: current_year, organisation_id: params[:organisation_id] }.compact)
end
end
@ -60,6 +60,6 @@ private
end
def form_params
params.fetch(:form, {}).permit(:year, :needstype, :file)
params.fetch(:form, {}).permit(:year, :needstype, :file, :organisation_id)
end
end

6
app/controllers/bulk_upload_sales_logs_controller.rb

@ -4,9 +4,9 @@ class BulkUploadSalesLogsController < ApplicationController
def start
if have_choice_of_year?
redirect_to bulk_upload_sales_log_path(id: "year")
redirect_to bulk_upload_sales_log_path(id: "year", form: { organisation_id: params[:organisation_id] }.compact)
else
redirect_to bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: current_year })
redirect_to bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: current_year, organisation_id: params[:organisation_id] }.compact)
end
end
@ -58,6 +58,6 @@ private
end
def form_params
params.fetch(:form, {}).permit(:year, :file)
params.fetch(:form, {}).permit(:year, :file, :organisation_id)
end
end

1
app/helpers/application_helper.rb

@ -2,6 +2,7 @@ module ApplicationHelper
include Pagy::Frontend
def browser_title(title, pagy, *resources)
title = sanitize(title)&.gsub("&amp;", "&")
if resources.any? { |r| r.present? && r.errors.present? }
"Error: #{[title, t('service_name'), 'GOV.UK'].select(&:present?).join(' - ')}"
else

10
app/helpers/filters_helper.rb

@ -142,11 +142,11 @@ module FiltersHelper
end
def collection_year_radio_options
{
current_collection_start_year.to_s => { label: year_combo(current_collection_start_year) },
previous_collection_start_year.to_s => { label: year_combo(previous_collection_start_year) },
archived_collection_start_year.to_s => { label: year_combo(archived_collection_start_year) },
}
options = {}
collection_year_options.map do |year, label|
options[year] = { label: }
end
options
end
def filters_applied_text(filter_type)

19
app/helpers/user_helper.rb

@ -56,4 +56,23 @@ module UserHelper
user.errors.add(attribute, message)
end
end
def display_pending_email_change_banner?(user)
user.unconfirmed_email.present? && user.email != user.unconfirmed_email
end
def pending_email_change_title_text(current_user, user)
if current_user == user
"You have requested to change your email address to #{user.unconfirmed_email}."
else
"There has been a request to change this user’s email address to #{user.unconfirmed_email}."
end
end
def pending_email_change_banner_text(current_user)
text = "A confirmation link has been sent to the new email address. The current email will continue to work until the change is confirmed."
text += " Deactivating this user will cancel the email change request." if current_user.support? || current_user.data_coordinator?
text
end
end

4
app/jobs/data_export_xml_job.rb

@ -3,8 +3,8 @@ class DataExportXmlJob < ApplicationJob
def perform(full_update: false)
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["EXPORT_BUCKET"])
export_service = Exports::LettingsLogExportService.new(storage_service)
export_service = Exports::ExportService.new(storage_service)
export_service.export_xml_lettings_logs(full_update:)
export_service.export_xml(full_update:)
end
end

2
app/models/export.rb

@ -0,0 +1,2 @@
class Export < ApplicationRecord
end

25
app/models/form/sales/pages/not_retired_value_check.rb

@ -0,0 +1,25 @@
class Form::Sales::Pages::NotRetiredValueCheck < Form::Sales::Pages::Person
def initialize(id, hsh, subsection, person_index:)
super
@depends_on = [
{
"person_#{person_index}_not_retired_over_soft_max_age?" => true,
},
]
@person_index = person_index
@title_text = {
"translation" => "soft_validations.retirement.max.title",
}
@informative_text = {
"translation" => "soft_validations.retirement.max.hint_text",
}
end
def questions
@questions ||= [Form::Sales::Questions::NotRetiredValueCheck.new(nil, nil, self, person_index: @person_index)]
end
def interruption_screen_question_ids
%W[age#{@person_index} ecstat#{@person_index}]
end
end

24
app/models/form/sales/questions/not_retired_value_check.rb

@ -0,0 +1,24 @@
class Form::Sales::Questions::NotRetiredValueCheck < ::Form::Question
def initialize(id, hsh, page, person_index:)
super(id, hsh, page)
@id = "retirement_value_check"
@check_answer_label = "Retirement confirmation"
@type = "interruption_screen"
@answer_options = {
"0" => { "value" => "Yes" },
"1" => { "value" => "No" },
}
@hidden_in_check_answers = {
"depends_on" => [
{
"retirement_value_check" => 0,
},
{
"retirement_value_check" => 1,
},
],
}
@check_answers_card_number = person_index
@header = "Are you sure this person isn't retired?"
end
end

21
app/models/form/sales/subsections/household_characteristics.rb

@ -14,10 +14,10 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Sales::Pages::PrivacyNotice.new("privacy_notice", nil, self, joint_purchase: false) unless form.start_year_after_2024?),
Form::Sales::Pages::Age1.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("age_1_retirement_value_check", nil, self, person_index: 1),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_1_not_retired_value_check", nil, self, person_index: 1) if form.start_year_after_2024?),
Form::Sales::Pages::OldPersonsSharedOwnershipValueCheck.new("age_1_old_persons_shared_ownership_joint_purchase_value_check", nil, self, joint_purchase: true),
Form::Sales::Pages::OldPersonsSharedOwnershipValueCheck.new("age_1_old_persons_shared_ownership_value_check", nil, self, joint_purchase: false),
Form::Sales::Pages::GenderIdentity1.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("gender_1_retirement_value_check", nil, self, person_index: 1),
Form::Sales::Pages::Buyer1EthnicGroup.new(nil, nil, self),
Form::Sales::Pages::Buyer1EthnicBackgroundBlack.new(nil, nil, self),
Form::Sales::Pages::Buyer1EthnicBackgroundAsian.new(nil, nil, self),
@ -27,6 +27,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::Buyer1Nationality.new(nil, nil, self),
Form::Sales::Pages::Buyer1WorkingSituation.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_1_retirement_value_check", nil, self, person_index: 1),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_1_not_retired_value_check", nil, self, person_index: 1) if form.start_year_after_2024?),
Form::Sales::Pages::Buyer1IncomeMinValueCheck.new("working_situation_buyer_1_income_min_value_check", nil, self),
Form::Sales::Pages::Buyer1LiveInProperty.new(nil, nil, self),
Form::Sales::Pages::BuyerLiveInValueCheck.new("buyer_1_live_in_property_value_check", nil, self, person_index: 1),
@ -36,12 +37,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::OldPersonsSharedOwnershipValueCheck.new("age_2_old_persons_shared_ownership_joint_purchase_value_check", nil, self, joint_purchase: true),
Form::Sales::Pages::OldPersonsSharedOwnershipValueCheck.new("age_2_old_persons_shared_ownership_value_check", nil, self, joint_purchase: false),
Form::Sales::Pages::RetirementValueCheck.new("age_2_buyer_retirement_value_check", nil, self, person_index: 2),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_2_buyer_not_retired_value_check", nil, self, person_index: 2) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("buyer_2_age_student_not_child_value_check", nil, self, person_index: 2),
Form::Sales::Pages::GenderIdentity2.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2),
buyer_2_ethnicity_nationality_pages,
Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_2_not_retired_value_check_joint_purchase", nil, self, person_index: 2) if form.start_year_after_2024?),
Form::Sales::Pages::Buyer2IncomeMinValueCheck.new("working_situation_buyer_2_income_min_value_check", nil, self),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("buyer_2_working_situation_student_not_child_value_check", nil, self, person_index: 2),
Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self),
@ -55,12 +57,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_2_student_not_child_value_check", nil, self, person_index: 2),
Form::Sales::Pages::PersonAge.new("person_2_age", nil, self, person_index: 2),
Form::Sales::Pages::RetirementValueCheck.new("age_2_retirement_value_check", nil, self, person_index: 2),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_2_not_retired_value_check", nil, self, person_index: 2) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_2_student_not_child_value_check", nil, self, person_index: 2),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_2_partner_under_16_value_check", nil, self, person_index: 2) if form.start_year_after_2024?),
Form::Sales::Pages::PersonGenderIdentity.new("person_2_gender_identity", nil, self, person_index: 2),
Form::Sales::Pages::RetirementValueCheck.new("gender_2_retirement_value_check", nil, self, person_index: 2),
Form::Sales::Pages::PersonWorkingSituation.new("person_2_working_situation", nil, self, person_index: 2),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check", nil, self, person_index: 2),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_2_not_retired_value_check", nil, self, person_index: 2) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_2_student_not_child_value_check", nil, self, person_index: 2),
Form::Sales::Pages::PersonKnown.new("person_3_known", nil, self, person_index: 3),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_3_relationship_to_buyer_1", nil, self, person_index: 3),
@ -69,12 +72,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_3_student_not_child_value_check", nil, self, person_index: 3),
Form::Sales::Pages::PersonAge.new("person_3_age", nil, self, person_index: 3),
Form::Sales::Pages::RetirementValueCheck.new("age_3_retirement_value_check", nil, self, person_index: 3),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_3_not_retired_value_check", nil, self, person_index: 3) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_3_student_not_child_value_check", nil, self, person_index: 3),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_3_partner_under_16_value_check", nil, self, person_index: 3) if form.start_year_after_2024?),
Form::Sales::Pages::PersonGenderIdentity.new("person_3_gender_identity", nil, self, person_index: 3),
Form::Sales::Pages::RetirementValueCheck.new("gender_3_retirement_value_check", nil, self, person_index: 3),
Form::Sales::Pages::PersonWorkingSituation.new("person_3_working_situation", nil, self, person_index: 3),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_3_retirement_value_check", nil, self, person_index: 3),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_3_not_retired_value_check", nil, self, person_index: 3) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_3_student_not_child_value_check", nil, self, person_index: 3),
Form::Sales::Pages::PersonKnown.new("person_4_known", nil, self, person_index: 4),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_4_relationship_to_buyer_1", nil, self, person_index: 4),
@ -83,12 +87,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_4_student_not_child_value_check", nil, self, person_index: 4),
Form::Sales::Pages::PersonAge.new("person_4_age", nil, self, person_index: 4),
Form::Sales::Pages::RetirementValueCheck.new("age_4_retirement_value_check", nil, self, person_index: 4),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_4_not_retired_value_check", nil, self, person_index: 4) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_4_student_not_child_value_check", nil, self, person_index: 4),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_4_partner_under_16_value_check", nil, self, person_index: 4) if form.start_year_after_2024?),
Form::Sales::Pages::PersonGenderIdentity.new("person_4_gender_identity", nil, self, person_index: 4),
Form::Sales::Pages::RetirementValueCheck.new("gender_4_retirement_value_check", nil, self, person_index: 4),
Form::Sales::Pages::PersonWorkingSituation.new("person_4_working_situation", nil, self, person_index: 4),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_4_retirement_value_check", nil, self, person_index: 4),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_4_not_retired_value_check", nil, self, person_index: 4) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_4_student_not_child_value_check", nil, self, person_index: 4),
Form::Sales::Pages::PersonKnown.new("person_5_known", nil, self, person_index: 5),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_5_relationship_to_buyer_1", nil, self, person_index: 5),
@ -97,12 +102,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_5_student_not_child_value_check", nil, self, person_index: 5),
Form::Sales::Pages::PersonAge.new("person_5_age", nil, self, person_index: 5),
Form::Sales::Pages::RetirementValueCheck.new("age_5_retirement_value_check", nil, self, person_index: 5),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_5_not_retired_value_check", nil, self, person_index: 5) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_5_student_not_child_value_check", nil, self, person_index: 5),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_5_partner_under_16_value_check", nil, self, person_index: 5) if form.start_year_after_2024?),
Form::Sales::Pages::PersonGenderIdentity.new("person_5_gender_identity", nil, self, person_index: 5),
Form::Sales::Pages::RetirementValueCheck.new("gender_5_retirement_value_check", nil, self, person_index: 5),
Form::Sales::Pages::PersonWorkingSituation.new("person_5_working_situation", nil, self, person_index: 5),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_5_retirement_value_check", nil, self, person_index: 5),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_5_not_retired_value_check", nil, self, person_index: 5) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_5_student_not_child_value_check", nil, self, person_index: 5),
Form::Sales::Pages::PersonKnown.new("person_6_known", nil, self, person_index: 6),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_6_relationship_to_buyer_1", nil, self, person_index: 6),
@ -111,12 +117,13 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_6_student_not_child_value_check", nil, self, person_index: 6),
Form::Sales::Pages::PersonAge.new("person_6_age", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("age_6_retirement_value_check", nil, self, person_index: 6),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_6_not_retired_value_check", nil, self, person_index: 6) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_6_student_not_child_value_check", nil, self, person_index: 6),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_6_partner_under_16_value_check", nil, self, person_index: 6) if form.start_year_after_2024?),
Form::Sales::Pages::PersonGenderIdentity.new("person_6_gender_identity", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("gender_6_retirement_value_check", nil, self, person_index: 6),
Form::Sales::Pages::PersonWorkingSituation.new("person_6_working_situation", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_6_retirement_value_check", nil, self, person_index: 6),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_6_not_retired_value_check", nil, self, person_index: 6) if form.start_year_after_2024?),
Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_6_student_not_child_value_check", nil, self, person_index: 6),
].flatten.compact
end

7
app/models/forms/bulk_upload_lettings/checking_file.rb

@ -6,13 +6,18 @@ module Forms
include Rails.application.routes.url_helpers
attribute :year, :integer
attribute :organisation_id, :integer
def view_path
"bulk_upload_lettings_logs/forms/checking_file"
end
def back_path
bulk_upload_lettings_log_path(id: "start")
if organisation_id.present?
lettings_logs_organisation_path(organisation_id)
else
bulk_upload_lettings_log_path(id: "start")
end
end
def year_combo

3
app/models/forms/bulk_upload_lettings/guidance.rb

@ -7,6 +7,7 @@ module Forms
attribute :year, :integer
attribute :referrer
attribute :organisation_id, :integer
def view_path
"bulk_upload_shared/guidance"
@ -15,7 +16,7 @@ module Forms
def back_path
case referrer
when "prepare-your-file"
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: })
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, organisation_id: }.compact)
when "home"
root_path
else

5
app/models/forms/bulk_upload_lettings/needstype.rb

@ -7,6 +7,7 @@ module Forms
attribute :needstype, :integer
attribute :year, :integer
attribute :organisation_id, :integer
validates :needstype, presence: true
@ -19,11 +20,11 @@ module Forms
end
def back_path
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, needstype: })
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def next_path
bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype: })
bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def year_combo

8
app/models/forms/bulk_upload_lettings/prepare_your_file.rb

@ -7,6 +7,7 @@ module Forms
attribute :year, :integer
attribute :needstype, :integer
attribute :organisation_id, :integer
def view_path
case year
@ -19,15 +20,16 @@ module Forms
def back_path
if have_choice_of_year?
Rails.application.routes.url_helpers.bulk_upload_lettings_log_path(id: "year", form: { year: })
Rails.application.routes.url_helpers.bulk_upload_lettings_log_path(id: "year", form: { year:, organisation_id: }.compact)
elsif organisation_id.present?
lettings_logs_organisation_path(organisation_id)
else
Rails.application.routes.url_helpers.lettings_logs_path
end
end
def next_path
page_id = year == 2022 ? "needstype" : "upload-your-file"
bulk_upload_lettings_log_path(id: page_id, form: { year:, needstype: })
bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def legacy_template_path

7
app/models/forms/bulk_upload_lettings/upload_your_file.rb

@ -11,6 +11,7 @@ module Forms
attribute :needstype, :integer
attribute :file
attribute :current_user
attribute :organisation_id, :integer
validates :file, presence: true
validate :validate_file_is_csv
@ -20,8 +21,7 @@ module Forms
end
def back_path
page_id = year == 2022 ? "needstype" : "prepare-your-file"
bulk_upload_lettings_log_path(id: page_id, form: { year:, needstype: })
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, needstype:, organisation_id: }.compact)
end
def year_combo
@ -29,7 +29,7 @@ module Forms
end
def next_path
bulk_upload_lettings_log_path(id: "checking-file", form: { year: })
bulk_upload_lettings_log_path(id: "checking-file", form: { year:, organisation_id: }.compact)
end
def save!
@ -39,6 +39,7 @@ module Forms
year:,
needstype:,
filename: file.original_filename,
organisation_id: (organisation_id if current_user.support?) || current_user.organisation_id,
)
storage_service.write_file(bulk_upload.identifier, File.read(file.path))

9
app/models/forms/bulk_upload_lettings/year.rb

@ -6,6 +6,7 @@ module Forms
include Rails.application.routes.url_helpers
attribute :year, :integer
attribute :organisation_id, :integer
validates :year, presence: true
@ -20,11 +21,15 @@ module Forms
end
def back_path
lettings_logs_path
if organisation_id.present?
lettings_logs_organisation_path(organisation_id)
else
lettings_logs_path
end
end
def next_path
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year: })
bulk_upload_lettings_log_path(id: "prepare-your-file", form: { year:, organisation_id: }.compact)
end
def save!

7
app/models/forms/bulk_upload_sales/checking_file.rb

@ -6,13 +6,18 @@ module Forms
include Rails.application.routes.url_helpers
attribute :year, :integer
attribute :organisation_id, :integer
def view_path
"bulk_upload_sales_logs/forms/checking_file"
end
def back_path
bulk_upload_sales_log_path(id: "start")
if organisation_id.present?
sales_logs_organisation_path(organisation_id)
else
bulk_upload_sales_log_path(id: "start")
end
end
def year_combo

3
app/models/forms/bulk_upload_sales/guidance.rb

@ -7,6 +7,7 @@ module Forms
attribute :year, :integer
attribute :referrer
attribute :organisation_id, :integer
def view_path
"bulk_upload_shared/guidance"
@ -15,7 +16,7 @@ module Forms
def back_path
case referrer
when "prepare-your-file"
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: })
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year:, organisation_id: }.compact)
when "home"
root_path
else

7
app/models/forms/bulk_upload_sales/prepare_your_file.rb

@ -6,6 +6,7 @@ module Forms
include Rails.application.routes.url_helpers
attribute :year, :integer
attribute :organisation_id, :integer
def view_path
case year
@ -18,14 +19,16 @@ module Forms
def back_path
if have_choice_of_year?
Rails.application.routes.url_helpers.bulk_upload_sales_log_path(id: "year", form: { year: })
Rails.application.routes.url_helpers.bulk_upload_sales_log_path(id: "year", form: { year: }.compact)
elsif organisation_id.present?
sales_logs_organisation_path(organisation_id)
else
Rails.application.routes.url_helpers.sales_logs_path
end
end
def next_path
bulk_upload_sales_log_path(id: "upload-your-file", form: { year: })
bulk_upload_sales_log_path(id: "upload-your-file", form: { year:, organisation_id: }.compact)
end
def legacy_template_path

6
app/models/forms/bulk_upload_sales/upload_your_file.rb

@ -10,6 +10,7 @@ module Forms
attribute :year, :integer
attribute :file
attribute :current_user
attribute :organisation_id, :integer
validates :file, presence: true
validate :validate_file_is_csv
@ -19,7 +20,7 @@ module Forms
end
def back_path
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: })
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year:, organisation_id: }.compact)
end
def year_combo
@ -27,7 +28,7 @@ module Forms
end
def next_path
bulk_upload_sales_log_path(id: "checking-file", form: { year: })
bulk_upload_sales_log_path(id: "checking-file", form: { year:, organisation_id: }.compact)
end
def save!
@ -36,6 +37,7 @@ module Forms
log_type: BulkUpload.log_types[:sales],
year:,
filename: file.original_filename,
organisation_id: (organisation_id if current_user.support?) || current_user.organisation_id,
)
storage_service.write_file(bulk_upload.identifier, File.read(file.path))

9
app/models/forms/bulk_upload_sales/year.rb

@ -6,6 +6,7 @@ module Forms
include Rails.application.routes.url_helpers
attribute :year, :integer
attribute :organisation_id, :integer
validates :year, presence: true
@ -20,11 +21,15 @@ module Forms
end
def back_path
sales_logs_path
if organisation_id.present?
sales_logs_organisation_path(organisation_id)
else
sales_logs_path
end
end
def next_path
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year: })
bulk_upload_sales_log_path(id: "prepare-your-file", form: { year:, organisation_id: }.compact)
end
def save!

18
app/models/lettings_log.rb

@ -59,17 +59,25 @@ class LettingsLog < Log
query.all
}
scope :search_by, lambda { |param|
by_id = Arel.sql("CASE WHEN lettings_logs.id = #{param.to_i} THEN 0 ELSE 1 END")
by_tenant_code = Arel.sql("CASE WHEN tenancycode = '#{param}' THEN 0 WHEN tenancycode ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_propcode = Arel.sql("CASE WHEN propcode = '#{param}' THEN 0 WHEN propcode ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = '#{param.delete(' ')}' THEN 0 when REPLACE(postcode_full, ' ', '') ILIKE '%#{param.delete(' ')}%' then 1 ELSE 2 END")
sanitized_param = ActiveRecord::Base.sanitize_sql(param)
param_without_spaces = sanitized_param.delete(" ")
by_id = Arel.sql("CASE WHEN lettings_logs.id = ? THEN 0 ELSE 1 END")
by_tenant_code = Arel.sql("CASE WHEN tenancycode = ? THEN 0 WHEN tenancycode ILIKE ? THEN 1 ELSE 2 END")
by_propcode = Arel.sql("CASE WHEN propcode = ? THEN 0 WHEN propcode ILIKE ? THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = ? THEN 0 WHEN REPLACE(postcode_full, ' ', '') ILIKE ? THEN 1 ELSE 2 END")
filter_by_location_postcode(param)
.or(filter_by_tenant_code(param))
.or(filter_by_propcode(param))
.or(filter_by_postcode(param))
.or(filter_by_id(param.gsub(/log/i, "")))
.order(by_id, by_tenant_code, by_propcode, by_postcode)
.order(
[by_id, sanitized_param.to_i],
[by_tenant_code, sanitized_param, sanitized_param],
[by_propcode, sanitized_param, sanitized_param],
[by_postcode, param_without_spaces, param_without_spaces],
)
}
scope :after_date, ->(date) { where("lettings_logs.startdate >= ?", date) }
scope :before_date, ->(date) { where("lettings_logs.startdate < ?", date) }

2
app/models/logs_export.rb

@ -1,2 +0,0 @@
class LogsExport < ApplicationRecord
end

13
app/models/sales_log.rb

@ -46,14 +46,19 @@ class SalesLog < Log
}
scope :filter_by_purchaser_code, ->(purchid) { where("purchid ILIKE ?", "%#{purchid}%") }
scope :search_by, lambda { |param|
by_id = Arel.sql("CASE WHEN id = #{param.to_i} THEN 0 ELSE 1 END")
by_purchaser_code = Arel.sql("CASE WHEN purchid = '#{param}' THEN 0 WHEN purchid ILIKE '%#{param}%' THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = '#{param.delete(' ')}' THEN 0 WHEN REPLACE(postcode_full, ' ', '') ILIKE '%#{param.delete(' ')}%' THEN 1 ELSE 2 END")
sanitized_param = ActiveRecord::Base.sanitize_sql(param)
param_without_spaces = sanitized_param.delete(" ")
by_id = Arel.sql("CASE WHEN id = ? THEN 0 ELSE 1 END")
by_purchaser_code = Arel.sql("CASE WHEN purchid = ? THEN 0 WHEN purchid ILIKE ? THEN 1 ELSE 2 END")
by_postcode = Arel.sql("CASE WHEN REPLACE(postcode_full, ' ', '') = ? THEN 0 WHEN REPLACE(postcode_full, ' ', '') ILIKE ? THEN 1 ELSE 2 END")
filter_by_purchaser_code(param)
.or(filter_by_postcode(param))
.or(filter_by_id(param.gsub(/log/i, "")))
.order(by_id, by_purchaser_code, by_postcode)
.order([by_id, sanitized_param.to_i],
[by_purchaser_code, sanitized_param, sanitized_param],
[by_postcode, param_without_spaces, param_without_spaces])
}
scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) }
scope :duplicate_logs, lambda { |log|

4
app/models/user.rb

@ -212,6 +212,10 @@ class User < ApplicationRecord
end
def assignable_roles
if Rails.env.staging? && Rails.application.credentials[:staging_role_update_email_allowlist].include?(email.split("@").last.downcase)
return ROLES
end
return {} unless data_coordinator? || support?
return ROLES if support?

17
app/services/bulk_upload/lettings/validator.rb

@ -42,14 +42,25 @@ class BulkUpload::Lettings::Validator
def create_logs?
return false if any_setup_errors?
return false if row_parsers.any?(&:block_log_creation?)
return false if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
if row_parsers.any?(&:block_log_creation?)
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_message("Bulk upload log creation blocked due to duplicate logs: #{bulk_upload.id}.")
return false
end
row_parsers.each do |row_parser|
row_parser.log.blank_invalid_non_setup_fields!
end
return false if any_logs_invalid?
if any_logs_invalid?
Sentry.capture_message("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
end
true
end

24
app/services/bulk_upload/lettings/year2023/row_parser.rb

@ -555,7 +555,7 @@ private
return if field_3.blank?
unless assigned_to
errors.add(:field_3, "User with the specified email could not be found")
errors.add(:field_3, "User with the specified email could not be found.")
end
end
@ -565,7 +565,7 @@ private
return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation
block_log_creation!
errors.add(:field_3, "User must be related to owning organisation or managing organisation")
errors.add(:field_3, "User must be related to owning organisation or managing organisation.")
end
def assigned_to
@ -588,7 +588,7 @@ private
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(". ")
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end
end
@ -780,7 +780,7 @@ private
def validate_related_location_exists
if scheme && location_id.present? && location.nil? && location_field.present?
block_log_creation!
errors.add(location_field, "#{location_or_scheme.capitalize} code must relate to a #{location_or_scheme} that is owned by the owning organisation or managing organisation", category: :setup)
errors.add(location_field, "#{location_or_scheme.capitalize} code must relate to a #{location_or_scheme} that is owned by the owning organisation or managing organisation.", category: :setup)
end
end
@ -794,7 +794,7 @@ private
def validate_related_scheme_exists
if scheme_id.present? && scheme_field.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil?
block_log_creation!
errors.add(scheme_field, "This #{scheme_or_management_group} code does not belong to the owning organisation or managing organisation", category: :setup)
errors.add(scheme_field, "This #{scheme_or_management_group} code does not belong to the owning organisation or managing organisation.", category: :setup)
end
end
@ -810,7 +810,7 @@ private
block_log_creation!
if errors[:field_2].blank?
errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation", category: :setup)
errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation.", category: :setup)
end
end
end
@ -820,7 +820,7 @@ private
block_log_creation!
if errors[:field_2].blank?
errors.add(:field_2, "The managing organisation code is incorrect", category: :setup)
errors.add(:field_2, "The managing organisation code is incorrect.", category: :setup)
end
end
end
@ -828,7 +828,7 @@ private
def validate_managing_org_data_given
if field_2.blank?
block_log_creation!
errors.add(:field_2, "The managing organisation code is incorrect", category: :setup)
errors.add(:field_2, "The managing organisation code is incorrect.", category: :setup)
end
end
@ -837,7 +837,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock", category: :setup)
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock.", category: :setup)
end
end
end
@ -847,7 +847,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code is incorrect", category: :setup)
errors.add(:field_1, "The owning organisation code is incorrect.", category: :setup)
end
end
end
@ -864,7 +864,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "You do not have permission to add logs for this owning organisation", category: :setup)
errors.add(:field_1, "You do not have permission to add logs for this owning organisation.", category: :setup)
end
end
end
@ -887,7 +887,7 @@ private
def validate_if_log_already_exists
if log_already_exists?
error_message = "This is a duplicate log"
error_message = "This is a duplicate log."
errors.add(:field_1, error_message) # owning_organisation
errors.add(:field_7, error_message) # startdate

48
app/services/bulk_upload/lettings/year2024/row_parser.rb

@ -432,6 +432,7 @@ class BulkUpload::Lettings::Year2024::RowParser
validate :validate_assigned_to_exists, on: :after_log
validate :validate_assigned_to_related, on: :after_log
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_all_charges_given, on: :after_log, if: proc { is_carehome.zero? }
validate :validate_address_option_found, on: :after_log, unless: -> { supported_housing? }
@ -578,7 +579,13 @@ private
return if field_3.blank?
unless assigned_to
errors.add(:field_3, "User with the specified email could not be found")
errors.add(:field_3, "User with the specified email could not be found.")
end
end
def validate_assigned_to_when_support
if field_3.blank? && bulk_upload.user.support?
errors.add(:field_3, category: :setup, message: I18n.t("validations.not_answered", question: "what is the CORE username of the account this letting log should be assigned to?"))
end
end
@ -588,7 +595,7 @@ private
return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation
block_log_creation!
errors.add(:field_3, "User must be related to owning organisation or managing organisation")
errors.add(:field_3, "User must be related to owning organisation or managing organisation.")
end
def assigned_to
@ -643,7 +650,7 @@ private
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(". ")
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end
end
@ -825,14 +832,14 @@ private
def validate_related_location_exists
if scheme && field_6.present? && location.nil? && :field_6.present?
block_log_creation!
errors.add(:field_6, "Location code must relate to a location that is owned by the owning organisation or managing organisation", category: :setup)
errors.add(:field_6, "Location code must relate to a location that is owned by the owning organisation or managing organisation.", category: :setup)
end
end
def validate_related_scheme_exists
if field_5.present? && :field_5.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil?
block_log_creation!
errors.add(:field_5, "This scheme code does not belong to the owning organisation or managing organisation", category: :setup)
errors.add(:field_5, "This scheme code does not belong to the owning organisation or managing organisation.", category: :setup)
end
end
@ -841,7 +848,7 @@ private
block_log_creation!
if errors[:field_2].blank?
errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation", category: :setup)
errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation.", category: :setup)
end
end
end
@ -851,7 +858,7 @@ private
block_log_creation!
if field_2.present? && errors[:field_2].blank?
errors.add(:field_2, "The managing organisation code is incorrect", category: :setup)
errors.add(:field_2, "The managing organisation code is incorrect.", category: :setup)
end
end
end
@ -868,7 +875,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock", category: :setup)
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock.", category: :setup)
end
end
end
@ -878,7 +885,7 @@ private
block_log_creation!
if field_1.present? && errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code is incorrect", category: :setup)
errors.add(:field_1, "The owning organisation code is incorrect.", category: :setup)
end
end
end
@ -891,12 +898,17 @@ private
end
def validate_owning_org_permitted
if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation)
block_log_creation!
return unless owning_organisation
return if bulk_upload_organisation.affiliated_stock_owners.include?(owning_organisation)
if errors[:field_1].blank?
errors.add(:field_1, "You do not have permission to add logs for this owning organisation", category: :setup)
end
block_log_creation!
return if errors[:field_1].present?
if bulk_upload.user.support?
errors.add(:field_1, "This owning organisation is not affiliated with #{bulk_upload_organisation.name}.", category: :setup)
else
errors.add(:field_1, "You do not have permission to add logs for this owning organisation.", category: :setup)
end
end
@ -931,7 +943,7 @@ private
def validate_if_log_already_exists
if log_already_exists?
error_message = "This is a duplicate log"
error_message = "This is a duplicate log."
errors.add(:field_1, error_message) # owning_organisation
errors.add(:field_8, error_message) # startdate
@ -1137,7 +1149,7 @@ private
attributes["renewal"] = renewal
attributes["scheme"] = scheme
attributes["location"] = location
attributes["assigned_to"] = assigned_to || bulk_upload.user
attributes["assigned_to"] = assigned_to || (bulk_upload.user.support? ? nil : bulk_upload.user)
attributes["created_by"] = bulk_upload.user
attributes["needstype"] = field_4
attributes["rent_type"] = RENT_TYPE_BU_MAPPING[field_11]
@ -1625,4 +1637,8 @@ private
def reason_is_other?
field_98 == 20
end
def bulk_upload_organisation
Organisation.find(bulk_upload.organisation_id)
end
end

17
app/services/bulk_upload/sales/validator.rb

@ -41,14 +41,25 @@ class BulkUpload::Sales::Validator
def create_logs?
return false if any_setup_errors?
return false if row_parsers.any?(&:block_log_creation?)
return false if any_logs_already_exist? && FeatureToggle.bulk_upload_duplicate_log_check_enabled?
if row_parsers.any?(&:block_log_creation?)
Sentry.capture_exception("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}.")
return false
end
row_parsers.each do |row_parser|
row_parser.log.blank_invalid_non_setup_fields!
end
return false if any_logs_invalid?
if any_logs_invalid?
Sentry.capture_exception("Bulk upload log creation blocked due to invalid logs after blanking non setup fields: #{bulk_upload.id}.")
return false
end
true
end

20
app/services/bulk_upload/sales/year2023/row_parser.rb

@ -1158,7 +1158,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code is incorrect", category: :setup)
errors.add(:field_1, "The owning organisation code is incorrect.", category: :setup)
end
end
end
@ -1168,7 +1168,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code is incorrect", category: :setup)
errors.add(:field_1, "The owning organisation code is incorrect.", category: :setup)
end
end
end
@ -1178,7 +1178,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock", category: :setup)
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock.", category: :setup)
end
end
end
@ -1188,7 +1188,7 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "You do not have permission to add logs for this owning organisation", category: :setup)
errors.add(:field_1, "You do not have permission to add logs for this owning organisation.", category: :setup)
end
end
end
@ -1197,7 +1197,7 @@ private
return if field_2.blank?
unless assigned_to
errors.add(:field_2, "User with the specified email could not be found")
errors.add(:field_2, "User with the specified email could not be found.")
end
end
@ -1207,7 +1207,7 @@ private
return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation
block_log_creation!
errors.add(:field_2, "User must be related to owning organisation or managing organisation", category: :setup)
errors.add(:field_2, "User must be related to owning organisation or managing organisation.", category: :setup)
end
def managing_organisation
@ -1221,7 +1221,7 @@ private
block_log_creation!
if errors[:field_2].blank?
errors.add(:field_2, "This user belongs to an organisation that does not have a relationship with the owning organisation", category: :setup)
errors.add(:field_2, "This user belongs to an organisation that does not have a relationship with the owning organisation.", category: :setup)
end
end
end
@ -1296,7 +1296,7 @@ private
def validate_if_log_already_exists
if log_already_exists?
error_message = "This is a duplicate log"
error_message = "This is a duplicate log."
errors.add(:field_1, error_message) # Owning org
errors.add(:field_3, error_message) # Sale completion date
@ -1321,7 +1321,7 @@ private
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(". ")
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end
end
@ -1331,7 +1331,7 @@ private
def validate_buyer1_economic_status
if field_35 == 9
errors.add(:field_35, "Buyer 1 cannot be a child under 16")
errors.add(:field_35, "Buyer 1 cannot be a child under 16.")
end
end
end

42
app/services/bulk_upload/sales/year2024/row_parser.rb

@ -462,6 +462,7 @@ class BulkUpload::Sales::Year2024::RowParser
validate :validate_assigned_to_exists, on: :after_log
validate :validate_assigned_to_related, on: :after_log
validate :validate_assigned_to_when_support, on: :after_log
validate :validate_managing_org_related, on: :after_log
validate :validate_relevant_collection_window, on: :after_log
validate :validate_incomplete_soft_validations, on: :after_log
@ -925,7 +926,7 @@ private
attributes["owning_organisation"] = owning_organisation
attributes["managing_organisation"] = managing_organisation
attributes["assigned_to"] = assigned_to || bulk_upload.user
attributes["assigned_to"] = assigned_to || (bulk_upload.user.support? ? nil : bulk_upload.user)
attributes["created_by"] = bulk_upload.user
attributes["hhregres"] = field_72
attributes["hhregresstill"] = field_73
@ -1286,7 +1287,7 @@ private
block_log_creation!
if field_1.present? && errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code is incorrect", category: :setup)
errors.add(:field_1, "The owning organisation code is incorrect.", category: :setup)
end
end
end
@ -1296,18 +1297,23 @@ private
block_log_creation!
if errors[:field_1].blank?
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock", category: :setup)
errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock.", category: :setup)
end
end
end
def validate_owning_org_permitted
if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation)
block_log_creation!
return unless owning_organisation
return if bulk_upload_organisation.affiliated_stock_owners.include?(owning_organisation)
if errors[:field_1].blank?
errors.add(:field_1, "You do not have permission to add logs for this owning organisation", category: :setup)
end
block_log_creation!
return if errors[:field_1].present?
if bulk_upload.user.support?
errors.add(:field_1, "This owning organisation is not affiliated with #{bulk_upload_organisation.name}.", category: :setup)
else
errors.add(:field_1, "You do not have permission to add logs for this owning organisation.", category: :setup)
end
end
@ -1315,7 +1321,13 @@ private
return if field_3.blank?
unless assigned_to
errors.add(:field_3, "User with the specified email could not be found")
errors.add(:field_3, "User with the specified email could not be found.")
end
end
def validate_assigned_to_when_support
if field_3.blank? && bulk_upload.user.support?
errors.add(:field_3, category: :setup, message: I18n.t("validations.not_answered", question: "what is the CORE username of the account this sales log should be assigned to?"))
end
end
@ -1325,7 +1337,7 @@ private
return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation
block_log_creation!
errors.add(:field_3, "User must be related to owning organisation or managing organisation", category: :setup)
errors.add(:field_3, "User must be related to owning organisation or managing organisation.", category: :setup)
end
def managing_organisation
@ -1345,7 +1357,7 @@ private
block_log_creation!
if errors[:field_2].blank?
errors.add(:field_2, "This organisation does not have a relationship with the owning organisation", category: :setup)
errors.add(:field_2, "This organisation does not have a relationship with the owning organisation.", category: :setup)
end
end
end
@ -1420,7 +1432,7 @@ private
def validate_if_log_already_exists
if log_already_exists?
error_message = "This is a duplicate log"
error_message = "This is a duplicate log."
errors.add(:field_1, error_message) # Owning org
errors.add(:field_4, error_message) # Sale completion date
@ -1445,7 +1457,7 @@ private
field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(". ")
error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
errors.add(field, message: error_message, category: :soft_validation)
end
end
@ -1492,4 +1504,8 @@ private
def valid_nationality_options
%w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say"
end
def bulk_upload_organisation
Organisation.find(bulk_upload.organisation_id)
end
end

78
app/services/exports/export_service.rb

@ -0,0 +1,78 @@
module Exports
class ExportService
include CollectionTimeHelper
def initialize(storage_service, logger = Rails.logger)
@storage_service = storage_service
@logger = logger
end
def export_xml(full_update: false, collection: nil)
start_time = Time.zone.now
daily_run_number = get_daily_run_number
lettings_archives_for_manifest = {}
users_archives_for_manifest = {}
organisations_archives_for_manifest = {}
if collection.present?
case collection
when "users"
users_archives_for_manifest = get_user_archives(start_time, full_update)
when "organisations"
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
else
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
end
else
users_archives_for_manifest = get_user_archives(start_time, full_update)
organisations_archives_for_manifest = get_organisation_archives(start_time, full_update)
lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, collection)
end
write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest))
end
private
def get_daily_run_number
today = Time.zone.today
Export.where(created_at: today.beginning_of_day..today.end_of_day).select(:started_at).distinct.count + 1
end
def write_master_manifest(daily_run, archive_datetimes)
today = Time.zone.today
increment_number = daily_run.to_s.rjust(4, "0")
month = today.month.to_s.rjust(2, "0")
day = today.day.to_s.rjust(2, "0")
file_path = "Manifest_#{today.year}_#{month}_#{day}_#{increment_number}.csv"
string_io = build_manifest_csv_io(archive_datetimes)
@storage_service.write_file(file_path, string_io)
end
def build_manifest_csv_io(archive_datetimes)
headers = ["zip-name", "date-time zipped folder generated", "zip-file-uri"]
csv_string = CSV.generate do |csv|
csv << headers
archive_datetimes.each do |(archive, datetime)|
csv << [archive, datetime, "#{archive}.zip"]
end
end
StringIO.new(csv_string)
end
def get_user_archives(start_time, full_update)
users_export_service = Exports::UserExportService.new(@storage_service, start_time)
users_export_service.export_xml_users(full_update:)
end
def get_organisation_archives(start_time, full_update)
organisations_export_service = Exports::OrganisationExportService.new(@storage_service, start_time)
organisations_export_service.export_xml_organisations(full_update:)
end
def get_lettings_archives(start_time, full_update, collection)
lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time)
lettings_export_service.export_xml_lettings_logs(full_update:, collection_year: collection)
end
end
end

138
app/services/exports/lettings_log_export_service.rb

@ -1,22 +1,15 @@
module Exports
class LettingsLogExportService
class LettingsLogExportService < Exports::XmlExportService
include Exports::LettingsLogExportConstants
include CollectionTimeHelper
def initialize(storage_service, logger = Rails.logger)
@storage_service = storage_service
@logger = logger
end
def export_xml_lettings_logs(full_update: false, collection_year: nil)
start_time = Time.zone.now
daily_run_number = get_daily_run_number
archives_for_manifest = {}
recent_export = LogsExport.order("started_at").last
collection_years_to_export(collection_year).each do |collection|
base_number = LogsExport.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, start_time, base_number, full_update)
archives = write_export_archive(export, collection, start_time, recent_export, full_update)
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives = write_export_archive(export, collection, recent_export, full_update)
archives_for_manifest.merge!(archives)
@ -24,46 +17,11 @@ module Exports
export.save!
end
write_master_manifest(daily_run_number, archives_for_manifest)
archives_for_manifest
end
private
def get_daily_run_number
today = Time.zone.today
LogsExport.where(created_at: today.beginning_of_day..today.end_of_day).select(:started_at).distinct.count + 1
end
def build_export_run(collection, current_time, base_number, full_update)
@logger.info("Building export run for #{collection}")
previous_exports_with_data = LogsExport.where(collection:, empty_export: false)
increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1
if full_update
base_number += 1 if LogsExport.any? # Only increment when it's not the first run
increment_number = 1
else
increment_number += 1
end
if previous_exports_with_data.empty?
return LogsExport.new(collection:, base_number:, started_at: current_time)
end
LogsExport.new(collection:, started_at: current_time, base_number:, increment_number:)
end
def write_master_manifest(daily_run, archive_datetimes)
today = Time.zone.today
increment_number = daily_run.to_s.rjust(4, "0")
month = today.month.to_s.rjust(2, "0")
day = today.day.to_s.rjust(2, "0")
file_path = "Manifest_#{today.year}_#{month}_#{day}_#{increment_number}.csv"
string_io = build_manifest_csv_io(archive_datetimes)
@storage_service.write_file(file_path, string_io)
end
def get_archive_name(collection, base_number, increment)
return unless collection
@ -72,88 +30,14 @@ module Exports
"core_#{collection}_#{collection + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def write_export_archive(export, collection, start_time, recent_export, full_update)
archive = get_archive_name(collection, export.base_number, export.increment_number) # archive name would be the same for all logs because they're already filtered by year (?)
initial_logs_count = retrieve_lettings_logs(start_time, recent_export, full_update).filter_by_year(collection).count
@logger.info("Creating #{archive} - #{initial_logs_count} logs")
return {} if initial_logs_count.zero?
zip_file = Zip::File.open_buffer(StringIO.new)
part_number = 1
last_processed_marker = nil
logs_count_after_export = 0
loop do
lettings_logs_slice = if last_processed_marker.present?
retrieve_lettings_logs(start_time, recent_export, full_update).filter_by_year(collection)
.where("created_at > ?", last_processed_marker)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
else
retrieve_lettings_logs(start_time, recent_export, full_update).filter_by_year(collection)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
end
break if lettings_logs_slice.empty?
data_xml = build_export_xml(lettings_logs_slice)
part_number_str = "pt#{part_number.to_s.rjust(3, '0')}"
zip_file.add("#{archive}_#{part_number_str}.xml", data_xml)
part_number += 1
last_processed_marker = lettings_logs_slice.last.created_at
logs_count_after_export += lettings_logs_slice.count
@logger.info("Added #{archive}_#{part_number_str}.xml")
end
manifest_xml = build_manifest_xml(logs_count_after_export)
zip_file.add("manifest.xml", manifest_xml)
# Required by S3 to avoid Aws::S3::Errors::BadDigest
zip_io = zip_file.write_buffer
zip_io.rewind
@logger.info("Writing #{archive}.zip")
@storage_service.write_file("#{archive}.zip", zip_io)
{ archive => Time.zone.now }
end
def retrieve_lettings_logs(start_time, recent_export, full_update)
def retrieve_resources(recent_export, full_update, collection)
if !full_update && recent_export
params = { from: recent_export.started_at, to: start_time }
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params)
params = { from: recent_export.started_at, to: @start_time }
LettingsLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(collection)
else
params = { to: start_time }
LettingsLog.exportable.where("updated_at <= :to", params)
end
end
def build_manifest_csv_io(archive_datetimes)
headers = ["zip-name", "date-time zipped folder generated", "zip-file-uri"]
csv_string = CSV.generate do |csv|
csv << headers
archive_datetimes.each do |(archive, datetime)|
csv << [archive, datetime, "#{archive}.zip"]
end
params = { to: @start_time }
LettingsLog.exportable.where("updated_at <= :to", params).filter_by_year(collection)
end
StringIO.new(csv_string)
end
def xml_doc_to_temp_file(xml_doc)
file = Tempfile.new
xml_doc.write_xml_to(file, encoding: "UTF-8")
file.rewind
file
end
def build_manifest_xml(record_number)
doc = Nokogiri::XML("<report/>")
doc.at("report") << doc.create_element("form-data-summary")
doc.at("form-data-summary") << doc.create_element("records")
doc.at("records") << doc.create_element("count-of-records", record_number)
xml_doc_to_temp_file(doc)
end
def apply_cds_transformation(lettings_log, export_mode)

27
app/services/exports/organisation_export_constants.rb

@ -0,0 +1,27 @@
module Exports::OrganisationExportConstants
MAX_XML_RECORDS = 10_000
EXPORT_FIELDS = Set[
"id",
"name",
"phone",
"provider_type",
"address_line1",
"address_line2",
"postcode",
"holds_own_stock",
"housing_registration_no",
"active",
"old_org_id",
"old_visible_id",
"merge_date",
"absorbing_organisation_id",
"available_from",
"deleted_at",
"dsa_signed",
"dsa_signed_at",
"dpo_email",
"profit_status",
"group"
]
end

72
app/services/exports/organisation_export_service.rb

@ -0,0 +1,72 @@
module Exports
class OrganisationExportService < Exports::XmlExportService
include Exports::OrganisationExportConstants
include CollectionTimeHelper
def export_xml_organisations(full_update: false)
collection = "organisations"
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
export.empty_export = archives_for_manifest.empty?
export.save!
archives_for_manifest
end
private
def get_archive_name(collection, base_number, increment)
return unless collection
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
Organisation.where("(updated_at >= :from AND updated_at <= :to)", params)
else
params = { to: @start_time }
Organisation.where("updated_at <= :to", params)
end
end
def build_export_xml(organisations)
doc = Nokogiri::XML("<forms/>")
organisations.each do |organisation|
attribute_hash = apply_cds_transformation(organisation)
form = doc.create_element("form")
doc.at("forms") << form
attribute_hash.each do |key, value|
if !EXPORT_FIELDS.include?(key)
next
else
form << doc.create_element(key, value)
end
end
end
xml_doc_to_temp_file(doc)
end
def apply_cds_transformation(organisation)
attribute_hash = organisation.attributes
attribute_hash["deleted_at"] = organisation.discarded_at
attribute_hash["dsa_signed"] = organisation.data_protection_confirmed?
attribute_hash["dsa_signed_at"] = organisation.data_protection_confirmation&.signed_at
attribute_hash["dpo_email"] = organisation.data_protection_confirmation&.data_protection_officer_email
attribute_hash["provider_type"] = organisation.provider_type_before_type_cast
attribute_hash["profit_status"] = nil # will need update when we add the field to the org
attribute_hash["group"] = nil # will need update when we add the field to the org
attribute_hash
end
end
end

18
app/services/exports/user_export_constants.rb

@ -0,0 +1,18 @@
module Exports::UserExportConstants
MAX_XML_RECORDS = 10_000
EXPORT_FIELDS = Set[
"id",
"email",
"name",
"phone",
"organisation_id",
"organisation_name",
"role",
"is_dpo",
"is_key_contact",
"active",
"sign_in_count",
"last_sign_in_at",
]
end

68
app/services/exports/user_export_service.rb

@ -0,0 +1,68 @@
module Exports
class UserExportService < Exports::XmlExportService
include Exports::UserExportConstants
include CollectionTimeHelper
def export_xml_users(full_update: false)
collection = "users"
recent_export = Export.where(collection:).order("started_at").last
base_number = Export.where(empty_export: false, collection:).maximum(:base_number) || 1
export = build_export_run(collection, base_number, full_update)
archives_for_manifest = write_export_archive(export, collection, recent_export, full_update)
export.empty_export = archives_for_manifest.empty?
export.save!
archives_for_manifest
end
private
def get_archive_name(collection, base_number, increment)
return unless collection
base_number_str = "f#{base_number.to_s.rjust(4, '0')}"
increment_str = "inc#{increment.to_s.rjust(4, '0')}"
"#{collection}_2024_2025_apr_mar_#{base_number_str}_#{increment_str}".downcase
end
def retrieve_resources(recent_export, full_update, _collection)
if !full_update && recent_export
params = { from: recent_export.started_at, to: @start_time }
User.where("(updated_at >= :from AND updated_at <= :to)", params)
else
params = { to: @start_time }
User.where("updated_at <= :to", params)
end
end
def build_export_xml(users)
doc = Nokogiri::XML("<forms/>")
users.each do |user|
attribute_hash = apply_cds_transformation(user)
form = doc.create_element("form")
doc.at("forms") << form
attribute_hash.each do |key, value|
if !EXPORT_FIELDS.include?(key)
next
else
form << doc.create_element(key, value)
end
end
end
xml_doc_to_temp_file(doc)
end
def apply_cds_transformation(user)
attribute_hash = user.attributes_before_type_cast
attribute_hash["role"] = user.role
attribute_hash["organisation_name"] = user.organisation.name
attribute_hash["active"] = user.active?
attribute_hash["phone"] = [user.phone, user.phone_extension].compact.join(" ")
attribute_hash
end
end
end

97
app/services/exports/xml_export_service.rb

@ -0,0 +1,97 @@
module Exports
class XmlExportService
include Exports::LettingsLogExportConstants
include CollectionTimeHelper
def initialize(storage_service, start_time, logger = Rails.logger)
@storage_service = storage_service
@logger = logger
@start_time = start_time
end
private
def build_export_run(collection, base_number, full_update)
@logger.info("Building export run for #{collection}")
previous_exports_with_data = Export.where(collection:, empty_export: false)
increment_number = previous_exports_with_data.where(base_number:).maximum(:increment_number) || 1
if full_update
base_number += 1 if Export.any? # Only increment when it's not the first run
increment_number = 1
else
increment_number += 1
end
if previous_exports_with_data.empty?
return Export.new(collection:, base_number:, started_at: @start_time)
end
Export.new(collection:, started_at: @start_time, base_number:, increment_number:)
end
def write_export_archive(export, collection, recent_export, full_update)
archive = get_archive_name(collection, export.base_number, export.increment_number) # archive name would be the same for all logs because they're already filtered by year (?)
initial_count = retrieve_resources(recent_export, full_update, collection).count
@logger.info("Creating #{archive} - #{initial_count} resources")
return {} if initial_count.zero?
zip_file = Zip::File.open_buffer(StringIO.new)
part_number = 1
last_processed_marker = nil
count_after_export = 0
loop do
slice = if last_processed_marker.present?
retrieve_resources(recent_export, full_update, collection)
.where("created_at > ?", last_processed_marker)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
else
retrieve_resources(recent_export, full_update, collection)
.order(:created_at)
.limit(MAX_XML_RECORDS).to_a
end
break if slice.empty?
data_xml = build_export_xml(slice)
part_number_str = "pt#{part_number.to_s.rjust(3, '0')}"
zip_file.add("#{archive}_#{part_number_str}.xml", data_xml)
part_number += 1
last_processed_marker = slice.last.created_at
count_after_export += slice.count
@logger.info("Added #{archive}_#{part_number_str}.xml")
end
manifest_xml = build_manifest_xml(count_after_export)
zip_file.add("manifest.xml", manifest_xml)
# Required by S3 to avoid Aws::S3::Errors::BadDigest
zip_io = zip_file.write_buffer
zip_io.rewind
@logger.info("Writing #{archive}.zip")
@storage_service.write_file("#{archive}.zip", zip_io)
{ archive => Time.zone.now }
end
def xml_doc_to_temp_file(xml_doc)
file = Tempfile.new
xml_doc.write_xml_to(file, encoding: "UTF-8")
file.rewind
file
end
def build_manifest_xml(record_number)
doc = Nokogiri::XML("<report/>")
doc.at("report") << doc.create_element("form-data-summary")
doc.at("form-data-summary") << doc.create_element("records")
doc.at("records") << doc.create_element("count-of-records", record_number)
xml_doc_to_temp_file(doc)
end
end
end

1
app/views/bulk_upload_lettings_logs/forms/checking_file.html.erb

@ -6,6 +6,7 @@
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<span class="govuk-caption-l">Upload lettings logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">We’re checking the file</h1>

1
app/views/bulk_upload_lettings_logs/forms/needstype.erb

@ -7,6 +7,7 @@
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "needstype"), method: :patch do |f| %>
<%= f.govuk_error_summary %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_collection_radio_buttons :needstype,
@form.options,

1
app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb

@ -6,6 +6,7 @@
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<span class="govuk-caption-l">Upload lettings logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">Prepare your file</h1>

1
app/views/bulk_upload_lettings_logs/forms/upload_your_file.html.erb

@ -7,6 +7,7 @@
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "upload-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :needstype %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_error_summary %>

1
app/views/bulk_upload_lettings_logs/forms/year.html.erb

@ -4,6 +4,7 @@
<%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "year"), method: :patch do |f| %>
<%= f.govuk_error_summary %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_collection_radio_buttons :year,
@form.options,

2
app/views/bulk_upload_lettings_results/show.html.erb

@ -25,4 +25,4 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>

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

@ -29,4 +29,4 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path(organisation_id: @bulk_upload.organisation_id) %>

1
app/views/bulk_upload_sales_logs/forms/checking_file.html.erb

@ -6,6 +6,7 @@
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %><%= f.hidden_field :organisation_id %>
<span class="govuk-caption-l">Upload sales logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">We’re checking the file</h1>

1
app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb

@ -6,6 +6,7 @@
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "prepare-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<span class="govuk-caption-l">Upload sales logs in bulk (<%= @form.year_combo %>)</span>
<h1 class="govuk-heading-l">Prepare your file</h1>

1
app/views/bulk_upload_sales_logs/forms/upload_your_file.html.erb

@ -6,6 +6,7 @@
<div class="govuk-grid-column-two-thirds">
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "upload-your-file"), method: :patch do |f| %>
<%= f.hidden_field :year %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_error_summary %>

1
app/views/bulk_upload_sales_logs/forms/year.html.erb

@ -4,6 +4,7 @@
<%= form_with model: @form, scope: :form, url: bulk_upload_sales_log_path(id: "year"), method: :patch do |f| %>
<%= f.govuk_error_summary %>
<%= f.hidden_field :organisation_id %>
<%= f.govuk_collection_radio_buttons :year,
@form.options,

2
app/views/bulk_upload_sales_results/show.html.erb

@ -25,4 +25,4 @@
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>

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

@ -29,4 +29,4 @@
<% end %>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path %>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_sales_logs_path(organisation_id: @bulk_upload.organisation_id) %>

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

@ -1,10 +1,12 @@
<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) %>
<% end %>
<%= 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 %>
<% 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) %>
<% end %>
<%= 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 %>
<% end %>
<% end %>
</div>

4
app/views/schemes/index.html.erb

@ -6,7 +6,9 @@
<%= render partial: "organisations/headings", locals: current_user.support? ? { main: "Supported housing schemes", sub: nil } : { main: "Supported housing schemes", sub: current_user.organisation.name } %>
<% if SchemePolicy.new(current_user, nil).create? %>
<%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %>
<div class="govuk-button-group govuk-!-margin-bottom-6">
<%= govuk_button_link_to "Create a new supported housing scheme", new_scheme_path, html: { method: :post } %>
</div>
<% end %>
<div class="app-filter-layout" data-controller="filter-layout">
<%= render partial: "schemes/scheme_filters" %>

4
app/views/users/index.html.erb

@ -6,7 +6,9 @@
<%= render partial: "organisations/headings", locals: current_user.support? ? { main: "Users", sub: nil } : { main: "Users", sub: current_user.organisation.name } %>
<% if current_user.data_coordinator? || current_user.support? %>
<%= govuk_button_link_to "Invite user", new_user_path, html: { method: :get } %>
<div class="govuk-button-group govuk-!-margin-bottom-6">
<%= govuk_button_link_to "Invite user", new_user_path, html: { method: :get } %>
</div>
<% end %>
<div class="app-filter-layout" data-controller="filter-layout">
<%= render partial: "users/user_filters" %>

9
app/views/users/show.html.erb

@ -11,6 +11,15 @@
<% end %>
<% end %>
<% if display_pending_email_change_banner?(@user) %>
<%= govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
<%= pending_email_change_title_text(current_user, @user) %>
</p>
<%= pending_email_change_banner_text(current_user) %>
<% end %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">

2
config/credentials.yml.enc

@ -1 +1 @@
EZNV2LiNWzf52erbQ41Dz3Bh+2f3Uih8liEyhXp5XzHCLzAbmN6/IJqr7b9cTZiCiroFo4n/dFoG3yYrospp3frKsDXxF1K2/MTCJWjpgnn7wc+HiPQWG0W3HRtQCNkyyrHes0YKcYyDWIP6kztYv1I/Me3p0pGEx6t3CpSTg1v46eRnOlDWiUz3rVxPauwq9IYZ75gmnThqvg/Z8wcYsWLx0arago0SXtRPASCNj4uO/lbqTcAfyIXOTSiOlcAIjoPFRSQY7UqY0o2p8jRR/1L16SmGDsk8ijm+UygNmMexa3Khy5WcKctpQICakHs4NRjHNflqgXpXKL9dVBmNc9d7h+gbhbGJQ53Y0d+a35UbhPRMiv4SRH98FwB+WEsLCDdGSHvdmM6ArfOLljTrqrsmSRf0JfUrvzyVYmMCxjv4xgJwUS/TD5lQD1yPwkp2ss00kQJqzNmB7qwFhA8a3e2iNzV8qtAV/Nj+tMlr99Hb7vZZs98/38G2p5RAsE/5Xl9taKhc/ACnVc/bwJND4JWaBB7duCa08xVB8nkjlt5cCwMurzAcy1ZT+e8JepR+g6s8fpScMEWVJXE0hd8=--rZ41rY9TMXmiBUJw--QiLRVNVXZzTW446s7cec1g==
QGn9IiI91BaO4IGAtfy92FrNP46X9T2jJErRv+o/PRG9LrimEGeuOE+FwhArKZQ5cTipaDqo8u9Ajv45Kitv3c0GynOOvz0r3OjPRHO/p4hW8BFWQDv581cWWPsyZT2JO51zZ5LnwNFvWrjEB2q49YESgtfADPkJWmtx/By5Cg2/PVIRxvhGKOnheme5cih050wqg/43BdiF0PD9FDTZXJDLJg/QQ8nQYkvQe2jN4nM4mTVpkQkmzDKgGknmUWFfW3qWFzlsdMkdkPdeP9wLnJVbFTeyaaJT3wv6l19d2rKqo8iVvacdaQjRev+LVXqOsNAjVHwcPNQVq9s8pxG24HLk3aQ14Eyjf6tHAuZAV4jLnNqQtBQ0AIldWeOl6SKmlTom1P1tcLp9KpajEADplmWSwUktIGmaakFjk/ApYaUBiYTku2iLHMrT/xSc3jPj5W/ZggeJ0Ij6nuGYE1cmBxWGxda9PzOrDP8coEK9vPHiNeDDM1RoukVmf8gwDmshILi5EwIAsO2gJXM1wtPYMu41+H4/y3c0GIwgfv9QP11q+nqhG1MMcOrAUKGhypAS+M+uLwfGQudfQDKP9Zv3VCnOk3mkKlpIzMMD4UdJxQeE/8sfwIsEhWggEo3oa93ptbRdvJ7YYcVvmMmkVBxk0KWFprl4i/BkFHLWrKNl5LBOGA==--ziMOTnYBB5TDyXYU--3FJMs8e6R8lheqcqB8p8uQ==

715
config/locales/en.yml

File diff suppressed because it is too large Load Diff

5
db/migrate/20240802093255_rename_export_table.rb

@ -0,0 +1,5 @@
class RenameExportTable < ActiveRecord::Migration[7.0]
def change
rename_table :logs_exports, :exports
end
end

5
db/migrate/20240905092332_add_organisation_id_to_bulk_uploads.rb

@ -0,0 +1,5 @@
class AddOrganisationIdToBulkUploads < ActiveRecord::Migration[7.0]
def change
add_column :bulk_uploads, :organisation_id, :integer
end
end

5
db/migrate/20240923145326_add_validation_checked_field.rb

@ -0,0 +1,5 @@
class AddValidationCheckedField < ActiveRecord::Migration[7.0]
def change
add_column :log_validations, :checked, :boolean
end
end

22
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_18_112702) do
ActiveRecord::Schema[7.0].define(version: 2024_09_23_145326) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -42,6 +42,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_18_112702) do
t.text "choice"
t.integer "total_logs_count"
t.string "rent_type_fix_status", default: "not_applied"
t.integer "organisation_id"
t.integer "moved_user_id"
t.index ["identifier"], name: "index_bulk_uploads_on_identifier", unique: true
t.index ["user_id"], name: "index_bulk_uploads_on_user_id"
@ -77,6 +78,15 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_18_112702) do
t.index ["organisation_id"], name: "index_data_protection_confirmations_on_organisation_id"
end
create_table "exports", force: :cascade do |t|
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }
t.datetime "started_at", null: false
t.integer "base_number", default: 1, null: false
t.integer "increment_number", default: 1, null: false
t.boolean "empty_export", default: false, null: false
t.string "collection"
end
create_table "la_rent_ranges", force: :cascade do |t|
t.integer "ranges_rent_id"
t.integer "lettype"
@ -410,15 +420,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_09_18_112702) do
t.string "other_validated_models"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "logs_exports", force: :cascade do |t|
t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }
t.datetime "started_at", null: false
t.integer "base_number", default: 1, null: false
t.integer "increment_number", default: 1, null: false
t.boolean "empty_export", default: false, null: false
t.string "collection"
t.boolean "checked"
end
create_table "merge_request_organisations", force: :cascade do |t|

2
docs/Gemfile.lock

@ -253,7 +253,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
webrick (1.8.2)
PLATFORMS
arm64-darwin-21

10
lib/tasks/data_export.rake

@ -7,11 +7,13 @@ namespace :core do
end
desc "Export all data XMLs for import into Central Data System (CDS)"
task :full_data_export_xml, %i[year] => :environment do |_task, args|
collection_year = args[:year].present? ? args[:year].to_i : nil
task :full_data_export_xml, %i[collection] => :environment do |_task, args|
collection = args[:collection].presence
collection = collection.to_i if collection.present? && collection.scan(/\D/).empty?
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["EXPORT_BUCKET"])
export_service = Exports::LettingsLogExportService.new(storage_service)
export_service = Exports::ExportService.new(storage_service)
export_service.export_xml_lettings_logs(full_update: true, collection_year:)
export_service.export_xml(full_update: true, collection:)
end
end

20
lib/tasks/recalculate_status_after_sales_over_retirement_age_validation.rake

@ -0,0 +1,20 @@
desc "Recalculates status for 2024 logs that will trigger new sales over retirement age soft validation"
task recalculate_status_over_retirement: :environment do
validation_trigger_condition = "(ecstat1 != 5 AND age1 > 66) OR (ecstat2 != 5 AND age2 > 66) OR (ecstat3 != 5 AND age3 > 66) OR (ecstat4 != 5 AND age4 > 66) OR (ecstat5 != 5 AND age5 > 66) OR (ecstat6 != 5 AND age6 > 66)"
SalesLog.filter_by_year(2024).where(status: "pending", status_cache: "completed").where(validation_trigger_condition).find_each do |log|
log.status_cache = log.calculate_status
log.skip_update_status = true
unless log.save
Rails.logger.info "Could not save changes to pending sales log #{log.id}"
end
end
SalesLog.filter_by_year(2024).where(status: "completed").where(validation_trigger_condition).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

1
spec/factories/bulk_upload.rb

@ -9,6 +9,7 @@ FactoryBot.define do
sequence(:filename) { |n| "bulk-upload-#{n}.csv" }
needstype { 1 }
rent_type_fix_status { BulkUpload.rent_type_fix_statuses.values.sample }
organisation_id { user.organisation_id }
trait(:sales) do
log_type { BulkUpload.log_types[:sales] }

8
spec/features/organisation_spec.rb

@ -139,6 +139,10 @@ RSpec.describe "User Features" do
expect(page).to have_button("Create a new lettings log for this organisation")
end
it "shows a upload lettings logs in bulk link" do
expect(page).to have_link("Upload lettings logs in bulk")
end
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")
@ -230,6 +234,10 @@ RSpec.describe "User Features" do
expect(page).to have_button("Create a new sales log for this organisation")
end
it "shows a upload sales logs in bulk link" do
expect(page).to have_link("Upload sales logs in bulk")
end
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")

26
spec/fixtures/exports/organisation.xml vendored

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<id>{id}</id>
<name>MHCLG</name>
<phone/>
<provider_type>1</provider_type>
<address_line1>2 Marsham Street</address_line1>
<address_line2>London</address_line2>
<postcode>SW1P 4DF</postcode>
<holds_own_stock>true</holds_own_stock>
<active>true</active>
<housing_registration_no>1234</housing_registration_no>
<old_org_id/>
<old_visible_id/>
<merge_date/>
<absorbing_organisation_id/>
<available_from/>
<deleted_at/>
<dsa_signed>true</dsa_signed>
<dsa_signed_at>{dsa_signed_at}</dsa_signed_at>
<dpo_email>{dpo_email}</dpo_email>
<profit_status/>
<group/>
</form>
</forms>

17
spec/fixtures/exports/user.xml vendored

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<id>{id}</id>
<email>{email}</email>
<name>Danny Rojas</name>
<organisation_id>{organisation_id}</organisation_id>
<sign_in_count>5</sign_in_count>
<last_sign_in_at/>
<role>data_provider</role>
<phone>1234512345123 123</phone>
<is_dpo>false</is_dpo>
<is_key_contact>false</is_key_contact>
<active>true</active>
<organisation_name>MHCLG</organisation_name>
</form>
</forms>

50
spec/helpers/filters_helper_spec.rb

@ -527,6 +527,56 @@ RSpec.describe FiltersHelper do
end
end
describe "#collection_year_radio_options" do
context "with 23/24 as the current collection year" do
before do
allow(Time).to receive(:now).and_return(Time.zone.local(2023, 5, 1))
end
context "and in crossover period" do
before do
allow(FormHandler.instance).to receive(:in_crossover_period?).and_return(true)
end
it "has the correct options" do
expect(collection_year_radio_options).to eq(
{
"2023" => { label: "2023/24" }, "2022" => { label: "2022/23" }, "2021" => { label: "2021/22" }
},
)
end
end
context "and not in crossover period" do
before do
allow(FormHandler.instance).to receive(:in_crossover_period?).and_return(false)
end
it "has the correct options" do
expect(collection_year_radio_options).to eq(
{
"2023" => { label: "2023/24" }, "2022" => { label: "2022/23" }
},
)
end
end
end
context "with 24/25 as the current collection year" do
before do
allow(Time).to receive(:now).and_return(Time.zone.local(2024, 5, 1))
end
it "has the correct options" do
expect(collection_year_radio_options).to eq(
{
"2024" => { label: "2024/25" }, "2023" => { label: "2023/24" }, "2022" => { label: "2022/23" }
},
)
end
end
end
describe "#filters_applied_text" do
let(:filter_type) { "lettings_logs" }
let(:result) { filters_applied_text(filter_type) }

4
spec/helpers/interruption_screen_helper_spec.rb

@ -151,7 +151,7 @@ RSpec.describe InterruptionScreenHelper do
},
],
}
expect(display_informative_text(informative_text_hash, lettings_log)).to eq("You said this: £12,345.00")
expect(display_informative_text(informative_text_hash, lettings_log)).to eq("You said this: £12,345.00.")
end
end
@ -216,7 +216,7 @@ RSpec.describe InterruptionScreenHelper do
},
],
}
expect(display_title_text(title_text, lettings_log)).to eq("You said this: £12,345.00")
expect(display_title_text(title_text, lettings_log)).to eq("You said this: £12,345.00.")
end
end
end

61
spec/helpers/user_helper_spec.rb

@ -105,6 +105,32 @@ RSpec.describe UserHelper do
end
end
describe "display_pending_email_change_banner?" do
context "when the user doesn't have an unconfirmed email" do
let(:user) { FactoryBot.create(:user, :data_provider, unconfirmed_email: nil) }
it "does not display pending email change banner" do
expect(display_pending_email_change_banner?(user)).to be false
end
end
context "when the user has the same unconfirmed email as current email" do
let(:user) { FactoryBot.create(:user, :data_provider, unconfirmed_email: "updated_email@example.com", email: "updated_email@example.com") }
it "does not display pending email change banner" do
expect(display_pending_email_change_banner?(user)).to be false
end
end
context "when the user has a different unconfirmed email" do
let(:user) { FactoryBot.create(:user, :data_provider, unconfirmed_email: "updated_email@example.com", email: "old_email@example.com") }
it "displays pending email change banner" do
expect(display_pending_email_change_banner?(user)).to be true
end
end
end
describe "organisation_change_confirmation_warning" do
context "when user owns logs" do
before do
@ -147,4 +173,39 @@ RSpec.describe UserHelper do
end
end
end
describe "pending_email_change_title_text" do
let(:user) { FactoryBot.create(:user, :data_provider, unconfirmed_email: "updated_email@example.com", email: "old_email@example.com") }
let(:current_user) { FactoryBot.create(:user, :support) }
context "when viewing own profile" do
it "returns the correct text" do
expect(pending_email_change_title_text(user, user)).to eq("You have requested to change your email address to updated_email@example.com.")
end
end
context "when viewing another user's profile" do
it "returns the correct text" do
expect(pending_email_change_title_text(current_user, user)).to eq("There has been a request to change this user’s email address to updated_email@example.com.")
end
end
end
describe "pending_email_change_banner_text" do
context "with provider user" do
let(:user) { FactoryBot.create(:user, :data_provider) }
it "returns the correct text" do
expect(pending_email_change_banner_text(user)).to eq("A confirmation link has been sent to the new email address. The current email will continue to work until the change is confirmed.")
end
end
context "with support user" do
let(:user) { FactoryBot.create(:user, :support) }
it "returns the correct text" do
expect(pending_email_change_banner_text(user)).to eq("A confirmation link has been sent to the new email address. The current email will continue to work until the change is confirmed. Deactivating this user will cancel the email change request.")
end
end
end
end

20
spec/jobs/data_export_xml_job_spec.rb

@ -1,25 +1,33 @@
require "rails_helper"
describe DataExportXmlJob do
let(:storage_service) { instance_double(Storage::S3Service) }
let(:storage_service) { instance_double(Storage::S3Service, write_file: nil) }
let(:env_config_service) { instance_double(Configuration::EnvConfigurationService) }
let(:export_service) { instance_double(Exports::LettingsLogExportService) }
let(:lettings_export_service) { instance_double(Exports::LettingsLogExportService, export_xml_lettings_logs: {}) }
let(:user_export_service) { instance_double(Exports::UserExportService, export_xml_users: {}) }
let(:organisation_export_service) { instance_double(Exports::OrganisationExportService, export_xml_organisations: {}) }
before do
allow(Storage::S3Service).to receive(:new).and_return(storage_service)
allow(Configuration::EnvConfigurationService).to receive(:new).and_return(env_config_service)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(export_service)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_export_service)
allow(Exports::UserExportService).to receive(:new).and_return(user_export_service)
allow(Exports::OrganisationExportService).to receive(:new).and_return(organisation_export_service)
end
it "calls the export service" do
expect(export_service).to receive(:export_xml_lettings_logs)
it "calls the export services" do
expect(lettings_export_service).to receive(:export_xml_lettings_logs)
expect(user_export_service).to receive(:export_xml_users)
expect(organisation_export_service).to receive(:export_xml_organisations)
described_class.perform_now
end
context "with full update enabled" do
it "calls the export service" do
expect(export_service).to receive(:export_xml_lettings_logs).with(full_update: true)
expect(lettings_export_service).to receive(:export_xml_lettings_logs).with(full_update: true, collection_year: nil)
expect(user_export_service).to receive(:export_xml_users).with(full_update: true)
expect(organisation_export_service).to receive(:export_xml_organisations).with(full_update: true)
described_class.perform_now(full_update: true)
end

10
spec/lib/tasks/data_export_spec.rb

@ -4,7 +4,7 @@ require "rake"
describe "rake core:data_export", type: task do
let(:export_bucket) { "export_bucket" }
let(:storage_service) { instance_double(Storage::S3Service) }
let(:export_service) { instance_double(Exports::LettingsLogExportService) }
let(:export_service) { instance_double(Exports::ExportService) }
before do
Rake.application.rake_require("tasks/data_export")
@ -12,7 +12,7 @@ describe "rake core:data_export", type: task do
task.reenable
allow(Storage::S3Service).to receive(:new).and_return(storage_service)
allow(Exports::LettingsLogExportService).to receive(:new).and_return(export_service)
allow(Exports::ExportService).to receive(:new).and_return(export_service)
allow(ENV).to receive(:[])
allow(ENV).to receive(:[]).with("EXPORT_BUCKET").and_return(export_bucket)
end
@ -30,15 +30,15 @@ describe "rake core:data_export", type: task do
context "with all available years" do
it "calls the export service" do
expect(export_service).to receive(:export_xml_lettings_logs).with(full_update: true, collection_year: nil)
expect(export_service).to receive(:export_xml).with(full_update: true, collection: nil)
task.invoke
end
end
context "with a specific year" do
context "with a specific collection" do
it "calls the export service" do
expect(export_service).to receive(:export_xml_lettings_logs).with(full_update: true, collection_year: 2022)
expect(export_service).to receive(:export_xml).with(full_update: true, collection: 2022)
task.invoke("2022")
end

55
spec/lib/tasks/recalculate_status_after_sales_over_retirement_age_validation_spec.rb

@ -0,0 +1,55 @@
require "rails_helper"
require "rake"
RSpec.describe "recalculate_status_after_sales_over_retirement_age_validation" do
describe ":recalculate_status_over_retirement", type: :task do
subject(:task) { Rake::Task["recalculate_status_over_retirement"] }
before do
Rake.application.rake_require("tasks/recalculate_status_after_sales_over_retirement_age_validation")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
context "and there is a completed sales log that trips the validation" do
let(:log) { create(:sales_log, :completed, ecstat1: 1, age1: 67) }
before do
log.status = "completed"
log.skip_update_status = true
log.save!
end
it "sets the log to in progress" do
task.invoke
log.reload
expect(log.status).to eq("in_progress")
end
end
context "and there is a pending sales log that trips the validation" do
let(:log) { create(:sales_log, :completed, ecstat2: 1, age2: 70) }
before do
log.status = "pending"
log.status_cache = "completed"
log.skip_update_status = true
log.save!
end
it "updates the status cache" do
task.invoke
log.reload
expect(log.status_cache).to eq("in_progress")
end
it "does not change the log status" do
task.invoke
log.reload
expect(log.status).to eq("pending")
end
end
end
end
end

2
spec/lib/tasks/update_schemes_and_locations_from_csv_spec.rb

@ -445,7 +445,7 @@ RSpec.describe "bulk_update" do
expect(Rails.logger).to receive(:info).with("Will not export log #{lettings_log_5.id} as it is before the exportable date")
expect(Rails.logger).to receive(:info).with("No changes to location #{locations[1].id}.")
expect(Rails.logger).to receive(:info).with("Cannot update location #{locations[2].id} with postcode: SWAAA. Enter a postcode in the correct format, for example AA1 1AA")
expect(Rails.logger).to receive(:info).with("Cannot update location #{locations[2].id} with postcode: SWAAA. Enter a postcode in the correct format, for example AA1 1AA.")
expect(Rails.logger).to receive(:info).with("Cannot update location #{locations[2].id} with scheme_code: S. Scheme with id S is not in the database")
expect(Rails.logger).to receive(:info).with("Cannot update location #{locations[2].id} with location_admin_district: Westminst. Location admin distrint Westminst is not a valid option")
expect(Rails.logger).to receive(:info).with("Cannot update location #{locations[2].id} with type_of_unit: elf-contained house. 'elf-contained house' is not a valid type_of_unit")

4
spec/models/form/lettings/questions/declaration_spec.rb

@ -63,7 +63,7 @@ RSpec.describe Form::Lettings::Questions::Declaration, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the tenant before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the tenant before you can submit this log.")
end
end
@ -87,7 +87,7 @@ RSpec.describe Form::Lettings::Questions::Declaration, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show or give the tenant access to the MHCLG privacy notice before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show or give the tenant access to the MHCLG privacy notice before you can submit this log.")
end
end
end

2
spec/models/form/lettings/questions/offered_spec.rb

@ -18,7 +18,7 @@ RSpec.describe Form::Lettings::Questions::Offered, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq "Times previously offered since becoming available"
expect(question.check_answer_label).to eq "Times previously offered since becoming available."
end
it "has the correct type" do

2
spec/models/form/lettings/questions/uprn_spec.rb

@ -40,7 +40,7 @@ RSpec.describe Form::Lettings::Questions::Uprn, type: :model do
end
it "has the correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("UPRN must be 12 digits or less")
expect(question.unanswered_error_message).to eq("UPRN must be 12 digits or less.")
end
describe "get_extra_check_answer_value" do

4
spec/models/form/sales/questions/prevown_spec.rb

@ -22,7 +22,7 @@ RSpec.describe Form::Sales::Questions::Prevown, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyer previously owned a property")
expect(question.check_answer_label).to eq("Buyer previously owned a property.")
end
end
@ -34,7 +34,7 @@ RSpec.describe Form::Sales::Questions::Prevown, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyers previously owned a property")
expect(question.check_answer_label).to eq("Buyers previously owned a property.")
end
end

8
spec/models/form/sales/questions/privacy_notice_spec.rb

@ -60,7 +60,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the buyer before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the buyer before you can submit this log.")
end
end
@ -78,7 +78,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the buyers before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show the MHCLG privacy notice to the buyers before you can submit this log.")
end
end
end
@ -100,7 +100,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show or give the buyer access to the MHCLG privacy notice before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show or give the buyer access to the MHCLG privacy notice before you can submit this log.")
end
end
@ -118,7 +118,7 @@ RSpec.describe Form::Sales::Questions::PrivacyNotice, type: :model do
end
it "returns correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("You must show or give the buyers access to the MHCLG privacy notice before you can submit this log")
expect(question.unanswered_error_message).to eq("You must show or give the buyers access to the MHCLG privacy notice before you can submit this log.")
end
end
end

4
spec/models/form/sales/questions/staircase_owned_spec.rb

@ -24,7 +24,7 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyers now own in total")
expect(question.check_answer_label).to eq("Percentage the buyers now own in total.")
end
end
@ -34,7 +34,7 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyer now owns in total")
expect(question.check_answer_label).to eq("Percentage the buyer now owns in total.")
end
end

2
spec/models/form/sales/questions/uprn_spec.rb

@ -40,7 +40,7 @@ RSpec.describe Form::Sales::Questions::Uprn, type: :model do
end
it "has the correct unanswered_error_message" do
expect(question.unanswered_error_message).to eq("UPRN must be 12 digits or less")
expect(question.unanswered_error_message).to eq("UPRN must be 12 digits or less.")
end
describe "get_extra_check_answer_value" do

35
spec/models/form/sales/subsections/household_characteristics_spec.rb

@ -34,7 +34,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_1_old_persons_shared_ownership_joint_purchase_value_check
age_1_old_persons_shared_ownership_value_check
buyer_1_gender_identity
gender_1_retirement_value_check
buyer_1_ethnic_group
buyer_1_ethnic_background_black
buyer_1_ethnic_background_asian
@ -55,7 +54,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_buyer_retirement_value_check
buyer_2_age_student_not_child_value_check
buyer_2_gender_identity
gender_2_buyer_retirement_value_check
buyer_2_working_situation
working_situation_2_retirement_value_check_joint_purchase
working_situation_buyer_2_income_min_value_check
@ -71,7 +69,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_retirement_value_check
age_2_student_not_child_value_check
person_2_gender_identity
gender_2_retirement_value_check
person_2_working_situation
working_situation_2_retirement_value_check
working_situation_2_student_not_child_value_check
@ -82,7 +79,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_3_retirement_value_check
age_3_student_not_child_value_check
person_3_gender_identity
gender_3_retirement_value_check
person_3_working_situation
working_situation_3_retirement_value_check
working_situation_3_student_not_child_value_check
@ -93,7 +89,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_4_retirement_value_check
age_4_student_not_child_value_check
person_4_gender_identity
gender_4_retirement_value_check
person_4_working_situation
working_situation_4_retirement_value_check
working_situation_4_student_not_child_value_check
@ -104,7 +99,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_5_retirement_value_check
age_5_student_not_child_value_check
person_5_gender_identity
gender_5_retirement_value_check
person_5_working_situation
working_situation_5_retirement_value_check
working_situation_5_student_not_child_value_check
@ -115,7 +109,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_6_retirement_value_check
age_6_student_not_child_value_check
person_6_gender_identity
gender_6_retirement_value_check
person_6_working_situation
working_situation_6_retirement_value_check
working_situation_6_student_not_child_value_check
@ -142,7 +135,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_1_old_persons_shared_ownership_joint_purchase_value_check
age_1_old_persons_shared_ownership_value_check
buyer_1_gender_identity
gender_1_retirement_value_check
buyer_1_ethnic_group
buyer_1_ethnic_background_black
buyer_1_ethnic_background_asian
@ -163,7 +155,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_buyer_retirement_value_check
buyer_2_age_student_not_child_value_check
buyer_2_gender_identity
gender_2_buyer_retirement_value_check
buyer_2_ethnic_group
buyer_2_ethnic_background_black
buyer_2_ethnic_background_asian
@ -186,7 +177,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_retirement_value_check
age_2_student_not_child_value_check
person_2_gender_identity
gender_2_retirement_value_check
person_2_working_situation
working_situation_2_retirement_value_check
working_situation_2_student_not_child_value_check
@ -197,7 +187,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_3_retirement_value_check
age_3_student_not_child_value_check
person_3_gender_identity
gender_3_retirement_value_check
person_3_working_situation
working_situation_3_retirement_value_check
working_situation_3_student_not_child_value_check
@ -208,7 +197,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_4_retirement_value_check
age_4_student_not_child_value_check
person_4_gender_identity
gender_4_retirement_value_check
person_4_working_situation
working_situation_4_retirement_value_check
working_situation_4_student_not_child_value_check
@ -219,7 +207,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_5_retirement_value_check
age_5_student_not_child_value_check
person_5_gender_identity
gender_5_retirement_value_check
person_5_working_situation
working_situation_5_retirement_value_check
working_situation_5_student_not_child_value_check
@ -230,7 +217,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_6_retirement_value_check
age_6_student_not_child_value_check
person_6_gender_identity
gender_6_retirement_value_check
person_6_working_situation
working_situation_6_retirement_value_check
working_situation_6_student_not_child_value_check
@ -250,10 +236,10 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
%w[
buyer_1_age
age_1_retirement_value_check
age_1_not_retired_value_check
age_1_old_persons_shared_ownership_joint_purchase_value_check
age_1_old_persons_shared_ownership_value_check
buyer_1_gender_identity
gender_1_retirement_value_check
buyer_1_ethnic_group
buyer_1_ethnic_background_black
buyer_1_ethnic_background_asian
@ -263,6 +249,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
buyer_1_nationality
buyer_1_working_situation
working_situation_1_retirement_value_check
working_situation_1_not_retired_value_check
working_situation_buyer_1_income_min_value_check
buyer_1_live_in_property
buyer_1_live_in_property_value_check
@ -272,9 +259,9 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_old_persons_shared_ownership_joint_purchase_value_check
age_2_old_persons_shared_ownership_value_check
age_2_buyer_retirement_value_check
age_2_buyer_not_retired_value_check
buyer_2_age_student_not_child_value_check
buyer_2_gender_identity
gender_2_buyer_retirement_value_check
buyer_2_ethnic_group
buyer_2_ethnic_background_black
buyer_2_ethnic_background_asian
@ -284,6 +271,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
buyer_2_nationality
buyer_2_working_situation
working_situation_2_retirement_value_check_joint_purchase
working_situation_2_not_retired_value_check_joint_purchase
working_situation_buyer_2_income_min_value_check
buyer_2_working_situation_student_not_child_value_check
buyer_2_live_in_property
@ -297,12 +285,13 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
relationship_2_student_not_child_value_check
person_2_age
age_2_retirement_value_check
age_2_not_retired_value_check
age_2_student_not_child_value_check
age_2_partner_under_16_value_check
person_2_gender_identity
gender_2_retirement_value_check
person_2_working_situation
working_situation_2_retirement_value_check
working_situation_2_not_retired_value_check
working_situation_2_student_not_child_value_check
person_3_known
person_3_relationship_to_buyer_1
@ -311,12 +300,13 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
relationship_3_student_not_child_value_check
person_3_age
age_3_retirement_value_check
age_3_not_retired_value_check
age_3_student_not_child_value_check
age_3_partner_under_16_value_check
person_3_gender_identity
gender_3_retirement_value_check
person_3_working_situation
working_situation_3_retirement_value_check
working_situation_3_not_retired_value_check
working_situation_3_student_not_child_value_check
person_4_known
person_4_relationship_to_buyer_1
@ -325,12 +315,13 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
relationship_4_student_not_child_value_check
person_4_age
age_4_retirement_value_check
age_4_not_retired_value_check
age_4_student_not_child_value_check
age_4_partner_under_16_value_check
person_4_gender_identity
gender_4_retirement_value_check
person_4_working_situation
working_situation_4_retirement_value_check
working_situation_4_not_retired_value_check
working_situation_4_student_not_child_value_check
person_5_known
person_5_relationship_to_buyer_1
@ -339,12 +330,13 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
relationship_5_student_not_child_value_check
person_5_age
age_5_retirement_value_check
age_5_not_retired_value_check
age_5_student_not_child_value_check
age_5_partner_under_16_value_check
person_5_gender_identity
gender_5_retirement_value_check
person_5_working_situation
working_situation_5_retirement_value_check
working_situation_5_not_retired_value_check
working_situation_5_student_not_child_value_check
person_6_known
person_6_relationship_to_buyer_1
@ -353,12 +345,13 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
relationship_6_student_not_child_value_check
person_6_age
age_6_retirement_value_check
age_6_not_retired_value_check
age_6_student_not_child_value_check
age_6_partner_under_16_value_check
person_6_gender_identity
gender_6_retirement_value_check
person_6_working_situation
working_situation_6_retirement_value_check
working_situation_6_not_retired_value_check
working_situation_6_student_not_child_value_check
],
)

7
spec/models/lettings_log_spec.rb

@ -1345,6 +1345,13 @@ RSpec.describe LettingsLog do
expect(result.third.id).to eq lettings_log_with_postcode.id
end
end
it "sanitises input for order" do
lettings_log_to_search.update!(tenancycode: "' 1234")
result = described_class.search_by(lettings_log_to_search.tenancycode)
expect(result.count).to eq(1)
expect(result.first.id).to eq lettings_log_to_search.id
end
end
end

6
spec/models/location_deactivation_period_spec.rb

@ -23,7 +23,7 @@ RSpec.describe LocationDeactivationPeriod do
record.deactivation_date = current_collection_start_date - 1.year
location.location_deactivation_periods.clear
validator.validate(record)
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2023"
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2023."
end
end
@ -47,7 +47,7 @@ RSpec.describe LocationDeactivationPeriod do
record.deactivation_date = previous_collection_start_date - 2.years
location.location_deactivation_periods.clear
validator.validate(record)
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2022"
expect(record.errors[:deactivation_date]).to include "The date must be on or after the 1 April 2022."
end
end
@ -80,7 +80,7 @@ RSpec.describe LocationDeactivationPeriod do
location.location_deactivation_periods.clear
validator.validate(record)
start_date = startdate.to_formatted_s(:govuk_date)
expect(record.errors[:deactivation_date]).to include "The location cannot be deactivated before #{start_date}, the date when it was first available"
expect(record.errors[:deactivation_date]).to include "The location cannot be deactivated before #{start_date}, the date when it was first available."
end
end
end

4
spec/models/notification_spec.rb

@ -9,7 +9,7 @@ RSpec.describe Notification, type: :model do
it "adds an error to page_content" do
notification.valid?
expect(notification.errors[:page_content]).to include("Enter the page content")
expect(notification.errors[:page_content]).to include("Enter the page content.")
end
end
@ -19,7 +19,7 @@ RSpec.describe Notification, type: :model do
it "adds an error to link_text" do
notification.valid?
expect(notification.errors[:link_text]).to include("Enter the link text")
expect(notification.errors[:link_text]).to include("Enter the link text.")
end
end
end

7
spec/models/sales_log_spec.rb

@ -214,6 +214,13 @@ RSpec.describe SalesLog, type: :model do
expect(result).to include(have_attributes(id: sales_log_to_search.id))
end
end
it "sanitises input for order" do
sales_log_to_search.update!(purchid: "' 123456")
result = described_class.search_by(sales_log_to_search.purchid)
expect(result.count).to be >= 1
expect(result).to include(have_attributes(id: sales_log_to_search.id))
end
end
context "when filtering by year or nil" do

4
spec/models/scheme_deactivation_period_spec.rb

@ -23,7 +23,7 @@ RSpec.describe SchemeDeactivationPeriod do
record.deactivation_date = current_collection_start_date - 1.year
scheme.scheme_deactivation_periods.clear
validator.validate(record)
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2023")
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2023.")
end
end
@ -47,7 +47,7 @@ RSpec.describe SchemeDeactivationPeriod do
record.deactivation_date = previous_collection_start_date - 2.years
scheme.scheme_deactivation_periods.clear
validator.validate(record)
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2022")
expect(record.errors[:deactivation_date]).to include("The date must be on or after the 1 April 2022.")
end
end

122
spec/models/user_spec.rb

@ -243,6 +243,128 @@ RSpec.describe User, type: :model do
expect(user.need_two_factor_authentication?(nil)).to be false
end
end
context "when the user is in non staging environment" do
before do
allow(Rails.env).to receive(:staging?).and_return(false)
end
context "and the user is in the staging role update email allowlist" do
before do
allow(Rails.application.credentials).to receive(:[]).with(:staging_role_update_email_allowlist).and_return(["example.com"])
end
context "when the user is a data provider" do
it "cannot assign roles" do
expect(user.assignable_roles).to eq({})
end
end
context "when the user is a data coordinator" do
let(:user) { create(:user, :data_coordinator) }
it "can assign all roles except support" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
})
end
end
context "when the user is a Support user" do
let(:user) { create(:user, :support) }
it "can assign all roles" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
support: 99,
})
end
end
end
end
context "when the user is in staging environment" do
before do
allow(Rails.env).to receive(:staging?).and_return(true)
end
context "and the user is not in the staging role update email allowlist" do
context "when the user is a data provider" do
let(:user) { create(:user, :data_provider) }
it "cannot assign roles" do
expect(user.assignable_roles).to eq({})
end
end
context "when the user is a data coordinator" do
let(:user) { create(:user, :data_coordinator) }
it "can assign all roles except support" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
})
end
end
context "when the user is a Support user" do
let(:user) { create(:user, :support) }
it "can assign all roles" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
support: 99,
})
end
end
end
context "and the user is in the staging role update email allowlist" do
before do
allow(Rails.application.credentials).to receive(:[]).with(:staging_role_update_email_allowlist).and_return(["example.com"])
end
context "when the user is a data provider" do
let(:user) { create(:user, :data_provider) }
it "can assign all roles" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
support: 99,
})
end
end
context "when the user is a data coordinator" do
let(:user) { create(:user, :data_coordinator) }
it "can assign all roles" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
support: 99,
})
end
end
context "when the user is a Support user" do
let(:user) { create(:user, :support) }
it "can assign all roles" do
expect(user.assignable_roles).to eq({
data_provider: 1,
data_coordinator: 2,
support: 99,
})
end
end
end
end
end
describe "paper trail" do

46
spec/models/validations/financial_validations_spec.rb

@ -237,9 +237,9 @@ RSpec.describe Validations::FinancialValidations do
record.ecstat1 = 1
financial_validator.validate_net_income(record)
expect(record.errors["earnings"])
.to eq(["The household’s income cannot be greater than £1,230.00 per week given the household’s working situation"])
.to eq(["The household’s income cannot be greater than £1,230.00 per week given the household’s working situation."])
expect(record.errors["ecstat1"])
.to eq(["The household’s income of £5,000.00 weekly is too high given the household’s working situation"])
.to eq(["The household’s income of £5,000.00 weekly is too high given the household’s working situation."])
expect(record.errors["hhmemb"])
.to eq(["The household’s income of £5,000.00 weekly is too high for this number of tenants. Change either the household income or number of tenants."])
end
@ -254,9 +254,9 @@ RSpec.describe Validations::FinancialValidations do
record.ecstat1 = 1
financial_validator.validate_net_income(record)
expect(record.errors["earnings"])
.to eq(["The household’s income cannot be less than £90.00 per week given the household’s working situation"])
.to eq(["The household’s income cannot be less than £90.00 per week given the household’s working situation."])
expect(record.errors["ecstat1"])
.to eq(["The household’s income of £50.00 weekly is too low given the household’s working situation"])
.to eq(["The household’s income of £50.00 weekly is too low given the household’s working situation."])
expect(record.errors["hhmemb"])
.to eq(["The household’s income of £50.00 weekly is too low for this number of tenants. Change either the household income or number of tenants."])
end
@ -286,7 +286,7 @@ RSpec.describe Validations::FinancialValidations do
record.ecstat3 = 9
financial_validator.validate_net_income(record)
expect(record.errors["earnings"])
.to eq(["The household’s income cannot be less than £150.00 per week given the household’s working situation"])
.to eq(["The household’s income cannot be less than £150.00 per week given the household’s working situation."])
end
it "adds errors to relevant fields for each tenant when income is too high" do
@ -301,7 +301,7 @@ RSpec.describe Validations::FinancialValidations do
financial_validator.validate_net_income(record)
(1..record.hhmemb).each do |n|
expect(record.errors["ecstat#{n}"])
.to eq(["The household’s income of £5,000.00 weekly is too high given the household’s working situation"])
.to eq(["The household’s income of £5,000.00 weekly is too high given the household’s working situation."])
end
expect(record.errors["age1"]).to be_empty
expect(record.errors["age2"]).to be_empty
@ -325,7 +325,7 @@ RSpec.describe Validations::FinancialValidations do
financial_validator.validate_net_income(record)
(1..record.hhmemb).each do |n|
expect(record.errors["ecstat#{n}"])
.to eq(["The household’s income of £50.00 weekly is too low given the household’s working situation"])
.to eq(["The household’s income of £50.00 weekly is too low given the household’s working situation."])
end
(record.hhmemb + 1..8).each do |n|
expect(record.errors["ecstat#{n}"]).to be_empty
@ -1121,9 +1121,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 5001
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks")
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks")
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks.")
end
it "validates charge when period is monthly" do
@ -1131,9 +1131,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 21_667
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month")
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month")
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month.")
end
it "validates charge when period is every 2 weeks" do
@ -1141,9 +1141,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 12_001
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks")
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks")
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks.")
end
it "validates charge when period is every 4 weeks" do
@ -1151,9 +1151,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 24_001
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks")
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks")
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks.")
end
end
@ -1209,9 +1209,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 9
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks")
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks")
.to include("Household rent and other charges must be between £10.00 and £5,000.00 if paying weekly for 52 weeks.")
end
it "validates charge when period is monthly" do
@ -1219,9 +1219,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 42
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month")
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month")
.to include("Household rent and other charges must be between £43.00 and £21,666.00 if paying every calendar month.")
end
it "validates charge when period is every 2 weeks" do
@ -1229,9 +1229,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 19
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks")
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks")
.to include("Household rent and other charges must be between £20.00 and £10,000.00 if paying every 2 weeks.")
end
it "validates charge when period is every 4 weeks" do
@ -1239,9 +1239,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 39
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks")
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks.")
expect(record.errors["period"])
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks")
.to include("Household rent and other charges must be between £40.00 and £20,000.00 if paying every 4 weeks.")
end
end
end

4
spec/models/validations/household_validations_spec.rb

@ -763,7 +763,7 @@ RSpec.describe Validations::HouseholdValidations do
it "is invalid" do
household_validator.validate_combination_of_housing_needs_responses(record)
error_message = ["If somebody in the household has disabled access needs, they must have the access needs listed, or other access needs"]
error_message = ["If somebody in the household has disabled access needs, they must have the access needs listed, or other access needs."]
expect(record.errors["housingneeds"]).to eq(error_message)
expect(record.errors["housingneeds_type"]).to eq(error_message)
@ -779,7 +779,7 @@ RSpec.describe Validations::HouseholdValidations do
it "is invalid" do
household_validator.validate_combination_of_housing_needs_responses(record)
error_message = ["If somebody in the household has disabled access needs, they must have the access needs listed, or other access needs"]
error_message = ["If somebody in the household has disabled access needs, they must have the access needs listed, or other access needs."]
expect(record.errors["housingneeds"]).to eq(error_message)
expect(record.errors["housingneeds_type"]).to eq(error_message)

4
spec/models/validations/property_validations_spec.rb

@ -276,7 +276,7 @@ RSpec.describe Validations::PropertyValidations do
it "adds an error" do
property_validator.validate_uprn(log)
expect(log.errors.added?(:uprn, "UPRN must be 12 digits or less")).to be true
expect(log.errors.added?(:uprn, "UPRN must be 12 digits or less.")).to be true
end
end
@ -285,7 +285,7 @@ RSpec.describe Validations::PropertyValidations do
it "adds an error" do
property_validator.validate_uprn(log)
expect(log.errors.added?(:uprn, "UPRN must be 12 digits or less")).to be true
expect(log.errors.added?(:uprn, "UPRN must be 12 digits or less.")).to be true
end
end

12
spec/models/validations/sales/financial_validations_spec.rb

@ -187,7 +187,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairowned = 40
record.jointpur = 1
financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record)
expect(record.errors["stairowned"]).to include("Total percentage buyers now own must be more than percentage bought in this transaction")
expect(record.errors["stairowned"]).to include("Total percentage buyers now own must be more than percentage bought in this transaction.")
end
it "adds an error to stairowned and not stairbought if the percentage bought is more than the percentage owned for non joint purchase" do
@ -195,7 +195,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairowned = 40
record.jointpur = 2
financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record)
expect(record.errors["stairowned"]).to include("Total percentage buyer now owns must be more than percentage bought in this transaction")
expect(record.errors["stairowned"]).to include("Total percentage buyer now owns must be more than percentage bought in this transaction.")
end
end
@ -270,8 +270,8 @@ RSpec.describe Validations::Sales::FinancialValidations do
[2, 16, 18, 24].each do |type|
record.type = type
financial_validator.validate_percentage_bought_at_least_threshold(record)
expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 10%"])
expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 10% for this shared ownership type"])
expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 10%."])
expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 10% for this shared ownership type."])
record.errors.clear
end
@ -279,8 +279,8 @@ RSpec.describe Validations::Sales::FinancialValidations do
[28, 30, 31, 32].each do |type|
record.type = type
financial_validator.validate_percentage_bought_at_least_threshold(record)
expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 1%"])
expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 1% for this shared ownership type"])
expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 1%."])
expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 1% for this shared ownership type."])
record.errors.clear
end
end

8
spec/models/validations/sales/household_validations_spec.rb

@ -345,8 +345,8 @@ RSpec.describe Validations::Sales::HouseholdValidations do
[3, 4, 5, 6, 7, 9, 0].each do |prevten|
record.prevten = prevten
household_validator.validate_buyer1_previous_tenure(record)
expect(record.errors["prevten"]).to include("Buyer 1’s previous tenure should be “local authority tenant” or “private registered provider or housing association tenant” for discounted sales")
expect(record.errors["ownershipsch"]).to include("Buyer 1’s previous tenure should be “local authority tenant” or “private registered provider or housing association tenant” for discounted sales")
expect(record.errors["prevten"]).to include("Buyer 1’s previous tenure should be “local authority tenant” or “private registered provider or housing association tenant” for discounted sales.")
expect(record.errors["ownershipsch"]).to include("Buyer 1’s previous tenure should be “local authority tenant” or “private registered provider or housing association tenant” for discounted sales.")
end
end
@ -420,7 +420,7 @@ RSpec.describe Validations::Sales::HouseholdValidations do
record.ecstat1 = 9
household_validator.validate_buyer_not_child(record)
expect(record.errors["ecstat1"])
.to include("Buyer 1 cannot have a working situation of child under 16")
.to include("Buyer 1 cannot have a working situation of child under 16.")
end
it "validates buyer 2 isn't a child" do
@ -428,7 +428,7 @@ RSpec.describe Validations::Sales::HouseholdValidations do
record.ecstat2 = 9
household_validator.validate_buyer_not_child(record)
expect(record.errors["ecstat2"])
.to include("Buyer 2 cannot have a working situation of child under 16")
.to include("Buyer 2 cannot have a working situation of child under 16.")
end
it "allows person 2 to be a child" do

20
spec/models/validations/sales/property_validations_spec.rb

@ -51,9 +51,9 @@ RSpec.describe Validations::Sales::PropertyValidations do
record.ppostcode_full = "SW1A 0AA"
record.jointpur = 1
property_validator.validate_postcodes_match_if_discounted_ownership(record)
expect(record.errors["postcode_full"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match")
expect(record.errors["ppostcode_full"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match")
expect(record.errors["ownershipsch"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match")
expect(record.errors["postcode_full"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match.")
expect(record.errors["ppostcode_full"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match.")
expect(record.errors["ownershipsch"]).to include("Buyers’ last accommodation and discounted ownership postcodes must match.")
end
it "when postcodes do not match an error is added for non joint purchase" do
@ -61,9 +61,9 @@ RSpec.describe Validations::Sales::PropertyValidations do
record.ppostcode_full = "SW1A 0AA"
record.jointpur = 2
property_validator.validate_postcodes_match_if_discounted_ownership(record)
expect(record.errors["postcode_full"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match")
expect(record.errors["ppostcode_full"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match")
expect(record.errors["ownershipsch"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match")
expect(record.errors["postcode_full"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match.")
expect(record.errors["ppostcode_full"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match.")
expect(record.errors["ownershipsch"]).to include("Buyer’s last accommodation and discounted ownership postcodes must match.")
end
it "does not add error for 2024 log" do
@ -93,8 +93,8 @@ RSpec.describe Validations::Sales::PropertyValidations do
it "does add an error if it's a bedsit" do
property_validator.validate_bedsit_number_of_beds(record)
expect(record.errors.added?(:proptype, "Answer cannot be 'Bedsit' if the property has 2 or more bedrooms")).to be true
expect(record.errors.added?(:beds, "Number of bedrooms must be 1 if the property is a bedsit")).to be true
expect(record.errors.added?(:proptype, "Answer cannot be 'Bedsit' if the property has 2 or more bedrooms.")).to be true
expect(record.errors.added?(:beds, "Number of bedrooms must be 1 if the property is a bedsit.")).to be true
end
it "does not add an error if proptype is undefined" do
@ -120,7 +120,7 @@ RSpec.describe Validations::Sales::PropertyValidations do
it "adds an error" do
property_validator.validate_uprn(record)
expect(record.errors.added?(:uprn, "UPRN must be 12 digits or less")).to be true
expect(record.errors.added?(:uprn, "UPRN must be 12 digits or less.")).to be true
end
end
@ -129,7 +129,7 @@ RSpec.describe Validations::Sales::PropertyValidations do
it "adds an error" do
property_validator.validate_uprn(record)
expect(record.errors.added?(:uprn, "UPRN must be 12 digits or less")).to be true
expect(record.errors.added?(:uprn, "UPRN must be 12 digits or less.")).to be true
end
end

12
spec/models/validations/sales/sale_information_validations_spec.rb

@ -147,10 +147,10 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
sale_information_validator.validate_exchange_date(record)
expect(record.errors[:exdate]).to eq(
["Contract exchange date must be less than 1 year before sale completion date"],
["Contract exchange date must be less than 1 year before sale completion date."],
)
expect(record.errors[:saledate]).to eq(
["Sale completion date must be less than 1 year after contract exchange date"],
["Sale completion date must be less than 1 year after contract exchange date."],
)
end
end
@ -162,10 +162,10 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
sale_information_validator.validate_exchange_date(record)
expect(record.errors[:exdate]).to eq(
["Contract exchange date must be before sale completion date"],
["Contract exchange date must be before sale completion date."],
)
expect(record.errors[:saledate]).to eq(
["Sale completion date must be after contract exchange date"],
["Sale completion date must be after contract exchange date."],
)
end
end
@ -671,7 +671,7 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
it "adds an error" do
sale_information_validator.validate_grant_amount(record)
expect(record.errors[:grant]).to include("Loan, grants or subsidies must be between £9,000 and £16,000")
expect(record.errors[:grant]).to include("Loan, grants or subsidies must be between £9,000 and £16,000.")
end
end
@ -681,7 +681,7 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
it "adds an error" do
sale_information_validator.validate_grant_amount(record)
expect(record.errors[:grant]).to include("Loan, grants or subsidies must be between £9,000 and £16,000")
expect(record.errors[:grant]).to include("Loan, grants or subsidies must be between £9,000 and £16,000.")
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save