Browse Source

CLDC-4177: New Sex At Birth question for sales logs (#3144)

* CLDC-4177: Create new sex-at-birth question

* CLDC-4177: Update database

* CLDC-4177: Adds SAB form for the primary buyer.

* CLDC-4177: Adds sex-at-birth question for other household members

* CLCD-4177: Unit tests

* CLDC-4177: Misc inclusions where sex is also used

* CLDC-4177: Lint fixes

* CLDC-4177: Do not include sexRAB in pre-2026 exports

* CLDC-4177: Fixes bug with copy key in new page

* CLDC-4177: Changes logic used to decide which fields to export

* CLDC-4177: Linting fixes

* CLDC-4177: Converts new field to lowercase

* CLDC-4177: Satisfy bulk upload tests (not tested manually)

* CLDC-4177: Rename new fields to lowercase

* CLDC-4177: Rename field in bulk upload.

* Fix db casing

* CLDC-4177: remove frozen string literals

* Delete redundant migration

* Remove extraneous lines from schema.rb

* Add new sexrab fields to the :completed trait in the Sales Log factory

* CLDC-4177: remove redundant inferred CYA vals

* CLDC-4177: update bu files

* CLDC-4177: update sales log export service

* CLDC-4177: update schema

* CLDC-4177: linting

* CLDC-4177: update sales log export constants

* CLDC-4177: update factories

* CLDC-4177: lint

* CLDC-4177: csv parser updates

* CLDC-4177: bu field updates

* CLDC-4177: update var def spec

* CLDC-4177: make to_2026_row explicit

* CLDC-4177: use shared field count

* CLDC-4177: refactor to remove max cols going forwards

* CLDC-4141: update schema

* CLDC-4141: update schema

* CLDC-4141: update schema

* CLDC-4141: remove X from factory options

* CLDC-4177: update log var defs

* CLDC-4177: use correct shared field count

* CLDC-4177: remove trailing commas

* CLDC-4177: update bulk upload csv example

* CLDC-4177: update bulk upload csv example

* CLDC-4177: remove sexrab from expected sales export for now

---------

Co-authored-by: oscar-richardson-softwire <oscar.richardson@softwire.com>
Co-authored-by: Nat Dean-Lewis <nat.dean-lewis@softwire.com>
pull/3171/head
Katherine Langford 1 week ago committed by GitHub
parent
commit
d2fff16808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 142
      app/helpers/bulk_upload/sales_log_to_csv.rb
  2. 16
      app/models/form/sales/pages/person_sex_registered_at_birth.rb
  3. 20
      app/models/form/sales/pages/sex_registered_at_birth1.rb
  4. 23
      app/models/form/sales/pages/sex_registered_at_birth2.rb
  5. 19
      app/models/form/sales/questions/person_sex_registered_at_birth.rb
  6. 19
      app/models/form/sales/questions/sex_registered_at_birth1.rb
  7. 20
      app/models/form/sales/questions/sex_registered_at_birth2.rb
  8. 7
      app/models/form/sales/subsections/household_characteristics.rb
  9. 2
      app/services/bulk_upload/sales/validator.rb
  10. 3
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  11. 40
      app/services/bulk_upload/sales/year2026/row_parser.rb
  12. 1
      app/services/csv/sales_log_csv_service.rb
  13. 268
      app/services/exports/sales_log_export_constants.rb
  14. 26
      app/services/exports/sales_log_export_service.rb
  15. 49
      config/locales/forms/2026/sales/household_characteristics.en.yml
  16. 12
      db/migrate/20260113143404_add_sex_registered_at_birth_to_sales_logs.rb
  17. 8
      db/schema.rb
  18. 13
      spec/factories/sales_log.rb
  19. 20
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  20. 287
      spec/fixtures/variable_definitions/sales_download_26_27.csv
  21. 5
      spec/lib/tasks/log_variable_definitions_spec.rb
  22. 104
      spec/models/form/sales/pages/person_sex_registered_at_birth_spec.rb
  23. 29
      spec/models/form/sales/pages/sex_registered_at_birth1_spec.rb
  24. 38
      spec/models/form/sales/pages/sex_registered_at_birth2_spec.rb
  25. 123
      spec/models/form/sales/questions/person_sex_registered_at_birth_spec.rb
  26. 49
      spec/models/form/sales/questions/sex_registered_at_birth1_spec.rb
  27. 49
      spec/models/form/sales/questions/sex_registered_at_birth2_spec.rb
  28. 131
      spec/models/form/sales/subsections/household_characteristics_spec.rb
  29. 2
      spec/services/bulk_upload/sales/validator_spec.rb
  30. 8
      spec/services/bulk_upload/sales/year2026/csv_parser_spec.rb
  31. 8
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  32. 4
      spec/services/imports/variable_definitions_service_spec.rb

142
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -70,8 +70,7 @@ class BulkUpload::SalesLogToCsv
when 2025
(1..121).to_a
when 2026
# TODO: CLDC-4162: Replace with actual field numbers when 2026 format is known
(1..121).to_a
(1..BulkUpload::Sales::Year2026::CsvParser::FIELDS).to_a
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
@ -537,7 +536,144 @@ class BulkUpload::SalesLogToCsv
def to_2026_row
# TODO: CLDC-4162: Implement when 2026 template is available
to_2025_row
[
log.saledate&.day,
log.saledate&.month,
log.saledate&.strftime("%y"),
overrides[:organisation_id] || log.owning_organisation&.old_visible_id,
overrides[:managing_organisation_id] || log.managing_organisation&.old_visible_id,
log.assigned_to&.email,
log.purchid,
log.ownershipsch,
log.ownershipsch == 1 ? log.type : "", # field_9: "What is the type of shared ownership sale?",
log.staircase, # 10
log.ownershipsch == 2 ? log.type : "", # field_11: "What is the type of discounted ownership sale?",
log.jointpur,
log.jointmore,
log.noint,
log.privacynotice,
log.uprn,
log.address_line1&.tr(",", " "), # 20
log.address_line2&.tr(",", " "),
log.town_or_city&.tr(",", " "),
log.county&.tr(",", " "),
((log.postcode_full || "").split(" ") || [""]).first,
((log.postcode_full || "").split(" ") || [""]).last,
log.la,
log.proptype,
log.beds,
log.builtype,
log.wchair,
log.age1,
log.sex1,
log.ethnic, # 30
log.nationality_all_group,
log.ecstat1,
log.buy1livein,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat2],
log.age2,
log.sex2,
log.ethnic_group2,
log.nationality_all_buyer2_group,
log.ecstat2,
log.buy2livein, # 40
log.hholdcount,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat3],
log.age3,
log.sex3,
log.ecstat3,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat4],
log.age4,
log.sex4,
log.ecstat4,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat5], # 50
log.age5,
log.sex5,
log.ecstat5,
{ "P" => 1, "X" => 2, "R" => 3 }[log.relat6],
log.age6,
log.sex6,
log.ecstat6,
log.prevten,
log.ppcodenk,
((log.ppostcode_full || "").split(" ") || [""]).first, # 60
((log.ppostcode_full || "").split(" ") || [""]).last,
log.prevloc,
log.buy2living,
log.prevtenbuy2,
log.hhregres,
log.hhregresstill,
log.armedforcesspouse,
log.disabled,
log.wheel,
log.income1, # 70
log.inc1mort,
log.income2,
log.inc2mort,
log.hb,
log.savings.present? || "R",
log.prevown,
log.prevshared,
log.resale,
log.proplen,
log.hodate&.day, # 80
log.hodate&.month,
log.hodate&.strftime("%y"),
log.frombeds,
log.fromprop,
log.socprevten,
log.value,
log.equity,
log.mortgageused,
log.mortgage,
log.mortlen, # 90
log.deposit,
log.cashdis,
log.mrent,
log.mscharge,
log.management_fee,
log.stairbought,
log.stairowned,
log.staircasesale,
log.firststair,
log.initialpurchase&.day, # 100
log.initialpurchase&.month,
log.initialpurchase&.strftime("%y"),
log.numstair,
log.lasttransaction&.day,
log.lasttransaction&.month,
log.lasttransaction&.strftime("%y"),
log.value,
log.equity,
log.mortgageused,
log.mrentprestaircasing, # 110
log.mrent,
log.proplen,
log.value,
log.grant,
log.discount,
log.mortgageused,
log.mortgage,
log.mortlen,
log.extrabor,
log.deposit, # 120
log.mscharge,
log.sexrab1,
log.sexrab2,
log.sexrab3,
log.sexrab4,
log.sexrab5,
log.sexrab6, # 127
]
end
def custom_field_numbers_row(seed: nil, field_numbers: nil)

16
app/models/form/sales/pages/person_sex_registered_at_birth.rb

@ -0,0 +1,16 @@
class Form::Sales::Pages::PersonSexRegisteredAtBirth < ::Form::Page
def initialize(id, hsh, subsection, person_index:)
super(id, hsh, subsection)
@copy_key = "sales.household_characteristics.sexrab2.person" if person_index == 2
@person_index = person_index
@depends_on = [
{ "details_known_#{person_index}" => 1 },
]
end
def questions
@questions ||= [
Form::Sales::Questions::PersonSexRegisteredAtBirth.new("sexrab#{@person_index}", nil, self, person_index: @person_index),
]
end
end

20
app/models/form/sales/pages/sex_registered_at_birth1.rb

@ -0,0 +1,20 @@
class Form::Sales::Pages::SexRegisteredAtBirth1 < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "buyer_1_sex_registered_at_birth"
@depends_on = [
{
"buyer_has_seen_privacy_notice?" => true,
},
{
"buyer_not_interviewed?" => true,
},
]
end
def questions
@questions ||= [
Form::Sales::Questions::SexRegisteredAtBirth1.new(nil, nil, self),
]
end
end

23
app/models/form/sales/pages/sex_registered_at_birth2.rb

@ -0,0 +1,23 @@
class Form::Sales::Pages::SexRegisteredAtBirth2 < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "buyer_2_sex_registered_at_birth"
@copy_key = "sales.household_characteristics.sexrab2.buyer"
@depends_on = [
{
"joint_purchase?" => true,
"buyer_has_seen_privacy_notice?" => true,
},
{
"joint_purchase?" => true,
"buyer_not_interviewed?" => true,
},
]
end
def questions
@questions ||= [
Form::Sales::Questions::SexRegisteredAtBirth2.new(nil, nil, self),
]
end
end

19
app/models/form/sales/questions/person_sex_registered_at_birth.rb

@ -0,0 +1,19 @@
class Form::Sales::Questions::PersonSexRegisteredAtBirth < ::Form::Question
def initialize(id, hsh, page, person_index:)
super(id, hsh, page)
@type = "radio"
@copy_key = "sales.household_characteristics.sexrab2.person" if person_index == 2
@check_answers_card_number = person_index
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Person prefers not to say" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2026 => 0 }.freeze
end

19
app/models/form/sales/questions/sex_registered_at_birth1.rb

@ -0,0 +1,19 @@
class Form::Sales::Questions::SexRegisteredAtBirth1 < ::Form::Question
def initialize(id, hsh, page)
super
@id = "sexrab1"
@type = "radio"
@check_answers_card_number = 1
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Buyer prefers not to say" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2026 => 0 }.freeze
end

20
app/models/form/sales/questions/sex_registered_at_birth2.rb

@ -0,0 +1,20 @@
class Form::Sales::Questions::SexRegisteredAtBirth2 < ::Form::Question
def initialize(id, hsh, page)
super
@id = "sexrab2"
@type = "radio"
@copy_key = "sales.household_characteristics.sexrab2.buyer"
@check_answers_card_number = 2
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
ANSWER_OPTIONS = {
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Buyer prefers not to say" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2026 => 0 }.freeze
end

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

@ -24,6 +24,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(Form::Sales::Pages::NotRetiredValueCheck.new("age_1_not_retired_value_check", nil, self, person_index: 1) if form.start_year_2024_or_later?),
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::Buyer1EthnicGroup.new(nil, nil, self),
Form::Sales::Pages::Buyer1EthnicBackgroundBlack.new(nil, nil, self),
@ -46,6 +47,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
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_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),
buyer_2_ethnicity_nationality_pages,
Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self),
@ -67,6 +69,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(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),
@ -82,6 +85,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(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),
@ -97,6 +101,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(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),
@ -112,6 +117,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(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),
@ -127,6 +133,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
(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),

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

@ -167,7 +167,7 @@ private
column_count = rows.map(&:size).max
errors.add(:base, I18n.t("validations.sales.#{@bulk_upload.year}.bulk_upload.wrong_template.over_max_column_count")) if column_count > csv_parser.class::MAX_COLUMNS
errors.add(:base, I18n.t("validations.sales.#{@bulk_upload.year}.bulk_upload.wrong_template.over_max_column_count")) if column_count > csv_parser.class::FIELDS + 1
end
def validate_correct_template

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

@ -4,8 +4,7 @@ class BulkUpload::Sales::Year2026::CsvParser
include CollectionTimeHelper
# TODO: CLDC-4162: Update when 2026 format is known
FIELDS = 121
MAX_COLUMNS = 142
FIELDS = 127
FORM_YEAR = 2026
attr_reader :path

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

@ -135,6 +135,13 @@ class BulkUpload::Sales::Year2026::RowParser
field_119: "Does this include any extra borrowing?",
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",
}.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
@ -275,6 +282,13 @@ class BulkUpload::Sales::Year2026::RowParser
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
validates :field_1,
presence: {
message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."),
@ -513,6 +527,7 @@ class BulkUpload::Sales::Year2026::RowParser
"field_22", # postcode
"field_28", # age1
"field_29", # sex1
"field_122", # sexrab1
"field_32", # ecstat1
)
end
@ -780,6 +795,12 @@ 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],
}
end
@ -815,6 +836,13 @@ private
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["relat2"] = relationship_from_is_partner(field_34)
attributes["relat3"] = relationship_from_is_partner(field_42)
attributes["relat4"] = relationship_from_is_partner(field_46)
@ -1013,23 +1041,23 @@ private
end
def person_2_present?
field_35.present? || field_36.present? || field_34.present?
field_35.present? || field_36.present? || field_34.present? || field_123.present?
end
def person_3_present?
field_43.present? || field_44.present? || field_42.present?
field_43.present? || field_44.present? || field_42.present? || field_124.present?
end
def person_4_present?
field_47.present? || field_48.present? || field_46.present?
field_47.present? || field_48.present? || field_46.present? || field_125.present?
end
def person_5_present?
field_51.present? || field_52.present? || field_50.present?
field_51.present? || field_52.present? || field_50.present? || field_126.present?
end
def person_6_present?
field_55.present? || field_56.present? || field_54.present?
field_55.present? || field_56.present? || field_54.present? || field_127.present?
end
def relationship_from_is_partner(is_partner)
@ -1241,6 +1269,7 @@ private
%w[
saledate
age1
sexrab1
sex1
ecstat1
owning_organisation
@ -1419,6 +1448,7 @@ private
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_32, error_message) # Buyer 1 working situation
errors.add(:field_7, error_message) # Purchaser code
end

1
app/services/csv/sales_log_csv_service.rb

@ -114,6 +114,7 @@ module Csv
hash["age1"] = { "refused_code" => "-9", "refused_label" => "Not known", "age_known_field" => "age1_known" }
(2..6).each do |i|
hash["age#{i}"] = { "refused_code" => "-9", "refused_label" => "Not known", "details_known_field" => "details_known_#{i}", "age_known_field" => "age#{i}_known" }
hash["sexrab#{i}"] = { "refused_code" => "R", "refused_label" => "Prefers not to say", "details_known_field" => "details_known_#{i}" }
hash["sex#{i}"] = { "refused_code" => "R", "refused_label" => "Prefers not to say", "details_known_field" => "details_known_#{i}" }
hash["relat#{i}"] = { "refused_code" => "R", "refused_label" => "Prefers not to say", "details_known_field" => "details_known_#{i}" }
hash["ecstat#{i}"] = { "refused_code" => "10", "refused_label" => "Prefers not to say", "details_known_field" => "details_known_#{i}" }

268
app/services/exports/sales_log_export_constants.rb

@ -7,140 +7,148 @@ module Exports::SalesLogExportConstants
csv: 2,
}.freeze
EXPORT_FIELDS = Set["ID",
"STATUS",
"DAY",
"MONTH",
"YEAR",
"DUPLICATESET",
"CREATEDDATE",
"UPLOADDATE",
"OWNINGORGID",
"OWNINGORGNAME",
"MANINGORGID",
"MANINGORGNAME",
"USERNAME",
"USERNAMEID",
"PURCHID",
"TYPE",
"OWNERSHIP",
"COLLECTIONYEAR",
"JOINTMORE",
"JOINT",
"BEDS",
"ETHNIC",
"ETHNICGROUP1",
"LIVEINBUYER1",
"BUILTYPE",
"PROPTYPE",
"NOINT",
"LIVEINBUYER2",
"PRIVACYNOTICE",
"WHEEL",
"HHOLDCOUNT",
"LA",
"INCOME1",
"INC1NK",
"INC1MORT",
"INCOME2",
"INC2NK",
"SAVINGSNK",
"SAVINGS",
"PREVOWN",
"AMENDEDBY",
"AMENDEDBYID",
"MORTGAGE",
"INC2MORT",
"HB",
"FROMBEDS",
"STAIRCASE",
"STAIRBOUGHT",
"STAIROWNED",
"MRENT",
"MRENTPRESTAIRCASING",
"RESALE",
"DEPOSIT",
"CASHDIS",
"DISABLED",
"VALUE",
"EQUITY",
"DISCOUNT",
"GRANT",
"PPCODENK",
"PPOSTC1",
"PPOSTC2",
"PREVLOC",
"PREVLOCNAME",
"PREVIOUSLAKNOWN",
"HHREGRES",
"HHREGRESSTILL",
"PROPLEN",
"HASMSCHARGE",
"MSCHARGE",
"PREVTEN",
"MORTGAGEUSED",
"WCHAIR",
"ARMEDFORCESSPOUSE",
"HODAY",
"HOMONTH",
"HOYEAR",
"FROMPROP",
"SOCPREVTEN",
"MORTLEN1",
"EXTRABOR",
"HHTYPE",
"POSTCODE",
"ISLAINFERRED",
"BULKUPLOADID",
"VALUE_VALUE_CHECK",
"PREVSHARED",
"STAIRCASETOSALE",
"ETHNICGROUP2",
"ETHNIC2",
"BUY2LIVING",
"PREVTEN2",
"UPRN",
"ADDRESS1",
"ADDRESS2",
"TOWNCITY",
"COUNTY",
"LANAME",
"CREATIONMETHOD",
"NATIONALITYALL1",
"NATIONALITYALL2",
"MSCHARGE_VALUE_CHECK",
"ADDRESS1INPUT",
"POSTCODEINPUT",
"ADDRESS_SEARCH_VALUE_CHECK",
"UPRNSELECTED",
"BULKADDRESS1",
"BULKADDRESS2",
"BULKTOWNCITY",
"BULKCOUNTY",
"BULKPOSTCODE",
"BULKLA",
"CREATEDBY",
"CREATEDBYID",
"HASESTATEFEE",
"ESTATEFEE",
"FIRSTSTAIR",
"NUMSTAIR",
"STAIRLASTDAY",
"STAIRLASTMONTH",
"STAIRLASTYEAR",
"STAIRINITIALYEAR",
"STAIRINITIALMONTH",
"STAIRINITIALDAY",
"HASSERVICECHARGES",
"SERVICECHARGES",]
ALL_YEAR_EXPORT_FIELDS = Set[
"ID",
"STATUS",
"DAY",
"MONTH",
"YEAR",
"DUPLICATESET",
"CREATEDDATE",
"UPLOADDATE",
"OWNINGORGID",
"OWNINGORGNAME",
"MANINGORGID",
"MANINGORGNAME",
"USERNAME",
"USERNAMEID",
"PURCHID",
"TYPE",
"OWNERSHIP",
"COLLECTIONYEAR",
"JOINTMORE",
"JOINT",
"BEDS",
"ETHNIC",
"ETHNICGROUP1",
"LIVEINBUYER1",
"BUILTYPE",
"PROPTYPE",
"NOINT",
"LIVEINBUYER2",
"PRIVACYNOTICE",
"WHEEL",
"HHOLDCOUNT",
"LA",
"INCOME1",
"INC1NK",
"INC1MORT",
"INCOME2",
"INC2NK",
"SAVINGSNK",
"SAVINGS",
"PREVOWN",
"AMENDEDBY",
"AMENDEDBYID",
"MORTGAGE",
"INC2MORT",
"HB",
"FROMBEDS",
"STAIRCASE",
"STAIRBOUGHT",
"STAIROWNED",
"MRENT",
"MRENTPRESTAIRCASING",
"RESALE",
"DEPOSIT",
"CASHDIS",
"DISABLED",
"VALUE",
"EQUITY",
"DISCOUNT",
"GRANT",
"PPCODENK",
"PPOSTC1",
"PPOSTC2",
"PREVLOC",
"PREVLOCNAME",
"PREVIOUSLAKNOWN",
"HHREGRES",
"HHREGRESSTILL",
"PROPLEN",
"HASMSCHARGE",
"MSCHARGE",
"PREVTEN",
"MORTGAGEUSED",
"WCHAIR",
"ARMEDFORCESSPOUSE",
"HODAY",
"HOMONTH",
"HOYEAR",
"FROMPROP",
"SOCPREVTEN",
"MORTLEN1",
"EXTRABOR",
"HHTYPE",
"POSTCODE",
"ISLAINFERRED",
"BULKUPLOADID",
"VALUE_VALUE_CHECK",
"PREVSHARED",
"STAIRCASETOSALE",
"ETHNICGROUP2",
"ETHNIC2",
"BUY2LIVING",
"PREVTEN2",
"UPRN",
"ADDRESS1",
"ADDRESS2",
"TOWNCITY",
"COUNTY",
"LANAME",
"CREATIONMETHOD",
"NATIONALITYALL1",
"NATIONALITYALL2",
"MSCHARGE_VALUE_CHECK",
"ADDRESS1INPUT",
"POSTCODEINPUT",
"ADDRESS_SEARCH_VALUE_CHECK",
"UPRNSELECTED",
"BULKADDRESS1",
"BULKADDRESS2",
"BULKTOWNCITY",
"BULKCOUNTY",
"BULKPOSTCODE",
"BULKLA",
"CREATEDBY",
"CREATEDBYID",
"HASESTATEFEE",
"ESTATEFEE",
"FIRSTSTAIR",
"NUMSTAIR",
"STAIRLASTDAY",
"STAIRLASTMONTH",
"STAIRLASTYEAR",
"STAIRINITIALYEAR",
"STAIRINITIALMONTH",
"STAIRINITIALDAY",
"HASSERVICECHARGES",
"SERVICECHARGES",
]
(1..6).each do |index|
EXPORT_FIELDS << "AGE#{index}"
EXPORT_FIELDS << "ECSTAT#{index}"
EXPORT_FIELDS << "SEX#{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|
EXPORT_FIELDS << "RELAT#{index}"
ALL_YEAR_EXPORT_FIELDS << "RELAT#{index}"
end
YEAR_2026_EXPORT_FIELDS = Set[]
(1..6).each do |index|
YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}"
end
end

26
app/services/exports/sales_log_export_service.rb

@ -150,8 +150,23 @@ module Exports
attribute_hash
end
def is_omitted_field?(field_name, _sales_log)
!EXPORT_FIELDS.include?(field_name)
def is_included_field?(field_name, included_fields)
included_fields.include?(field_name)
end
def get_included_fields(sales_log)
included_fields = Set[]
included_fields.merge(ALL_YEAR_EXPORT_FIELDS)
year_fields = case sales_log.collection_start_year
when 2026
YEAR_2026_EXPORT_FIELDS
else
Set[]
end
included_fields.merge(year_fields)
included_fields
end
def build_export_xml(sales_logs)
@ -161,11 +176,12 @@ module Exports
attribute_hash = apply_cds_transformation(sales_log, EXPORT_MODE[:xml])
form = doc.create_element("form")
doc.at("forms") << form
included_fields = get_included_fields(sales_log)
attribute_hash.each do |key, value|
if is_omitted_field?(key, sales_log)
next
else
if is_included_field?(key, included_fields)
form << doc.create_element(key, value)
else
next
end
end
end

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

@ -16,6 +16,13 @@ en:
hint_text: ""
question_text: "Age"
sexrab1:
page_header: ""
check_answer_label: "Buyer 1’s sex registered at birth"
check_answer_prompt: ""
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"
@ -130,6 +137,20 @@ en:
hint_text: ""
question_text: "Age"
sexrab2:
buyer:
page_header: ""
check_answer_label: "Buyer 2’s sex registered at birth"
check_answer_prompt: ""
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 2's sex at birth?"
person:
page_header: ""
check_answer_label: "Person 2’s sex registered at birth"
check_answer_prompt: ""
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: ""
@ -266,6 +287,13 @@ en:
hint_text: ""
question_text: "Age"
sexrab3:
page_header: ""
check_answer_label: "Person 3’s sex registered at birth"
check_answer_prompt: ""
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"
@ -307,6 +335,13 @@ en:
hint_text: ""
question_text: "Age"
sexrab4:
page_header: ""
check_answer_label: "Person 4’s sex registered at birth"
check_answer_prompt: ""
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"
@ -348,6 +383,13 @@ en:
hint_text: ""
question_text: "Age"
sexrab5:
page_header: ""
check_answer_label: "Person 5’s sex registered at birth"
check_answer_prompt: ""
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"
@ -389,6 +431,13 @@ en:
hint_text: ""
question_text: "Age"
sexrab6:
page_header: ""
check_answer_label: "Person 6’s sex registered at birth"
check_answer_prompt: ""
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"

12
db/migrate/20260113143404_add_sex_registered_at_birth_to_sales_logs.rb

@ -0,0 +1,12 @@
class AddSexRegisteredAtBirthToSalesLogs < ActiveRecord::Migration[7.2]
def change
change_table :sales_logs, bulk: true do |t|
t.column :sexrab1, :string
t.column :sexrab2, :string
t.column :sexrab3, :string
t.column :sexrab4, :string
t.column :sexrab5, :string
t.column :sexrab6, :string
end
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_01_23_150201) do
ActiveRecord::Schema[7.2].define(version: 2026_01_28_121417) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -788,6 +788,12 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_23_150201) do
t.datetime "lasttransaction"
t.datetime "initialpurchase"
t.boolean "manual_address_entry_selected", default: false
t.string "sexrab1"
t.string "sexrab2"
t.string "sexrab3"
t.string "sexrab4"
t.string "sexrab5"
t.string "sexrab6"
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"

13
spec/factories/sales_log.rb

@ -64,6 +64,7 @@ FactoryBot.define do
saledate_today
age1_known { 1 }
age1 { 20 }
sexrab1 { "F" }
sex1 { "F" }
ecstat1 { 1 }
postcode_full { "A1 1AA" }
@ -83,6 +84,7 @@ FactoryBot.define do
privacynotice { 1 }
age1_known { 0 }
age1 { Faker::Number.within(range: 27..45) }
sexrab1 { %w[F M R].sample }
sex1 { %w[F M X R].sample }
national { 18 }
buy1livein { 1 }
@ -93,6 +95,7 @@ FactoryBot.define do
builtype { 1 }
ethnic { 3 }
ethnic_group { 17 }
sexrab2 { %w[F M R].sample }
sex2 { "X" }
buy2livein { "1" }
ecstat1 { "1" }
@ -125,9 +128,13 @@ FactoryBot.define do
savingsnk { 1 }
prevown { 1 }
prevshared { 2 }
sexrab3 { %w[F M R].sample }
sex3 { %w[F M X R].sample }
sexrab4 { %w[F M R].sample }
sex4 { %w[F M X R].sample }
sexrab5 { %w[F M R].sample }
sex5 { %w[F M X R].sample }
sexrab6 { %w[F M R].sample }
sex6 { %w[F M X R].sample }
mortgage { 20_000 }
ecstat3 { 9 }
@ -280,6 +287,7 @@ FactoryBot.define do
privacynotice { 1 }
age1_known { 0 }
age1 { 27 }
sexrab1 { "F" }
sex1 { "F" }
national { 18 }
buy1livein { 1 }
@ -290,6 +298,7 @@ FactoryBot.define do
builtype { 1 }
ethnic { 3 }
ethnic_group { 17 }
sexrab2 { "X" }
sex2 { "X" }
buy2livein { "1" }
ecstat1 { "1" }
@ -322,9 +331,13 @@ FactoryBot.define do
savingsnk { 1 }
prevown { 1 }
prevshared { 2 }
sexrab3 { "F" }
sex3 { "F" }
sexrab4 { "X" }
sex4 { "X" }
sexrab5 { "M" }
sex5 { "M" }
sexrab6 { "X" }
sex6 { "X" }
mortgage { 20_000 }
ecstat3 { 9 }

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

File diff suppressed because one or more lines are too long

287
spec/fixtures/variable_definitions/sales_download_26_27.csv vendored

@ -0,0 +1,287 @@
ID,Log ID
STATUS,Status of log
DUPLICATESET,ID of a set of duplicate logs
CREATEDDATE,Time and date the log was created
UPLOADDATE,Time and date the log was last updated
COLLECTIONYEAR,Year collection period opened
CREATIONMETHOD,Was the log submitted in-service or via bulk upload?
BULKUPLOADID,ID of a set of bulk uploaded logs
DATAPROTECT,Is the user in the created_by column the data protection officer?
OWNINGORGNAME,Which organisation owned this property before the sale?
MANINGORGNAME,Which organisation reported the sale?
CREATEDBY,User that created the log
USERNAME,User the log is assigned to
DAY,Day of sale completion date
MONTH,Month of sale completion date
YEAR,Year of sale completion date
PURCHID,What is the purchaser code?
OWNERSHIP,Was this purchase made through an ownership scheme?
TYPE,What is the type of shared ownership/discounted ownership/outright sale?
OTHTYPE,If type = 'Other', what is the type of outright sale?
COMPANY,Is the buyer a company?
LIVEINBUYER,Will the buyer(s) live in the property?
JOINT,Is this a joint purchase?
JOINTMORE,Are there more than 2 joint buyers of this property?
NOINT,Did you interview the buyer to answer these questions?
PRIVACYNOTICE,Has the buyer seen the MHCLG privacy notice?
UPRN,What is the UPRN of the property?
ADDRESS1,Address line 1
ADDRESS2,Address line 2
TOWNCITY,Town/City
COUNTY,County
POSTCODE,Postcode
ISLAINFERRED,The internal value to indicate if the LA was inferred from the postcode
LANAME,LA name
LA,LA code
UPRNSELECTED,UPRN of the address selected
ADDRESS_SEARCH_VALUE_CHECK,Was the 'No address found' page seen?
ADDRESS1INPUT,Address line 1 input from address matching feature
POSTCODEINPUT,Postcode input from address matching feature
BULKADDRESS1,Address line 1 entered in bulk upload file
BULKADDRESS2,Address line 2 entered in bulk upload file
BULKTOWNCITY,Town or city entered in bulk upload file
BULKCOUNTY,County entered in bulk upload file
BULKPOSTCODE,Postcode entered in bulk upload file
BULKLA,Local authority entered in bulk upload file
BEDS,How many bedrooms does the property have?
PROPTYPE,What type of unit is the property?
BUILTYPE,Which type of building is the property?
WCHAIR,Is the property built or adapted to wheelchair-user standards?
AGE1,What is buyer 1's age?
SEX1,Which of these best describes buyer 1's gender identity?
ETHNICGROUP1,What is buyer 1's ethnic group?
ETHNIC,Which of the following best describes buyer 1's ethnic background?
NATIONALITYALL1,What is buyer 1's nationality?
ECSTAT1,Which of these best describes buyer 1's working situation?
LIVEINBUYER1,Will buyer 1 live in the property?
RELAT2,What is buyer 2 or person 2's relationship to buyer 1?
AGE2,What is buyer 2 or person 2's age?
SEX2,Which of these best describes buyer 2 or person 2's gender identity?
ETHNICGROUP2,What is buyer 2's ethnic group?
ETHNIC2,Which of the following best describes buyer 2's ethnic background?
NATIONALITYALL2,What is buyer 2's nationality?
ECSTAT2,What is buyer 2 or person 2's working situation?
LIVEINBUYER2,Will buyer 2 live in the property?
HHTYPE,Besides the buyer(s), how many other people live or will live in the property?
RELAT3,What is person 3's relationship to buyer 1?
AGE3,What is person 3's age?
SEX3,What is person 3's gender identity?
ECSTAT3,What is person 3's working situation?
RELAT4,What is person 4's relationship to buyer 1?
AGE4,What is person 4's age?
SEX4,What is person 4's gender identity?
ECSTAT4,What is person 4's working situation?
RELAT5,What is person 5's relationship to buyer 1?
AGE5,What is person 5's age?
SEX5,What is person 5's gender identity?
ECSTAT5,What is person 5's working situation?
RELAT6,What is person 6's relationship to buyer 1?
AGE6,What is person 6's age?
SEX6,What is person 6's gender identity?
ECSTAT6,What is person 6's working situation?
PREVTEN,What was buyer 1's previous tenure?
PPCODENK,Do you know the postcode of buyer 1's last settled accommodation?
PPOSTC1,Part 1 of postcode of buyer 1's last settled accommodation
PPOSTC2,Part 2 of postcode of buyer 1's last settled accommodation
PREVIOUSLAKNOWN,Do you know the local authority of buyer 1's last settled accommodation?
PREVLOC,The local authority code of buyer 1's last settled accommodation
PREVLOCNAME,The local authority name of buyer 1's last settled accommodation
PREGYRHA,Was the buyer registered with their PRP (HA)?
PREGOTHER,Was the buyer registered with another PRP (HA)?
PREGLA,Was the buyer registered with the local authority?
PREGGHB,Was the buyer registered with a Help to Buy agent?
PREGBLANK,Populated if pregyrha, pregother, pregla and pregghb are blank
BUY2LIVING,At the time of purchase, was buyer 2 living at the same address as buyer 1?
PREVTEN2,What was buyer 2's previous tenure?
HHREGRES,Have any of the buyers ever served as a regular in the UK armed forces?
HHREGRESSTILL,Is the buyer still serving in the UK armed forces?
ARMEDFORCESSPOUSE,Are any of the buyers a spouse or civil partner of a UK armed forces regular who died in service within the last 2 years?
DISABLED,Does anyone in the household consider themselves to have a disability?
WHEEL,Does anyone in the household use a wheelchair?
INC1NK,Is buyer 1's annual income known?
INCOME1,What is buyer 1's annual income?
INC1MORT,Was buyer 1's income used for a mortgage application?
INC2NK,Is buyer 1's annual income known?
INCOME2,What is buyer 2's annual income?
INC2MORT,Was buyer 2's income used for a mortgage application?
HB,Were the buyers receiving any of these housing-related benefits immediately before buying this property?
SAVINGSNK,Is the the total amount the buyers had in savings known?
SAVINGS,What is the total amount the buyers had in savings before they paid any deposit for the property?
PREVOWN,Have any of the buyers previously owned a property?
PREVSHARED,Was the previous property under shared ownership?
PROPLEN,How long did the buyer(s) live in the property before purchasing it?
STAIRCASE,Is this a staircasing transaction?
STAIRBOUGHT,What percentage of the property has been bought in this staircasing transaction?
STAIROWNED,What percentage of the property do the buyers now own in total?
STAIRCASETOSALE,Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?
RESALE,Is this a resale?
EXDAY,Day of the exchange of contracts
EXMONTH,Month of the exchange of contracts
EXYEAR,Year of the exchange of contracts
HODAY,Day of the practical completion or handover date
HOMONTH,Month of the practical completion or handover date
HOYEAR,Year of the practical completion or handover date
LANOMAGR,Was the household rehoused under a local authority nominations agreement?
SOCTEN,Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?
FROMBEDS,How many bedrooms did the buyer's previous property have?
FROMPROP,What was the previous property type?
SOCPREVTEN,What was the rent type of buyer's previous tenure?
VALUE,What is the full purchase price?
VALUE_VALUE_CHECK,Populated if a soft validation is confirmed.
EQUITY,What was the initial percentage equity stake purchased?
MORTGAGEUSED,Was a mortgage used to buy this property?
MORTGAGE,What is the mortgage amount?
MORTGAGELENDER,What is the name of the mortgage lender?
MORTGAGELENDEROTHER,If mortgagelender = 'Other', what is the name of the mortgage lender?
MORTLEN1,What is the length of the mortgage in years?
EXTRABOR,Does this include any extra borrowing?
DEPOSIT,How much was the cash deposit paid on the property?
CASHDIS,How much cash discount was given through Social Homebuy?
MRENT,What is the basic monthly rent?
HASMSCHARGE,Does the property have any monthly leasehold charges?
MSCHARGE,What are the total monthly leasehold charges for the property?
MSCHARGE_VALUE_CHECK,Populated if a soft validation is confirmed.
DISCOUNT,What was the percentage discount?
GRANT,What was the amount of any loan, grant, discount or subsidy given?
id,Log ID
status,Status of log
duplicate_set_id,ID of a set of duplicate logs
created_at,Time and date the log was created
updated_at,Time and date the log was last updated
old_form_id,The ID on the old service
collection_start_year,Year collection period opened
creation_method,Was the log submitted in-service or via bulk upload?
is_dpo,Is the user in the assigned_to column the data protection officer?
owning_organisation_name,Which organisation owned this property before the sale?
managing_organisation_name,Which organisation reported the sale?
assigned_to,User the log is assigned to
day,Day of sale completion date
month,Month of sale completion date
year,Year of sale completion date
purchid,What is the purchaser code?
ownershipsch,Was this purchase made through an ownership scheme?
type,What is the type of shared ownership/discounted ownership/outright sale?
othtype,If type = 'Other', what is the type of outright sale?
companybuy,Is the buyer a company?
buylivein,Will the buyer(s) live in the property?
jointpur,Is this a joint purchase?
jointmore,Are there more than 2 joint buyers of this property?
beds,How many bedrooms does the property have?
proptype,What type of unit is the property?
builtype,Which type of building is the property?
uprn,What is the UPRN of the property?
uprn_confirmed,We found an address that might be this property. Is this the property address?
address_line1_input,Address line 1 input from address matching feature
postcode_full_input,Postcode input from address matching feature
uprn_selection,UPRN of the address selected
address_line1,Address line 1
address_line2,Address line 2
town_or_city,Town/City
county,County
pcode1,Part 1 of the property's postcode
pcode2,Part 2 of the property's postcode
la,LA code
la_label,LA name
wchair,Is the property built or adapted to wheelchair-user standards?
noint,Did you interview the buyer to answer these questions?
privacynotice,Has the buyer seen the MHCLG privacy notice?
age1,What is buyer 1's age?
sex1,Which of these best describes buyer 1's gender identity?
ethnic_group,What is buyer 1's ethnic group?
ethnic,Which of the following best describes buyer 1's ethnic background?
nationality_all,What is buyer 1's nationality?
ecstat1,Which of these best describes buyer 1's working situation?
buy1livein,Will buyer 1 live in the property?
relat2,What is buyer 2 or person 2's relationship to buyer 1?
age2,What is buyer 2 or person 2's age?
sex2,Which of these best describes buyer 2 or person 2's gender identity?
ethnic_group2,What is buyer 2's ethnic group?
ethnicbuy2,Which of the following best describes buyer 2's ethnic background?
nationality_all_buyer2,What is buyer 2's nationality?
ecstat2,What is buyer 2 or person 2's working situation?
buy2livein,Will buyer 2 live in the property?
hholdcount,Besides the buyer(s), how many other people live or will live in the property?
relat3,What is person 3's relationship to buyer 1?
age3,What is person 3's age?
sex3,What is person 3's gender identity?
ecstat3,What is person 3's working situation?
relat4,What is person 4's relationship to buyer 1?
age4,What is person 4's age?
sex4,What is person 4's gender identity?
ecstat4,What is person 4's working situation?
relat5,What is person 5's relationship to buyer 1?
age5,What is person 5's age?
sex5,What is person 5's gender identity?
ecstat5,What is person 5's working situation?
relat6,What is person 6's relationship to buyer 1?
age6,What is person 6's age?
sex6,What is person 6's gender identity?
ecstat6,What is person 6's working situation?
prevten,What was buyer 1's previous tenure?
ppcodenk,Do you know the postcode of buyer 1's last settled accommodation?
ppostc1,Part 1 of postcode of buyer 1's last settled accommodation
ppostc2,Part 2 of postcode of buyer 1's last settled accommodation
previous_la_known,Do you know the local authority of buyer 1's last settled accommodation?
prevloc,The local authority code of buyer 1's last settled accommodation
prevloc_label,The local authority name of buyer 1's last settled accommodation
pregyrha,Was the buyer registered with their PRP (HA)?
pregother,Was the buyer registered with another PRP (HA)?
pregla,Was the buyer registered with the local authority?
pregghb,Was the buyer registered with a Help to Buy agent?
pregblank,Populated if pregyrha, pregother, pregla and pregghb are blank
buy2living,At the time of purchase, was buyer 2 living at the same address as buyer 1?
prevtenbuy2,What was buyer 2's previous tenure?
hhregres,Have any of the buyers ever served as a regular in the UK armed forces?
hhregresstill,Is the buyer still serving in the UK armed forces?
armedforcesspouse,Are any of the buyers a spouse or civil partner of a UK armed forces regular who died in service within the last 2 years?
disabled,Does anyone in the household consider themselves to have a disability?
wheel,Does anyone in the household use a wheelchair?
income1nk,Is buyer 1's annual income known?
income1,What is buyer 1's annual income?
inc1mort,Was buyer 1's income used for a mortgage application?
income2nk,Is buyer 2's annual income known?
income2,What is buyer 2's annual income?
inc2mort,Was buyer 2's income used for a mortgage application?
hb,Were the buyers receiving any of these housing-related benefits immediately before buying this property?
savingsnk,Is the the total amount the buyers had in savings known?
savings,What is the total amount the buyers had in savings before they paid any deposit for the property?
prevown,Have any of the buyers previously owned a property?
prevshared,Was the previous property under shared ownership?
proplen,How long did the buyer(s) live in the property before purchasing it?
staircase,Is this a staircasing transaction?
stairbought,What percentage of the property has been bought in this staircasing transaction?
stairowned,What percentage of the property do the buyers now own in total?
staircasesale,Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?
resale,Is this a resale?
exday,Day of the exchange of contracts
exmonth,Month of the exchange of contracts
exyear,Year of the exchange of contracts
hoday,Day of the practical completion or handover date
homonth,Month of the practical completion or handover date
hoyear,Year of the practical completion or handover date
lanomagr,Was the household rehoused under a local authority nominations agreement?
soctenant,Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?
frombeds,How many bedrooms did the buyer's previous property have?
fromprop,What was the previous property type?
socprevten,What was the rent type of buyer's previous tenure?
value,What is the full purchase price?
equity,What was the initial percentage equity stake purchased?
mortgageused,Was a mortgage used to buy this property?
mortgage,What is the mortgage amount?
mortgagelender,What is the name of the mortgage lender?
mortgagelenderother,If mortgagelender = 'Other', what is the name of the mortgage lender?
mortlen,What is the length of the mortgage in years?
extrabor,Does this include any extra borrowing?
deposit,How much was the cash deposit paid on the property?
cashdis,How much cash discount was given through Social Homebuy?
mrent,What is the basic monthly rent?
has_mscharge,Does the property have any monthly leasehold charges?
mscharge,What are the total monthly leasehold charges for the property?
discount,What was the percentage discount?
grant,What was the amount of any loan, grant, discount or subsidy given?
sexrab1,What was buyer 1's sex at birth?
sexrab2,What was buyer/person 2's sex at birth?
sexrab3,What was person 3's sex at birth?
sexrab4,What was person 4's sex at birth?
sexrab5,What was person 5's sex at birth?
sexrab6,What was person 6's sex at birth?
Can't render this file because it has a wrong number of fields in line 20.

5
spec/lib/tasks/log_variable_definitions_spec.rb

@ -6,6 +6,7 @@ RSpec.describe "log_variable_definitions" do
subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
let(:path) { "spec/fixtures/variable_definitions" }
let(:total_variable_definitions_count) { 423 }
before do
Rake.application.rake_require("tasks/log_variable_definitions")
@ -14,7 +15,7 @@ RSpec.describe "log_variable_definitions" do
end
it "adds CsvVariableDefinition records from each file in the specified directory" do
expect { task.invoke(path) }.to change(CsvVariableDefinition, :count).by(417)
expect { task.invoke(path) }.to change(CsvVariableDefinition, :count).by(total_variable_definitions_count)
end
it "handles an empty directory without errors" do
@ -34,7 +35,7 @@ RSpec.describe "log_variable_definitions" do
task.invoke(path)
second_run_count = CsvVariableDefinition.count
expect(first_run_count).to eq(initial_count + 417)
expect(first_run_count).to eq(initial_count + total_variable_definitions_count)
expect(second_run_count).to eq(first_run_count)
end
end

104
spec/models/form/sales/pages/person_sex_registered_at_birth_spec.rb

@ -0,0 +1,104 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PersonSexRegisteredAtBirth, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection, person_index:) }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2026, 4, 1))) }
let(:person_index) { 1 }
let(:page_id) { "person_2_sex_registered_at_birth" }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has the correct description" do
expect(page.description).to be_nil
end
context "with person 2" do
let(:person_index) { 2 }
let(:page_id) { "person_2_sex_registered_at_birth" }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab2])
end
it "has the correct id" do
expect(page.id).to eq("person_2_sex_registered_at_birth")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "details_known_2" => 1 }])
end
end
context "with person 3" do
let(:person_index) { 3 }
let(:page_id) { "person_3_sex_registered_at_birth" }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab3])
end
it "has the correct id" do
expect(page.id).to eq("person_3_sex_registered_at_birth")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "details_known_3" => 1 }])
end
end
context "with person 4" do
let(:person_index) { 4 }
let(:page_id) { "person_4_sex_registered_at_birth" }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab4])
end
it "has the correct id" do
expect(page.id).to eq("person_4_sex_registered_at_birth")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "details_known_4" => 1 }])
end
end
context "with person 5" do
let(:person_index) { 5 }
let(:page_id) { "person_5_sex_registered_at_birth" }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab5])
end
it "has the correct id" do
expect(page.id).to eq("person_5_sex_registered_at_birth")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "details_known_5" => 1 }])
end
end
context "with person 6" do
let(:person_index) { 6 }
let(:page_id) { "person_6_sex_registered_at_birth" }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab6])
end
it "has the correct id" do
expect(page.id).to eq("person_6_sex_registered_at_birth")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "details_known_6" => 1 }])
end
end
end

29
spec/models/form/sales/pages/sex_registered_at_birth1_spec.rb

@ -0,0 +1,29 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::SexRegisteredAtBirth1, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2026, 4, 1))) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab1])
end
it "has the correct id" do
expect(page.id).to eq("buyer_1_sex_registered_at_birth")
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "buyer_has_seen_privacy_notice?" => true }, { "buyer_not_interviewed?" => true }])
end
end

38
spec/models/form/sales/pages/sex_registered_at_birth2_spec.rb

@ -0,0 +1,38 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::SexRegisteredAtBirth2, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2026, 4, 1))) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[sexrab2])
end
it "has the correct id" do
expect(page.id).to eq("buyer_2_sex_registered_at_birth")
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has correct depends_on" do
expect(page.depends_on).to eq([
{
"joint_purchase?" => true,
"buyer_has_seen_privacy_notice?" => true,
},
{
"joint_purchase?" => true,
"buyer_not_interviewed?" => true,
},
])
end
end

123
spec/models/form/sales/questions/person_sex_registered_at_birth_spec.rb

@ -0,0 +1,123 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::PersonSexRegisteredAtBirth, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page, person_index:) }
let(:question_id) { "sexrab2" }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:person_index) { 2 }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form, start_date: Time.zone.local(2026, 4, 1)) }
before do
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(form)
end
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?(nil)).to be false
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Person prefers not to say" },
})
end
context "when person 2" do
let(:question_id) { "sexrab2" }
let(:person_index) { 2 }
it "has the correct id" do
expect(question.id).to eq("sexrab2")
end
it "has expected check answers card number" do
expect(question.check_answers_card_number).to eq(2)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end
context "when person 3" do
let(:question_id) { "sexrab3" }
let(:person_index) { 3 }
it "has the correct id" do
expect(question.id).to eq("sexrab3")
end
it "has expected check answers card number" do
expect(question.check_answers_card_number).to eq(3)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end
context "when person 4" do
let(:question_id) { "sexrab4" }
let(:person_index) { 4 }
it "has the correct id" do
expect(question.id).to eq("sexrab4")
end
it "has expected check answers card number" do
expect(question.check_answers_card_number).to eq(4)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end
context "when person 5" do
let(:question_id) { "sexrab5" }
let(:person_index) { 5 }
it "has the correct id" do
expect(question.id).to eq("sexrab5")
end
it "has expected check answers card number" do
expect(question.check_answers_card_number).to eq(5)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end
context "when person 6" do
let(:question_id) { "sexrab6" }
let(:person_index) { 6 }
it "has the correct id" do
expect(question.id).to eq("sexrab6")
end
it "has expected check answers card number" do
expect(question.check_answers_card_number).to eq(6)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end
end

49
spec/models/form/sales/questions/sex_registered_at_birth1_spec.rb

@ -0,0 +1,49 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::SexRegisteredAtBirth1, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form, start_date: Time.zone.local(2026, 4, 1)) }
before do
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(form)
end
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("sexrab1")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?(nil)).to be false
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Buyer prefers not to say" },
})
end
it "has the correct check_answers_card_number" do
expect(question.check_answers_card_number).to eq(1)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end

49
spec/models/form/sales/questions/sex_registered_at_birth2_spec.rb

@ -0,0 +1,49 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::SexRegisteredAtBirth2, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form, start_date: Time.zone.local(2026, 4, 1)) }
before do
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(form)
end
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("sexrab2")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?(nil)).to be false
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"divider" => { "value" => true },
"R" => { "value" => "Buyer prefers not to say" },
})
end
it "has the correct check_answers_card_number" do
expect(question.check_answers_card_number).to eq(2)
end
it "has the correct inferred_check_answers_value" do
expect(question.inferred_check_answers_value).to be_nil
end
end

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

@ -21,6 +21,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
allow(form).to receive(:start_date).and_return(Time.zone.local(2023, 4, 1))
allow(form).to receive(:start_year_2024_or_later?).and_return(false)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end
it "has correct pages" do
@ -130,6 +131,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
allow(form).to receive(:start_date).and_return(Time.zone.local(2024, 4, 1))
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(false)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end
it "has correct depends on" do
@ -284,6 +286,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
allow(form).to receive(:start_date).and_return(Time.zone.local(2025, 4, 1))
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end
it "has correct pages" do
@ -398,4 +401,132 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }])
end
end
context "with 2026/27 form" do
before do
allow(form).to receive(:start_date).and_return(Time.zone.local(2026, 4, 1))
allow(form).to receive(:start_year_2024_or_later?).and_return(true)
allow(form).to receive(:start_year_2025_or_later?).and_return(true)
allow(form).to receive(:start_year_2026_or_later?).and_return(true)
end
it "has correct pages" do
expect(household_characteristics.pages.map(&:id)).to eq(
%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_sex_registered_at_birth
buyer_1_gender_identity
buyer_1_ethnic_group
buyer_1_ethnic_background_black
buyer_1_ethnic_background_asian
buyer_1_ethnic_background_arab
buyer_1_ethnic_background_mixed
buyer_1_ethnic_background_white
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_value_check
buyer_1_live_in_property
buyer_1_live_in_property_value_check
buyer_2_relationship_to_buyer_1
buyer_2_age
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_sex_registered_at_birth
buyer_2_gender_identity
buyer_2_ethnic_group
buyer_2_ethnic_background_black
buyer_2_ethnic_background_asian
buyer_2_ethnic_background_arab
buyer_2_ethnic_background_mixed
buyer_2_ethnic_background_white
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_value_check
buyer_2_live_in_property
buyer_2_live_in_property_value_check
number_of_others_in_property
number_of_others_in_property_joint_purchase
person_2_known
person_2_relationship_to_buyer_1
relationship_2_partner_under_16_value_check
relationship_2_multiple_partners_value_check
person_2_age
age_2_retirement_value_check
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
person_3_known
person_3_relationship_to_buyer_1
relationship_3_partner_under_16_value_check
relationship_3_multiple_partners_value_check
person_3_age
age_3_retirement_value_check
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
person_4_known
person_4_relationship_to_buyer_1
relationship_4_partner_under_16_value_check
relationship_4_multiple_partners_value_check
person_4_age
age_4_retirement_value_check
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
person_5_known
person_5_relationship_to_buyer_1
relationship_5_partner_under_16_value_check
relationship_5_multiple_partners_value_check
person_5_age
age_5_retirement_value_check
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
person_6_known
person_6_relationship_to_buyer_1
relationship_6_partner_under_16_value_check
relationship_6_multiple_partners_value_check
person_6_age
age_6_retirement_value_check
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
],
)
end
it "has correct depends on" do
expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }])
end
end
end

2
spec/services/bulk_upload/sales/validator_spec.rb

@ -11,7 +11,7 @@ RSpec.describe BulkUpload::Sales::Validator do
let(:organisation) { create(:organisation, old_visible_id: "123") }
let(:log) { build(:sales_log, :completed, assigned_to: user) }
let(:log_to_csv) { BulkUpload::SalesLogToCsv.new(log:) }
let(:bulk_upload) { create(:bulk_upload, user:, year: log.collection_start_year) }
let(:bulk_upload) { create(:bulk_upload, :sales, user:, year:) }
let(:path) { file.path }
let(:file) { Tempfile.new }

8
spec/services/bulk_upload/sales/year2026/csv_parser_spec.rb

@ -15,8 +15,8 @@ RSpec.describe BulkUpload::Sales::Year2026::CsvParser do
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025))
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2026))
file.write("\n")
file.rewind
end
@ -81,8 +81,8 @@ RSpec.describe BulkUpload::Sales::Year2026::CsvParser do
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025, seed:))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025, seed:))
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2026, seed:))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2026, seed:))
file.rewind
end

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

@ -112,6 +112,12 @@ 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",
}
end
@ -1432,7 +1438,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
let(:attributes) { { bulk_upload: } }
before do
build(:sales_log, owning_organisation: nil, saledate: nil, purchid: nil, age1: nil, sex1: nil, ecstat1: nil).save(validate: false)
build(:sales_log, owning_organisation: nil, saledate: nil, purchid: nil, age1: nil, sexrab1: nil, sex1: nil, ecstat1: nil).save(validate: false)
end
it "does not add duplicate logs validation to the blank row" do

4
spec/services/imports/variable_definitions_service_spec.rb

@ -13,13 +13,13 @@ RSpec.describe Imports::VariableDefinitionsService, type: :service do
describe "#call" do
before do
allow(Dir).to receive(:glob).and_return(%w[lettings_download_23_24.csv lettings_download_24_25.csv sales_download_23_24.csv sales_download_24_25.csv])
allow(Dir).to receive(:glob).and_return(%w[lettings_download_23_24.csv lettings_download_24_25.csv sales_download_23_24.csv sales_download_24_25.csv sales_download_26_27.csv])
allow(service).to receive(:process_file)
end
it "processes each file in the directory" do
service.call
%w[lettings_download_23_24.csv lettings_download_24_25.csv sales_download_23_24.csv sales_download_24_25.csv].each do |file|
%w[lettings_download_23_24.csv lettings_download_24_25.csv sales_download_23_24.csv sales_download_24_25.csv sales_download_26_27.csv].each do |file|
expect(service).to have_received(:process_file).with(file)
end
end

Loading…
Cancel
Save