Browse Source

Merge branch 'main' into CLDC-4167-4168-add-what-is-length-of-the-mortgage-dont-know-option

# Conflicts:
#	db/schema.rb
#	spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
pull/3195/head
samyou-softwire 2 weeks ago
parent
commit
f0028082ec
  1. 20
      app/helpers/bulk_upload/sales_log_to_csv.rb
  2. 106
      app/models/form/sales/subsections/household_characteristics.rb
  3. 2
      app/models/validations/financial_validations.rb
  4. 4
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  5. 100
      app/services/bulk_upload/sales/year2026/row_parser.rb
  6. 13
      app/services/exports/sales_log_export_constants.rb
  7. 4
      app/services/exports/sales_log_export_service.rb
  8. 2
      app/views/form/page.html.erb
  9. 56
      config/locales/forms/2026/lettings/household_characteristics.en.yml
  10. 49
      config/locales/forms/2026/sales/household_characteristics.en.yml
  11. 25
      db/migrate/20260225135309_add_composite_indexes_for_logs_organisation_lookup.rb
  12. 8
      db/schema.rb
  13. 6
      spec/factories/sales_log.rb
  14. 18
      spec/fixtures/exports/sales_log_26_27.xml
  15. 24
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  16. 3
      spec/fixtures/files/sales_logs_csv_export_codes_23.csv
  17. 2
      spec/fixtures/files/sales_logs_csv_export_codes_24.csv
  18. 2
      spec/fixtures/files/sales_logs_csv_export_codes_25.csv
  19. 6
      spec/fixtures/files/sales_logs_csv_export_codes_26.csv
  20. 3
      spec/fixtures/files/sales_logs_csv_export_labels_23.csv
  21. 2
      spec/fixtures/files/sales_logs_csv_export_labels_24.csv
  22. 2
      spec/fixtures/files/sales_logs_csv_export_labels_25.csv
  23. 6
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  24. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv
  25. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv
  26. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_25.csv
  27. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv
  28. 7
      spec/models/form/sales/subsections/household_characteristics_spec.rb
  29. 16
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  30. 94
      spec/services/csv/sales_log_csv_service_spec.rb
  31. 10
      spec/services/exports/sales_log_export_service_spec.rb

20
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -565,14 +565,14 @@ class BulkUpload::SalesLogToCsv
log.wchair,
log.age1,
log.sex1,
log.sexrab1,
log.ethnic, # 30
log.nationality_all_group,
log.ecstat1,
log.buy1livein,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat2],
log.age2,
log.sex2,
log.sexrab2,
log.ethnic_group2,
log.nationality_all_buyer2_group,
log.ecstat2,
@ -581,19 +581,19 @@ class BulkUpload::SalesLogToCsv
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat3],
log.age3,
log.sex3,
log.sexrab3,
log.ecstat3,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat4],
log.age4,
log.sex4,
log.sexrab4,
log.ecstat4,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat5], # 50
log.age5,
log.sex5,
log.sexrab5,
log.ecstat5,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat6],
log.age6,
log.sex6,
log.sexrab6,
log.ecstat6,
log.prevten,
@ -665,13 +665,7 @@ class BulkUpload::SalesLogToCsv
log.extrabor,
log.deposit, # 120
log.mscharge,
log.sexrab1,
log.sexrab2,
log.sexrab3,
log.sexrab4,
log.sexrab5,
log.sexrab6,
log.buildheightclass, # 128
log.buildheightclass, # 122
]
end

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

@ -25,7 +25,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
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::SexRegisteredAtBirth1.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::GenderIdentity1.new(nil, nil, self),
(Form::Sales::Pages::GenderIdentity1.new(nil, nil, self) unless form.start_year_2026_or_later?),
Form::Sales::Pages::Buyer1EthnicGroup.new(nil, nil, self),
Form::Sales::Pages::Buyer1EthnicBackgroundBlack.new(nil, nil, self),
Form::Sales::Pages::Buyer1EthnicBackgroundAsian.new(nil, nil, self),
@ -48,7 +48,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Sales::Pages::NotRetiredValueCheck.new("age_2_buyer_not_retired_value_check", nil, self, person_index: 2) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("buyer_2_age_student_not_child_value_check", nil, self, person_index: 2) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::SexRegisteredAtBirth2.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::GenderIdentity2.new(nil, nil, self),
(Form::Sales::Pages::GenderIdentity2.new(nil, nil, self) unless form.start_year_2026_or_later?),
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),
@ -59,89 +59,31 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::BuyerLiveInValueCheck.new("buyer_2_live_in_property_value_check", nil, self, person_index: 2),
Form::Sales::Pages::NumberOfOthersInProperty.new("number_of_others_in_property", nil, self, joint_purchase: false),
Form::Sales::Pages::NumberOfOthersInProperty.new("number_of_others_in_property_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::PersonKnown.new("person_2_known", nil, self, person_index: 2),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_2_relationship_to_buyer_1", nil, self, person_index: 2) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_2_relationship_to_buyer_1", nil, self, person_index: 2)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_2_partner_under_16_value_check", nil, self, person_index: 2) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_2_multiple_partners_value_check", nil, self, person_index: 2) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_2_student_not_child_value_check", nil, self, person_index: 2) unless form.start_year_2025_or_later?),
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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_2_student_not_child_value_check", nil, self, person_index: 2) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_2_partner_under_16_value_check", nil, self, person_index: 2) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_2_sex_registered_at_birth", nil, self, person_index: 2) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonGenderIdentity.new("person_2_gender_identity", 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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_2_student_not_child_value_check", nil, self, person_index: 2) unless form.start_year_2025_or_later?),
Form::Sales::Pages::PersonKnown.new("person_3_known", nil, self, person_index: 3),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_3_relationship_to_buyer_1", nil, self, person_index: 3) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_3_relationship_to_buyer_1", nil, self, person_index: 3)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_3_partner_under_16_value_check", nil, self, person_index: 3) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_3_multiple_partners_value_check", nil, self, person_index: 3) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_3_student_not_child_value_check", nil, self, person_index: 3) unless form.start_year_2025_or_later?),
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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_3_student_not_child_value_check", nil, self, person_index: 3) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_3_partner_under_16_value_check", nil, self, person_index: 3) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_3_sex_registered_at_birth", nil, self, person_index: 3) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonGenderIdentity.new("person_3_gender_identity", 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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_3_student_not_child_value_check", nil, self, person_index: 3) unless form.start_year_2025_or_later?),
Form::Sales::Pages::PersonKnown.new("person_4_known", nil, self, person_index: 4),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_4_relationship_to_buyer_1", nil, self, person_index: 4) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_4_relationship_to_buyer_1", nil, self, person_index: 4)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_4_partner_under_16_value_check", nil, self, person_index: 4) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_4_multiple_partners_value_check", nil, self, person_index: 4) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_4_student_not_child_value_check", nil, self, person_index: 4) unless form.start_year_2025_or_later?),
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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_4_student_not_child_value_check", nil, self, person_index: 4) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_4_partner_under_16_value_check", nil, self, person_index: 4) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_4_sex_registered_at_birth", nil, self, person_index: 4) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonGenderIdentity.new("person_4_gender_identity", 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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_4_student_not_child_value_check", nil, self, person_index: 4) unless form.start_year_2025_or_later?),
Form::Sales::Pages::PersonKnown.new("person_5_known", nil, self, person_index: 5),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_5_relationship_to_buyer_1", nil, self, person_index: 5) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_5_relationship_to_buyer_1", nil, self, person_index: 5)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_5_partner_under_16_value_check", nil, self, person_index: 5) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_5_multiple_partners_value_check", nil, self, person_index: 5) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_5_student_not_child_value_check", nil, self, person_index: 5) unless form.start_year_2025_or_later?),
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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_5_student_not_child_value_check", nil, self, person_index: 5) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_5_partner_under_16_value_check", nil, self, person_index: 5) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_5_sex_registered_at_birth", nil, self, person_index: 5) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonGenderIdentity.new("person_5_gender_identity", 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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_5_student_not_child_value_check", nil, self, person_index: 5) unless form.start_year_2025_or_later?),
Form::Sales::Pages::PersonKnown.new("person_6_known", nil, self, person_index: 6),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_6_relationship_to_buyer_1", nil, self, person_index: 6) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_6_relationship_to_buyer_1", nil, self, person_index: 6)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_6_partner_under_16_value_check", nil, self, person_index: 6) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_6_multiple_partners_value_check", nil, self, person_index: 6) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_6_student_not_child_value_check", nil, self, person_index: 6) unless form.start_year_2025_or_later?),
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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_6_student_not_child_value_check", nil, self, person_index: 6) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_6_partner_under_16_value_check", nil, self, person_index: 6) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_6_sex_registered_at_birth", nil, self, person_index: 6) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonGenderIdentity.new("person_6_gender_identity", 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_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_6_student_not_child_value_check", nil, self, person_index: 6) unless form.start_year_2025_or_later?),
(2..6).flat_map { |index| person_pages(index) },
].flatten.compact
end
def person_pages(person_index)
[
Form::Sales::Pages::PersonKnown.new("person_#{person_index}_known", nil, self, person_index:),
(form.start_year_2025_or_later? ? Form::Sales::Pages::PersonRelationshipToBuyer1YesNo.new("person_#{person_index}_relationship_to_buyer_1", nil, self, person_index:) : Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_#{person_index}_relationship_to_buyer_1", nil, self, person_index:)),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("relationship_#{person_index}_partner_under_16_value_check", nil, self, person_index:) if form.start_year_2024_or_later?),
(Form::Sales::Pages::MultiplePartnersValueCheck.new("relationship_#{person_index}_multiple_partners_value_check", nil, self, person_index:) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("relationship_#{person_index}_student_not_child_value_check", nil, self, person_index:) unless form.start_year_2025_or_later?),
Form::Sales::Pages::PersonAge.new("person_#{person_index}_age", nil, self, person_index:),
Form::Sales::Pages::RetirementValueCheck.new("age_#{person_index}_retirement_value_check", nil, self, person_index:),
(Form::Sales::Pages::NotRetiredValueCheck.new("age_#{person_index}_not_retired_value_check", nil, self, person_index:) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("age_#{person_index}_student_not_child_value_check", nil, self, person_index:) unless form.start_year_2025_or_later?),
(Form::Sales::Pages::PartnerUnder16ValueCheck.new("age_#{person_index}_partner_under_16_value_check", nil, self, person_index:) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonGenderIdentity.new("person_#{person_index}_gender_identity", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Sales::Pages::PersonSexRegisteredAtBirth.new("person_#{person_index}_sex_registered_at_birth", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Sales::Pages::PersonWorkingSituation.new("person_#{person_index}_working_situation", nil, self, person_index:),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_#{person_index}_retirement_value_check", nil, self, person_index:),
(Form::Sales::Pages::NotRetiredValueCheck.new("working_situation_#{person_index}_not_retired_value_check", nil, self, person_index:) if form.start_year_2024_or_later?),
(Form::Sales::Pages::PersonStudentNotChildValueCheck.new("working_situation_#{person_index}_student_not_child_value_check", nil, self, person_index:) unless form.start_year_2025_or_later?),
]
end
def buyer_2_ethnicity_nationality_pages
if form.start_date.year >= 2023
[

2
app/models/validations/financial_validations.rb

@ -228,7 +228,7 @@ private
end
if record.weekly_value(record["brent"]) > rent_range.hard_max
record.errors.add :brent, :over_hard_max, message: I18n.t("validations.lettings.financial.brent.above_hard_max")
record.errors.add :brent, :above_hard_max, message: I18n.t("validations.lettings.financial.brent.above_hard_max")
record.errors.add :beds, I18n.t("validations.lettings.financial.beds.rent_above_hard_max")
record.errors.add :uprn, I18n.t("validations.lettings.financial.uprn.rent_above_hard_max")
record.errors.add :la, I18n.t("validations.lettings.financial.la.rent_above_hard_max")

4
app/services/bulk_upload/sales/year2026/csv_parser.rb

@ -4,7 +4,7 @@ class BulkUpload::Sales::Year2026::CsvParser
include CollectionTimeHelper
# TODO: CLDC-4162: Update when 2026 format is known
FIELDS = 128
FIELDS = 122
FORM_YEAR = 2026
attr_reader :path
@ -27,7 +27,7 @@ class BulkUpload::Sales::Year2026::CsvParser
def cols
# TODO: CLDC-4162: Update when 2026 format is known
@cols ||= ("A".."DS").to_a
@cols ||= ("A".."DP").to_a
end
def row_parsers

100
app/services/bulk_upload/sales/year2026/row_parser.rb

@ -35,14 +35,14 @@ class BulkUpload::Sales::Year2026::RowParser
field_27: "Is the property built or adapted to wheelchair user standards?",
field_28: "Age of buyer 1",
field_29: "Gender identity of buyer 1",
field_29: "Buyer 1's sex, as registered at birth",
field_30: "What is buyer 1’s ethnic group?",
field_31: "What is buyer 1’s nationality?",
field_32: "Working situation of buyer 1",
field_33: "Will buyer 1 live in the property?",
field_34: "Is buyer 2 or person 2 the partner of buyer 1?",
field_35: "Age of person 2",
field_36: "Gender identity of person 2",
field_36: "Buyer/Person 2's sex, as registered at birth",
field_37: "Which of the following best describes buyer 2’s ethnic background?",
field_38: "What is buyer 2’s nationality?",
field_39: "What is buyer 2 or person 2’s working situation?",
@ -51,19 +51,19 @@ class BulkUpload::Sales::Year2026::RowParser
field_42: "Is person 3 the partner of buyer 1?",
field_43: "Age of person 3",
field_44: "Gender identity of person 3",
field_44: "Person 3's sex, as registered at birth",
field_45: "Working situation of person 3",
field_46: "Is person 4 the partner of buyer 1?",
field_47: "Age of person 4",
field_48: "Gender identity of person 4",
field_48: "Person 4's sex, as registered at birth",
field_49: "Working situation of person 4",
field_50: "Is person 5 the partner of buyer 1?",
field_51: "Age of person 5",
field_52: "Gender identity of person 5",
field_52: "Person 5's sex, as registered at birth",
field_53: "Working situation of person 5",
field_54: "Is person 6 the partner of buyer 1?",
field_55: "Age of person 6",
field_56: "Gender identity of person 6",
field_56: "Person 6's sex, as registered at birth",
field_57: "Working situation of person 6",
field_58: "What was buyer 1’s previous tenure?",
@ -136,13 +136,7 @@ class BulkUpload::Sales::Year2026::RowParser
field_120: "How much was the cash deposit paid on the property?",
field_121: "What are the total monthly leasehold charges for the property?",
field_122: "Buyer 1's sex, as registered at birth",
field_123: "Buyer/Person 2's sex, as registered at birth",
field_124: "Person 3's sex, as registered at birth",
field_125: "Person 4's sex, as registered at birth",
field_126: "Person 5's sex, as registered at birth",
field_127: "Person 6's sex, as registered at birth",
field_128: "What is the building height classification?",
field_122: "What is the building height classification?",
}.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
@ -155,12 +149,12 @@ class BulkUpload::Sales::Year2026::RowParser
:field_51, # Age of person 5
:field_55, # Age of person 6
:field_122, # Buyer 1's sex, as registered at birth
:field_123, # Buyer/Person 2's sex, as registered at birth
:field_124, # Person 3's sex, as registered at birth
:field_125, # Person 4's sex, as registered at birth
:field_126, # Person 5's sex, as registered at birth
:field_127, # Person 6's sex, as registered at birth
:field_29, # Buyer 1's sex, as registered at birth
:field_36, # Buyer/Person 2's sex, as registered at birth
:field_44, # Person 3's sex, as registered at birth
:field_48, # Person 4's sex, as registered at birth
:field_52, # Person 5's sex, as registered at birth
:field_56, # Person 6's sex, as registered at birth
:field_64, # What was buyer 2’s previous tenure?
@ -307,14 +301,7 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_119, :integer
attribute :field_120, :decimal
attribute :field_121, :decimal
attribute :field_122, :string
attribute :field_123, :string
attribute :field_124, :string
attribute :field_125, :string
attribute :field_126, :string
attribute :field_127, :string
attribute :field_128, :integer
attribute :field_122, :integer
validates :field_1,
presence: {
@ -572,8 +559,7 @@ class BulkUpload::Sales::Year2026::RowParser
"field_21", # postcode
"field_22", # postcode
"field_28", # age1
"field_29", # sex1
"field_122", # sexrab1
"field_29", # sexrab1
"field_32", # ecstat1
)
end
@ -733,13 +719,6 @@ private
age5: %i[field_51],
age6_known: %i[field_55],
age6: %i[field_55],
sex1: %i[field_29],
sex2: %i[field_36],
sex3: %i[field_44],
sex4: %i[field_48],
sex5: %i[field_52],
sex6: %i[field_56],
relat2: %i[field_34],
relat3: %i[field_42],
relat4: %i[field_46],
@ -853,13 +832,13 @@ private
lasttransaction: %i[field_104 field_105 field_106],
initialpurchase: %i[field_100 field_101 field_102],
sexrab1: %i[field_122],
sexrab2: %i[field_123],
sexrab3: %i[field_124],
sexrab4: %i[field_125],
sexrab5: %i[field_126],
sexrab6: %i[field_127],
buildheightclass: %i[field_128],
sexrab1: %i[field_29],
sexrab2: %i[field_36],
sexrab3: %i[field_44],
sexrab4: %i[field_48],
sexrab5: %i[field_52],
sexrab6: %i[field_56],
buildheightclass: %i[field_122],
}
end
@ -888,20 +867,13 @@ private
attributes["age6_known"] = age6_known?
attributes["age6"] = field_55 if attributes["age6_known"]&.zero? && field_55&.match(/\A\d{1,3}\z|\AR\z/)
attributes["sex1"] = field_29
attributes["sex2"] = field_36
attributes["sex3"] = field_44
attributes["sex4"] = field_48
attributes["sex5"] = field_52
attributes["sex6"] = field_56
attributes["sexrab1"] = field_122
attributes["sexrab2"] = field_123
attributes["sexrab3"] = field_124
attributes["sexrab4"] = field_125
attributes["sexrab5"] = field_126
attributes["sexrab6"] = field_127
attributes["buildheightclass"] = field_128
attributes["sexrab1"] = field_29
attributes["sexrab2"] = field_36
attributes["sexrab3"] = field_44
attributes["sexrab4"] = field_48
attributes["sexrab5"] = field_52
attributes["sexrab6"] = field_56
attributes["buildheightclass"] = field_122
attributes["relat2"] = relationship_from_is_partner(field_34)
attributes["relat3"] = relationship_from_is_partner(field_42)
@ -1103,23 +1075,23 @@ private
end
def person_2_present?
field_35.present? || field_36.present? || field_34.present? || field_123.present?
field_35.present? || field_36.present? || field_34.present?
end
def person_3_present?
field_43.present? || field_44.present? || field_42.present? || field_124.present?
field_43.present? || field_44.present? || field_42.present?
end
def person_4_present?
field_47.present? || field_48.present? || field_46.present? || field_125.present?
field_47.present? || field_48.present? || field_46.present?
end
def person_5_present?
field_51.present? || field_52.present? || field_50.present? || field_126.present?
field_51.present? || field_52.present? || field_50.present?
end
def person_6_present?
field_55.present? || field_56.present? || field_54.present? || field_127.present?
field_55.present? || field_56.present? || field_54.present?
end
def relationship_from_is_partner(is_partner)
@ -1352,7 +1324,6 @@ private
saledate
age1
sexrab1
sex1
ecstat1
owning_organisation
postcode_full
@ -1529,8 +1500,7 @@ private
errors.add(:field_21, error_message) # Postcode
errors.add(:field_22, error_message) # Postcode
errors.add(:field_28, error_message) # Buyer 1 age
errors.add(:field_29, error_message) # Buyer 1 gender
errors.add(:field_122, error_message) # Buyer 1 sex registered at birth
errors.add(:field_29, error_message) # Buyer 1 sex registered at birth
errors.add(:field_32, error_message) # Buyer 1 working situation
errors.add(:field_7, error_message) # Purchaser code
end

13
app/services/exports/sales_log_export_constants.rb

@ -139,13 +139,24 @@ module Exports::SalesLogExportConstants
(1..6).each do |index|
ALL_YEAR_EXPORT_FIELDS << "AGE#{index}"
ALL_YEAR_EXPORT_FIELDS << "ECSTAT#{index}"
ALL_YEAR_EXPORT_FIELDS << "SEX#{index}"
end
(2..6).each do |index|
ALL_YEAR_EXPORT_FIELDS << "RELAT#{index}"
end
YEAR_2024_EXPORT_FIELDS = Set[]
(1..6).each do |index|
YEAR_2024_EXPORT_FIELDS << "SEX#{index}"
end
YEAR_2025_EXPORT_FIELDS = Set[]
(1..6).each do |index|
YEAR_2025_EXPORT_FIELDS << "SEX#{index}"
end
YEAR_2026_EXPORT_FIELDS = Set["BUILDHEIGHTCLASS"]
(1..6).each do |index|

4
app/services/exports/sales_log_export_service.rb

@ -159,6 +159,10 @@ module Exports
included_fields.merge(ALL_YEAR_EXPORT_FIELDS)
year_fields = case sales_log.collection_start_year
when 2024
YEAR_2024_EXPORT_FIELDS
when 2025
YEAR_2025_EXPORT_FIELDS
when 2026
YEAR_2026_EXPORT_FIELDS
else

2
app/views/form/page.html.erb

@ -76,6 +76,8 @@
<% end %>
<% if @pages_with_errors_count > 1 %>
<%# Hidden form submit ensures pressing Enter triggers "Save and continue" as the user would expect rather than "See all related answers"%>
<%= f.submit submit_button_text(@page, request.query_parameters["referrer"]), class: "govuk-visually-hidden", tabindex: -1, "aria-hidden": true %>
<div class="govuk-button-group">
<%= f.submit "See all related answers", name: "check_errors", class: "govuk-body govuk-link submit-button-link" %>
</div>

56
config/locales/forms/2026/lettings/household_characteristics.en.yml

@ -44,13 +44,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex1:
page_header: ""
check_answer_label: "Lead tenant’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes the lead tenant’s gender identity?"
ethnic_group:
page_header: ""
check_answer_label: "Lead tenant’s ethnic group"
@ -158,13 +151,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex2:
page_header: ""
check_answer_label: "Person 2’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 2’s gender identity?"
ecstat2:
page_header: ""
check_answer_label: "Person 2’s working situation"
@ -220,13 +206,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex3:
page_header: ""
check_answer_label: "Person 3’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 3’s gender identity?"
ecstat3:
page_header: ""
check_answer_label: "Person 3’s working situation"
@ -282,13 +261,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex4:
page_header: ""
check_answer_label: "Person 4’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 4’s gender identity?"
ecstat4:
page_header: ""
check_answer_label: "Person 4’s working situation"
@ -344,13 +316,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex5:
page_header: ""
check_answer_label: "Person 5’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 5’s gender identity?"
ecstat5:
page_header: ""
check_answer_label: "Person 5’s working situation"
@ -406,13 +371,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex6:
page_header: ""
check_answer_label: "Person 6’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 6’s gender identity?"
ecstat6:
page_header: ""
check_answer_label: "Person 6’s working situation"
@ -468,13 +426,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex7:
page_header: ""
check_answer_label: "Person 7’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 7’s gender identity?"
ecstat7:
page_header: ""
check_answer_label: "Person 7’s working situation"
@ -530,13 +481,6 @@ en:
hint_text: ""
question_text: "Enter gender identity"
sex8:
page_header: ""
check_answer_label: "Person 8’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 8’s gender identity?"
ecstat8:
page_header: ""
check_answer_label: "Person 8’s working situation"

49
config/locales/forms/2026/sales/household_characteristics.en.yml

@ -23,13 +23,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the buyer's gender identity."
question_text: "What was buyer 1's sex at birth?"
sex1:
page_header: ""
check_answer_label: "Buyer 1’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes buyer 1’s gender identity?"
ethnic_group:
page_header: ""
check_answer_label: "Buyer 1’s ethnic group"
@ -151,20 +144,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the person's gender identity."
question_text: "What was person 2's sex at birth?"
sex2:
buyer:
page_header: ""
check_answer_label: "Buyer 2’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes buyer 2’s gender identity?"
person:
page_header: ""
check_answer_label: "Person 2’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 2’s gender identity?"
ethnic_group2:
page_header: ""
check_answer_label: "Buyer 2’s ethnic group"
@ -294,13 +273,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the person's gender identity."
question_text: "What was person 3's sex at birth?"
sex3:
page_header: ""
check_answer_label: "Person 3’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 3’s gender identity?"
ecstat3:
page_header: ""
check_answer_label: "Person 3’s working situation"
@ -342,13 +314,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the person's gender identity."
question_text: "What was person 4's sex at birth?"
sex4:
page_header: ""
check_answer_label: "Person 4’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 4’s gender identity?"
ecstat4:
page_header: ""
check_answer_label: "Person 4’s working situation"
@ -390,13 +355,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the person's gender identity."
question_text: "What was person 5's sex at birth?"
sex5:
page_header: ""
check_answer_label: "Person 5’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 5’s gender identity?"
ecstat5:
page_header: ""
check_answer_label: "Person 5’s working situation"
@ -438,13 +396,6 @@ en:
hint_text: "This is the sex that was registered at birth. The next question will ask about the person's gender identity."
question_text: "What was person 6's sex at birth?"
sex6:
page_header: ""
check_answer_label: "Person 6’s gender identity"
check_answer_prompt: ""
hint_text: "This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth."
question_text: "Which of these best describes person 6’s gender identity?"
ecstat6:
page_header: ""
check_answer_label: "Person 6’s working situation"

25
db/migrate/20260225135309_add_composite_indexes_for_logs_organisation_lookup.rb

@ -0,0 +1,25 @@
class AddCompositeIndexesForLogsOrganisationLookup < ActiveRecord::Migration[7.2]
disable_ddl_transaction!
def change
add_index :lettings_logs, %i[owning_organisation_id id],
order: { id: :desc },
name: "index_lettings_logs_on_owning_org_and_id_desc",
algorithm: :concurrently
add_index :lettings_logs, %i[managing_organisation_id id],
order: { id: :desc },
name: "index_lettings_logs_on_managing_org_and_id_desc",
algorithm: :concurrently
add_index :sales_logs, %i[owning_organisation_id id],
order: { id: :desc },
name: "index_sales_logs_on_owning_org_and_id_desc",
algorithm: :concurrently
add_index :sales_logs, %i[managing_organisation_id id],
order: { id: :desc },
name: "index_sales_logs_on_managing_org_and_id_desc",
algorithm: :concurrently
end
end

8
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -408,8 +408,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
t.index ["bulk_upload_id"], name: "index_lettings_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_lettings_logs_on_created_by_id"
t.index ["location_id"], name: "index_lettings_logs_on_location_id"
t.index ["managing_organisation_id", "id"], name: "index_lettings_logs_on_managing_org_and_id_desc", order: { id: :desc }
t.index ["managing_organisation_id"], name: "index_lettings_logs_on_managing_organisation_id"
t.index ["old_id"], name: "index_lettings_logs_on_old_id", unique: true
t.index ["owning_organisation_id", "id"], name: "index_lettings_logs_on_owning_org_and_id_desc", order: { id: :desc }
t.index ["owning_organisation_id"], name: "index_lettings_logs_on_owning_organisation_id"
t.index ["scheme_id"], name: "index_lettings_logs_on_scheme_id"
t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id"
@ -822,13 +824,15 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
t.string "sexrab4"
t.string "sexrab5"
t.string "sexrab6"
t.integer "buildheightclass"
t.integer "mortlen_known"
t.integer "buildheightclass"
t.index ["assigned_to_id"], name: "index_sales_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"
t.index ["managing_organisation_id", "id"], name: "index_sales_logs_on_managing_org_and_id_desc", order: { id: :desc }
t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id"
t.index ["old_id"], name: "index_sales_logs_on_old_id", unique: true
t.index ["owning_organisation_id", "id"], name: "index_sales_logs_on_owning_org_and_id_desc", order: { id: :desc }
t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id"
t.index ["updated_by_id"], name: "index_sales_logs_on_updated_by_id"
end

6
spec/factories/sales_log.rb

@ -301,7 +301,7 @@ FactoryBot.define do
builtype { 1 }
ethnic { 3 }
ethnic_group { 17 }
sexrab2 { "X" }
sexrab2 { "R" }
sex2 { "X" }
buy2livein { "1" }
ecstat1 { "1" }
@ -336,11 +336,11 @@ FactoryBot.define do
prevshared { 2 }
sexrab3 { "F" }
sex3 { "F" }
sexrab4 { "X" }
sexrab4 { "R" }
sex4 { "X" }
sexrab5 { "M" }
sex5 { "M" }
sexrab6 { "X" }
sexrab6 { "R" }
sex6 { "X" }
mortgage { 20_000 }
ecstat3 { 9 }

18
spec/fixtures/exports/sales_log_26_27.xml vendored

@ -8,13 +8,13 @@
<JOINTMORE>1</JOINTMORE>
<BEDS>2</BEDS>
<AGE1>27</AGE1>
<SEX1>F</SEX1>
<SEXRAB1>F</SEXRAB1>
<ETHNIC>17</ETHNIC>
<BUILTYPE>1</BUILTYPE>
<PROPTYPE>1</PROPTYPE>
<AGE2>33</AGE2>
<RELAT2>P</RELAT2>
<SEX2>X</SEX2>
<SEXRAB2>R</SEXRAB2>
<NOINT>2</NOINT>
<ECSTAT2>1</ECSTAT2>
<PRIVACYNOTICE>1</PRIVACYNOTICE>
@ -32,7 +32,7 @@
<SAVINGSNK>1</SAVINGSNK>
<SAVINGS/>
<PREVOWN>1</PREVOWN>
<SEX3>F</SEX3>
<SEXRAB3>F</SEXRAB3>
<MORTGAGE>20000.0</MORTGAGE>
<INC2MORT>1</INC2MORT>
<ECSTAT3>9</ECSTAT3>
@ -44,9 +44,9 @@
<RELAT5>R</RELAT5>
<RELAT6>R</RELAT6>
<HB>4</HB>
<SEX4>X</SEX4>
<SEX5>M</SEX5>
<SEX6>X</SEX6>
<SEXRAB4>R</SEXRAB4>
<SEXRAB5>M</SEXRAB5>
<SEXRAB6>R</SEXRAB6>
<FROMBEDS/>
<STAIRCASE/>
<STAIRBOUGHT/>
@ -88,12 +88,6 @@
<FIRSTSTAIR/>
<NUMSTAIR/>
<MRENTPRESTAIRCASING/>
<SEXRAB1>F</SEXRAB1>
<SEXRAB2/>
<SEXRAB3>F</SEXRAB3>
<SEXRAB4/>
<SEXRAB5>M</SEXRAB5>
<SEXRAB6/>
<BUILDHEIGHTCLASS>2</BUILDHEIGHTCLASS>
<DAY>1</DAY>
<MONTH>4</MONTH>

24
spec/fixtures/files/2026_27_sales_bulk_upload.csv vendored

File diff suppressed because one or more lines are too long

3
spec/fixtures/files/sales_logs_csv_export_codes_23.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_codes_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_codes_25.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_codes_26.csv vendored

File diff suppressed because one or more lines are too long

3
spec/fixtures/files/sales_logs_csv_export_labels_23.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_25.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_25.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv vendored

File diff suppressed because one or more lines are too long

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

@ -407,7 +407,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_sex_registered_at_birth
buyer_1_gender_identity
buyer_1_ethnic_group
buyer_1_ethnic_background_black
buyer_1_ethnic_background_asian
@ -428,7 +427,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_buyer_retirement_value_check
age_2_buyer_not_retired_value_check
buyer_2_sex_registered_at_birth
buyer_2_gender_identity
buyer_2_ethnic_group
buyer_2_ethnic_background_black
buyer_2_ethnic_background_asian
@ -453,7 +451,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_2_not_retired_value_check
age_2_partner_under_16_value_check
person_2_sex_registered_at_birth
person_2_gender_identity
person_2_working_situation
working_situation_2_retirement_value_check
working_situation_2_not_retired_value_check
@ -466,7 +463,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_3_not_retired_value_check
age_3_partner_under_16_value_check
person_3_sex_registered_at_birth
person_3_gender_identity
person_3_working_situation
working_situation_3_retirement_value_check
working_situation_3_not_retired_value_check
@ -479,7 +475,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_4_not_retired_value_check
age_4_partner_under_16_value_check
person_4_sex_registered_at_birth
person_4_gender_identity
person_4_working_situation
working_situation_4_retirement_value_check
working_situation_4_not_retired_value_check
@ -492,7 +487,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_5_not_retired_value_check
age_5_partner_under_16_value_check
person_5_sex_registered_at_birth
person_5_gender_identity
person_5_working_situation
working_situation_5_retirement_value_check
working_situation_5_not_retired_value_check
@ -505,7 +499,6 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
age_6_not_retired_value_check
age_6_partner_under_16_value_check
person_6_sex_registered_at_birth
person_6_gender_identity
person_6_working_situation
working_situation_6_retirement_value_check
working_situation_6_not_retired_value_check

16
spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

@ -112,13 +112,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
field_105: "07",
field_106: "2023",
field_110: "900",
field_122: "F",
field_123: "F",
field_124: "M",
field_125: "M",
field_126: "R",
field_127: "R",
field_128: "1",
field_122: "1",
}
end
@ -301,7 +295,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
end
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_122 field_123 field_124 field_125 field_126 field_127] }
let(:case_insensitive_fields) { %w[field_29 field_36 field_44 field_48 field_52 field_56] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72 field_90 field_118] }
let(:attributes) do
valid_attributes
@ -321,7 +315,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
it "fetches the question's check_answer_label if it exists" do
parser.valid?
expect(parser.errors[:field_29]).to eql([I18n.t("validations.not_answered", question: "buyer 1’s gender identity.")])
expect(parser.errors[:field_29]).to eql([I18n.t("validations.not_answered", question: "buyer 1’s sex registered at birth.")])
end
end
@ -794,7 +788,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_21, # Postcode
:field_22, # Postcode
:field_28, # Buyer 1 age
:field_29, # Buyer 1 gender
:field_29, # Buyer 1 sex registered at birth
:field_32, # Buyer 1 working situation
:field_7, # Purchaser code
].each do |field|
@ -824,7 +818,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_21, # Postcode
:field_22, # Postcode
:field_28, # Buyer 1 age
:field_29, # Buyer 1 gender
:field_29, # Buyer 1 sex registered at birth
:field_32, # Buyer 1 working situation
:field_7, # Purchaser code
].each do |field|

94
spec/services/csv/sales_log_csv_service_spec.rb

@ -1,6 +1,8 @@
require "rails_helper"
RSpec.describe Csv::SalesLogCsvService do
include CollectionTimeHelper
subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
let(:form_handler_mock) { instance_double(FormHandler) }
@ -197,25 +199,10 @@ RSpec.describe Csv::SalesLogCsvService do
expect(la_label_value).to eq "Westminster"
end
context "when the requested form is 2023" do
let(:now) { Time.zone.local(2024, 1, 1) }
let(:year) { 2023 }
it "exports the CSV with the 2023 ordering and all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_labels_23.csv")
values_to_delete = %w[ID]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
context "when the requested form is 2024" do
let(:now) { Time.zone.local(2024, 5, 1) }
context "when the requested form is 2024", metadata: { year: 24 } do
let(:now) { collection_start_date_for_year(2024) }
let(:year) { 2024 }
let(:fixed_time) { Time.zone.local(2024, 5, 1) }
let(:fixed_time) { collection_start_date_for_year(2024) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
@ -232,10 +219,10 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "when the requested form is 2025" do
let(:now) { Time.zone.local(2025, 5, 1) }
context "when the requested form is 2025", metadata: { year: 25 } do
let(:now) { collection_start_date_for_year(2025) }
let(:year) { 2025 }
let(:fixed_time) { Time.zone.local(2025, 5, 1) }
let(:fixed_time) { collection_start_date_for_year(2025) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
@ -252,13 +239,13 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "when the requested form is 2026" do
let(:now) { Time.zone.local(2026, 5, 1) }
context "when the requested form is 2026", metadata: { year: 26 } do
let(:now) { collection_start_date_for_year(2026) }
let(:year) { 2026 }
let(:fixed_time) { Time.zone.local(2026, 5, 1) }
let(:fixed_time) { collection_start_date_for_year(2026) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1, buildheightclass: 2)
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
end
it "exports the CSV with the 2026 ordering and all values correct" do
@ -268,7 +255,7 @@ RSpec.describe Csv::SalesLogCsvService do
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv[1..]).to eq expected_content[1..] # Skip the first line as it contains the definitions
expect(csv).to eq expected_content
end
end
@ -325,24 +312,9 @@ RSpec.describe Csv::SalesLogCsvService do
expect(la_label_value).to eq "Westminster"
end
context "when the requested form is 2023" do
let(:now) { Time.zone.local(2024, 1, 1) }
let(:year) { 2023 }
it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_23.csv")
values_to_delete = %w[ID]
values_to_delete.each do |attribute|
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv).to eq expected_content
end
end
context "when the requested form is 2024" do
let(:now) { Time.zone.local(2024, 5, 1) }
let(:fixed_time) { Time.zone.local(2024, 5, 1) }
context "when the requested form is 2024", metadata: { year: 24 } do
let(:now) { collection_start_date_for_year(2024) }
let(:fixed_time) { collection_start_date_for_year(2024) }
let(:year) { 2024 }
before do
@ -360,9 +332,9 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "when the requested form is 2025" do
let(:now) { Time.zone.local(2025, 5, 1) }
let(:fixed_time) { Time.zone.local(2025, 5, 1) }
context "when the requested form is 2025", metadata: { year: 25 } do
let(:now) { collection_start_date_for_year(2025) }
let(:fixed_time) { collection_start_date_for_year(2025) }
let(:year) { 2025 }
before do
@ -380,13 +352,13 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "when the requested form is 2026" do
let(:now) { Time.zone.local(2026, 5, 1) }
let(:fixed_time) { Time.zone.local(2026, 5, 1) }
context "when the requested form is 2026", metadata: { year: 26 } do
let(:now) { collection_start_date_for_year(2026) }
let(:fixed_time) { collection_start_date_for_year(2026) }
let(:year) { 2026 }
before do
log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1, buildheightclass: 2)
log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
end
it "exports the CSV with all values correct" do
@ -396,7 +368,7 @@ RSpec.describe Csv::SalesLogCsvService do
index = attribute_line.index(attribute)
content_line[index] = nil
end
expect(csv[1..]).to eq expected_content[1..] # Skip the first line as it contains the definitions
expect(csv).to eq expected_content
end
end
@ -420,10 +392,10 @@ RSpec.describe Csv::SalesLogCsvService do
expect(attribute_line).not_to include(*%w[address_line1_as_entered address_line2_as_entered town_or_city_as_entered county_as_entered postcode_full_as_entered la_as_entered created_by value_value_check monthly_charges_value_check])
end
context "and the requested form is 2024" do
context "and the requested form is 2024", metadata: { year: 24 } do
let(:year) { 2024 }
let(:now) { Time.zone.local(2024, 5, 1) }
let(:fixed_time) { Time.zone.local(2024, 5, 1) }
let(:now) { collection_start_date_for_year(2024) }
let(:fixed_time) { collection_start_date_for_year(2024) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
@ -444,10 +416,10 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "and the requested form is 2025" do
context "and the requested form is 2025", metadata: { year: 25 } do
let(:year) { 2025 }
let(:now) { Time.zone.local(2025, 5, 1) }
let(:fixed_time) { Time.zone.local(2025, 5, 1) }
let(:now) { collection_start_date_for_year(2025) }
let(:fixed_time) { collection_start_date_for_year(2025) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
@ -468,10 +440,10 @@ RSpec.describe Csv::SalesLogCsvService do
end
end
context "and the requested form is 2026" do
context "and the requested form is 2026", metadata: { year: 26 } do
let(:year) { 2026 }
let(:now) { Time.zone.local(2026, 5, 1) }
let(:fixed_time) { Time.zone.local(2026, 5, 1) }
let(:now) { collection_start_date_for_year(2026) }
let(:fixed_time) { collection_start_date_for_year(2026) }
before do
log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1, buildheightclass: 2)

10
spec/services/exports/sales_log_export_service_spec.rb

@ -333,7 +333,7 @@ RSpec.describe Exports::SalesLogExportService do
end
end
context "when exporting only 24/25 collection period" do
context "when exporting only 24/25 collection period", metadata: { year: 24 } do
let(:start_time) { Time.zone.local(2024, 4, 3) }
before do
@ -365,8 +365,8 @@ RSpec.describe Exports::SalesLogExportService do
end
end
context "when exporting only 25/26 collection period" do
let(:start_time) { Time.zone.local(2025, 4, 1) }
context "when exporting only 25/26 collection period", metadata: { year: 25 } do
let(:start_time) { collection_start_date_for_year(2025) }
before do
Timecop.freeze(start_time)
@ -397,8 +397,8 @@ RSpec.describe Exports::SalesLogExportService do
end
end
context "when exporting only 26/27 collection period" do
let(:start_time) { Time.zone.local(2026, 4, 1) }
context "when exporting only 26/27 collection period", metadata: { year: 26 } do
let(:start_time) { collection_start_date_for_year(2026) }
before do
Timecop.freeze(start_time)

Loading…
Cancel
Save