Browse Source

Merge branch 'main' into CLDC-4215-4216-add-mortgage-used-dont-know-option

pull/3208/head
Samuel Young 2 months ago committed by GitHub
parent
commit
4fb7e767e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      app/helpers/bulk_upload/sales_log_to_csv.rb
  2. 14
      app/models/form/sales/pages/service_charge_changed.rb
  3. 27
      app/models/form/sales/questions/has_service_charges_changed.rb
  4. 17
      app/models/form/sales/questions/new_service_charges.rb
  5. 2
      app/models/form/sales/questions/purchase_price.rb
  6. 2
      app/models/form/sales/questions/value.rb
  7. 1
      app/models/form/sales/subsections/shared_ownership_staircasing_transaction.rb
  8. 57
      app/models/sales_log.rb
  9. 9
      app/models/validations/sales/financial_validations.rb
  10. 17
      app/models/validations/sales/sale_information_validations.rb
  11. 1
      app/models/validations/sales/soft_validations.rb
  12. 6
      app/services/bulk_upload/sales/year2025/row_parser.rb
  13. 4
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  14. 18
      app/services/bulk_upload/sales/year2026/row_parser.rb
  15. 6
      app/services/exports/sales_log_export_constants.rb
  16. 3
      app/services/exports/sales_log_export_service.rb
  17. 16
      config/locales/forms/2025/lettings/tenancy_information.en.yml
  18. 16
      config/locales/forms/2026/lettings/tenancy_information.en.yml
  19. 13
      config/locales/forms/2026/sales/sale_information.en.yml
  20. 4
      config/locales/validations/sales/financial.en.yml
  21. 1185
      config/sale_range_data/2026.csv
  22. 8
      db/migrate/20260305095832_add_service_charge_changed_to_sales_logs.rb
  23. 6
      db/schema.rb
  24. 1
      spec/factories/sales_log.rb
  25. 10
      spec/features/sales_log_spec.rb
  26. 2
      spec/fixtures/exports/sales_log_26_27.xml
  27. 10
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  28. 6
      spec/fixtures/files/sales_logs_csv_export_codes_26.csv
  29. 6
      spec/fixtures/files/sales_logs_csv_export_labels_26.csv
  30. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv
  31. 6
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv
  32. 2
      spec/fixtures/variable_definitions/sales_download_26_27.csv
  33. 2
      spec/lib/tasks/log_variable_definitions_spec.rb
  34. 2
      spec/models/form/sales/pages/purchase_price_outright_ownership_spec.rb
  35. 2
      spec/models/form/sales/pages/purchase_price_spec.rb
  36. 31
      spec/models/form/sales/pages/service_charge_changed_spec.rb
  37. 2
      spec/models/form/sales/pages/value_shared_ownership_spec.rb
  38. 56
      spec/models/form/sales/questions/has_service_charges_changed_spec.rb
  39. 53
      spec/models/form/sales/questions/new_service_charges_spec.rb
  40. 23
      spec/models/form/sales/questions/purchase_price_spec.rb
  41. 19
      spec/models/form/sales/questions/value_spec.rb
  42. 1
      spec/models/form/sales/subsections/shared_ownership_staircasing_transaction_spec.rb
  43. 59
      spec/models/validations/sales/financial_validations_spec.rb
  44. 116
      spec/models/validations/sales/sale_information_validations_spec.rb
  45. 18
      spec/requests/duplicate_logs_controller_spec.rb
  46. 2
      spec/services/bulk_upload/sales/validator_spec.rb
  47. 4
      spec/services/bulk_upload/sales/year2025/row_parser_spec.rb
  48. 6
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb
  49. 87
      spec/services/exports/sales_log_export_service_spec.rb

4
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -678,7 +678,9 @@ 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, # 134 log.gender_description6,
log.hasservicechargeschanged,
log.newservicecharges, # 136
] ]
end end

14
app/models/form/sales/pages/service_charge_changed.rb

@ -0,0 +1,14 @@
class Form::Sales::Pages::ServiceChargeChanged < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "service_charge_changed"
@copy_key = "sales.sale_information.servicecharges_changed"
end
def questions
@questions ||= [
Form::Sales::Questions::HasServiceChargesChanged.new(nil, nil, self),
Form::Sales::Questions::NewServiceCharges.new(nil, nil, self),
]
end
end

27
app/models/form/sales/questions/has_service_charges_changed.rb

@ -0,0 +1,27 @@
class Form::Sales::Questions::HasServiceChargesChanged < ::Form::Question
def initialize(id, hsh, page)
super
@id = "hasservicechargeschanged"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = {
"newservicecharges" => [1],
}
@hidden_in_check_answers = {
"depends_on" => [
{
"hasservicechargeschanged" => 1,
},
],
}
@copy_key = "sales.sale_information.servicecharges_changed.has_service_charges_changed"
@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" => "Yes" },
"2" => { "value" => "No" },
}.freeze
QUESTION_NUMBER_FROM_YEAR = { 2026 => 0 }.freeze
end

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

@ -0,0 +1,17 @@
class Form::Sales::Questions::NewServiceCharges < ::Form::Question
def initialize(id, hsh, page)
super
@id = "newservicecharges"
@type = "numeric"
@min = 0
@max = 9999.99
@step = 0.01
@width = 5
@prefix = "£"
@copy_key = "sales.sale_information.servicecharges_changed.new_service_charges"
@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 = { 2026 => 0 }.freeze
end

2
app/models/form/sales/questions/purchase_price.rb

@ -3,7 +3,7 @@ class Form::Sales::Questions::PurchasePrice < ::Form::Question
super(id, hsh, page) super(id, hsh, page)
@id = "value" @id = "value"
@type = "numeric" @type = "numeric"
@min = 0 @min = form.start_year_2026_or_later? ? 15_000 : 0
@step = 0.01 @step = 0.01
@width = 5 @width = 5
@prefix = "£" @prefix = "£"

2
app/models/form/sales/questions/value.rb

@ -4,7 +4,7 @@ class Form::Sales::Questions::Value < ::Form::Question
@id = "value" @id = "value"
@copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value" @copy_key = form.start_year_2025_or_later? ? "sales.sale_information.value.#{page.id}" : "sales.sale_information.value"
@type = "numeric" @type = "numeric"
@min = 0 @min = form.start_year_2026_or_later? ? 15_000 : 0
@step = 1 @step = 1
@width = 5 @width = 5
@prefix = "£" @prefix = "£"

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

@ -26,6 +26,7 @@ class Form::Sales::Subsections::SharedOwnershipStaircasingTransaction < ::Form::
Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self), Form::Sales::Pages::MonthlyRentStaircasingOwned.new(nil, nil, self),
Form::Sales::Pages::MonthlyRentStaircasing.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::ServiceChargeStaircasing.new("service_charge_staircasing", nil, self) if form.start_year_2026_or_later?),
(Form::Sales::Pages::ServiceChargeChanged.new(nil, nil, self) if form.start_year_2026_or_later?),
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self), Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self),
].compact ].compact
end end

57
app/models/sales_log.rb

@ -18,6 +18,7 @@ class SalesLog < Log
include Validations::Sales::SoftValidations include Validations::Sales::SoftValidations
include Validations::SoftValidations include Validations::SoftValidations
include MoneyFormattingHelper include MoneyFormattingHelper
include CollectionTimeHelper
self.inheritance_column = :_type_disabled self.inheritance_column = :_type_disabled
@ -37,6 +38,8 @@ class SalesLog < Log
belongs_to :managing_organisation, class_name: "Organisation", optional: true belongs_to :managing_organisation, class_name: "Organisation", optional: true
scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) } scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) }
scope :filter_by_year_or_later, ->(year) { where("sales_logs.saledate >= ?", Time.zone.local(year.to_i, 4, 1)) }
scope :filter_by_year_or_earlier, ->(year) { where("sales_logs.saledate < ?", Time.zone.local(year.to_i + 1, 4, 1)) }
scope :filter_by_years_or_nil, lambda { |years, _user = nil| scope :filter_by_years_or_nil, lambda { |years, _user = nil|
first_year = years.shift first_year = years.shift
query = filter_by_year(first_year) query = filter_by_year(first_year)
@ -69,12 +72,14 @@ class SalesLog < Log
} }
scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) } scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) }
scope :ecstat1_answered, -> { where.not(ecstat1: nil).or(where("saledate >= ?", Time.zone.local(2025, 4, 1))) } scope :ecstat1_answered, -> { where.not(ecstat1: nil).or(where("saledate >= ?", Time.zone.local(2025, 4, 1))) }
scope :sex1_answered, -> { where.not(sex1: nil).filter_by_year_or_earlier(2025).or(where.not(sexrab1: nil).filter_by_year_or_later(2026)) }
scope :address_answered, -> { where.not(postcode_full: nil).where.not(address_line1: nil).or(where.not(uprn: nil)) }
scope :duplicate_logs, lambda { |log| scope :duplicate_logs, lambda { |log|
visible.where(log.slice(*DUPLICATE_LOG_ATTRIBUTES)) visible.where(log.slice(*DUPLICATE_LOG_ATTRIBUTES))
.where.not(id: log.id) .where.not(id: log.id)
.where.not(saledate: nil) .where.not(saledate: nil)
.where.not(sex1: nil) .sex1_answered
.where.not(postcode_full: nil) .address_answered
.ecstat1_answered .ecstat1_answered
.age1_answered .age1_answered
} }
@ -84,8 +89,8 @@ class SalesLog < Log
scope = visible scope = visible
.group(*DUPLICATE_LOG_ATTRIBUTES) .group(*DUPLICATE_LOG_ATTRIBUTES)
.where.not(saledate: nil) .where.not(saledate: nil)
.where.not(sex1: nil) .sex1_answered
.where.not(postcode_full: nil) .address_answered
.age1_answered .age1_answered
.ecstat1_answered .ecstat1_answered
.having("COUNT(*) > 1") .having("COUNT(*) > 1")
@ -98,7 +103,7 @@ class SalesLog < Log
} }
OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 ecstat1 postcode_full].freeze DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 sexrab1 ecstat1 postcode_full uprn address_line1].freeze
def lettings? def lettings?
false false
@ -268,6 +273,22 @@ class SalesLog < Log
value * equity / 100 value * equity / 100
end end
def expected_shared_ownership_deposit_value_tolerance
return 1 unless value && equity
# we found that a fixed tolerance was not quite what we wanted here.
# CORE wants it so if a user say, has a 66.666% equity they can enter either 66.6% or 66.7% (or 66.5%)
# so in 2026 we base our tolerance off of a discount 0.1% higher or lower
if form.start_year_2026_or_later?
lower_bound = value * ((equity - 0.1) / 100)
upper_bound = value * ((equity + 0.1) / 100)
(upper_bound - lower_bound) / 2
else
1
end
end
def stairbought_part_of_value def stairbought_part_of_value
return unless value && stairbought return unless value && stairbought
@ -468,6 +489,22 @@ class SalesLog < Log
value - discount_amount value - discount_amount
end end
def value_with_discount_tolerance
return 1 if value.blank? || discount.nil?
# we found that a simple tolerance was not quite what we wanted here.
# CORE wants it so if a user say, has a 66.6% discount then can enter either 66.6% or 66.7%
# so in 2026 we base our tolerance off of a discount 0.1% higher or lower
if form.start_year_2026_or_later?
discount_amount_lower_bound = value * (discount - 0.1) / 100
discount_amount_upper_bound = value * (discount + 0.1) / 100
(discount_amount_upper_bound - discount_amount_lower_bound) / 2
else
discount ? value * 0.05 / 100 : 1
end
end
def mortgage_deposit_and_grant_total def mortgage_deposit_and_grant_total
return if deposit.blank? return if deposit.blank?
@ -506,10 +543,12 @@ class SalesLog < Log
["owning_organisation_id", ["owning_organisation_id",
"saledate", "saledate",
"purchid", "purchid",
"address_line1",
"postcode_full",
"uprn",
"age1", "age1",
"sex1",
"ecstat1", "ecstat1",
uprn.blank? ? "postcode_full" : "uprn"].compact form.start_year_2026_or_later? ? "sexrab1" : "sex1"].compact
end end
def soctenant_is_inferred? def soctenant_is_inferred?
@ -583,4 +622,8 @@ class SalesLog < Log
def mscharge_value def mscharge_value
mscharge if discounted_ownership_sale? || !form.start_year_2025_or_later? mscharge if discounted_ownership_sale? || !form.start_year_2025_or_later?
end end
def hasservicechargeschanged_label
form.get_question(:hasservicechargeschanged, self)&.label_from_value(hasservicechargeschanged)
end
end end

9
app/models/validations/sales/financial_validations.rb

@ -139,6 +139,15 @@ module Validations::Sales::FinancialValidations
end end
end end
def validate_newservicecharges_different_from_mscharge(record)
return unless record.hasservicechargeschanged == 1 && record.newservicecharges && record.has_mscharge == 1 && record.mscharge
if record.newservicecharges == record.mscharge
record.errors.add :newservicecharges, I18n.t("validations.sales.financial.newservicecharges.same_as_previous")
record.errors.add :mscharge, I18n.t("validations.sales.financial.mscharge.same_as_new")
end
end
private private
def is_relationship_child?(relationship) def is_relationship_child?(relationship)

17
app/models/validations/sales/sale_information_validations.rb

@ -80,10 +80,9 @@ module Validations::Sales::SaleInformationValidations
return unless record.mortgage || record.mortgageused == 2 || record.mortgageused == 3 return unless record.mortgage || record.mortgageused == 2 || record.mortgageused == 3
return unless record.discount || record.grant || record.type == 29 return unless record.discount || record.grant || record.type == 29
# When a percentage discount is used, a percentage tolerance is needed to account for rounding errors tolerance = record.value_with_discount_tolerance
tolerance = record.discount ? record.value * 0.05 / 100 : 1
if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil?) && record.discounted_ownership_sale? if over_tolerance?(record.mortgage_deposit_and_grant_total, record.value_with_discount, tolerance, strict: !record.discount.nil? || record.form.start_year_2026_or_later?) && record.discounted_ownership_sale?
deposit_and_grant_sentence = record.grant.present? ? ", cash deposit (#{record.field_formatted_as_currency('deposit')}), and grant (#{record.field_formatted_as_currency('grant')})" : " and cash deposit (#{record.field_formatted_as_currency('deposit')})" deposit_and_grant_sentence = record.grant.present? ? ", cash deposit (#{record.field_formatted_as_currency('deposit')}), and grant (#{record.field_formatted_as_currency('grant')})" : " and cash deposit (#{record.field_formatted_as_currency('deposit')})"
discount_sentence = record.discount.present? ? " (#{record.field_formatted_as_currency('value')}) subtracted by the sum of the full purchase price (#{record.field_formatted_as_currency('value')}) multiplied by the percentage discount (#{record.discount}%)" : "" discount_sentence = record.discount.present? ? " (#{record.field_formatted_as_currency('value')}) subtracted by the sum of the full purchase price (#{record.field_formatted_as_currency('value')}) multiplied by the percentage discount (#{record.discount}%)" : ""
%i[mortgageused mortgage value deposit discount grant].each do |field| %i[mortgageused mortgage value deposit discount grant].each do |field|
@ -204,10 +203,12 @@ module Validations::Sales::SaleInformationValidations
def check_non_staircasing_socialhomebuy_mortgage(record) def check_non_staircasing_socialhomebuy_mortgage(record)
return unless record.cashdis return unless record.cashdis
tolerance = record.expected_shared_ownership_deposit_value_tolerance
if record.mortgage_used? if record.mortgage_used?
return unless record.mortgage return unless record.mortgage
if over_tolerance?(record.mortgage_deposit_and_discount_total, record.expected_shared_ownership_deposit_value, 1) if over_tolerance?(record.mortgage_deposit_and_discount_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgage value deposit cashdis equity].each do |field| %i[mortgage value deposit cashdis equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used_socialhomebuy", record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used_socialhomebuy",
mortgage: record.field_formatted_as_currency("mortgage"), mortgage: record.field_formatted_as_currency("mortgage"),
@ -228,7 +229,7 @@ module Validations::Sales::SaleInformationValidations
expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe
end end
elsif record.mortgage_not_used? elsif record.mortgage_not_used?
if over_tolerance?(record.deposit_and_discount_total, record.expected_shared_ownership_deposit_value, 1) if over_tolerance?(record.deposit_and_discount_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgageused value deposit cashdis equity].each do |field| %i[mortgageused value deposit cashdis equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used_socialhomebuy", record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used_socialhomebuy",
deposit_and_discount_total: record.field_formatted_as_currency("deposit_and_discount_total"), deposit_and_discount_total: record.field_formatted_as_currency("deposit_and_discount_total"),
@ -250,10 +251,12 @@ module Validations::Sales::SaleInformationValidations
end end
def check_non_staircasing_non_socialhomebuy_mortgage(record) def check_non_staircasing_non_socialhomebuy_mortgage(record)
tolerance = record.expected_shared_ownership_deposit_value_tolerance
if record.mortgage_used? if record.mortgage_used?
return unless record.mortgage return unless record.mortgage
if over_tolerance?(record.mortgage_and_deposit_total, record.expected_shared_ownership_deposit_value, 1) if over_tolerance?(record.mortgage_and_deposit_total, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgage value deposit equity].each do |field| %i[mortgage value deposit equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used", record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_used",
mortgage: record.field_formatted_as_currency("mortgage"), mortgage: record.field_formatted_as_currency("mortgage"),
@ -272,7 +275,7 @@ module Validations::Sales::SaleInformationValidations
expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe expected_shared_ownership_deposit_value: record.field_formatted_as_currency("expected_shared_ownership_deposit_value")).html_safe
end end
elsif record.mortgage_not_used? elsif record.mortgage_not_used?
if over_tolerance?(record.deposit, record.expected_shared_ownership_deposit_value, 1) if over_tolerance?(record.deposit, record.expected_shared_ownership_deposit_value, tolerance, strict: record.form.start_year_2026_or_later?)
%i[mortgageused value deposit equity].each do |field| %i[mortgageused value deposit equity].each do |field|
record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used", record.errors.add field, I18n.t("validations.sales.sale_information.#{field}.non_staircasing_mortgage.mortgage_not_used",
deposit: record.field_formatted_as_currency("deposit"), deposit: record.field_formatted_as_currency("deposit"),

1
app/models/validations/sales/soft_validations.rb

@ -117,6 +117,7 @@ module Validations::Sales::SoftValidations
def mortgage_plus_deposit_less_than_discounted_value? def mortgage_plus_deposit_less_than_discounted_value?
return unless mortgage && deposit && value && discount return unless mortgage && deposit && value && discount
return if form.start_year_2026_or_later?
discounted_value = value * (100 - discount) / 100 discounted_value = value * (100 - discount) / 100
mortgage + deposit < discounted_value mortgage + deposit < discounted_value

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

@ -533,6 +533,8 @@ class BulkUpload::Sales::Year2025::RowParser
"field_2", # saledate "field_2", # saledate
"field_3", # saledate "field_3", # saledate
"field_7", # purchaser_code "field_7", # purchaser_code
"field_16", # uprn
"field_17", # address_line1
"field_21", # postcode "field_21", # postcode
"field_22", # postcode "field_22", # postcode
"field_28", # age1 "field_28", # age1
@ -1287,6 +1289,8 @@ private
ecstat1 ecstat1
owning_organisation owning_organisation
postcode_full postcode_full
uprn
address_line1
purchid purchid
] ]
end end
@ -1457,6 +1461,8 @@ private
errors.add(:field_1, error_message) # Sale completion date errors.add(:field_1, error_message) # Sale completion date
errors.add(:field_2, error_message) # Sale completion date errors.add(:field_2, error_message) # Sale completion date
errors.add(:field_3, error_message) # Sale completion date errors.add(:field_3, error_message) # Sale completion date
errors.add(:field_16, error_message) # UPRN
errors.add(:field_17, error_message) # Address line 1
errors.add(:field_21, error_message) # Postcode errors.add(:field_21, error_message) # Postcode
errors.add(:field_22, error_message) # Postcode errors.add(:field_22, error_message) # Postcode
errors.add(:field_28, error_message) # Buyer 1 age errors.add(:field_28, error_message) # Buyer 1 age

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 = 134 FIELDS = 136
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".."ED").to_a @cols ||= ("A".."EF").to_a
end end
def row_parsers def row_parsers

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

@ -149,6 +149,8 @@ class BulkUpload::Sales::Year2026::RowParser
field_132: "If 'No', enter person 5's gender identity", field_132: "If 'No', enter person 5's gender identity",
field_133: "Is the gender person 6 identifies with the same as their sex registered at birth?", field_133: "Is the gender person 6 identifies with the same as their sex registered at birth?",
field_134: "If 'No', enter person 6's gender identity", field_134: "If 'No', enter person 6's gender identity",
field_135: "Will the service charge change after this staircasing transaction takes place?",
field_136: "What are the new total monthly service charges for the property?",
}.freeze }.freeze
ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze
@ -328,6 +330,9 @@ class BulkUpload::Sales::Year2026::RowParser
attribute :field_133, :integer attribute :field_133, :integer
attribute :field_134, :string attribute :field_134, :string
attribute :field_135, :integer
attribute :field_136, :decimal
validates :field_1, validates :field_1,
presence: { presence: {
message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."), message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."),
@ -581,6 +586,8 @@ class BulkUpload::Sales::Year2026::RowParser
"field_2", # saledate "field_2", # saledate
"field_3", # saledate "field_3", # saledate
"field_7", # purchaser_code "field_7", # purchaser_code
"field_16", # uprn
"field_17", # address_line1
"field_21", # postcode "field_21", # postcode
"field_22", # postcode "field_22", # postcode
"field_28", # age1 "field_28", # age1
@ -863,6 +870,7 @@ private
sexrab4: %i[field_48], sexrab4: %i[field_48],
sexrab5: %i[field_52], sexrab5: %i[field_52],
sexrab6: %i[field_56], sexrab6: %i[field_56],
buildheightclass: %i[field_122], buildheightclass: %i[field_122],
gender_same_as_sex1: %i[field_123], gender_same_as_sex1: %i[field_123],
@ -877,6 +885,9 @@ private
gender_description5: %i[field_132], gender_description5: %i[field_132],
gender_same_as_sex6: %i[field_133], gender_same_as_sex6: %i[field_133],
gender_description6: %i[field_134], gender_description6: %i[field_134],
hasservicechargeschanged: %i[field_135],
newservicecharges: %i[field_136],
} }
end end
@ -926,6 +937,9 @@ private
attributes["gender_same_as_sex6"] = field_133 attributes["gender_same_as_sex6"] = field_133
attributes["gender_description6"] = field_134 attributes["gender_description6"] = field_134
attributes["hasservicechargeschanged"] = field_135
attributes["newservicecharges"] = field_136
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)
attributes["relat4"] = relationship_from_is_partner(field_46) attributes["relat4"] = relationship_from_is_partner(field_46)
@ -1378,6 +1392,8 @@ private
ecstat1 ecstat1
owning_organisation owning_organisation
postcode_full postcode_full
uprn
address_line1
purchid purchid
] ]
end end
@ -1548,6 +1564,8 @@ private
errors.add(:field_1, error_message) # Sale completion date errors.add(:field_1, error_message) # Sale completion date
errors.add(:field_2, error_message) # Sale completion date errors.add(:field_2, error_message) # Sale completion date
errors.add(:field_3, error_message) # Sale completion date errors.add(:field_3, error_message) # Sale completion date
errors.add(:field_16, error_message) # UPRN
errors.add(:field_17, error_message) # Address line 1
errors.add(:field_21, error_message) # Postcode errors.add(:field_21, error_message) # Postcode
errors.add(:field_22, error_message) # Postcode errors.add(:field_22, error_message) # Postcode
errors.add(:field_28, error_message) # Buyer 1 age errors.add(:field_28, error_message) # Buyer 1 age

6
app/services/exports/sales_log_export_constants.rb

@ -157,7 +157,11 @@ module Exports::SalesLogExportConstants
YEAR_2025_EXPORT_FIELDS << "SEX#{index}" YEAR_2025_EXPORT_FIELDS << "SEX#{index}"
end end
YEAR_2026_EXPORT_FIELDS = Set["BUILDHEIGHTCLASS"] YEAR_2026_EXPORT_FIELDS = Set[
"BUILDHEIGHTCLASS",
"HASSERVICECHARGESCHANGED",
"NEWSERVICECHARGES",
]
(1..6).each do |index| (1..6).each do |index|
YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}" YEAR_2026_EXPORT_FIELDS << "SEXRAB#{index}"

3
app/services/exports/sales_log_export_service.rb

@ -135,6 +135,9 @@ module Exports
attribute_hash["hasestatefee"] = sales_log.has_management_fee attribute_hash["hasestatefee"] = sales_log.has_management_fee
attribute_hash["estatefee"] = sales_log.management_fee attribute_hash["estatefee"] = sales_log.management_fee
attribute_hash["hasservicechargeschanged"] = sales_log.hasservicechargeschanged
attribute_hash["newservicecharges"] = sales_log.newservicecharges
attribute_hash["stairlastday"] = sales_log.lasttransaction&.day attribute_hash["stairlastday"] = sales_log.lasttransaction&.day
attribute_hash["stairlastmonth"] = sales_log.lasttransaction&.month attribute_hash["stairlastmonth"] = sales_log.lasttransaction&.month
attribute_hash["stairlastyear"] = sales_log.lasttransaction&.year attribute_hash["stairlastyear"] = sales_log.lasttransaction&.year

16
config/locales/forms/2025/lettings/tenancy_information.en.yml

@ -21,27 +21,27 @@ en:
tenancy_type: tenancy_type:
page_header: "" page_header: ""
tenancy: tenancy:
check_answer_label: "Type of main tenancy" check_answer_label: "Type of tenancy"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "What is the type of tenancy?" question_text: "What is the type of tenancy?"
tenancyother: tenancyother:
check_answer_label: "" check_answer_label: "Type of tenancy (other)"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Please state the tenancy type" question_text: "Please state the type of tenancy"
starter_tenancy_type: starter_tenancy_type:
page_header: "" page_header: ""
tenancy: tenancy:
check_answer_label: "Type of main tenancy after the starter or introductory period has ended" check_answer_label: "Type of tenancy"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: "This is for the main tenancy after any starter or introductory period."
question_text: "What is the type of tenancy after the starter or introductory period has ended?" question_text: "What is the type of tenancy?"
tenancyother: tenancyother:
check_answer_label: "" check_answer_label: "Type of tenancy (other)"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Please state the tenancy type" question_text: "Please state the type of tenancy"
tenancylength: tenancylength:
tenancy_length: tenancy_length:

16
config/locales/forms/2026/lettings/tenancy_information.en.yml

@ -21,27 +21,27 @@ en:
tenancy_type: tenancy_type:
page_header: "" page_header: ""
tenancy: tenancy:
check_answer_label: "Type of main tenancy" check_answer_label: "Type of tenancy"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "What is the type of tenancy?" question_text: "What is the type of tenancy?"
tenancyother: tenancyother:
check_answer_label: "" check_answer_label: "Type of tenancy (other)"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Please state the tenancy type" question_text: "Please state the type of tenancy"
starter_tenancy_type: starter_tenancy_type:
page_header: "" page_header: ""
tenancy: tenancy:
check_answer_label: "Type of main tenancy after the starter or introductory period has ended" check_answer_label: "Type of tenancy"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: "This is for the main tenancy after any starter or introductory period."
question_text: "What is the type of tenancy after the starter or introductory period has ended?" question_text: "What is the type of tenancy?"
tenancyother: tenancyother:
check_answer_label: "" check_answer_label: "Type of tenancy (other)"
check_answer_prompt: "" check_answer_prompt: ""
hint_text: "" hint_text: ""
question_text: "Please state the tenancy type" question_text: "Please state the type of tenancy"
tenancylength: tenancylength:
tenancy_length: tenancy_length:

13
config/locales/forms/2026/sales/sale_information.en.yml

@ -285,6 +285,19 @@ en:
hint_text: "" hint_text: ""
question_text: "Enter the total monthly charge" question_text: "Enter the total monthly charge"
servicecharges_changed:
page_header: ""
has_service_charges_changed:
check_answer_label: "Service charge will change"
check_answer_prompt: "Tell us if the service charge will change"
hint_text: "This includes any charges for day-to-day maintenance and repairs, building insurance, and any contributions to a sinking or reserved fund. It does not include estate management fees."
question_text: "Will the service charge change after this staircasing transaction takes place?"
new_service_charges:
check_answer_label: "New monthly service charges"
check_answer_prompt: ""
hint_text: ""
question_text: "Enter the new total monthly charge"
purchase_price: purchase_price:
discounted_ownership: discounted_ownership:
page_header: "About the price of the property" page_header: "About the price of the property"

4
config/locales/validations/sales/financial.en.yml

@ -48,6 +48,10 @@ en:
mscharge: mscharge:
monthly_leasehold_charges: monthly_leasehold_charges:
not_zero: "Monthly leasehold charges cannot be £0 if the property has monthly charges." not_zero: "Monthly leasehold charges cannot be £0 if the property has monthly charges."
same_as_new: "You said that the service charge will change and you entered the same amount as the new service charge. If the service charge will not change, answer 'No' to that question before entering the service charge here."
newservicecharges:
same_as_previous: "You said that the service charge will change and you entered the same amount as the previous question. If the service charge will not change, answer 'No'."
resale: resale:
equity_over_max: "The maximum initial equity stake is %{max_equity}%." equity_over_max: "The maximum initial equity stake is %{max_equity}%."

1185
config/sale_range_data/2026.csv

File diff suppressed because it is too large Load Diff

8
db/migrate/20260305095832_add_service_charge_changed_to_sales_logs.rb

@ -0,0 +1,8 @@
class AddServiceChargeChangedToSalesLogs < ActiveRecord::Migration[7.2]
def change
change_table :sales_logs, bulk: true do |t|
t.column :hasservicechargeschanged, :integer
t.column :newservicecharges, :decimal, precision: 10, scale: 2
end
end
end

6
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do ActiveRecord::Schema[7.2].define(version: 2026_03_05_095832) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -824,8 +824,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_02_25_135309) do
t.string "sexrab4" t.string "sexrab4"
t.string "sexrab5" t.string "sexrab5"
t.string "sexrab6" t.string "sexrab6"
t.integer "mortlen_known"
t.integer "buildheightclass" t.integer "buildheightclass"
t.integer "mortlen_known"
t.integer "hasservicechargeschanged"
t.decimal "newservicecharges", precision: 10, scale: 2
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"

1
spec/factories/sales_log.rb

@ -67,6 +67,7 @@ FactoryBot.define do
sexrab1 { "F" } sexrab1 { "F" }
sex1 { "F" } sex1 { "F" }
ecstat1 { 1 } ecstat1 { 1 }
address_line1 { "same address line 1" }
postcode_full { "A1 1AA" } postcode_full { "A1 1AA" }
noint { 2 } noint { 2 }
uprn_known { 0 } uprn_known { 0 }

10
spec/features/sales_log_spec.rb

@ -352,16 +352,6 @@ RSpec.describe "Sales Log Features" do
end end
context "when a log becomes a duplicate" do context "when a log becomes a duplicate" do
before do
Timecop.freeze(Time.zone.local(2024, 3, 3))
Singleton.__init__(FormHandler)
end
after do
Timecop.return
Singleton.__init__(FormHandler)
end
context "and updating duplicate log" do context "and updating duplicate log" do
let(:user) { create(:user, :data_coordinator) } let(:user) { create(:user, :data_coordinator) }
let(:sales_log) { create(:sales_log, :duplicate, assigned_to: user) } let(:sales_log) { create(:sales_log, :duplicate, assigned_to: user) }

2
spec/fixtures/exports/sales_log_26_27.xml vendored

@ -163,5 +163,7 @@
<MSCHARGE_VALUE_CHECK/> <MSCHARGE_VALUE_CHECK/>
<DUPLICATESET/> <DUPLICATESET/>
<STAIRCASETOSALE/> <STAIRCASETOSALE/>
<HASSERVICECHARGESCHANGED/>
<NEWSERVICECHARGES/>
</form> </form>
</forms> </forms>

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

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/sales_logs_csv_export_non_support_labels_26.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/variable_definitions/sales_download_26_27.csv vendored

@ -17,3 +17,5 @@ gender_same_as_sex5,Is the gender person 5 identifies with the same as their sex
gender_description5,If 'No', enter person 5's gender identity gender_description5,If 'No', enter person 5's gender identity
gender_same_as_sex6,Is the gender person 6 identifies with the same as their sex registered at birth? gender_same_as_sex6,Is the gender person 6 identifies with the same as their sex registered at birth?
gender_description6,If 'No', enter person 6's gender identity gender_description6,If 'No', enter person 6's gender identity
hasservicechargeschanged,Will the service charge change after this staircasing transaction takes place?
newservicecharges,What are the new total monthly service charges for the property?

1 sexrab1,What was buyer 1's sex at birth?
17 gender_description5,If 'No', enter person 5's gender identity
18 gender_same_as_sex6,Is the gender person 6 identifies with the same as their sex registered at birth?
19 gender_description6,If 'No', enter person 6's gender identity
20 hasservicechargeschanged,Will the service charge change after this staircasing transaction takes place?
21 newservicecharges,What are the new total monthly service charges for the property?

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) { 463 } let(:total_variable_definitions_count) { 465 }
before do before do
Rake.application.rake_require("tasks/log_variable_definitions") Rake.application.rake_require("tasks/log_variable_definitions")

2
spec/models/form/sales/pages/purchase_price_outright_ownership_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::PurchasePriceOutrightOwnership, type: :model
let(:page_id) { "purchase_price" } let(:page_id) { "purchase_price" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(: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: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false)) }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)

2
spec/models/form/sales/pages/purchase_price_spec.rb

@ -8,7 +8,7 @@ RSpec.describe Form::Sales::Pages::PurchasePrice, type: :model do
let(:subsection) { instance_double(Form::Subsection) } let(:subsection) { instance_double(Form::Subsection) }
before do before do
allow(subsection).to receive(:form).and_return(instance_double(Form, start_year_2024_or_later?: false, start_date: Time.zone.local(2023, 4, 1))) allow(subsection).to receive(:form).and_return(instance_double(Form, start_year_2024_or_later?: false, start_year_2026_or_later?: false, start_date: Time.zone.local(2023, 4, 1)))
end end
it "has correct subsection" do it "has correct subsection" do

31
spec/models/form/sales/pages/service_charge_changed_spec.rb

@ -0,0 +1,31 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::ServiceChargeChanged, 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: collection_start_date_for_year(2026))) }
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[hasservicechargeschanged newservicecharges])
end
it "has the correct id" do
expect(page.id).to eq("service_charge_changed")
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

2
spec/models/form/sales/pages/value_shared_ownership_spec.rb

@ -5,7 +5,7 @@ RSpec.describe Form::Sales::Pages::ValueSharedOwnership, type: :model do
let(:page_id) { "value_shared_ownership" } let(:page_id) { "value_shared_ownership" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)), id: "shared_ownership") } let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1), start_year_2026_or_later?: false), id: "shared_ownership") }
before do before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false) allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)

56
spec/models/form/sales/questions/has_service_charges_changed_spec.rb

@ -0,0 +1,56 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::HasServiceChargesChanged, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:page) { instance_double(Form::Page, subsection:) }
let(:start_date) { collection_start_date_for_year(2026) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("hasservicechargeschanged")
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" => "Yes" },
"2" => { "value" => "No" },
})
end
it "has correct conditional for" do
expect(question.conditional_for).to eq({
"newservicecharges" => [1],
})
end
it "has correct hidden_in_check_answers for" do
expect(question.hidden_in_check_answers).to eq({
"depends_on" => [
{
"hasservicechargeschanged" => 1,
},
],
})
end
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end

53
spec/models/form/sales/questions/new_service_charges_spec.rb

@ -0,0 +1,53 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::NewServiceCharges, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:page) { instance_double(Form::Page, subsection:) }
let(:start_date) { collection_start_date_for_year(2026) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("newservicecharges")
end
it "has the correct type" do
expect(question.type).to eq("numeric")
end
it "is not marked as derived" do
expect(question.derived?(nil)).to be false
end
it "has the correct width" do
expect(question.width).to be 5
end
it "has the correct min" do
expect(question.min).to be 0
end
it "has the correct max" do
expect(question.max).to be 9999.99
end
it "has the correct step" do
expect(question.step).to be 0.01
end
it "has the correct prefix" do
expect(question.prefix).to eq("£")
end
it "has the correct question number" do
expect(question.question_number).to eq(0)
end
end

23
spec/models/form/sales/questions/purchase_price_spec.rb

@ -1,11 +1,15 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 1) } subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 1) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { 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(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?))) }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -30,6 +34,8 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
context "when discounted ownership scheme" do context "when discounted ownership scheme" do
subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 2) } subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 2) }
let(:start_year) { 2023 }
it "has the correct question_number" do it "has the correct question_number" do
expect(question.question_number).to eq(100) expect(question.question_number).to eq(100)
end end
@ -38,6 +44,8 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
context "when outright sale" do context "when outright sale" do
subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 3) } subject(:question) { described_class.new(question_id, question_definition, page, ownershipsch: 3) }
let(:start_year) { 2023 }
it "has the correct question_number" do it "has the correct question_number" do
expect(question.question_number).to eq(110) expect(question.question_number).to eq(110)
end end
@ -51,7 +59,20 @@ RSpec.describe Form::Sales::Questions::PurchasePrice, type: :model do
expect(question.prefix).to eq("£") expect(question.prefix).to eq("£")
end end
context "with year 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
it "has correct min" do it "has correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(0)
end end
end
context "with year 2026", metadata: { year: 26 } do
let(:start_year) { 2026 }
let(:start_year_2026_or_later?) { true }
it "has correct min" do
expect(question.min).to eq(15_000)
end
end
end end

19
spec/models/form/sales/questions/value_spec.rb

@ -1,11 +1,15 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::Value, type: :model do RSpec.describe Form::Sales::Questions::Value, type: :model do
include CollectionTimeHelper
subject(:question) { described_class.new(question_id, question_definition, page) } subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, id: "value_shared_ownership", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: Time.zone.local(2023, 4, 1)), id: "shared_ownership")) } let(:start_year) { current_collection_start_year }
let(:start_year_2026_or_later?) { false }
let(:page) { instance_double(Form::Page, id: "value_shared_ownership", subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date: collection_start_date_for_year(start_year), start_year_2026_or_later?: start_year_2026_or_later?), id: "shared_ownership")) }
before do before do
allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false) allow(page.subsection.form).to receive(:start_year_2025_or_later?).and_return(false)
@ -35,7 +39,20 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do
expect(question.prefix).to eq("£") expect(question.prefix).to eq("£")
end end
context "with year 2025", metadata: { year: 25 } do
let(:start_year) { 2025 }
it "has correct min" do it "has correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(0)
end end
end
context "with year 2026", metadata: { year: 26 } do
let(:start_year) { 2026 }
let(:start_year_2026_or_later?) { true }
it "has correct min" do
expect(question.min).to eq(15_000)
end
end
end end

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

@ -77,6 +77,7 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipStaircasingTransaction,
monthly_rent_staircasing_owned monthly_rent_staircasing_owned
monthly_rent_staircasing monthly_rent_staircasing
service_charge_staircasing service_charge_staircasing
service_charge_changed
monthly_charges_shared_ownership_value_check monthly_charges_shared_ownership_value_check
], ],
) )

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

@ -478,4 +478,63 @@ RSpec.describe Validations::Sales::FinancialValidations do
expect(record.errors).to be_empty expect(record.errors).to be_empty
end end
end end
describe "#validate_newservicecharges_different_from_mscharge" do
let(:record) { FactoryBot.build(:sales_log, ownershipsch: 1, staircase: 1) }
it "does not add errors when hasservicechargeschanged is nil" do
record.hasservicechargeschanged = nil
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when hasservicechargeschanged is 2 (No)" do
record.hasservicechargeschanged = 2
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when newservicecharges is nil" do
record.hasservicechargeschanged = 1
record.newservicecharges = nil
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when mscharge is nil" do
record.hasservicechargeschanged = 1
record.newservicecharges = 100
record.has_mscharge = 2
record.mscharge = nil
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "does not add errors when newservicecharges is different from mscharge" do
record.hasservicechargeschanged = 1
record.newservicecharges = 150
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors).to be_empty
end
it "adds an error when hasservicechargeschanged is 1 (Yes) and newservicecharges equals mscharge" do
record.hasservicechargeschanged = 1
record.newservicecharges = 100
record.has_mscharge = 1
record.mscharge = 100
financial_validator.validate_newservicecharges_different_from_mscharge(record)
expect(record.errors["newservicecharges"]).to include(match I18n.t("validations.sales.financial.newservicecharges.same_as_previous"))
expect(record.errors["mscharge"]).to include(match I18n.t("validations.sales.financial.mscharge.same_as_new"))
end
end
end end

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

@ -663,6 +663,46 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
expect(record.errors["grant"]).to be_empty expect(record.errors["grant"]).to be_empty
end end
end end
context "with year 2026", :aggregate_failures do
let(:saledate) { Time.zone.local(2026, 4, 1) }
context "when mortgage and deposit is exact" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10, ownershipsch: 2, type: 9) }
it "does not add an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["discount"]).to be_empty
end
end
context "when mortgage and deposit is within 0.1% discount tolerance" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10.1, ownershipsch: 2, type: 9) }
it "does not add an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["discount"]).to be_empty
end
end
context "when mortgage and deposit is outside 0.1% discount tolerance" do
let(:record) { FactoryBot.build(:sales_log, saledate:, mortgage: 85_000, deposit: 5_000, value: 100_000, discount: 10.2, ownershipsch: 2, type: 9) }
it "adds an error" do
sale_information_validator.validate_discounted_ownership_value(record)
expect(record.errors["mortgage"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["discount"]).not_to be_empty
end
end
end
end end
describe "#validate_outright_sale_value_matches_mortgage_plus_deposit" do describe "#validate_outright_sale_value_matches_mortgage_plus_deposit" do
@ -1171,6 +1211,82 @@ RSpec.describe Validations::Sales::SaleInformationValidations do
end end
end end
end end
context "with year 2026", :aggregate_failures do
let(:saledate) { Time.zone.local(2026, 4, 1) }
context "when mortgage and deposit is exact" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when mortgage and deposit is within 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15.1, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when mortgage and deposit is outside 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 1, mortgage: 10_000, staircase: 2, deposit: 5_000, value: 100_000, equity: 15.2, ownershipsch: 1, type: 30, saledate:) }
it "adds an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgage"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["equity"]).not_to be_empty
end
end
context "when deposit (no mortgage) is exact" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when deposit (no mortgage) is within 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15.1, ownershipsch: 1, type: 30, saledate:) }
it "does not add an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).to be_empty
expect(record.errors["value"]).to be_empty
expect(record.errors["deposit"]).to be_empty
expect(record.errors["equity"]).to be_empty
end
end
context "when deposit (no mortgage) is outside 0.1% equity tolerance" do
let(:record) { FactoryBot.build(:sales_log, mortgageused: 2, staircase: 2, deposit: 15_000, value: 100_000, equity: 15.2, ownershipsch: 1, type: 30, saledate:) }
it "adds an error" do
sale_information_validator.validate_non_staircasing_mortgage(record)
expect(record.errors["mortgageused"]).not_to be_empty
expect(record.errors["value"]).not_to be_empty
expect(record.errors["deposit"]).not_to be_empty
expect(record.errors["equity"]).not_to be_empty
end
end
end
end end
describe "#validate_staircasing_mortgage" do describe "#validate_staircasing_mortgage" do

18
spec/requests/duplicate_logs_controller_spec.rb

@ -170,7 +170,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 3) expect(page).to have_content("- Buyer 1’s gender identity", count: 3)
expect(page).to have_content("- Buyer 1’s working situation", count: 3) expect(page).to have_content("- Buyer 1’s working situation", count: 3)
expect(page).to have_content("- Postcode", count: 3) expect(page).to have_content("- Postcode", count: 3)
expect(page).to have_link("Change", count: 21) expect(page).to have_content("- Address line 1", count: 3)
expect(page).to have_link("Change", count: 24)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
@ -216,7 +217,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 7) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 8)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -242,7 +244,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 7) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 8)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -377,7 +380,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 3) expect(page).to have_content("- Buyer 1’s gender identity", count: 3)
expect(page).to have_content("- Buyer 1’s working situation", count: 3) expect(page).to have_content("- Buyer 1’s working situation", count: 3)
expect(page).to have_content("- Postcode", count: 3) expect(page).to have_content("- Postcode", count: 3)
expect(page).to have_link("Change", count: 18) expect(page).to have_content("- Address line 1", count: 3)
expect(page).to have_link("Change", count: 21)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
@ -405,7 +409,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 6) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 7)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -431,7 +436,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 6) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 7)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end

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

@ -225,7 +225,7 @@ RSpec.describe BulkUpload::Sales::Validator do
end end
it "creates errors" do it "creates errors" do
expect { validator.call }.to change(BulkUploadError.where(category: :setup, error: "This is a duplicate of a log in your file."), :count).by(20) expect { validator.call }.to change(BulkUploadError.where(category: :setup, error: "This is a duplicate of a log in your file."), :count).by(24)
end end
end end
end end

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

@ -784,6 +784,8 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age
@ -814,6 +816,8 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age

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

@ -116,6 +116,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
field_123: "1", field_123: "1",
field_125: "2", field_125: "2",
field_126: "Non-binary", field_126: "Non-binary",
field_135: "1",
field_136: "150",
} }
end end
@ -788,6 +790,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age
@ -818,6 +822,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age

87
spec/services/exports/sales_log_export_service_spec.rb

@ -451,18 +451,12 @@ RSpec.describe Exports::SalesLogExportService do
end end
context "with shared ownership and mscharge" do context "with shared ownership and mscharge" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 2, type: 30, mscharge: 321, has_management_fee: 1, management_fee: 222) } def replace_shared_ownership_values(export_file)
def replace_mscharge_and_shared_ownership_values(export_file)
export_file.sub!("<HASSERVICECHARGES/>", "<HASSERVICECHARGES>1</HASSERVICECHARGES>") export_file.sub!("<HASSERVICECHARGES/>", "<HASSERVICECHARGES>1</HASSERVICECHARGES>")
export_file.sub!("<SERVICECHARGES/>", "<SERVICECHARGES>321.0</SERVICECHARGES>") export_file.sub!("<SERVICECHARGES/>", "<SERVICECHARGES>321.0</SERVICECHARGES>")
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<MSCHARGE>100.0</MSCHARGE>", "<MSCHARGE/>") export_file.sub!("<MSCHARGE>100.0</MSCHARGE>", "<MSCHARGE/>")
export_file.sub!("<HASMSCHARGE>1</HASMSCHARGE>", "<HASMSCHARGE/>") export_file.sub!("<HASMSCHARGE>1</HASMSCHARGE>", "<HASMSCHARGE/>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>30</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>2</STAIRCASE>")
export_file.sub!("<GRANT>10000.0</GRANT>", "<GRANT/>") export_file.sub!("<GRANT>10000.0</GRANT>", "<GRANT/>")
export_file.sub!("<PPCODENK>0</PPCODENK>", "<PPCODENK>1</PPCODENK>") export_file.sub!("<PPCODENK>0</PPCODENK>", "<PPCODENK>1</PPCODENK>")
export_file.sub!("<PPOSTC1>SW1A</PPOSTC1>", "<PPOSTC1/>") export_file.sub!("<PPOSTC1>SW1A</PPOSTC1>", "<PPOSTC1/>")
@ -474,9 +468,20 @@ RSpec.describe Exports::SalesLogExportService do
export_file.sub!("<PREVLOCNAME>Westminster</PREVLOCNAME>", "<PREVLOCNAME/>") export_file.sub!("<PREVLOCNAME>Westminster</PREVLOCNAME>", "<PREVLOCNAME/>")
end end
context "when not staircasing" do
let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 2, type: 30, mscharge: 321, has_management_fee: 1, management_fee: 222) }
def replace_non_staircasing_values(export_file)
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>30</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>2</STAIRCASE>")
end
it "exports mscharge fields as hasmscharge and mscharge" do it "exports mscharge fields as hasmscharge and mscharge" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read) expected_content = replace_entity_ids(sales_log, xml_export_file.read)
expected_content = replace_mscharge_and_shared_ownership_values(expected_content) replace_shared_ownership_values(expected_content)
replace_non_staircasing_values(expected_content)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| 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) entry = Zip::File.open_buffer(content).find_entry(expected_data_filename)
expect(entry).not_to be_nil expect(entry).not_to be_nil
@ -486,6 +491,72 @@ RSpec.describe Exports::SalesLogExportService do
export_service.export_xml_sales_logs export_service.export_xml_sales_logs
end end
end end
context "when staircasing" do
context "when exporting only 26/27 collection period", metadata: { year: 26 } do
let(:start_time) { collection_start_date_for_year(2026) }
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") }
let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 1, type: 2, mscharge: 321, has_management_fee: 1, management_fee: 222, hasservicechargeschanged: 1, newservicecharges: 150) }
def replace_staircasing_values(export_file)
export_file.sub!("<HASESTATEFEE/>", "<HASESTATEFEE>1</HASESTATEFEE>")
export_file.sub!("<ESTATEFEE/>", "<ESTATEFEE>222.0</ESTATEFEE>")
export_file.sub!("<TYPE>8</TYPE>", "<TYPE>2</TYPE>")
export_file.sub!("<STAIRCASE/>", "<STAIRCASE>1</STAIRCASE>")
export_file.sub!("<ARMEDFORCESSPOUSE>5</ARMEDFORCESSPOUSE>", "<ARMEDFORCESSPOUSE/>")
export_file.sub!("<BUILTYPE>1</BUILTYPE>", "<BUILTYPE/>")
export_file.sub!("<BUY2LIVING>3</BUY2LIVING>", "<BUY2LIVING/>")
export_file.sub!("<DEPOSIT>80000.0</DEPOSIT>", "<DEPOSIT/>")
export_file.sub!("<DISABLED>1</DISABLED>", "<DISABLED/>")
export_file.sub!("<ECSTAT1>1</ECSTAT1>", "<ECSTAT1/>")
export_file.sub!("<ECSTAT2>1</ECSTAT2>", "<ECSTAT2/>")
export_file.sub!("<ESTATEFEE>222.0</ESTATEFEE>", "<ESTATEFEE/>")
export_file.sub!("<ETHNIC>17</ETHNIC>", "<ETHNIC/>")
export_file.sub!("<ETHNICGROUP1>17</ETHNICGROUP1>", "<ETHNICGROUP1/>")
export_file.sub!("<ETHNICGROUP2>17</ETHNICGROUP2>", "<ETHNICGROUP2/>")
export_file.sub!("<HASESTATEFEE>1</HASESTATEFEE>", "<HASESTATEFEE/>")
export_file.sub!("<HB>4</HB>", "<HB/>")
export_file.sub!("<HHOLDCOUNT>4</HHOLDCOUNT>", "<HHOLDCOUNT/>")
export_file.sub!("<HHREGRES>7</HHREGRES>", "<HHREGRES/>")
export_file.sub!("<INC1MORT>1</INC1MORT>", "<INC1MORT/>")
export_file.sub!("<INC1NK>0</INC1NK>", "<INC1NK/>")
export_file.sub!("<INC2MORT>1</INC2MORT>", "<INC2MORT/>")
export_file.sub!("<INC2NK>0</INC2NK>", "<INC2NK/>")
export_file.sub!("<INCOME1>10000</INCOME1>", "<INCOME1/>")
export_file.sub!("<INCOME2>10000</INCOME2>", "<INCOME2/>")
export_file.sub!("<LIVEINBUYER1>1</LIVEINBUYER1>", "<LIVEINBUYER1/>")
export_file.sub!("<LIVEINBUYER2>1</LIVEINBUYER2>", "<LIVEINBUYER2/>")
export_file.sub!("<MORTGAGE>20000.0</MORTGAGE>", "<MORTGAGE/>")
export_file.sub!("<MORTLEN1>10</MORTLEN1>", "<MORTLEN1/>")
export_file.sub!("<NATIONALITYALL1>826</NATIONALITYALL1>", "<NATIONALITYALL1/>")
export_file.sub!("<NATIONALITYALL2>826</NATIONALITYALL2>", "<NATIONALITYALL2/>")
export_file.sub!("<PREVOWN>1</PREVOWN>", "<PREVOWN/>")
export_file.sub!("<PREVSHARED>2</PREVSHARED>", "<PREVSHARED/>")
export_file.sub!("<PREVTEN>1</PREVTEN>", "<PREVTEN/>")
export_file.sub!("<SAVINGSNK>1</SAVINGSNK>", "<SAVINGSNK/>")
export_file.sub!("<WCHAIR>1</WCHAIR>", "<WCHAIR/>")
export_file.sub!("<WHEEL>1</WHEEL>", "<WHEEL/>")
export_file.sub!("<HASSERVICECHARGESCHANGED/>", "<HASSERVICECHARGESCHANGED>1</HASSERVICECHARGESCHANGED>")
export_file.sub!("<NEWSERVICECHARGES/>", "<NEWSERVICECHARGES>150.0</NEWSERVICECHARGES>")
end
it "exports mscharge fields and hasservicechargeschanged and newservicecharges" do
expected_content = replace_entity_ids(sales_log, xml_export_file.read)
replace_shared_ownership_values(expected_content)
replace_staircasing_values(expected_content)
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
end
end end
context "and one sales log has not been updated in the time range" do context "and one sales log has not been updated in the time range" do

Loading…
Cancel
Save