Browse Source

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

# Conflicts:
#	db/schema.rb
pull/3195/head
samyou-softwire 3 weeks ago
parent
commit
6dc16a7fd1
  1. 3
      app/helpers/bulk_upload/sales_log_to_csv.rb
  2. 17
      app/models/form/sales/pages/building_height_class.rb
  3. 4
      app/models/form/sales/pages/service_charge.rb
  4. 13
      app/models/form/sales/pages/service_charge_staircasing.rb
  5. 17
      app/models/form/sales/questions/building_height_class.rb
  6. 15
      app/models/form/sales/questions/has_service_charge.rb
  7. 16
      app/models/form/sales/questions/service_charge.rb
  8. 1
      app/models/form/sales/subsections/property_information.rb
  9. 1
      app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb
  10. 4
      app/models/validations/shared_validations.rb
  11. 29
      app/services/bulk_upload/lettings/year2025/row_parser.rb
  12. 29
      app/services/bulk_upload/lettings/year2026/row_parser.rb
  13. 31
      app/services/bulk_upload/sales/year2025/row_parser.rb
  14. 4
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  15. 35
      app/services/bulk_upload/sales/year2026/row_parser.rb
  16. 11
      app/services/csv/sales_log_csv_service.rb
  17. 2
      app/services/exports/sales_log_export_constants.rb
  18. 7
      config/locales/forms/2026/sales/property_information.en.yml
  19. 5
      db/migrate/20260219093257_add_buildheightclass_to_sales_logs.rb
  20. 7
      db/schema.rb
  21. 2
      spec/factories/sales_log.rb
  22. 154
      spec/fixtures/exports/sales_log_25_26.xml
  23. 161
      spec/fixtures/exports/sales_log_26_27.xml
  24. 18
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  25. 3
      spec/fixtures/files/sales_logs_csv_export_codes_26.csv
  26. 3
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  27. 3
      spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv
  28. 3
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv
  29. 1
      spec/fixtures/variable_definitions/sales_download_26_27.csv
  30. 2
      spec/lib/tasks/log_variable_definitions_spec.rb
  31. 36
      spec/models/form/sales/pages/building_height_class_spec.rb
  32. 29
      spec/models/form/sales/pages/service_charge_staircasing_spec.rb
  33. 39
      spec/models/form/sales/questions/building_height_class_spec.rb
  34. 48
      spec/models/form/sales/questions/has_service_charge_spec.rb
  35. 47
      spec/models/form/sales/questions/service_charge_spec.rb
  36. 53
      spec/models/form/sales/subsections/property_information_spec.rb
  37. 85
      spec/models/form/sales/subsections/shared_ownership_staircasing_transaction_spec.rb
  38. 14
      spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb
  39. 14
      spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb
  40. 14
      spec/services/bulk_upload/sales/year2025/row_parser_spec.rb
  41. 15
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  42. 131
      spec/services/csv/sales_log_csv_service_spec.rb
  43. 64
      spec/services/exports/sales_log_export_service_spec.rb
  44. 6
      yarn.lock

3
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -670,7 +670,8 @@ class BulkUpload::SalesLogToCsv
log.sexrab3,
log.sexrab4,
log.sexrab5,
log.sexrab6, # 127
log.sexrab6,
log.buildheightclass, # 128
]
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

4
app/models/form/sales/pages/service_charge.rb

@ -6,8 +6,8 @@ class Form::Sales::Pages::ServiceCharge < ::Form::Page
def questions
@questions ||= [
Form::Sales::Questions::HasServiceCharge.new(nil, nil, self),
Form::Sales::Questions::ServiceCharge.new(nil, nil, self),
Form::Sales::Questions::HasServiceCharge.new(nil, nil, self, staircasing: false),
Form::Sales::Questions::ServiceCharge.new(nil, nil, self, staircasing: false),
]
end
end

13
app/models/form/sales/pages/service_charge_staircasing.rb

@ -0,0 +1,13 @@
class Form::Sales::Pages::ServiceChargeStaircasing < ::Form::Page
def initialize(id, hsh, subsection)
super
@copy_key = "sales.sale_information.servicecharges"
end
def questions
@questions ||= [
Form::Sales::Questions::HasServiceCharge.new(nil, nil, self, staircasing: true),
Form::Sales::Questions::ServiceCharge.new(nil, nil, self, staircasing: true),
]
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

15
app/models/form/sales/questions/has_service_charge.rb

@ -1,6 +1,6 @@
class Form::Sales::Questions::HasServiceCharge < ::Form::Question
def initialize(id, hsh, subsection)
super
def initialize(id, hsh, subsection, staircasing:)
super(id, hsh, subsection)
@id = "has_mscharge"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@ -15,7 +15,8 @@ class Form::Sales::Questions::HasServiceCharge < ::Form::Question
],
}
@copy_key = "sales.sale_information.servicecharges.has_servicecharge"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@staircasing = staircasing
@question_number = question_number_from_year[form.start_date.year] || question_number_from_year[question_number_from_year.keys.max]
end
ANSWER_OPTIONS = {
@ -23,5 +24,11 @@ class Form::Sales::Questions::HasServiceCharge < ::Form::Question
"0" => { "value" => "No" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2025 => 88 }.freeze
def question_number_from_year
if @staircasing
{ 2026 => 0 }.freeze
else
{ 2025 => 88, 2026 => 0 }.freeze
end
end
end

16
app/models/form/sales/questions/service_charge.rb

@ -1,16 +1,24 @@
class Form::Sales::Questions::ServiceCharge < ::Form::Question
def initialize(id, hsh, subsection)
super
def initialize(id, hsh, subsection, staircasing:)
super(id, hsh, subsection)
@id = "mscharge"
@type = "numeric"
@min = 1
@max = 9999.99
@step = 0.01
@width = 5
@prefix = "£"
@copy_key = "sales.sale_information.servicecharges.servicecharge"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@staircasing = staircasing
@question_number = question_number_from_year[form.start_date.year] || question_number_from_year[question_number_from_year.keys.max]
@strip_commas = true
end
QUESTION_NUMBER_FROM_YEAR = { 2025 => 88 }.freeze
def question_number_from_year
if @staircasing
{ 2026 => 0 }.freeze
else
{ 2025 => 88, 2026 => 0 }.freeze
end
end
end

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

@ -10,6 +10,7 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection
@pages ||= [
(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::BuildingHeightClass.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::PropertyNumberOfBedrooms.new(nil, 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?),

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

@ -25,6 +25,7 @@ class Form::Sales::Subsections::SharedOwnershipStaircasingTransaction < ::Form::
Form::Sales::Pages::Mortgageused.new("staircase_mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self),
Form::Sales::Pages::MonthlyRentStaircasing.new(nil, nil, self),
(Form::Sales::Pages::ServiceChargeStaircasing.new("service_charge_staircasing", nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self),
].compact
end

4
app/models/validations/shared_validations.rb

@ -105,8 +105,8 @@ private
def add_range_error(record, question)
field = question.check_answer_label || question.id
min = [question.prefix, number_with_delimiter(question.min, delimiter: ","), question.suffix].join("") if question.min
max = [question.prefix, number_with_delimiter(question.max, delimiter: ","), question.suffix].join("") if question.max
min = [question.prefix, number_with_delimiter(question.min, delimiter: ","), question.suffix_label(record)].join("") if question.min
max = [question.prefix, number_with_delimiter(question.max, delimiter: ","), question.suffix_label(record)].join("") if question.max
if min && max
record.errors.add question.id.to_sym, :outside_the_range, message: I18n.t("validations.shared.numeric.within_range", field:, min:, max:)

29
app/services/bulk_upload/lettings/year2025/row_parser.rb

@ -148,6 +148,26 @@ class BulkUpload::Lettings::Year2025::RowParser
ERROR_BASE_KEY = "validations.lettings.2025.bulk_upload".freeze
CASE_INSENSITIVE_FIELDS = [
:field_42, # What is the lead tenant’s age?
:field_48, # What is person 2’s age?
:field_52, # What is person 3’s age?
:field_56, # What is person 4’s age?
:field_60, # What is person 5’s age?
:field_64, # What is person 6’s age?
:field_68, # What is person 7’s age?
:field_72, # What is person 8’s age?
:field_43, # Which of these best describes the lead tenant’s gender identity?
:field_49, # Which of these best describes person 2’s gender identity?
:field_53, # Which of these best describes person 3’s gender identity?
:field_57, # Which of these best describes person 4’s gender identity?
:field_61, # Which of these best describes person 5’s gender identity?
:field_65, # Which of these best describes person 6’s gender identity?
:field_69, # Which of these best describes person 7’s gender identity?
:field_73, # Which of these best describes person 8’s gender identity?
].freeze
attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
@ -459,6 +479,8 @@ class BulkUpload::Lettings::Year2025::RowParser
return @valid = true if blank_row?
normalise_case_insensitive_fields
super(:before_log)
@before_errors = errors.dup
@ -560,6 +582,13 @@ class BulkUpload::Lettings::Year2025::RowParser
private
def normalise_case_insensitive_fields
CASE_INSENSITIVE_FIELDS.each do |field|
value = send(field)
send("#{field}=", value.upcase) if value.present?
end
end
def validate_valid_radio_option
log.attributes.each_key do |question_id|
question = log.form.get_question(question_id, log)

29
app/services/bulk_upload/lettings/year2026/row_parser.rb

@ -166,6 +166,26 @@ class BulkUpload::Lettings::Year2026::RowParser
ERROR_BASE_KEY = "validations.lettings.2026.bulk_upload".freeze
CASE_INSENSITIVE_FIELDS = [
:field_41, # What is the lead tenant's age?
:field_48, # What is person 2's age?
:field_54, # What is person 3's age?
:field_60, # What is person 4's age?
:field_66, # What is person 5's age?
:field_72, # What is person 6's age?
:field_78, # What is person 7's age?
:field_84, # What is person 8's age?
:field_42, # What is the lead tenant's sex?
:field_50, # What is person 2's sex?
:field_56, # What is person 3's sex?
:field_62, # What is person 4's sex?
:field_68, # What is person 5's sex?
:field_74, # What is person 6's sex?
:field_80, # What is person 7's sex?
:field_86, # What is person 8's sex?
].freeze
attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
@ -494,6 +514,8 @@ class BulkUpload::Lettings::Year2026::RowParser
return @valid = true if blank_row?
normalise_case_insensitive_fields
super(:before_log)
@before_errors = errors.dup
@ -600,6 +622,13 @@ class BulkUpload::Lettings::Year2026::RowParser
private
def normalise_case_insensitive_fields
CASE_INSENSITIVE_FIELDS.each do |field|
value = send(field)
send("#{field}=", value.upcase) if value.present?
end
end
def validate_valid_radio_option
log.attributes.each_key do |question_id|
question = log.form.get_question(question_id, log)

31
app/services/bulk_upload/sales/year2025/row_parser.rb

@ -139,6 +139,28 @@ class BulkUpload::Sales::Year2025::RowParser
ERROR_BASE_KEY = "validations.sales.2025.bulk_upload".freeze
CASE_INSENSITIVE_FIELDS = [
:field_28, # Age of buyer 1
:field_35, # Age of person 2
:field_43, # Age of person 3
:field_47, # Age of person 4
:field_51, # Age of person 5
:field_55, # Age of person 6
:field_29, # Gender identity of buyer 1
:field_36, # Gender identity of person 2
:field_44, # Gender identity of person 3
:field_48, # Gender identity of person 4
:field_52, # Gender identity of person 5
:field_56, # Gender identity of person 6
:field_64, # What was buyer 2’s previous tenure?
:field_75, # What is the total amount the buyers had in savings before they paid any deposit for the property?
:field_70, # What is buyer 1’s gross annual income?
:field_72, # What is buyer 2’s gross annual income?
].freeze
attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
@ -454,6 +476,8 @@ class BulkUpload::Sales::Year2025::RowParser
return true if blank_row?
normalise_case_insensitive_fields
super(:before_log)
@before_errors = errors.dup
@ -525,6 +549,13 @@ class BulkUpload::Sales::Year2025::RowParser
private
def normalise_case_insensitive_fields
CASE_INSENSITIVE_FIELDS.each do |field|
value = send(field)
send("#{field}=", value.upcase) if value.present?
end
end
def prevtenbuy2
case field_64
when "R"

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

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

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

@ -142,10 +142,33 @@ class BulkUpload::Sales::Year2026::RowParser
field_125: "Person 4's sex, as registered at birth",
field_126: "Person 5's sex, as registered at birth",
field_127: "Person 6's sex, as registered at birth",
field_128: "What is the building height classification?",
}.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
CASE_INSENSITIVE_FIELDS = [
:field_28, # Age of buyer 1
:field_35, # Age of person 2
:field_43, # Age of person 3
:field_47, # Age of person 4
:field_51, # Age of person 5
:field_55, # Age of person 6
:field_122, # Buyer 1's sex, as registered at birth
:field_123, # Buyer/Person 2's sex, as registered at birth
:field_124, # Person 3's sex, as registered at birth
:field_125, # Person 4's sex, as registered at birth
:field_126, # Person 5's sex, as registered at birth
:field_127, # Person 6's sex, as registered at birth
:field_64, # What was buyer 2’s previous tenure?
:field_75, # What is the total amount the buyers had in savings before they paid any deposit for the property?
:field_70, # What is buyer 1’s gross annual income?
:field_72, # What is buyer 2’s gross annual income?
].freeze
attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
@ -288,6 +311,7 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_125, :string
attribute :field_126, :string
attribute :field_127, :string
attribute :field_128, :integer
validates :field_1,
presence: {
@ -485,6 +509,8 @@ class BulkUpload::Sales::Year2026::RowParser
return true if blank_row?
normalise_case_insensitive_fields
super(:before_log)
@before_errors = errors.dup
@ -557,6 +583,13 @@ class BulkUpload::Sales::Year2026::RowParser
private
def normalise_case_insensitive_fields
CASE_INSENSITIVE_FIELDS.each do |field|
value = send(field)
send("#{field}=", value.upcase) if value.present?
end
end
def prevtenbuy2
case field_64
when "R"
@ -823,6 +856,7 @@ private
sexrab4: %i[field_125],
sexrab5: %i[field_126],
sexrab6: %i[field_127],
buildheightclass: %i[field_128],
}
end
@ -864,6 +898,7 @@ private
attributes["sexrab4"] = field_125
attributes["sexrab5"] = field_126
attributes["sexrab6"] = field_127
attributes["buildheightclass"] = field_128
attributes["relat2"] = relationship_from_is_partner(field_34)
attributes["relat3"] = relationship_from_is_partner(field_42)

11
app/services/csv/sales_log_csv_service.rb

@ -249,7 +249,7 @@ module Csv
return @attributes unless @user.support?
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|
mappings[attribute] || attribute.upcase
@ -297,11 +297,10 @@ module Csv
end
def attribute_mappings
mappings = case @year
when 2024
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024)
when 2025
mappings = if @year >= 2025
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024).merge(ATTRIBUTE_MAPPINGS_2025)
elsif @year == 2024
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024)
else
ATTRIBUTE_MAPPINGS
end
@ -348,6 +347,8 @@ module Csv
%w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo]
when 2025
%w[id status duplicate_set_id created_at created_by_id updated_at updated_by_id creation_method bulk_upload_id]
when 2026
%w[id status duplicate_set_id created_at created_by_id updated_at updated_by_id creation_method bulk_upload_id]
else
%w[id status duplicate_set_id created_at updated_at collection_start_year creation_method bulk_upload_id is_dpo]
end

2
app/services/exports/sales_log_export_constants.rb

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

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

@ -59,6 +59,13 @@ en:
hint_text: ""
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:
page_header: ""
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

7
db/schema.rb

@ -376,6 +376,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
t.boolean "manual_address_entry_selected", default: false
t.integer "referral_type"
t.integer "working_situation_illness_check"
t.integer "referral_register"
t.integer "referral_noms"
t.integer "referral_org"
t.string "sexrab1"
t.string "sexrab2"
t.string "sexrab3"
@ -400,9 +403,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
t.string "gender_description6"
t.string "gender_description7"
t.string "gender_description8"
t.integer "referral_register"
t.integer "referral_noms"
t.integer "referral_org"
t.integer "tenancyother_value_check"
t.index ["assigned_to_id"], name: "index_lettings_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_lettings_logs_on_bulk_upload_id"
@ -822,6 +822,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_24_141705) do
t.string "sexrab4"
t.string "sexrab5"
t.string "sexrab6"
t.integer "buildheightclass"
t.integer "mortlen_known"
t.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"

2
spec/factories/sales_log.rb

@ -90,6 +90,7 @@ FactoryBot.define do
buy1livein { 1 }
relat2 { "P" }
proptype { 1 }
buildheightclass { 2 }
age2_known { 0 }
age2 { Faker::Number.within(range: 25..45) }
builtype { 1 }
@ -294,6 +295,7 @@ FactoryBot.define do
buy1livein { 1 }
relat2 { "P" }
proptype { 1 }
buildheightclass { 2 }
age2_known { 0 }
age2 { 33 }
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

3
spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv vendored

File diff suppressed because one or more lines are too long

3
spec/fixtures/files/sales_logs_csv_export_non_support_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?
sexrab5,What was person 5'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"] }
let(:path) { "spec/fixtures/variable_definitions" }
let(:total_variable_definitions_count) { 450 }
let(:total_variable_definitions_count) { 451 }
before do
Rake.application.rake_require("tasks/log_variable_definitions")

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

@ -0,0 +1,36 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::BuildingHeightClass, type: :model do
include CollectionTimeHelper
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: current_collection_start_date)) }
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

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

@ -0,0 +1,29 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::ServiceChargeStaircasing, 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[has_mscharge mscharge])
end
it "has the correct id" do
expect(page.id).to be_nil
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 be_nil
end
end

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

@ -0,0 +1,39 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::BuildingHeightClass, type: :model do
include CollectionTimeHelper
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: current_collection_start_date))) }
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

48
spec/models/form/sales/questions/has_service_charge_spec.rb

@ -1,12 +1,14 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::HasServiceCharge, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
subject(:question) { described_class.new(question_id, question_definition, page, staircasing:) }
let(:form) { instance_double(Form, start_date: Time.zone.local(2025, 4, 4)) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, id: "shared_ownership", form:)) }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:page) { instance_double(Form::Page, subsection:) }
let(:start_date) { Time.utc(2025, 5, 1) }
let(:staircasing) { false }
it "has correct page" do
expect(question.page).to eq(page)
@ -46,4 +48,44 @@ RSpec.describe Form::Sales::Questions::HasServiceCharge, type: :model do
],
})
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
before do
allow(subsection.form).to receive(:start_year_2025_or_later?).and_return(true)
end
context "when not staircasing" do
let(:staircasing) { false }
it "has the correct question number" do
expect(question.question_number).to eq(88)
end
end
end
context "with 2026/27 form" do
let(:start_date) { Time.utc(2026, 4, 1) }
before do
allow(subsection.form).to receive(:start_year_2026_or_later?).and_return(true)
end
context "when staircasing" do
let(:staircasing) { true }
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end
context "when not staircasing" do
let(:staircasing) { false }
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end
end
end

47
spec/models/form/sales/questions/service_charge_spec.rb

@ -1,11 +1,14 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::ServiceCharge, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
subject(:question) { described_class.new(question_id, question_definition, page, staircasing:) }
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(2023, 4, 1)))) }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:page) { instance_double(Form::Page, subsection:) }
let(:start_date) { Time.utc(2023, 4, 1) }
let(:staircasing) { false }
it "has correct page" do
expect(question.page).to eq(page)
@ -34,4 +37,44 @@ RSpec.describe Form::Sales::Questions::ServiceCharge, type: :model do
it "has the correct prefix" do
expect(question.prefix).to eq("£")
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
before do
allow(subsection.form).to receive(:start_year_2025_or_later?).and_return(true)
end
context "when not staircasing" do
let(:staircasing) { false }
it "has the correct question number" do
expect(question.question_number).to eq(88)
end
end
end
context "with 2026/27 form" do
let(:start_date) { Time.utc(2026, 4, 1) }
before do
allow(subsection.form).to receive(:start_year_2026_or_later?).and_return(true)
end
context "when staircasing" do
let(:staircasing) { true }
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end
context "when not staircasing" do
let(:staircasing) { false }
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end
end
end

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

@ -1,6 +1,8 @@
require "rails_helper"
RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
include CollectionTimeHelper
subject(:property_information) { described_class.new(nil, nil, section) }
let(:section) { instance_double(Form::Sales::Sections::PropertyInformation) }
@ -11,14 +13,16 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
describe "pages" do
let(:section) { instance_double(Form::Sales::Sections::Household, form:) }
let(:form) { instance_double(Form, start_date:) }
before do
allow(form).to receive_messages(start_year_2024_or_later?: false, start_year_2025_or_later?: false)
end
let(:start_year_2024_or_later?) { true }
let(:start_year_2025_or_later?) { true }
let(:start_year_2026_or_later?) { true }
let(:form) { instance_double(Form, start_date:, start_year_2024_or_later?: start_year_2024_or_later?, start_year_2025_or_later?: start_year_2025_or_later?, start_year_2026_or_later?: start_year_2026_or_later?) }
context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) }
let(:start_date) { collection_start_date_for_year(2023) }
let(:start_year_2024_or_later?) { false }
let(:start_year_2025_or_later?) { false }
let(:start_year_2026_or_later?) { false }
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
@ -44,11 +48,9 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
end
context "when 2024" do
let(:start_date) { Time.utc(2024, 2, 8) }
before do
allow(form).to receive_messages(start_year_2024_or_later?: true, start_year_2025_or_later?: false)
end
let(:start_date) { collection_start_date_for_year(2024) }
let(:start_year_2025_or_later?) { false }
let(:start_year_2026_or_later?) { false }
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
@ -73,11 +75,33 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
end
context "when 2025" do
let(:start_date) { Time.utc(2025, 2, 8) }
let(:start_date) { collection_start_date_for_year(2025) }
let(:start_year_2026_or_later?) { false }
before do
allow(form).to receive_messages(start_year_2024_or_later?: true, start_year_2025_or_later?: true)
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
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
context "when 2026" do
let(:start_date) { collection_start_date_for_year(2026) }
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
@ -90,6 +114,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
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

85
spec/models/form/sales/subsections/shared_ownership_staircasing_transaction_spec.rb

@ -0,0 +1,85 @@
require "rails_helper"
RSpec.describe Form::Sales::Subsections::SharedOwnershipStaircasingTransaction, type: :model do
subject(:shared_ownership_staircasing_transaction) { described_class.new(nil, nil, section) }
let(:form) { instance_double(Form, start_year_2026_or_later?: false) }
let(:section) { instance_double(Form::Sales::Sections::SaleInformation, form:) }
it "has correct section" do
expect(shared_ownership_staircasing_transaction.section).to eq(section)
end
it "has the correct depends_on" do
expect(shared_ownership_staircasing_transaction.depends_on).to eq([{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 1 }])
end
it "has the correct id" do
expect(shared_ownership_staircasing_transaction.id).to eq("shared_ownership_staircasing_transaction")
end
it "has the correct label" do
expect(shared_ownership_staircasing_transaction.label).to eq("Shared ownership - staircasing transaction")
end
it "has the correct copy key" do
expect(shared_ownership_staircasing_transaction.copy_key).to eq("sale_information")
end
context "when the start year is 2025" do
let(:form) { instance_double(Form, start_year_2025_or_later?: true, start_year_2026_or_later?: false, start_date: Time.utc(2025, 4, 1)) }
it "has correct pages" do
expect(shared_ownership_staircasing_transaction.pages.map(&:id)).to eq(
%w[
about_staircasing_joint_purchase
about_staircasing_not_joint_purchase
staircase_sale
staircase_bought_value_check
staircase_owned_value_check_joint_purchase
staircase_owned_value_check_not_joint_purchase
staircase_first_time
staircase_previous
staircase_initial_date
value_shared_ownership_staircase
about_price_shared_ownership_value_check_staircasing
staircase_equity
shared_ownership_equity_value_check_staircasing
staircase_mortgage_used_shared_ownership
monthly_rent_staircasing_owned
monthly_rent_staircasing
monthly_charges_shared_ownership_value_check
],
)
end
end
context "when the start year is 2026" do
let(:form) { instance_double(Form, start_year_2025_or_later?: true, start_year_2026_or_later?: true, start_date: Time.utc(2026, 4, 1)) }
it "has correct pages" do
expect(shared_ownership_staircasing_transaction.pages.map(&:id)).to eq(
%w[
about_staircasing_joint_purchase
about_staircasing_not_joint_purchase
staircase_sale
staircase_bought_value_check
staircase_owned_value_check_joint_purchase
staircase_owned_value_check_not_joint_purchase
staircase_first_time
staircase_previous
staircase_initial_date
value_shared_ownership_staircase
about_price_shared_ownership_value_check_staircasing
staircase_equity
shared_ownership_equity_value_check_staircasing
staircase_mortgage_used_shared_ownership
monthly_rent_staircasing_owned
monthly_rent_staircasing
service_charge_staircasing
monthly_charges_shared_ownership_value_check
],
)
end
end
end

14
spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb

@ -541,6 +541,20 @@ RSpec.describe BulkUpload::Lettings::Year2025::RowParser do
end
end
end
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_43 field_49 field_53 field_57 field_61 field_65 field_69 field_73] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_42 field_48 field_52 field_56 field_60 field_64 field_68 field_72] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
.merge(case_insensitive_integer_fields_with_r_option.each_with_object({}) { |field, h| h[field.to_sym] = "r" })
end
it "is still valid" do
expect(parser).to be_valid
end
end
end
context "when valid row with valid decimal (integer) field_11" do

14
spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb

@ -434,6 +434,20 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
end
end
end
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_42 field_50 field_56 field_62 field_68 field_74 field_80 field_86] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_41 field_48 field_54 field_60 field_66 field_72 field_78 field_84] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
.merge(case_insensitive_integer_fields_with_r_option.each_with_object({}) { |field, h| h[field.to_sym] = "r" })
end
it "is still valid" do
expect(parser).to be_valid
end
end
end
context "when valid row with valid decimal (integer) field_11" do

14
spec/services/bulk_upload/sales/year2025/row_parser_spec.rb

@ -292,6 +292,20 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
expect(questions.map(&:id).size).to eq(0)
expect(questions.map(&:id)).to eql([])
end
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_29 field_36 field_44 field_48 field_52 field_56] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
.merge(case_insensitive_integer_fields_with_r_option.each_with_object({}) { |field, h| h[field.to_sym] = "r" })
end
it "is still valid" do
expect(parser).to be_valid
end
end
end
describe "#validate_nulls" do

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

@ -118,6 +118,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
field_125: "M",
field_126: "R",
field_127: "R",
field_128: "1",
}
end
@ -298,6 +299,20 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
expect(questions.map(&:id).size).to eq(0)
expect(questions.map(&:id)).to eql([])
end
context "and case insensitive fields are set to lowercase" do
let(:case_insensitive_fields) { %w[field_122 field_123 field_124 field_125 field_126 field_127] }
let(:case_insensitive_integer_fields_with_r_option) { %w[field_28 field_35 field_43 field_47 field_51 field_55 field_64 field_75 field_70 field_72] }
let(:attributes) do
valid_attributes
.merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase })
.merge(case_insensitive_integer_fields_with_r_option.each_with_object({}) { |field, h| h[field.to_sym] = "r" })
end
it "is still valid" do
expect(parser).to be_valid
end
end
end
describe "#validate_nulls" do

131
spec/services/csv/sales_log_csv_service_spec.rb

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

64
spec/services/exports/sales_log_export_service_spec.rb

@ -365,6 +365,70 @@ RSpec.describe Exports::SalesLogExportService do
end
end
context "when exporting only 25/26 collection period" do
let(:start_time) { Time.zone.local(2025, 4, 1) }
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 "with discounted ownership and mscharge" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, mscharge: 123) }

6
yarn.lock

@ -3434,9 +3434,9 @@ mini-css-extract-plugin@^2.6.0:
tapable "^2.2.1"
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"

Loading…
Cancel
Save