Browse Source

Merge branch 'CLDC-4173-new-building-height-question-sales' into CLDC-4178-add-gender-same-as-sex-question-for-sales

pull/3188/head
Nat Dean-Lewis 2 months ago
parent
commit
5a32d6c348
  1. 7
      app/helpers/bulk_upload/sales_log_to_csv.rb
  2. 17
      app/models/form/sales/pages/building_height_class.rb
  3. 17
      app/models/form/sales/questions/building_height_class.rb
  4. 1
      app/models/form/sales/subsections/property_information.rb
  5. 4
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  6. 104
      app/services/bulk_upload/sales/year2026/row_parser.rb
  7. 6
      app/services/csv/sales_log_csv_service.rb
  8. 2
      app/services/exports/sales_log_export_constants.rb
  9. 7
      config/locales/forms/2026/sales/property_information.en.yml
  10. 5
      db/migrate/20260219093257_add_buildheightclass_to_sales_logs.rb
  11. 1
      db/schema.rb
  12. 2
      spec/factories/sales_log.rb
  13. 154
      spec/fixtures/exports/sales_log_25_26.xml
  14. 161
      spec/fixtures/exports/sales_log_26_27.xml
  15. 18
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  16. 3
      spec/fixtures/files/sales_logs_csv_export_codes_26.csv
  17. 3
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  18. 1
      spec/fixtures/variable_definitions/sales_download_26_27.csv
  19. 2
      spec/lib/tasks/log_variable_definitions_spec.rb
  20. 34
      spec/models/form/sales/pages/building_height_class_spec.rb
  21. 37
      spec/models/form/sales/questions/building_height_class_spec.rb
  22. 35
      spec/models/form/sales/subsections/property_information_spec.rb
  23. 3
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  24. 93
      spec/services/csv/sales_log_csv_service_spec.rb
  25. 64
      spec/services/exports/sales_log_export_service_spec.rb

7
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -674,10 +674,11 @@ class BulkUpload::SalesLogToCsv
log.sexrab4, log.sexrab4,
log.sexrab5, log.sexrab5,
log.sexrab6, log.sexrab6,
log.buildheightclass,
log.gender_same_as_sex1, log.gender_same_as_sex1,
log.gender_description1, log.gender_description1, # 130
log.gender_same_as_sex2, # 130
log.gender_same_as_sex2,
log.gender_description2, log.gender_description2,
log.gender_same_as_sex3, log.gender_same_as_sex3,
log.gender_description3, log.gender_description3,
@ -686,7 +687,7 @@ class BulkUpload::SalesLogToCsv
log.gender_same_as_sex5, log.gender_same_as_sex5,
log.gender_description5, log.gender_description5,
log.gender_same_as_sex6, log.gender_same_as_sex6,
log.gender_description6, # 139 log.gender_description6, # 140
] ]
end end

17
app/models/form/sales/pages/building_height_class.rb

@ -0,0 +1,17 @@
class Form::Sales::Pages::BuildingHeightClass < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "building_height_class"
@depends_on = [
{ "proptype" => 1 },
{ "proptype" => 2 },
{ "proptype" => 9 },
]
end
def questions
@questions ||= [
Form::Sales::Questions::BuildingHeightClass.new(nil, nil, self),
]
end
end

17
app/models/form/sales/questions/building_height_class.rb

@ -0,0 +1,17 @@
class Form::Sales::Questions::BuildingHeightClass < ::Form::Question
def initialize(id, hsh, page)
super
@id = "buildheightclass"
@type = "radio"
@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 = {
"1" => { "value" => "High-rise" },
"2" => { "value" => "Low-rise" },
"3" => { "value" => "Don't know" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2026 => 17 }.freeze
end

1
app/models/form/sales/subsections/property_information.rb

@ -10,6 +10,7 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection
@pages ||= [ @pages ||= [
(uprn_questions if form.start_date.year >= 2024), (uprn_questions if form.start_date.year >= 2024),
(Form::Sales::Pages::PropertyUnitType.new(nil, nil, self) if form.start_year_2025_or_later?), (Form::Sales::Pages::PropertyUnitType.new(nil, nil, self) if form.start_year_2025_or_later?),
(Form::Sales::Pages::BuildingHeightClass.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::PropertyNumberOfBedrooms.new(nil, nil, self), Form::Sales::Pages::PropertyNumberOfBedrooms.new(nil, nil, self),
Form::Sales::Pages::AboutPriceValueCheck.new("about_price_bedrooms_value_check", nil, self), Form::Sales::Pages::AboutPriceValueCheck.new("about_price_bedrooms_value_check", nil, self),
(Form::Sales::Pages::PropertyUnitType.new(nil, nil, self) unless form.start_year_2025_or_later?), (Form::Sales::Pages::PropertyUnitType.new(nil, nil, self) unless form.start_year_2025_or_later?),

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

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

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

@ -142,19 +142,20 @@ class BulkUpload::Sales::Year2026::RowParser
field_125: "Person 4'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_126: "Person 5's sex, as registered at birth",
field_127: "Person 6'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_129: "Is the gender buyer 1 identifies with the same as their sex registered at birth?",
field_130: "If 'No', enter buyer 1's gender identity",
field_131: "Is the gender buyer/person 2 identifies with the same as their sex registered at birth?",
field_132: "If 'No', enter buyer/person 2's gender identity",
field_133: "Is the gender person 3 identifies with the same as their sex registered at birth?",
field_134: "If 'No', enter person 3's gender identity",
field_135: "Is the gender person 4 identifies with the same as their sex registered at birth?",
field_136: "If 'No', enter person 4's gender identity",
field_137: "Is the gender person 5 identifies with the same as their sex registered at birth?",
field_138: "If 'No', enter person 5's gender identity",
field_139: "Is the gender person 6 identifies with the same as their sex registered at birth?",
field_140: "If 'No', enter person 6's gender identity",
field_128: "Is the gender buyer 1 identifies with the same as their sex registered at birth?",
field_129: "If 'No', enter buyer 1's gender identity",
field_130: "Is the gender buyer/person 2 identifies with the same as their sex registered at birth?",
field_131: "If 'No', enter buyer/person 2's gender identity",
field_132: "Is the gender person 3 identifies with the same as their sex registered at birth?",
field_133: "If 'No', enter person 3's gender identity",
field_134: "Is the gender person 4 identifies with the same as their sex registered at birth?",
field_135: "If 'No', enter person 4's gender identity",
field_136: "Is the gender person 5 identifies with the same as their sex registered at birth?",
field_137: "If 'No', enter person 5's gender identity",
field_138: "Is the gender person 6 identifies with the same as their sex registered at birth?",
field_139: "If 'No', enter person 6's gender identity",
}.freeze }.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
@ -301,19 +302,20 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_125, :string attribute :field_125, :string
attribute :field_126, :string attribute :field_126, :string
attribute :field_127, :string attribute :field_127, :string
attribute :field_128, :integer attribute :field_128, :integer
attribute :field_129, :string
attribute :field_130, :integer attribute :field_129, :integer
attribute :field_131, :string attribute :field_130, :string
attribute :field_132, :integer attribute :field_131, :integer
attribute :field_133, :string attribute :field_132, :string
attribute :field_134, :integer attribute :field_133, :integer
attribute :field_135, :string attribute :field_134, :string
attribute :field_136, :integer attribute :field_135, :integer
attribute :field_137, :string attribute :field_136, :string
attribute :field_138, :integer attribute :field_137, :integer
attribute :field_139, :string attribute :field_138, :string
attribute :field_139, :integer
attribute :field_140, :string
validates :field_1, validates :field_1,
presence: { presence: {
@ -827,19 +829,20 @@ private
sexrab4: %i[field_125], sexrab4: %i[field_125],
sexrab5: %i[field_126], sexrab5: %i[field_126],
sexrab6: %i[field_127], sexrab6: %i[field_127],
buildheightclass: %i[field_128],
gender_same_as_sex1: %i[field_128],
gender_description1: %i[field_129], gender_same_as_sex1: %i[field_129],
gender_same_as_sex2: %i[field_130], gender_description1: %i[field_130],
gender_description2: %i[field_131], gender_same_as_sex2: %i[field_131],
gender_same_as_sex3: %i[field_132], gender_description2: %i[field_132],
gender_description3: %i[field_133], gender_same_as_sex3: %i[field_133],
gender_same_as_sex4: %i[field_134], gender_description3: %i[field_134],
gender_description4: %i[field_135], gender_same_as_sex4: %i[field_135],
gender_same_as_sex5: %i[field_136], gender_description4: %i[field_136],
gender_description5: %i[field_137], gender_same_as_sex5: %i[field_137],
gender_same_as_sex6: %i[field_138], gender_description5: %i[field_138],
gender_description6: %i[field_139], gender_same_as_sex6: %i[field_139],
gender_description6: %i[field_140],
} }
end end
@ -881,19 +884,20 @@ private
attributes["sexrab4"] = field_125 attributes["sexrab4"] = field_125
attributes["sexrab5"] = field_126 attributes["sexrab5"] = field_126
attributes["sexrab6"] = field_127 attributes["sexrab6"] = field_127
attributes["buildheightclass"] = field_128
attributes["gender_same_as_sex1"] = field_128
attributes["gender_description1"] = field_129 attributes["gender_same_as_sex1"] = field_129
attributes["gender_same_as_sex2"] = field_130 attributes["gender_description1"] = field_130
attributes["gender_description2"] = field_131 attributes["gender_same_as_sex2"] = field_132
attributes["gender_same_as_sex3"] = field_132 attributes["gender_description2"] = field_132
attributes["gender_description3"] = field_133 attributes["gender_same_as_sex3"] = field_133
attributes["gender_same_as_sex4"] = field_134 attributes["gender_description3"] = field_134
attributes["gender_description4"] = field_135 attributes["gender_same_as_sex4"] = field_135
attributes["gender_same_as_sex5"] = field_136 attributes["gender_description4"] = field_136
attributes["gender_description5"] = field_137 attributes["gender_same_as_sex5"] = field_137
attributes["gender_same_as_sex6"] = field_138 attributes["gender_description5"] = field_138
attributes["gender_description6"] = field_139 attributes["gender_same_as_sex6"] = field_139
attributes["gender_description6"] = field_140
attributes["relat2"] = relationship_from_is_partner(field_34) attributes["relat2"] = relationship_from_is_partner(field_34)
attributes["relat3"] = relationship_from_is_partner(field_42) attributes["relat3"] = relationship_from_is_partner(field_42)

6
app/services/csv/sales_log_csv_service.rb

@ -250,7 +250,7 @@ module Csv
return @attributes unless @user.support? return @attributes unless @user.support?
mappings = SUPPORT_ATTRIBUTE_NAME_MAPPINGS mappings = SUPPORT_ATTRIBUTE_NAME_MAPPINGS
mappings = mappings.merge(SUPPORT_ATTRIBUTE_NAME_MAPPINGS_2025) if @year == 2025 mappings = mappings.merge(SUPPORT_ATTRIBUTE_NAME_MAPPINGS_2025) if @year >= 2025
@attributes.map do |attribute| @attributes.map do |attribute|
mappings[attribute] || attribute.upcase mappings[attribute] || attribute.upcase
@ -301,7 +301,7 @@ module Csv
mappings = case @year mappings = case @year
when 2024 when 2024
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024) ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024)
when 2025 when (2025..)
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024).merge(ATTRIBUTE_MAPPINGS_2025) ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024).merge(ATTRIBUTE_MAPPINGS_2025)
else else
ATTRIBUTE_MAPPINGS ATTRIBUTE_MAPPINGS
@ -347,7 +347,7 @@ module Csv
%w[id status duplicate_set_id created_at updated_at old_form_id collection_start_year creation_method is_dpo] %w[id status duplicate_set_id created_at updated_at old_form_id collection_start_year creation_method is_dpo]
when 2024 when 2024
%w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo] %w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo]
when 2025 when 2025, 2026
%w[id status duplicate_set_id created_at created_by_id updated_at updated_by_id creation_method bulk_upload_id] %w[id status duplicate_set_id created_at created_by_id updated_at updated_by_id creation_method bulk_upload_id]
else else
%w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo] %w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo]

2
app/services/exports/sales_log_export_constants.rb

@ -146,7 +146,7 @@ module Exports::SalesLogExportConstants
ALL_YEAR_EXPORT_FIELDS << "RELAT#{index}" ALL_YEAR_EXPORT_FIELDS << "RELAT#{index}"
end end
YEAR_2026_EXPORT_FIELDS = Set[] YEAR_2026_EXPORT_FIELDS = Set["BUILDHEIGHTCLASS"]
(1..6).each do |index| (1..6).each do |index|
YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}" YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}"

7
config/locales/forms/2026/sales/property_information.en.yml

@ -59,6 +59,13 @@ en:
hint_text: "" hint_text: ""
question_text: "What type of unit is the property?" question_text: "What type of unit is the property?"
buildheightclass:
page_header: ""
check_answer_label: "Building height classification"
check_answer_prompt: ""
hint_text: "High-rise residential buildings are those containing 2 or more residential units and either have 7 or more storeys or are at least 18 metres in height. If unsure, answer based on the number of storeys."
question_text: "What is the building height classification?"
builtype: builtype:
page_header: "" page_header: ""
check_answer_label: "Type of building" check_answer_label: "Type of building"

5
db/migrate/20260219093257_add_buildheightclass_to_sales_logs.rb

@ -0,0 +1,5 @@
class AddBuildheightclassToSalesLogs < ActiveRecord::Migration[7.2]
def change
add_column :sales_logs, :buildheightclass, :integer
end
end

1
db/schema.rb

@ -821,6 +821,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_20_141000) do
t.string "sexrab4" t.string "sexrab4"
t.string "sexrab5" t.string "sexrab5"
t.string "sexrab6" t.string "sexrab6"
t.integer "buildheightclass"
t.integer "gender_same_as_sex1" t.integer "gender_same_as_sex1"
t.integer "gender_same_as_sex2" t.integer "gender_same_as_sex2"
t.integer "gender_same_as_sex3" t.integer "gender_same_as_sex3"

2
spec/factories/sales_log.rb

@ -91,6 +91,7 @@ FactoryBot.define do
buy1livein { 1 } buy1livein { 1 }
relat2 { "P" } relat2 { "P" }
proptype { 1 } proptype { 1 }
buildheightclass { 2 }
age2_known { 0 } age2_known { 0 }
age2 { Faker::Number.within(range: 25..45) } age2 { Faker::Number.within(range: 25..45) }
builtype { 1 } builtype { 1 }
@ -301,6 +302,7 @@ FactoryBot.define do
buy1livein { 1 } buy1livein { 1 }
relat2 { "P" } relat2 { "P" }
proptype { 1 } proptype { 1 }
buildheightclass { 2 }
age2_known { 0 } age2_known { 0 }
age2 { 33 } age2 { 33 }
builtype { 1 } builtype { 1 }

154
spec/fixtures/exports/sales_log_25_26.xml vendored

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<ID>{id}</ID>
<STATUS>1</STATUS>
<PURCHID>123</PURCHID>
<TYPE>8</TYPE>
<JOINTMORE>1</JOINTMORE>
<BEDS>2</BEDS>
<AGE1>27</AGE1>
<SEX1>F</SEX1>
<ETHNIC>17</ETHNIC>
<BUILTYPE>1</BUILTYPE>
<PROPTYPE>1</PROPTYPE>
<AGE2>33</AGE2>
<RELAT2>P</RELAT2>
<SEX2>X</SEX2>
<NOINT>2</NOINT>
<ECSTAT2>1</ECSTAT2>
<PRIVACYNOTICE>1</PRIVACYNOTICE>
<ECSTAT1>1</ECSTAT1>
<WHEEL>1</WHEEL>
<HHOLDCOUNT>4</HHOLDCOUNT>
<AGE3>14</AGE3>
<LA>E09000033</LA>
<INCOME1>10000</INCOME1>
<AGE4>18</AGE4>
<AGE5>40</AGE5>
<AGE6>40</AGE6>
<INC1MORT>1</INC1MORT>
<INCOME2>10000</INCOME2>
<SAVINGSNK>1</SAVINGSNK>
<SAVINGS/>
<PREVOWN>1</PREVOWN>
<SEX3>F</SEX3>
<MORTGAGE>20000.0</MORTGAGE>
<INC2MORT>1</INC2MORT>
<ECSTAT3>9</ECSTAT3>
<ECSTAT4>3</ECSTAT4>
<ECSTAT5>2</ECSTAT5>
<ECSTAT6>1</ECSTAT6>
<RELAT3>X</RELAT3>
<RELAT4>X</RELAT4>
<RELAT5>R</RELAT5>
<RELAT6>R</RELAT6>
<HB>4</HB>
<SEX4>X</SEX4>
<SEX5>M</SEX5>
<SEX6>X</SEX6>
<FROMBEDS/>
<STAIRCASE/>
<STAIRBOUGHT/>
<STAIROWNED/>
<MRENT/>
<RESALE/>
<DEPOSIT>80000.0</DEPOSIT>
<CASHDIS/>
<DISABLED>1</DISABLED>
<VALUE>110000.0</VALUE>
<EQUITY/>
<DISCOUNT/>
<GRANT>10000.0</GRANT>
<PPCODENK>0</PPCODENK>
<PPOSTC1>SW1A</PPOSTC1>
<PPOSTC2>1AA</PPOSTC2>
<PREVLOC>E09000033</PREVLOC>
<HHREGRES>7</HHREGRES>
<HHREGRESSTILL/>
<PROPLEN/>
<MSCHARGE>100.0</MSCHARGE>
<PREVTEN>1</PREVTEN>
<MORTGAGEUSED>1</MORTGAGEUSED>
<WCHAIR>1</WCHAIR>
<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>
<HODAY/>
<HOMONTH/>
<HOYEAR/>
<FROMPROP/>
<SOCPREVTEN/>
<EXTRABOR>1</EXTRABOR>
<HHTYPE>6</HHTYPE>
<VALUE_VALUE_CHECK/>
<PREVSHARED>2</PREVSHARED>
<BUY2LIVING>3</BUY2LIVING>
<UPRN/>
<COUNTY/>
<ADDRESS_SEARCH_VALUE_CHECK/>
<FIRSTSTAIR/>
<NUMSTAIR/>
<MRENTPRESTAIRCASING/>
<DAY>1</DAY>
<MONTH>4</MONTH>
<YEAR>2025</YEAR>
<CREATEDDATE>2025-04-01T00:00:00+01:00</CREATEDDATE>
<CREATEDBY>{created_by_email}</CREATEDBY>
<CREATEDBYID>{created_by_id}</CREATEDBYID>
<USERNAME>{assigned_to_email}</USERNAME>
<USERNAMEID>{assigned_to_id}</USERNAMEID>
<UPLOADDATE>2025-04-01T00:00:00+01:00</UPLOADDATE>
<AMENDEDBY/>
<AMENDEDBYID/>
<OWNINGORGID>{owning_org_id}</OWNINGORGID>
<OWNINGORGNAME>{owning_org_name}</OWNINGORGNAME>
<MANINGORGID>{managing_org_id}</MANINGORGID>
<MANINGORGNAME>{managing_org_name}</MANINGORGNAME>
<CREATIONMETHOD>1</CREATIONMETHOD>
<BULKUPLOADID/>
<COLLECTIONYEAR>2025</COLLECTIONYEAR>
<OWNERSHIP>2</OWNERSHIP>
<JOINT>1</JOINT>
<ETHNICGROUP1>17</ETHNICGROUP1>
<ETHNICGROUP2>17</ETHNICGROUP2>
<PREVIOUSLAKNOWN>1</PREVIOUSLAKNOWN>
<HASMSCHARGE>1</HASMSCHARGE>
<HASSERVICECHARGES/>
<SERVICECHARGES/>
<INC1NK>0</INC1NK>
<INC2NK>0</INC2NK>
<POSTCODE>SW1A 1AA</POSTCODE>
<ISLAINFERRED>true</ISLAINFERRED>
<MORTLEN1>10</MORTLEN1>
<ETHNIC2/>
<PREVTEN2/>
<ADDRESS1>Address line 1</ADDRESS1>
<ADDRESS2/>
<TOWNCITY>City</TOWNCITY>
<LANAME>Westminster</LANAME>
<ADDRESS1INPUT>Address line 1</ADDRESS1INPUT>
<POSTCODEINPUT>SW1A 1AA</POSTCODEINPUT>
<UPRNSELECTED/>
<BULKADDRESS1/>
<BULKADDRESS2/>
<BULKTOWNCITY/>
<BULKCOUNTY/>
<BULKPOSTCODE/>
<BULKLA/>
<NATIONALITYALL1>826</NATIONALITYALL1>
<NATIONALITYALL2>826</NATIONALITYALL2>
<PREVLOCNAME>Westminster</PREVLOCNAME>
<LIVEINBUYER1>1</LIVEINBUYER1>
<LIVEINBUYER2>1</LIVEINBUYER2>
<HASESTATEFEE/>
<ESTATEFEE/>
<STAIRLASTDAY/>
<STAIRLASTMONTH/>
<STAIRLASTYEAR/>
<STAIRINITIALDAY/>
<STAIRINITIALMONTH/>
<STAIRINITIALYEAR/>
<MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/>
<STAIRCASETOSALE/>
</form>
</forms>

161
spec/fixtures/exports/sales_log_26_27.xml vendored

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<forms>
<form>
<ID>{id}</ID>
<STATUS>1</STATUS>
<PURCHID>123</PURCHID>
<TYPE>8</TYPE>
<JOINTMORE>1</JOINTMORE>
<BEDS>2</BEDS>
<AGE1>27</AGE1>
<SEX1>F</SEX1>
<ETHNIC>17</ETHNIC>
<BUILTYPE>1</BUILTYPE>
<PROPTYPE>1</PROPTYPE>
<AGE2>33</AGE2>
<RELAT2>P</RELAT2>
<SEX2>X</SEX2>
<NOINT>2</NOINT>
<ECSTAT2>1</ECSTAT2>
<PRIVACYNOTICE>1</PRIVACYNOTICE>
<ECSTAT1>1</ECSTAT1>
<WHEEL>1</WHEEL>
<HHOLDCOUNT>4</HHOLDCOUNT>
<AGE3>14</AGE3>
<LA>E09000033</LA>
<INCOME1>10000</INCOME1>
<AGE4>18</AGE4>
<AGE5>40</AGE5>
<AGE6>40</AGE6>
<INC1MORT>1</INC1MORT>
<INCOME2>10000</INCOME2>
<SAVINGSNK>1</SAVINGSNK>
<SAVINGS/>
<PREVOWN>1</PREVOWN>
<SEX3>F</SEX3>
<MORTGAGE>20000.0</MORTGAGE>
<INC2MORT>1</INC2MORT>
<ECSTAT3>9</ECSTAT3>
<ECSTAT4>3</ECSTAT4>
<ECSTAT5>2</ECSTAT5>
<ECSTAT6>1</ECSTAT6>
<RELAT3>X</RELAT3>
<RELAT4>X</RELAT4>
<RELAT5>R</RELAT5>
<RELAT6>R</RELAT6>
<HB>4</HB>
<SEX4>X</SEX4>
<SEX5>M</SEX5>
<SEX6>X</SEX6>
<FROMBEDS/>
<STAIRCASE/>
<STAIRBOUGHT/>
<STAIROWNED/>
<MRENT/>
<RESALE/>
<DEPOSIT>80000.0</DEPOSIT>
<CASHDIS/>
<DISABLED>1</DISABLED>
<VALUE>110000.0</VALUE>
<EQUITY/>
<DISCOUNT/>
<GRANT>10000.0</GRANT>
<PPCODENK>0</PPCODENK>
<PPOSTC1>SW1A</PPOSTC1>
<PPOSTC2>1AA</PPOSTC2>
<PREVLOC>E09000033</PREVLOC>
<HHREGRES>7</HHREGRES>
<HHREGRESSTILL/>
<PROPLEN/>
<MSCHARGE>100.0</MSCHARGE>
<PREVTEN>1</PREVTEN>
<MORTGAGEUSED>1</MORTGAGEUSED>
<WCHAIR>1</WCHAIR>
<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>
<HODAY/>
<HOMONTH/>
<HOYEAR/>
<FROMPROP/>
<SOCPREVTEN/>
<EXTRABOR>1</EXTRABOR>
<HHTYPE>6</HHTYPE>
<VALUE_VALUE_CHECK/>
<PREVSHARED>2</PREVSHARED>
<BUY2LIVING>3</BUY2LIVING>
<UPRN/>
<COUNTY/>
<ADDRESS_SEARCH_VALUE_CHECK/>
<FIRSTSTAIR/>
<NUMSTAIR/>
<MRENTPRESTAIRCASING/>
<SEXRAB1>F</SEXRAB1>
<SEXRAB2/>
<SEXRAB3>F</SEXRAB3>
<SEXRAB4/>
<SEXRAB5>M</SEXRAB5>
<SEXRAB6/>
<BUILDHEIGHTCLASS>2</BUILDHEIGHTCLASS>
<DAY>1</DAY>
<MONTH>4</MONTH>
<YEAR>2026</YEAR>
<CREATEDDATE>2026-04-01T00:00:00+01:00</CREATEDDATE>
<CREATEDBY>{created_by_email}</CREATEDBY>
<CREATEDBYID>{created_by_id}</CREATEDBYID>
<USERNAME>{assigned_to_email}</USERNAME>
<USERNAMEID>{assigned_to_id}</USERNAMEID>
<UPLOADDATE>2026-04-01T00:00:00+01:00</UPLOADDATE>
<AMENDEDBY/>
<AMENDEDBYID/>
<OWNINGORGID>{owning_org_id}</OWNINGORGID>
<OWNINGORGNAME>{owning_org_name}</OWNINGORGNAME>
<MANINGORGID>{managing_org_id}</MANINGORGID>
<MANINGORGNAME>{managing_org_name}</MANINGORGNAME>
<CREATIONMETHOD>1</CREATIONMETHOD>
<BULKUPLOADID/>
<COLLECTIONYEAR>2026</COLLECTIONYEAR>
<OWNERSHIP>2</OWNERSHIP>
<JOINT>1</JOINT>
<ETHNICGROUP1>17</ETHNICGROUP1>
<ETHNICGROUP2>17</ETHNICGROUP2>
<PREVIOUSLAKNOWN>1</PREVIOUSLAKNOWN>
<HASMSCHARGE>1</HASMSCHARGE>
<HASSERVICECHARGES/>
<SERVICECHARGES/>
<INC1NK>0</INC1NK>
<INC2NK>0</INC2NK>
<POSTCODE>SW1A 1AA</POSTCODE>
<ISLAINFERRED>true</ISLAINFERRED>
<MORTLEN1>10</MORTLEN1>
<ETHNIC2/>
<PREVTEN2/>
<ADDRESS1>Address line 1</ADDRESS1>
<ADDRESS2/>
<TOWNCITY>City</TOWNCITY>
<LANAME>Westminster</LANAME>
<ADDRESS1INPUT>Address line 1</ADDRESS1INPUT>
<POSTCODEINPUT>SW1A 1AA</POSTCODEINPUT>
<UPRNSELECTED/>
<BULKADDRESS1/>
<BULKADDRESS2/>
<BULKTOWNCITY/>
<BULKCOUNTY/>
<BULKPOSTCODE/>
<BULKLA/>
<NATIONALITYALL1>826</NATIONALITYALL1>
<NATIONALITYALL2>826</NATIONALITYALL2>
<PREVLOCNAME>Westminster</PREVLOCNAME>
<LIVEINBUYER1>1</LIVEINBUYER1>
<LIVEINBUYER2>1</LIVEINBUYER2>
<HASESTATEFEE/>
<ESTATEFEE/>
<STAIRLASTDAY/>
<STAIRLASTMONTH/>
<STAIRLASTYEAR/>
<STAIRINITIALDAY/>
<STAIRINITIALMONTH/>
<STAIRINITIALYEAR/>
<MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/>
<STAIRCASETOSALE/>
</form>
</forms>

18
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_26.csv vendored

File diff suppressed because one or more lines are too long

3
spec/fixtures/files/sales_logs_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

1
spec/fixtures/variable_definitions/sales_download_26_27.csv vendored

@ -4,3 +4,4 @@ sexrab3,What was person 3's sex at birth?
sexrab4,What was person 4's sex at birth? sexrab4,What was person 4's sex at birth?
sexrab5,What was person 5's sex at birth? sexrab5,What was person 5's sex at birth?
sexrab6,What was person 6's sex at birth? sexrab6,What was person 6's sex at birth?
buildheightclass, What is the building height classification?

1 sexrab1 What was buyer 1's sex at birth?
4 sexrab4 What was person 4's sex at birth?
5 sexrab5 What was person 5's sex at birth?
6 sexrab6 What was person 6's sex at birth?
7 buildheightclass What is the building height classification?

2
spec/lib/tasks/log_variable_definitions_spec.rb

@ -6,7 +6,7 @@ RSpec.describe "log_variable_definitions" do
subject(:task) { Rake::Task["data_import:add_variable_definitions"] } subject(:task) { Rake::Task["data_import:add_variable_definitions"] }
let(:path) { "spec/fixtures/variable_definitions" } let(:path) { "spec/fixtures/variable_definitions" }
let(:total_variable_definitions_count) { 450 } let(:total_variable_definitions_count) { 451 }
before do before do
Rake.application.rake_require("tasks/log_variable_definitions") Rake.application.rake_require("tasks/log_variable_definitions")

34
spec/models/form/sales/pages/building_height_class_spec.rb

@ -0,0 +1,34 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::BuildingHeightClass, 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))) }
let(:sales_log) { FactoryBot.create(:sales_log, :completed) }
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[buildheightclass])
end
it "has the correct id" do
expect(page.id).to eq("building_height_class")
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has the correct depends_on" do
expect(page.depends_on).to eq([
{ "proptype" => 1 },
{ "proptype" => 2 },
{ "proptype" => 9 },
])
end
end

37
spec/models/form/sales/questions/building_height_class_spec.rb

@ -0,0 +1,37 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::BuildingHeightClass, 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, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2026, 4, 1)))) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("buildheightclass")
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({
"1" => { "value" => "High-rise" },
"2" => { "value" => "Low-rise" },
"3" => { "value" => "Don't know" },
})
end
it "has the correct question_number" do
expect(question.question_number).to eq(17)
end
end

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

@ -16,6 +16,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(false) 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_2025_or_later?).and_return(false)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end end
context "when 2023" do context "when 2023" do
@ -50,6 +51,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true) 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_2025_or_later?).and_return(false)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end end
it "has correct pages" do it "has correct pages" do
@ -80,6 +82,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
before do before do
allow(form).to receive(:start_year_2024_or_later?).and_return(true) 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_2025_or_later?).and_return(true)
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end end
it "has correct pages" do it "has correct pages" do
@ -103,6 +106,38 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
) )
end end
end end
context "when 2026" do
let(:start_date) { Time.utc(2026, 2, 8) }
before do
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(property_information.pages.map(&:id)).to eq(
%w[
address_search
address
property_local_authority
local_authority_buyer_1_income_max_value_check
local_authority_buyer_2_income_max_value_check
local_authority_combined_income_max_value_check
about_price_la_value_check
property_unit_type
building_height_class
property_number_of_bedrooms
about_price_bedrooms_value_check
monthly_charges_property_type_value_check
percentage_discount_proptype_value_check
property_building_type
property_wheelchair_accessible
],
)
end
end
end end
it "has the correct id" do it "has the correct id" do

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

@ -119,7 +119,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
field_126: "R", field_126: "R",
field_127: "R", field_127: "R",
field_128: "1", field_128: "1",
field_130: "1", field_129: "1",
field_131: "1",
} }
end end

93
spec/services/csv/sales_log_csv_service_spec.rb

@ -21,17 +21,22 @@ RSpec.describe Csv::SalesLogCsvService do
purchid: nil, purchid: nil,
hholdcount: 3, hholdcount: 3,
age1: 30, age1: 30,
sexrab1: "F",
sex1: "X", sex1: "X",
age2: 35, age2: 35,
sexrab2: "M",
sex2: "X", sex2: "X",
sexrab3: "F",
sex3: "X", sex3: "X",
age4_known: 1, age4_known: 1,
sexrab4: "R",
sex4: "X", sex4: "X",
details_known_5: 2, details_known_5: 2,
age6_known: nil, age6_known: nil,
age6: nil, age6: nil,
ecstat6: nil, ecstat6: nil,
relat6: nil, relat6: nil,
sexrab6: nil,
sex6: nil, sex6: nil,
town_or_city: "Town or city", town_or_city: "Town or city",
address_line1_as_entered: "address line 1 as entered", address_line1_as_entered: "address line 1 as entered",
@ -193,6 +198,21 @@ RSpec.describe Csv::SalesLogCsvService do
expect(la_label_value).to eq "Westminster" expect(la_label_value).to eq "Westminster"
end 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 context "when the requested form is 2024" do
let(:now) { Time.zone.local(2024, 5, 1) } let(:now) { Time.zone.local(2024, 5, 1) }
let(:year) { 2024 } let(:year) { 2024 }
@ -233,18 +253,23 @@ RSpec.describe Csv::SalesLogCsvService do
end end
end end
context "when the requested form is 2023" do context "when the requested form is 2026" do
let(:now) { Time.zone.local(2024, 1, 1) } let(:now) { Time.zone.local(2026, 5, 1) }
let(:year) { 2023 } let(:year) { 2026 }
let(:fixed_time) { Time.zone.local(2026, 5, 1) }
it "exports the CSV with the 2023 ordering and all values correct" do before do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_labels_23.csv") log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1, buildheightclass: 2)
values_to_delete = %w[ID] end
it "exports the CSV with the 2026 ordering and all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_labels_26.csv")
values_to_delete = %w[ID OWNINGORGID MANINGORGID CREATEDBYID USERNAMEID AMENDEDBYID]
values_to_delete.each do |attribute| values_to_delete.each do |attribute|
index = attribute_line.index(attribute) index = attribute_line.index(attribute)
content_line[index] = nil content_line[index] = nil
end end
expect(csv).to eq expected_content expect(csv[1..]).to eq expected_content[1..] # Skip the first line as it contains the definitions
end end
end end
@ -301,23 +326,18 @@ RSpec.describe Csv::SalesLogCsvService do
expect(la_label_value).to eq "Westminster" expect(la_label_value).to eq "Westminster"
end end
context "when the requested form is 2025" do context "when the requested form is 2023" do
let(:now) { Time.zone.local(2025, 5, 1) } let(:now) { Time.zone.local(2024, 1, 1) }
let(:fixed_time) { Time.zone.local(2025, 5, 1) } let(:year) { 2023 }
let(:year) { 2025 }
before do
log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
end
it "exports the CSV with all values correct" do it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_25.csv") expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_23.csv")
values_to_delete = %w[ID OWNINGORGID MANINGORGID CREATEDBYID USERNAMEID AMENDEDBYID] values_to_delete = %w[ID]
values_to_delete.each do |attribute| values_to_delete.each do |attribute|
index = attribute_line.index(attribute) index = attribute_line.index(attribute)
content_line[index] = nil content_line[index] = nil
end 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
end end
@ -341,18 +361,43 @@ RSpec.describe Csv::SalesLogCsvService do
end end
end end
context "when the requested form is 2023" do context "when the requested form is 2025" do
let(:now) { Time.zone.local(2024, 1, 1) } let(:now) { Time.zone.local(2025, 5, 1) }
let(:year) { 2023 } let(:fixed_time) { Time.zone.local(2025, 5, 1) }
let(:year) { 2025 }
before do
log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1)
end
it "exports the CSV with all values correct" do it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_23.csv") expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_25.csv")
values_to_delete = %w[ID] values_to_delete = %w[ID OWNINGORGID MANINGORGID CREATEDBYID USERNAMEID AMENDEDBYID]
values_to_delete.each do |attribute| values_to_delete.each do |attribute|
index = attribute_line.index(attribute) index = attribute_line.index(attribute)
content_line[index] = nil content_line[index] = nil
end end
expect(csv).to eq expected_content expect(csv[1..]).to eq expected_content[1..] # Skip the first line as it contains the definitions
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) }
let(:year) { 2026 }
before do
log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1, buildheightclass: 2)
end
it "exports the CSV with all values correct" do
expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_26.csv")
values_to_delete = %w[ID OWNINGORGID MANINGORGID CREATEDBYID USERNAMEID AMENDEDBYID]
values_to_delete.each do |attribute|
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
end end
end end

64
spec/services/exports/sales_log_export_service_spec.rb

@ -365,6 +365,70 @@ RSpec.describe Exports::SalesLogExportService do
end end
end end
context "when exporting only 25/26 collection period" do
let(:start_time) { Time.zone.local(2025, 4, 1) }
before do
Timecop.freeze(start_time)
Singleton.__init__(FormHandler)
end
after do
Timecop.unfreeze
Singleton.__init__(FormHandler)
end
context "and one sales log is available for export" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export) }
let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001_pt001.xml" }
let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log_25_26.xml", "r:UTF-8") }
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs(full_update: true, collection_year: 2025)
end
end
end
context "when exporting only 26/27 collection period" do
let(:start_time) { Time.zone.local(2026, 4, 1) }
before do
Timecop.freeze(start_time)
Singleton.__init__(FormHandler)
end
after do
Timecop.unfreeze
Singleton.__init__(FormHandler)
end
context "and one sales log is available for export" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export) }
let(:expected_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" }
let(:expected_data_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml" }
let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log_26_27.xml", "r:UTF-8") }
it "generates an XML export file with the expected content within the ZIP file" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content|
entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil
expect(entry.get_input_stream.read).to have_same_xml_contents_as(expected_content)
end
export_service.export_xml_sales_logs(full_update: true, collection_year: 2026)
end
end
end
context "when exporting various fees, correctly maps the values" do context "when exporting various fees, correctly maps the values" do
context "with discounted ownership and mscharge" do context "with discounted ownership and mscharge" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, mscharge: 123) } let!(:sales_log) { FactoryBot.create(:sales_log, :export, mscharge: 123) }

Loading…
Cancel
Save