diff --git a/Gemfile.lock b/Gemfile.lock index 54b5b82a1..85aa27b6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,70 +1,72 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + actioncable (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailbox (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) mail (>= 2.8.0) - actionmailer (7.2.2.2) - actionpack (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailer (7.2.3.1) + actionpack (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activesupport (= 7.2.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.2) - actionview (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionpack (7.2.3.1) + actionview (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.2) - actionpack (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actiontext (7.2.3.1) + actionpack (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.2) - activesupport (= 7.2.2.2) + actionview (7.2.3.1) + activesupport (= 7.2.3.1) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.2.2) - activesupport (= 7.2.2.2) + activejob (7.2.3.1) + activesupport (= 7.2.3.1) globalid (>= 0.3.6) - activemodel (7.2.2.2) - activesupport (= 7.2.2.2) + activemodel (7.2.3.1) + activesupport (= 7.2.3.1) activemodel-serializers-xml (1.0.3) activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (7.2.2.2) - activemodel (= 7.2.2.2) - activesupport (= 7.2.2.2) + activerecord (7.2.3.1) + activemodel (= 7.2.3.1) + activesupport (= 7.2.3.1) timeout (>= 0.4.0) - activestorage (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activesupport (= 7.2.2.2) + activestorage (7.2.3.1) + actionpack (= 7.2.3.1) + activejob (= 7.2.3.1) + activerecord (= 7.2.3.1) + activesupport (= 7.2.3.1) marcel (~> 1.0) - activesupport (7.2.2.2) + activesupport (7.2.3.1) base64 benchmark (>= 0.3) bigdecimal @@ -73,7 +75,7 @@ GEM drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1) + minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.6) @@ -147,6 +149,7 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy + cgi (0.5.1) childprocess (5.0.0) coderay (1.1.3) coercible (1.0.0) @@ -283,9 +286,7 @@ GEM matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) - minitest (6.0.2) - drb (~> 2.0) - prism (~> 1.5) + minitest (5.27.0) msgpack (1.7.2) multipart-post (2.4.1) nested_form (0.3.2) @@ -368,20 +369,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (7.2.2.2) - actioncable (= 7.2.2.2) - actionmailbox (= 7.2.2.2) - actionmailer (= 7.2.2.2) - actionpack (= 7.2.2.2) - actiontext (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activemodel (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + rails (7.2.3.1) + actioncable (= 7.2.3.1) + actionmailbox (= 7.2.3.1) + actionmailer (= 7.2.3.1) + actionpack (= 7.2.3.1) + actiontext (= 7.2.3.1) + actionview (= 7.2.3.1) + activejob (= 7.2.3.1) + activemodel (= 7.2.3.1) + activerecord (= 7.2.3.1) + activestorage (= 7.2.3.1) + activesupport (= 7.2.3.1) bundler (>= 1.15.0) - railties (= 7.2.2.2) + railties (= 7.2.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -396,13 +397,15 @@ GEM nested_form (~> 0.3) rails (>= 6.0, < 9) turbo-rails (>= 1.0, < 3) - railties (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + railties (7.2.3.1) + actionpack (= 7.2.3.1) + activesupport (= 7.2.3.1) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.1) diff --git a/app/services/bulk_upload/lettings/year2025/row_parser.rb b/app/services/bulk_upload/lettings/year2025/row_parser.rb index d20a361e9..f3b6e7edd 100644 --- a/app/services/bulk_upload/lettings/year2025/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2025/row_parser.rb @@ -525,6 +525,9 @@ class BulkUpload::Lettings::Year2025::RowParser @log ||= LettingsLog.new(attributes_for_log) end + # Will send a "Bulk upload failed" email rather than an "Errors in bulk upload" email. + # The body of the "Bulk upload failed" email says there are errors in the setup section, + # so only use this method for setup section errors. def block_log_creation! self.block_log_creation = true end diff --git a/app/services/bulk_upload/lettings/year2026/row_parser.rb b/app/services/bulk_upload/lettings/year2026/row_parser.rb index c05bfe667..98a38d6e7 100644 --- a/app/services/bulk_upload/lettings/year2026/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2026/row_parser.rb @@ -560,6 +560,9 @@ class BulkUpload::Lettings::Year2026::RowParser @log ||= LettingsLog.new(attributes_for_log) end + # Will send a "Bulk upload failed" email rather than an "Errors in bulk upload" email. + # The body of the "Bulk upload failed" email says there are errors in the setup section, + # so only use this method for setup section errors. def block_log_creation! self.block_log_creation = true end diff --git a/app/services/bulk_upload/sales/year2025/row_parser.rb b/app/services/bulk_upload/sales/year2025/row_parser.rb index bcb00ffd9..fac377138 100644 --- a/app/services/bulk_upload/sales/year2025/row_parser.rb +++ b/app/services/bulk_upload/sales/year2025/row_parser.rb @@ -1290,6 +1290,9 @@ private end end + # Will send a "Bulk upload failed" email rather than an "Errors in bulk upload" email. + # The body of the "Bulk upload failed" email says there are errors in the setup section, + # so only use this method for setup section errors. def block_log_creation! self.block_log_creation = true end diff --git a/app/services/bulk_upload/sales/year2026/row_parser.rb b/app/services/bulk_upload/sales/year2026/row_parser.rb index e2c320659..08668d278 100644 --- a/app/services/bulk_upload/sales/year2026/row_parser.rb +++ b/app/services/bulk_upload/sales/year2026/row_parser.rb @@ -152,6 +152,7 @@ class BulkUpload::Sales::Year2026::RowParser }.freeze ERROR_BASE_KEY = "validations.sales.2026.bulk_upload".freeze + NUMBER_OR_R_FORMAT = /\A(\d+(\.\d+)?|R)\z/i CASE_INSENSITIVE_FIELDS = [ :field_29, # Age of buyer 1 @@ -176,6 +177,11 @@ class BulkUpload::Sales::Year2026::RowParser :field_103, # What is the length of the mortgage in years? - Shared ownership :field_133, # What is the length of the mortgage in years? - Discounted ownership + + :field_107, # What are the total monthly service charges for the property? + :field_125, # What are the monthly service charges for the property? + :field_126, # New monthly service charge amount + :field_136, # What are the total monthly leasehold charges for the property? ].freeze attribute :bulk_upload @@ -296,7 +302,7 @@ class BulkUpload::Sales::Year2026::RowParser attribute :field_104, :decimal attribute :field_105, :decimal attribute :field_106, :decimal - attribute :field_107, :decimal + attribute :field_107, :string attribute :field_108, :decimal attribute :field_109, :decimal @@ -315,8 +321,8 @@ class BulkUpload::Sales::Year2026::RowParser attribute :field_122, :integer attribute :field_123, :decimal attribute :field_124, :decimal - attribute :field_125, :integer - attribute :field_126, :decimal + attribute :field_125, :string + attribute :field_126, :string attribute :field_127, :integer attribute :field_128, :decimal @@ -327,7 +333,7 @@ class BulkUpload::Sales::Year2026::RowParser attribute :field_133, :string attribute :field_134, :integer attribute :field_135, :decimal - attribute :field_136, :decimal + attribute :field_136, :string validates :field_1, presence: { @@ -496,6 +502,8 @@ class BulkUpload::Sales::Year2026::RowParser validate :validate_buyer_2_nationality, on: :after_log validate :validate_mortlen_field_if_buyer_interviewed, on: :after_log + validate :validate_service_charge_fields, on: :after_log + validate :validate_nulls, on: :after_log def self.question_for_field(field) @@ -899,7 +907,7 @@ private gender_same_as_sex6: %i[field_68], gender_description6: %i[field_69], - hasservicechargeschanged: %i[field_125], + hasservicechargeschanged: %i[field_126], newservicecharges: %i[field_126], } end @@ -950,9 +958,6 @@ private attributes["gender_same_as_sex6"] = field_68 attributes["gender_description6"] = field_69 - attributes["hasservicechargeschanged"] = field_125 - attributes["newservicecharges"] = field_126 - attributes["relat2"] = relationship_from_is_partner(field_37) attributes["relat3"] = relationship_from_is_partner(field_47) attributes["relat4"] = relationship_from_is_partner(field_53) @@ -1026,8 +1031,6 @@ private attributes["cashdis"] = field_105 attributes["mrent"] = mrent - attributes["mscharge"] = mscharge if mscharge&.positive? - attributes["has_mscharge"] = attributes["mscharge"].present? ? 1 : 0 attributes["grant"] = field_129 attributes["discount"] = field_130 @@ -1097,6 +1100,11 @@ private attributes["management_fee"] = field_108 attributes["has_management_fee"] = field_108.present? && field_108.positive? ? 1 : 0 + attributes["has_mscharge"] = has_mscharge_value + attributes["mscharge"] = mscharge_value + attributes["hasservicechargeschanged"] = hasservicechargeschanged_value + attributes["newservicecharges"] = newservicecharges_value + attributes end @@ -1256,11 +1264,36 @@ private end def mscharge - return field_107 if shared_ownership? + return field_107 if shared_ownership_initial_purchase? + return field_125 if staircasing? field_136 if discounted_ownership? end + def has_mscharge_value + return unless mscharge.present? && mscharge.match?(NUMBER_OR_R_FORMAT) + + mscharge.casecmp?("R") ? 0 : 1 + end + + def mscharge_value + return unless mscharge.present? && mscharge.match?(NUMBER_OR_R_FORMAT) && !mscharge.casecmp?("R") + + mscharge.to_d + end + + def hasservicechargeschanged_value + return unless field_126.present? && field_126.match?(NUMBER_OR_R_FORMAT) + + field_126.casecmp?("R") ? 2 : 1 + end + + def newservicecharges_value + return unless field_126.present? && field_126.match?(NUMBER_OR_R_FORMAT) && !field_126.casecmp?("R") + + field_126.to_d + end + def mortlen return field_103 if shared_ownership? @@ -1333,10 +1366,11 @@ private end def mscharge_fields - return [:field_107] if shared_ownership? + return [:field_107] if shared_ownership_initial_purchase? + return [:field_125] if staircasing? return [:field_136] if discounted_ownership? - %i[field_107 field_136] + %i[field_107 field_125 field_136] end def mortlen_fields @@ -1389,6 +1423,29 @@ private end end + def validate_service_charge_fields + message = I18n.t("#{ERROR_BASE_KEY}.mscharge.invalid") + + if shared_ownership_initial_purchase? && field_107.present? && !field_107.match?(NUMBER_OR_R_FORMAT) + errors.add(:field_107, message) + end + + if staircasing? + if field_125.present? && !field_125.match?(NUMBER_OR_R_FORMAT) + errors.add(:field_125, message) + end + + if field_126.present? && !field_126.match?(NUMBER_OR_R_FORMAT) + errors.add(:field_126, I18n.t("#{ERROR_BASE_KEY}.newservicecharges.invalid")) + end + end + + if discounted_ownership? && field_136.present? && !field_136.match?(NUMBER_OR_R_FORMAT) + errors.add(:field_136, message) + end + end + + # Will send a "Bulk upload failed" email rather than an "Errors in bulk upload" email def block_log_creation! self.block_log_creation = true end diff --git a/config/locales/validations/sales/2026/bulk_upload.en.yml b/config/locales/validations/sales/2026/bulk_upload.en.yml index 6a406269b..89291803e 100644 --- a/config/locales/validations/sales/2026/bulk_upload.en.yml +++ b/config/locales/validations/sales/2026/bulk_upload.en.yml @@ -44,6 +44,10 @@ en: not_answered: "Enter either the UPRN or the full address." nationality: invalid: "Select a valid nationality." + mscharge: + invalid: "Service charge must be a positive number or the letter R." + newservicecharges: + invalid: "New service charge must be a number or the letter R." mortlen: invalid: "Mortgage length must be a number or the letter R" invalid_for_interviewed: "You indicated that you interviewed the buyer(s), but selected “Don’t know” for mortgage length. Please provide the mortgage length or update your response." diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 9e014a8bd..cd65dd5a6 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,22 +1,34 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (7.2.3.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.0.1) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.11.1) colorator (1.1.0) commonmarker (0.23.10) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) dnsruby (1.61.9) simpleidn (~> 0.1) + drb (2.2.3) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -88,7 +100,7 @@ GEM activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.8) concurrent-ruby (~> 1.0) jekyll (3.9.3) addressable (~> 2.4) @@ -213,7 +225,7 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.19.0) + minitest (5.27.0) net-http (0.9.1) uri (>= 0.11.1) nokogiri (1.19.1-arm64-darwin) @@ -244,6 +256,7 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) + securerandom (0.4.1) simpleidn (0.2.1) unf (~> 0.1.4) terminal-table (1.8.0) diff --git a/spec/services/bulk_upload/sales/year2023/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2023/row_parser_spec.rb index 874bc05f0..010820bdd 100644 --- a/spec/services/bulk_upload/sales/year2023/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2023/row_parser_spec.rb @@ -1334,7 +1334,7 @@ RSpec.describe BulkUpload::Sales::Year2023::RowParser do context "when mscharge is given, but is set to 0 for shared ownership" do let(:attributes) { valid_attributes.merge(field_114: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil @@ -1344,7 +1344,7 @@ RSpec.describe BulkUpload::Sales::Year2023::RowParser do context "when mscharge is given, but is set to 0 for discounted ownership" do let(:attributes) { valid_attributes.merge(field_7: "2", field_126: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil @@ -1354,7 +1354,7 @@ RSpec.describe BulkUpload::Sales::Year2023::RowParser do context "when mscharge is given, but is set to 0 for outright sale" do let(:attributes) { valid_attributes.merge(field_7: "3", field_135: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil diff --git a/spec/services/bulk_upload/sales/year2024/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2024/row_parser_spec.rb index 984cc2f33..898a7feee 100644 --- a/spec/services/bulk_upload/sales/year2024/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2024/row_parser_spec.rb @@ -1973,7 +1973,7 @@ RSpec.describe BulkUpload::Sales::Year2024::RowParser do context "when mscharge is given, but is set to 0 for shared ownership" do let(:attributes) { valid_attributes.merge(field_112: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil @@ -1983,7 +1983,7 @@ RSpec.describe BulkUpload::Sales::Year2024::RowParser do context "when mscharge is given, but is set to 0 for discounted ownership" do let(:attributes) { valid_attributes.merge(field_8: "2", field_124: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil @@ -1993,7 +1993,7 @@ RSpec.describe BulkUpload::Sales::Year2024::RowParser do context "when mscharge is given, but is set to 0 for outright sale" do let(:attributes) { valid_attributes.merge(field_8: "3", field_131: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil diff --git a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb index d9c6032b5..67f5c6116 100644 --- a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb @@ -1889,7 +1889,7 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do context "when mscharge is given, but is set to 0 for shared ownership" do let(:attributes) { valid_attributes.merge(field_94: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil @@ -1899,7 +1899,7 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do context "when mscharge is given, but is set to 0 for discounted ownership" do let(:attributes) { valid_attributes.merge(field_8: "2", field_121: "0") } - it "does not override variables correctly" do + it "does not override variables" do log = parser.log expect(log["has_mscharge"]).to eq(0) # no expect(log["mscharge"]).to be_nil diff --git a/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb index 605674914..91edf5c5e 100644 --- a/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2026/row_parser_spec.rb @@ -116,7 +116,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do field_31: "1", field_40: "2", field_41: "Non-binary", - field_125: "1", + field_125: "200", field_126: "150", } end @@ -301,7 +301,7 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do context "and case insensitive fields are set to lowercase" do let(:case_insensitive_fields) { %w[field_30 field_39 field_49 field_55 field_61 field_67] } - let(:case_insensitive_integer_fields_with_r_option) { %w[field_29 field_38 field_48 field_54 field_60 field_66 field_77 field_88 field_83 field_85 field_103 field_133] } + let(:case_insensitive_integer_fields_with_r_option) { %w[field_29 field_38 field_48 field_54 field_60 field_66 field_77 field_88 field_83 field_85 field_103 field_107 field_125 field_126 field_133 field_136] } let(:attributes) do valid_attributes .merge(case_insensitive_fields.each_with_object({}) { |field, h| h[field.to_sym] = valid_attributes[field.to_sym]&.downcase }) @@ -1948,23 +1948,555 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do end end - context "when mscharge is given, but is set to 0 for shared ownership" do - let(:attributes) { valid_attributes.merge(field_107: "0") } + context "with service charges fields" do + context "with mscharge for shared ownership initial purchase (field_107)" do + context "when positive" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "100") } - it "does not override variables correctly" do - log = parser.log - expect(log["has_mscharge"]).to eq(0) # no - expect(log["mscharge"]).to be_nil + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(100) + end + end + + context "when set to 1" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "1") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(1) + end + end + + context "when set to 0" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "0") } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_107]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_107]).to include(I18n.t("validations.sales.financial.mscharge.monthly_leasehold_charges.not_zero")) + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to 0.0" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "0.0") } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_107]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_107]).to include(I18n.t("validations.sales.financial.mscharge.monthly_leasehold_charges.not_zero")) + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to R" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "R") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when set to lowercase r" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "r") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when an invalid string" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "X") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when blank" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: nil) } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_107]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_107]).to include("You must answer property service charges.") + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when negative" do + let(:attributes) { valid_attributes.merge(field_10: "2", field_107: "-100") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_107]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end end - end - context "when mscharge is given, but is set to 0 for discounted ownership" do - let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "0") } + context "with mscharge for staircasing (field_125)" do + context "when positive" do + let(:attributes) { valid_attributes.merge(field_125: "100") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(100) + end + end + + context "when set to 1" do + let(:attributes) { valid_attributes.merge(field_125: "1") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(1) + end + end + + context "when set to 0" do + let(:attributes) { valid_attributes.merge(field_125: "0") } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_125]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_125]).to include(I18n.t("validations.sales.financial.mscharge.monthly_leasehold_charges.not_zero")) + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to 0.0" do + let(:attributes) { valid_attributes.merge(field_125: "0.0") } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_125]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_125]).to include(I18n.t("validations.sales.financial.mscharge.monthly_leasehold_charges.not_zero")) + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to R" do + let(:attributes) { valid_attributes.merge(field_125: "R") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when set to lowercase r" do + let(:attributes) { valid_attributes.merge(field_125: "r") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when an invalid string" do + let(:attributes) { valid_attributes.merge(field_125: "X") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when blank" do + let(:attributes) { valid_attributes.merge(field_125: nil) } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_125]).not_to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + expect(parser.errors[:field_125]).to include("You must answer property service charges.") + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when negative" do + let(:attributes) { valid_attributes.merge(field_125: "-100") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_125]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + end + + context "with mscharge for discounted ownership (field_136)" do + context "when positive" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "100") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(100) + end + end + + context "when set to 1" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "1") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(1) + end + end + + context "when set to 0" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "0") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to 0.0" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "0.0") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to yes and mscharge to the value" do + log = parser.log + expect(log["has_mscharge"]).to eq(1) + expect(log["mscharge"]).to eq(0) + end + end + + context "when set to R" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "R") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when set to lowercase r" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "r") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "sets has_mscharge to no and does not set mscharge" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) + expect(log["mscharge"]).to be_nil + end + end + + context "when an invalid string" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "X") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when blank" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: nil) } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to be_blank + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + + context "when negative" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_136: "-100") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_136]).to include(I18n.t("validations.sales.2026.bulk_upload.mscharge.invalid")) + end + + it "does not set has_mscharge or mscharge" do + log = parser.log + expect(log["has_mscharge"]).to be_nil + expect(log["mscharge"]).to be_nil + end + end + end + + context "with newservicecharges (field_126)" do + context "when positive" do + let(:attributes) { valid_attributes.merge(field_126: "150") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to be_blank + end + + it "sets hasservicechargeschanged to yes and newservicecharges to the value" do + log = parser.log + expect(log["hasservicechargeschanged"]).to eq(1) + expect(log["newservicecharges"]).to eq(150) + end + end + + context "when set to 0" do + let(:attributes) { valid_attributes.merge(field_126: "0") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to be_blank + end + + it "sets hasservicechargeschanged to yes and newservicecharges to 0" do + log = parser.log + expect(log["hasservicechargeschanged"]).to eq(1) + expect(log["newservicecharges"]).to eq(0) + end + end + + context "when set to 0.0" do + let(:attributes) { valid_attributes.merge(field_126: "0.0") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to be_blank + end + + it "sets hasservicechargeschanged to yes and newservicecharges to 0" do + log = parser.log + expect(log["hasservicechargeschanged"]).to eq(1) + expect(log["newservicecharges"]).to eq(0) + end + end + + context "when set to R" do + let(:attributes) { valid_attributes.merge(field_126: "R") } + + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to be_blank + end + + it "sets hasservicechargeschanged to no and does not set newservicecharges" do + log = parser.log + expect(log["hasservicechargeschanged"]).to eq(2) + expect(log["newservicecharges"]).to be_nil + end + end + + context "when set to lowercase r" do + let(:attributes) { valid_attributes.merge(field_126: "r") } - it "does not override variables correctly" do - log = parser.log - expect(log["has_mscharge"]).to eq(0) # no - expect(log["mscharge"]).to be_nil + it "does not add a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to be_blank + end + + it "sets hasservicechargeschanged to no and does not set newservicecharges" do + log = parser.log + expect(log["hasservicechargeschanged"]).to eq(2) + expect(log["newservicecharges"]).to be_nil + end + end + + context "when an invalid string" do + let(:attributes) { valid_attributes.merge(field_126: "X") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to include(I18n.t("validations.sales.2026.bulk_upload.newservicecharges.invalid")) + end + + it "does not set hasservicechargeschanged or newservicecharges" do + log = parser.log + expect(log["hasservicechargeschanged"]).to be_nil + expect(log["newservicecharges"]).to be_nil + end + end + + context "when blank" do + let(:attributes) { valid_attributes.merge(field_126: nil) } + + it "does not add a bulk upload format validation error but adds a site validation error" do + parser.valid? + expect(parser.errors[:field_126]).not_to include(I18n.t("validations.sales.2026.bulk_upload.newservicecharges.invalid")) + expect(parser.errors[:field_126]).to include("You must answer service charge will change.") + end + + it "does not set hasservicechargeschanged or newservicecharges" do + log = parser.log + expect(log["hasservicechargeschanged"]).to be_nil + expect(log["newservicecharges"]).to be_nil + end + end + + context "when negative" do + let(:attributes) { valid_attributes.merge(field_126: "-150") } + + it "adds a validation error" do + parser.valid? + expect(parser.errors[:field_126]).to include(I18n.t("validations.sales.2026.bulk_upload.newservicecharges.invalid")) + end + + it "does not set hasservicechargeschanged or newservicecharges" do + log = parser.log + expect(log["hasservicechargeschanged"]).to be_nil + expect(log["newservicecharges"]).to be_nil + end + end + end + + context "when newservicecharges equals mscharge (field_125 == field_126)" do + let(:attributes) { valid_attributes.merge(field_125: "200", field_126: "200") } + + it "adds validation errors to both fields" do + parser.valid? + expect(parser.errors[:field_125]).to include(I18n.t("validations.sales.financial.mscharge.same_as_new")) + expect(parser.errors[:field_126]).to include(I18n.t("validations.sales.financial.newservicecharges.same_as_previous")) + end end end