From 7630b49184d84d20bc91fb1046feda8ce4d58a42 Mon Sep 17 00:00:00 2001
From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com>
Date: Thu, 27 Feb 2025 13:41:01 +0000
Subject: [PATCH 1/6] CLDC-3829 Add guidance link and deadlines to log pages
(#2940)
* Add guidance link
* Add deadline text
* Add quarter to non overdue deadline
* Move styling
* Add test and fix cases when no quarters are returned
* Remove days of the week from copy
* Change some copy
---
app/frontend/styles/_task-list.scss | 4 ++
app/helpers/collection_deadline_helper.rb | 5 ++-
app/helpers/tasklist_helper.rb | 17 +++++++
app/views/logs/edit.html.erb | 2 +
spec/helpers/tasklist_helper_spec.rb | 45 +++++++++++++++++++
.../requests/lettings_logs_controller_spec.rb | 15 +++++++
spec/requests/sales_logs_controller_spec.rb | 11 +++++
7 files changed, 98 insertions(+), 1 deletion(-)
diff --git a/app/frontend/styles/_task-list.scss b/app/frontend/styles/_task-list.scss
index f8e2128dd..a739bc245 100644
--- a/app/frontend/styles/_task-list.scss
+++ b/app/frontend/styles/_task-list.scss
@@ -52,3 +52,7 @@
margin-bottom: 0;
}
}
+
+.app-red-text {
+ color: govuk-colour("red");
+}
diff --git a/app/helpers/collection_deadline_helper.rb b/app/helpers/collection_deadline_helper.rb
index 3c4f7e502..de9c12075 100644
--- a/app/helpers/collection_deadline_helper.rb
+++ b/app/helpers/collection_deadline_helper.rb
@@ -61,12 +61,15 @@ module CollectionDeadlineHelper
first_quarter(year).merge(quarter: "Q1"),
second_quarter(year).merge(quarter: "Q2"),
third_quarter(year).merge(quarter: "Q3"),
+ fourth_quarter(year).merge(quarter: "Q4"),
]
end
def quarter_for_date(date: Time.zone.now)
- quarters = quarter_dates(current_collection_start_year)
+ collection_start_year = collection_start_year_for_date(date)
+ return unless QUARTERLY_DEADLINES.key?(collection_start_year)
+ quarters = quarter_dates(collection_start_year)
quarter = quarters.find { |q| date.between?(q[:start_date], q[:cutoff_date] + 1.day) }
return unless quarter
diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb
index ff52f4094..631f4d315 100644
--- a/app/helpers/tasklist_helper.rb
+++ b/app/helpers/tasklist_helper.rb
@@ -2,6 +2,7 @@ module TasklistHelper
include GovukLinkHelper
include GovukVisuallyHiddenHelper
include CollectionTimeHelper
+ include CollectionDeadlineHelper
def breadcrumb_logs_title(log, current_user)
log_type = log.lettings? ? "Lettings" : "Sales"
@@ -70,6 +71,22 @@ module TasklistHelper
status == :cannot_start_yet ? "" : "govuk-task-list__item--with-link"
end
+ def deadline_text(log)
+ return if log.completed?
+ return if log.startdate.nil?
+
+ log_quarter = quarter_for_date(date: log.startdate)
+ return if log_quarter.nil?
+
+ deadline_for_log = log_quarter.cutoff_date
+
+ if deadline_for_log.beginning_of_day >= Time.zone.today.beginning_of_day
+ "
Upcoming #{log_quarter.quarter} deadline: #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.
".html_safe
+ else
+ "
Overdue: #{log_quarter.quarter} deadline #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.
".html_safe
+ end
+ end
+
private
def breadcrumb_organisation(log)
diff --git a/app/views/logs/edit.html.erb b/app/views/logs/edit.html.erb
index 3859ca6db..91d249362 100644
--- a/app/views/logs/edit.html.erb
+++ b/app/views/logs/edit.html.erb
@@ -25,6 +25,7 @@
<% end %>
<% elsif @log.status == "not_started" %>
+ <%= govuk_link_to "Guidance for submitting social housing lettings and sales data (opens in a new tab)", guidance_path, target: "#" %>
This log has not been started.
<% elsif @log.status == "completed" %>
@@ -36,6 +37,7 @@
<% end %>
+ <%= deadline_text(@log) %>
<%= render "tasklist" %>
<%= edit_actions_for_log(@log, bulk_upload_filter_applied) %>
diff --git a/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb
index c5e1bd784..dc683c7b2 100644
--- a/spec/helpers/tasklist_helper_spec.rb
+++ b/spec/helpers/tasklist_helper_spec.rb
@@ -204,4 +204,49 @@ RSpec.describe TasklistHelper do
end
end
end
+
+ describe "deadline text" do
+ context "when log does not have a sale/start date" do
+ let(:log) { build(:sales_log, saledate: nil) }
+
+ it "returns nil" do
+ expect(deadline_text(log)).to be_nil
+ end
+ end
+
+ context "when log is completed" do
+ let(:log) { build(:sales_log, :completed, status: "completed") }
+
+ it "returns nil" do
+ expect(deadline_text(log)).to be_nil
+ end
+ end
+
+ context "when today is before the deadline for log with sale/start date" do
+ let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 6, 1)) }
+
+ it "returns the deadline text" do
+ allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 5, 7))
+ expect(deadline_text(log)).to include("Upcoming Q1 deadline: 11 July 2025.")
+ end
+ end
+
+ context "when today is the deadline for log with sale/start date" do
+ let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 2, 1)) }
+
+ it "returns the overdue text" do
+ allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 6, 6))
+ expect(deadline_text(log)).to include("Upcoming Q4 deadline: 6 June 2025.")
+ end
+ end
+
+ context "when today is after the deadline for log with sale/start date" do
+ let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 2, 1)) }
+
+ it "returns the overdue text" do
+ allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 6, 7))
+ expect(deadline_text(log)).to include("Overdue: Q4 deadline 6 June 2025.")
+ end
+ end
+ end
end
diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb
index 55cf3d573..92683edb3 100644
--- a/spec/requests/lettings_logs_controller_spec.rb
+++ b/spec/requests/lettings_logs_controller_spec.rb
@@ -1155,6 +1155,21 @@ RSpec.describe LettingsLogsController, type: :request do
expect(lettings_log.status).to eq("completed")
expect(page).to have_link("review and make changes to this log", href: "/lettings-logs/#{lettings_log.id}/review")
end
+
+ it "does not show guidance link" do
+ expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
+ end
+ end
+
+ context "and the log is not started" do
+ let(:lettings_log) { create(:lettings_log, status: "not_started", assigned_to: user) }
+
+ it "shows guidance link" do
+ allow(Time.zone).to receive(:now).and_return(lettings_log.form.edit_end_date - 1.day)
+ get lettings_log_path(lettings_log)
+ expect(lettings_log.status).to eq("not_started")
+ expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
+ end
end
context "with bulk_upload_id filter" do
diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb
index d8209fa1d..e85ecb813 100644
--- a/spec/requests/sales_logs_controller_spec.rb
+++ b/spec/requests/sales_logs_controller_spec.rb
@@ -866,6 +866,7 @@ RSpec.describe SalesLogsController, type: :request do
context "when viewing a sales log" do
let(:headers) { { "Accept" => "text/html" } }
let(:completed_sales_log) { FactoryBot.create(:sales_log, :completed, owning_organisation: user.organisation, assigned_to: user) }
+ let(:not_started_sales_log) { FactoryBot.create(:sales_log, owning_organisation: user.organisation, assigned_to: user) }
before do
sign_in user
@@ -956,6 +957,16 @@ RSpec.describe SalesLogsController, type: :request do
expect(page).to have_content("This log is from the 2021 to 2022 collection window, which is now closed.")
end
end
+
+ it "does not show guidance link" do
+ get "/sales-logs/#{completed_sales_log.id}", headers:, params: {}
+ expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
+ end
+
+ it "shows guidance link for not_started log" do
+ get "/sales-logs/#{not_started_sales_log.id}", headers:, params: {}
+ expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
+ end
end
context "when requesting CSV download" do
From 0ebae474a179cd8eb23cb731f8d9ee894c6879f6 Mon Sep 17 00:00:00 2001
From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
Date: Thu, 27 Feb 2025 13:44:44 +0000
Subject: [PATCH 2/6] CLDC-3712: Discounted ownership scheme first question bug
fix (#2961)
* Fix bug, edit condition to only apply to shared ownership
* Add back missing logic to determine question number
* Update test
---
app/models/form/sales/pages/living_before_purchase.rb | 2 +-
app/models/form/sales/questions/living_before_purchase_years.rb | 2 +-
spec/models/form/sales/pages/living_before_purchase_spec.rb | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/form/sales/pages/living_before_purchase.rb b/app/models/form/sales/pages/living_before_purchase.rb
index b8797537b..3e2df425a 100644
--- a/app/models/form/sales/pages/living_before_purchase.rb
+++ b/app/models/form/sales/pages/living_before_purchase.rb
@@ -24,7 +24,7 @@ class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page
end
def page_routed_to?(log)
- return false if form.start_year_2025_or_later? && log.resale != 2
+ return false if form.start_year_2025_or_later? && log.resale != 2 && log.ownershipsch == 1
if @joint_purchase
log.joint_purchase?
diff --git a/app/models/form/sales/questions/living_before_purchase_years.rb b/app/models/form/sales/questions/living_before_purchase_years.rb
index d2df3209b..010aa6edc 100644
--- a/app/models/form/sales/questions/living_before_purchase_years.rb
+++ b/app/models/form/sales/questions/living_before_purchase_years.rb
@@ -9,7 +9,7 @@ class Form::Sales::Questions::LivingBeforePurchaseYears < ::Form::Question
@step = 1
@width = 5
@ownershipsch = ownershipsch
- @question_number = question_number
+ @question_number = QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.fetch(form.start_date.year, QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.max_by { |k, _v| k }.last)[ownershipsch]
end
def suffix_label(log)
diff --git a/spec/models/form/sales/pages/living_before_purchase_spec.rb b/spec/models/form/sales/pages/living_before_purchase_spec.rb
index b597f90e9..1f7cf6c51 100644
--- a/spec/models/form/sales/pages/living_before_purchase_spec.rb
+++ b/spec/models/form/sales/pages/living_before_purchase_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do
end
it "does not route to the page when resale is not 2" do
- log = build(:sales_log, jointpur: 1, resale: nil)
+ log = build(:sales_log, jointpur: 1, resale: nil, ownershipsch: 1)
expect(page.routed_to?(log, nil)).to eq(false)
end
end
From bb414650067e54764f410cc08fa74e907a730550 Mon Sep 17 00:00:00 2001
From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
Date: Thu, 27 Feb 2025 13:45:01 +0000
Subject: [PATCH 3/6] Update hint text (#2960)
---
config/locales/forms/2025/sales/sale_information.en.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/locales/forms/2025/sales/sale_information.en.yml b/config/locales/forms/2025/sales/sale_information.en.yml
index 93f5226b5..95eed6185 100644
--- a/config/locales/forms/2025/sales/sale_information.en.yml
+++ b/config/locales/forms/2025/sales/sale_information.en.yml
@@ -148,7 +148,7 @@ en:
value_shared_ownership:
check_answer_label: "Full purchase price"
check_answer_prompt: ""
- hint_text: "Enter the full purchase price of the property before any discounts are applied. For shared ownership, enter the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)."
+ hint_text: "Enter the full purchase price of the property before any discounts are applied. This is the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)."
question_text: "What was the full purchase price?"
value_shared_ownership_staircase:
check_answer_label: "Full purchase price"
From fe89619b1f7430df0db35abbdbc9b29fd0358dcd Mon Sep 17 00:00:00 2001
From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com>
Date: Thu, 27 Feb 2025 13:45:13 +0000
Subject: [PATCH 4/6] CLDC-3876: Household characteristic ecstat of child under
16 bug fix when age changed (#2959)
* Clear derived working situation of child under 16 when age is changed to 16 or above
* Add tests
---
.../lettings_log_variables.rb | 9 +++++++++
.../derived_variables/sales_log_variables.rb | 10 ++++++++++
.../lettings_log_derived_fields_spec.rb | 20 +++++++++++++++++++
spec/models/sales_log_derived_fields_spec.rb | 20 +++++++++++++++++++
4 files changed, 59 insertions(+)
diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb
index f584b6238..1e2aab790 100644
--- a/app/models/derived_variables/lettings_log_variables.rb
+++ b/app/models/derived_variables/lettings_log_variables.rb
@@ -72,6 +72,7 @@ module DerivedVariables::LettingsLogVariables
self.beds = 1
end
+ clear_child_ecstat_for_age_changes!
child_under_16_constraints!
self.hhtype = household_type
@@ -243,6 +244,14 @@ private
end
end
+ def clear_child_ecstat_for_age_changes!
+ (2..8).each do |idx|
+ if public_send("age#{idx}_changed?") && self["ecstat#{idx}"] == 9
+ self["ecstat#{idx}"] = nil
+ end
+ end
+ end
+
def household_type
return unless totelder && totadult && totchild
diff --git a/app/models/derived_variables/sales_log_variables.rb b/app/models/derived_variables/sales_log_variables.rb
index ef4997283..ff8cd4916 100644
--- a/app/models/derived_variables/sales_log_variables.rb
+++ b/app/models/derived_variables/sales_log_variables.rb
@@ -46,6 +46,7 @@ module DerivedVariables::SalesLogVariables
if saledate && form.start_year_2024_or_later?
self.soctenant = soctenant_from_prevten_values
+ clear_child_ecstat_for_age_changes!
child_under_16_constraints!
end
@@ -181,6 +182,15 @@ private
end
end
+ def clear_child_ecstat_for_age_changes!
+ start_index = joint_purchase? ? 3 : 2
+ (start_index..6).each do |idx|
+ if public_send("age#{idx}_changed?") && self["ecstat#{idx}"] == 9
+ self["ecstat#{idx}"] = nil
+ end
+ end
+ end
+
def household_type
return unless total_elder && total_adult && totchild
diff --git a/spec/models/lettings_log_derived_fields_spec.rb b/spec/models/lettings_log_derived_fields_spec.rb
index d315db51e..e4edb194e 100644
--- a/spec/models/lettings_log_derived_fields_spec.rb
+++ b/spec/models/lettings_log_derived_fields_spec.rb
@@ -1206,4 +1206,24 @@ RSpec.describe LettingsLog, type: :model do
end
end
end
+
+ describe "#clear_child_ecstat_for_age_changes!" do
+ it "clears the working situation of a person that was previously a child under 16" do
+ log = create(:lettings_log, :completed, age2: 13)
+ log.age2 = 17
+ expect { log.set_derived_fields! }.to change(log, :ecstat2).from(9).to(nil)
+ end
+
+ it "does not clear the working situation of a person that had an age change but is still a child under 16" do
+ log = create(:lettings_log, :completed, age2: 13)
+ log.age2 = 15
+ expect { log.set_derived_fields! }.to not_change(log, :ecstat2)
+ end
+
+ it "does not clear the working situation of a person that had an age change but is still an adult" do
+ log = create(:lettings_log, :completed, age2: 45)
+ log.age2 = 46
+ expect { log.set_derived_fields! }.to not_change(log, :ecstat2)
+ end
+ end
end
diff --git a/spec/models/sales_log_derived_fields_spec.rb b/spec/models/sales_log_derived_fields_spec.rb
index 8eda0c36f..7827ce282 100644
--- a/spec/models/sales_log_derived_fields_spec.rb
+++ b/spec/models/sales_log_derived_fields_spec.rb
@@ -78,6 +78,26 @@ RSpec.describe SalesLog, type: :model do
expect { log.set_derived_fields! }.to change(log, :mortgage).from(50_000).to(nil)
end
+ describe "#clear_child_ecstat_for_age_changes!" do
+ it "clears the working situation of a person that was previously a child under 16" do
+ log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
+ log.age3 = 17
+ expect { log.set_derived_fields! }.to change(log, :ecstat3).from(9).to(nil)
+ end
+
+ it "does not clear the working situation of a person that had an age change but is still a child under 16" do
+ log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
+ log.age3 = 15
+ expect { log.set_derived_fields! }.to not_change(log, :ecstat3)
+ end
+
+ it "does not clear the working situation of a person that had an age change but is still an adult" do
+ log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
+ log.age5 = 46
+ expect { log.set_derived_fields! }.to not_change(log, :ecstat5)
+ end
+ end
+
context "with a log that is not outright sales" do
it "does not derive deposit when mortgage used is no" do
log = build(:sales_log, :shared_ownership_setup_complete, value: 123_400, deposit: nil, mortgageused: 2)
From e5d10e219fe2ff1d8dc4b50de8f875d8eb44e91d Mon Sep 17 00:00:00 2001
From: carolynbarker <8038496+carolynbarker@users.noreply.github.com>
Date: Thu, 27 Feb 2025 14:13:35 +0000
Subject: [PATCH 5/6] CLDC-3802 sales bulk upload update 2025 (#2945)
* add 2025 sales bulk upload parser
* lint and fix typos
* fix typos, field numbers, staircasing tests
* order field_11 values
* test log creator selects correct year's parser
* fix log to csv helper field order
* update factory so test file fully succeeds
* add 2025 BU test file method
* apply new csv syntax
* lint
* lint
* CLDC-3893 update property information field order
* commonise prepare your file page
* also update prep file page for lettings
* CLDC-3893 update test
* lint
* don't error on blank discount if not RTB
---------
Co-authored-by: Carolyn
---
.rake_tasks~ | 0
app/controllers/test_data_controller.rb | 34 +-
app/helpers/bulk_upload/sales_log_to_csv.rb | 139 +-
app/models/bulk_upload.rb | 2 +
.../bulk_upload_form/prepare_your_file.rb | 9 +-
app/services/bulk_upload/sales/log_creator.rb | 2 +
app/services/bulk_upload/sales/validator.rb | 2 +
.../bulk_upload/sales/year2024/row_parser.rb | 1 +
.../bulk_upload/sales/year2025/csv_parser.rb | 124 ++
.../bulk_upload/sales/year2025/row_parser.rb | 1502 +++++++++++++
...24.html.erb => prepare_your_file.html.erb} | 4 +-
...24.html.erb => prepare_your_file.html.erb} | 6 +-
config/locales/en.yml | 2 +
.../validations/sales/2025/bulk_upload.en.yml | 46 +
session-manager-plugin.deb | Bin 0 -> 3908056 bytes
spec/models/bulk_upload_spec.rb | 1 +
.../bulk_upload/sales/log_creator_spec.rb | 309 +--
.../bulk_upload/sales/validator_spec.rb | 16 +
.../sales/year2025/csv_parser_spec.rb | 191 ++
.../sales/year2025/row_parser_spec.rb | 1915 +++++++++++++++++
20 files changed, 4137 insertions(+), 168 deletions(-)
create mode 100644 .rake_tasks~
create mode 100644 app/services/bulk_upload/sales/year2025/csv_parser.rb
create mode 100644 app/services/bulk_upload/sales/year2025/row_parser.rb
rename app/views/bulk_upload_lettings_logs/forms/{prepare_your_file_2024.html.erb => prepare_your_file.html.erb} (88%)
rename app/views/bulk_upload_sales_logs/forms/{prepare_your_file_2024.html.erb => prepare_your_file.html.erb} (82%)
create mode 100644 config/locales/validations/sales/2025/bulk_upload.en.yml
create mode 100644 session-manager-plugin.deb
create mode 100644 spec/services/bulk_upload/sales/year2025/csv_parser_spec.rb
create mode 100644 spec/services/bulk_upload/sales/year2025/row_parser_spec.rb
diff --git a/.rake_tasks~ b/.rake_tasks~
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/controllers/test_data_controller.rb b/app/controllers/test_data_controller.rb
index 2b049f176..77e8041f0 100644
--- a/app/controllers/test_data_controller.rb
+++ b/app/controllers/test_data_controller.rb
@@ -47,22 +47,24 @@ class TestDataController < ApplicationController
redirect_to sales_log_path(log)
end
- def create_2024_test_sales_bulk_upload
- return render_not_found unless FeatureToggle.create_test_logs_enabled?
-
- file = Tempfile.new("test_sales_log.csv")
+ [2024, 2025].each do |year|
+ define_method("create_#{year}_test_sales_bulk_upload") do
+ return render_not_found unless FeatureToggle.create_test_logs_enabled?
- log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000)
- log_to_csv = BulkUpload::SalesLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" })
- file.write(log_to_csv.default_field_numbers_row)
- file.write(log_to_csv.to_csv_row)
- file.rewind
- send_file file.path, type: "text/csv",
- filename: "test_sales_log.csv",
- disposition: "attachment",
- after_send: lambda {
- file.close
- file.unlink
- }
+ file = Tempfile.new("#{year}_test_sales_log.csv")
+ log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000, county: "Somerset", saledate: Time.zone.local(year.to_i, rand(4..12), rand(1..28)))
+ log_to_csv = BulkUpload::SalesLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" })
+ file.write(log_to_csv.default_field_numbers_row)
+ file.write(log_to_csv.to_csv_row)
+ file.rewind
+ send_file file.path,
+ type: "text/csv",
+ filename: "#{year}_test_sales_log.csv",
+ disposition: "attachment",
+ after_send: lambda {
+ file.close
+ file.unlink
+ }
+ end
end
end
diff --git a/app/helpers/bulk_upload/sales_log_to_csv.rb b/app/helpers/bulk_upload/sales_log_to_csv.rb
index bc1435186..5fad19e74 100644
--- a/app/helpers/bulk_upload/sales_log_to_csv.rb
+++ b/app/helpers/bulk_upload/sales_log_to_csv.rb
@@ -19,7 +19,7 @@ class BulkUpload::SalesLogToCsv
case year
when 2022
to_2022_csv_row
- when 2023, 2024
+ when 2023, 2024, 2025
to_year_csv_row(year, seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
@@ -67,6 +67,8 @@ class BulkUpload::SalesLogToCsv
[6, 3, 4, 5, nil, 28, 30, 38, 47, 51, 55, 59, 31, 39, 48, 52, 56, 60, 37, 46, 50, 54, 58, 35, 43, 49, 53, 57, 61, 32, 33, 78, 80, 79, 81, 83, 84, nil, 62, 66, 64, 65, 63, 67, 69, 70, 68, 76, 77, 16, 17, 18, 26, 24, 25, 27, 8, 91, 95, 96, 97, 92, 93, 94, 98, 100, 101, 103, 104, 106, 110, 111, 112, 113, 114, 9, 116, 117, 118, 120, 124, 125, 126, 10, 11, nil, 127, 129, 133, 134, 135, 1, 2, nil, 73, nil, 75, 107, 108, 121, 122, 130, 131, 82, 109, 123, 132, 115, 15, 86, 87, 29, 7, 12, 13, 14, 36, 44, 45, 88, 89, 102, 105, 119, 128, 19, 20, 21, 22, 23, 34, 40, 41, 42, 71, 72, 74, 85, 90, 99]
when 2024
(1..131).to_a
+ when 2025
+ (1..121).to_a
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
@@ -395,6 +397,141 @@ class BulkUpload::SalesLogToCsv
]
end
+ def to_2025_row
+ [
+ log.saledate&.day,
+ log.saledate&.month,
+ log.saledate&.strftime("%y"),
+ overrides[:organisation_id] || log.owning_organisation&.old_visible_id,
+ overrides[:managing_organisation_id] || log.managing_organisation&.old_visible_id,
+ log.assigned_to&.email,
+ log.purchid,
+ log.ownershipsch,
+ log.ownershipsch == 1 ? log.type : "", # field_9: "What is the type of shared ownership sale?",
+ log.staircase, # 10
+ log.ownershipsch == 2 ? log.type : "", # field_11: "What is the type of discounted ownership sale?",
+ log.jointpur,
+ log.jointmore,
+ log.noint,
+ log.privacynotice,
+
+ log.uprn,
+ log.address_line1&.tr(",", " "), # 20
+ log.address_line2&.tr(",", " "),
+ log.town_or_city&.tr(",", " "),
+ log.county&.tr(",", " "),
+ ((log.postcode_full || "").split(" ") || [""]).first,
+ ((log.postcode_full || "").split(" ") || [""]).last,
+ log.la,
+ log.proptype,
+ log.beds,
+ log.builtype,
+ log.wchair,
+
+ log.age1,
+ log.sex1,
+ log.ethnic, # 30
+ log.nationality_all_group,
+ log.ecstat1,
+ log.buy1livein,
+ log.relat2,
+ log.age2,
+ log.sex2,
+ log.ethnic_group2,
+ log.nationality_all_buyer2_group,
+ log.ecstat2,
+ log.buy2livein, # 40
+ log.hholdcount,
+
+ log.relat3,
+ log.age3,
+ log.sex3,
+ log.ecstat3,
+ log.relat4,
+ log.age4,
+ log.sex4,
+ log.ecstat4,
+ log.relat5, # 50
+ log.age5,
+ log.sex5,
+ log.ecstat5,
+ log.relat6,
+ log.age6,
+ log.sex6,
+ log.ecstat6,
+
+ log.prevten,
+ log.ppcodenk,
+ ((log.ppostcode_full || "").split(" ") || [""]).first, # 60
+ ((log.ppostcode_full || "").split(" ") || [""]).last,
+ log.prevloc,
+ log.buy2living,
+ log.prevtenbuy2,
+
+ log.hhregres,
+ log.hhregresstill,
+ log.armedforcesspouse,
+ log.disabled,
+ log.wheel,
+
+ log.income1, # 70
+ log.inc1mort,
+ log.income2,
+ log.inc2mort,
+ log.hb,
+ log.savings.present? || "R",
+ log.prevown,
+ log.prevshared,
+
+ log.resale,
+ log.proplen,
+ log.hodate&.day, # 80
+ log.hodate&.month,
+ log.hodate&.strftime("%y"),
+ log.frombeds,
+ log.fromprop,
+ log.socprevten,
+ log.value,
+ log.equity,
+ log.mortgageused,
+ log.mortgage,
+ log.mortlen, # 90
+ log.deposit,
+ log.cashdis,
+ log.mrent,
+ log.mscharge,
+ log.management_fee,
+
+ log.stairbought,
+ log.stairowned,
+ log.staircasesale,
+ log.firststair,
+ log.initialpurchase&.day, # 100
+ log.initialpurchase&.month,
+ log.initialpurchase&.strftime("%y"),
+ log.numstair,
+ log.lasttransaction&.day,
+ log.lasttransaction&.month,
+ log.lasttransaction&.strftime("%y"),
+ log.value,
+ log.equity,
+ log.mortgageused,
+ log.mrentprestaircasing, # 110
+ log.mrent,
+
+ log.proplen,
+ log.value,
+ log.grant,
+ log.discount,
+ log.mortgageused,
+ log.mortgage,
+ log.mortlen,
+ log.extrabor,
+ log.deposit, # 120
+ log.mscharge,
+ ]
+ end
+
def custom_field_numbers_row(seed: nil, field_numbers: nil)
if seed
["Field number"] + field_numbers.shuffle(random: Random.new(seed))
diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb
index 6616285b0..dd09b365b 100644
--- a/app/models/bulk_upload.rb
+++ b/app/models/bulk_upload.rb
@@ -104,6 +104,8 @@ class BulkUpload < ApplicationRecord
end
year_class = case year
+ when 2025
+ "Year2025"
when 2024
"Year2024"
when 2023
diff --git a/app/models/forms/bulk_upload_form/prepare_your_file.rb b/app/models/forms/bulk_upload_form/prepare_your_file.rb
index 911daa4fe..7f6cfd759 100644
--- a/app/models/forms/bulk_upload_form/prepare_your_file.rb
+++ b/app/models/forms/bulk_upload_form/prepare_your_file.rb
@@ -10,10 +10,7 @@ module Forms
attribute :organisation_id, :integer
def view_path
- case year
- when 2024
- "bulk_upload_#{log_type}_logs/forms/prepare_your_file_2024"
- end
+ "bulk_upload_#{log_type}_logs/forms/prepare_your_file"
end
def back_path
@@ -42,6 +39,10 @@ module Forms
"#{year} to #{year + 1}"
end
+ def slash_year_combo
+ "#{year}/#{(year + 1) % 100}"
+ end
+
def save!
true
end
diff --git a/app/services/bulk_upload/sales/log_creator.rb b/app/services/bulk_upload/sales/log_creator.rb
index 69f1580a0..a21e7a31a 100644
--- a/app/services/bulk_upload/sales/log_creator.rb
+++ b/app/services/bulk_upload/sales/log_creator.rb
@@ -33,6 +33,8 @@ private
BulkUpload::Sales::Year2023::CsvParser.new(path:)
when 2024
BulkUpload::Sales::Year2024::CsvParser.new(path:)
+ when 2025
+ BulkUpload::Sales::Year2025::CsvParser.new(path:)
else
raise "csv parser not found"
end
diff --git a/app/services/bulk_upload/sales/validator.rb b/app/services/bulk_upload/sales/validator.rb
index 7ad9638d7..0b2d68bc5 100644
--- a/app/services/bulk_upload/sales/validator.rb
+++ b/app/services/bulk_upload/sales/validator.rb
@@ -108,6 +108,8 @@ private
BulkUpload::Sales::Year2023::CsvParser.new(path:)
when 2024
BulkUpload::Sales::Year2024::CsvParser.new(path:)
+ when 2025
+ BulkUpload::Sales::Year2025::CsvParser.new(path:)
else
raise "csv parser not found"
end
diff --git a/app/services/bulk_upload/sales/year2024/row_parser.rb b/app/services/bulk_upload/sales/year2024/row_parser.rb
index 443d5e665..f33bfb5fa 100644
--- a/app/services/bulk_upload/sales/year2024/row_parser.rb
+++ b/app/services/bulk_upload/sales/year2024/row_parser.rb
@@ -375,6 +375,7 @@ class BulkUpload::Sales::Year2024::RowParser
greater_than_or_equal_to: 0,
less_than_or_equal_to: 70,
if: :discounted_ownership?,
+ allow_blank: true,
},
on: :before_log
diff --git a/app/services/bulk_upload/sales/year2025/csv_parser.rb b/app/services/bulk_upload/sales/year2025/csv_parser.rb
new file mode 100644
index 000000000..ec052dbfb
--- /dev/null
+++ b/app/services/bulk_upload/sales/year2025/csv_parser.rb
@@ -0,0 +1,124 @@
+require "csv"
+
+class BulkUpload::Sales::Year2025::CsvParser
+ include CollectionTimeHelper
+
+ FIELDS = 121
+ MAX_COLUMNS = 142
+ FORM_YEAR = 2025
+
+ attr_reader :path
+
+ def initialize(path:)
+ @path = path
+ end
+
+ def row_offset
+ if with_headers?
+ rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1
+ else
+ 0
+ end
+ end
+
+ def col_offset
+ with_headers? ? 1 : 0
+ end
+
+ def cols
+ @cols ||= ("A".."DR").to_a
+ end
+
+ def row_parsers
+ @row_parsers ||= body_rows.map { |row|
+ next if row.empty?
+
+ stripped_row = row[col_offset..]
+ hash = Hash[field_numbers.zip(stripped_row)]
+
+ BulkUpload::Sales::Year2025::RowParser.new(hash)
+ }.compact
+ end
+
+ def body_rows
+ rows[row_offset..]
+ end
+
+ def rows
+ @rows ||= CSV.parse(normalised_string, row_sep:)
+ end
+
+ def column_for_field(field)
+ cols[field_numbers.find_index(field) + col_offset]
+ end
+
+ def wrong_template_for_year?
+ collection_start_year_for_date(first_record_start_date) != FORM_YEAR
+ rescue Date::Error
+ false
+ end
+
+ def missing_required_headers?
+ !with_headers?
+ end
+
+ def correct_field_count?
+ valid_field_numbers_count = field_numbers.count { |f| f != "field_blank" }
+
+ valid_field_numbers_count == FIELDS
+ end
+
+private
+
+ def default_field_numbers
+ (1..FIELDS).map do |number|
+ if number.to_s.match?(/^[0-9]+$/)
+ "field_#{number}"
+ else
+ "field_blank"
+ end
+ end
+ end
+
+ def field_numbers
+ @field_numbers ||= if with_headers?
+ rows[row_offset - 1][col_offset..].map { |number| number.to_s.match?(/^[0-9]+$/) ? "field_#{number}" : "field_blank" }
+ else
+ default_field_numbers
+ end
+ end
+
+ def headers
+ @headers ||= ("field_1".."field_#{FIELDS}").to_a
+ end
+
+ def with_headers?
+ # we will eventually want to validate that headers exist for this year
+ rows.map { |r| r[0] }.any? { |cell| cell&.match?(/field number/i) }
+ end
+
+ def row_sep
+ "\n"
+ end
+
+ def normalised_string
+ return @normalised_string if @normalised_string
+
+ @normalised_string = File.read(path, encoding: "bom|utf-8")
+ @normalised_string.gsub!("\r\n", "\n")
+ @normalised_string.scrub!("")
+ @normalised_string.tr!("\r", "\n")
+
+ @normalised_string
+ end
+
+ def first_record_start_date
+ if with_headers?
+ year = row_parsers.first.field_3.to_s.strip.length.between?(1, 2) ? row_parsers.first.field_3.to_i + 2000 : row_parsers.first.field_3.to_i
+ Date.new(year, row_parsers.first.field_2.to_i, row_parsers.first.field_1.to_i)
+ else
+ year = rows.first[2].to_s.strip.length.between?(1, 2) ? rows.first[2].to_i + 2000 : rows.first[2].to_i
+ Date.new(year, rows.first[1].to_i, rows.first[0].to_i)
+ end
+ end
+end
diff --git a/app/services/bulk_upload/sales/year2025/row_parser.rb b/app/services/bulk_upload/sales/year2025/row_parser.rb
new file mode 100644
index 000000000..ee97d6776
--- /dev/null
+++ b/app/services/bulk_upload/sales/year2025/row_parser.rb
@@ -0,0 +1,1502 @@
+class BulkUpload::Sales::Year2025::RowParser
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+ include InterruptionScreenHelper
+ include FormattingHelper
+
+ QUESTIONS = {
+ field_1: "What is the day of the sale completion date? - DD",
+ field_2: "What is the month of the sale completion date? - MM",
+ field_3: "What is the year of the sale completion date? - YY",
+ field_4: "Which organisation owned this property before the sale?",
+ field_5: "Which organisation is reporting this sale?",
+ field_6: "Username",
+ field_7: "What is the purchaser code?",
+ field_8: "What is the sale type?",
+ field_9: "What is the type of shared ownership sale?",
+ field_10: "Is this a staircasing transaction?",
+ field_11: "What is the type of discounted ownership sale?",
+ field_12: "Is this a joint purchase?",
+ field_13: "Are there more than two joint purchasers of this property?",
+ field_14: "Was the buyer interviewed for any of the answers you will provide on this log?",
+ field_15: "Data Protection question",
+
+ field_16: "If known, enter this property's UPRN",
+ field_17: "Address line 1",
+ field_18: "Address line 2",
+ field_19: "Town or city",
+ field_20: "County",
+ field_21: "Part 1 of postcode of property",
+ field_22: "Part 2 of postcode of property",
+ field_23: "What is the local authority of the property?",
+ field_24: "What type of unit is the property?",
+ field_25: "How many bedrooms does the property have?",
+ field_26: "Which type of building is the property?",
+ field_27: "Is the property built or adapted to wheelchair user standards?",
+
+ field_28: "Age of buyer 1",
+ field_29: "Gender identity of buyer 1",
+ field_30: "What is buyer 1's ethnic group?",
+ field_31: "What is buyer 1's nationality?",
+ field_32: "Working situation of buyer 1",
+ field_33: "Will buyer 1 live in the property?",
+ field_34: "Is buyer 2 or person 2 the partner of buyer 1?",
+ field_35: "Age of person 2",
+ field_36: "Gender identity of person 2",
+ field_37: "Which of the following best describes buyer 2's ethnic background?",
+ field_38: "What is buyer 2's nationality?",
+ field_39: "What is buyer 2 or person 2's working situation?",
+ field_40: "Will buyer 2 live in the property?",
+ field_41: "Besides the buyers, how many people will live in the property?",
+
+ field_42: "Is person 3 the partner of buyer 1?",
+ field_43: "Age of person 3",
+ field_44: "Gender identity of person 3",
+ field_45: "Working situation of person 3",
+ field_46: "Is person 4 the partner of buyer 1?",
+ field_47: "Age of person 4",
+ field_48: "Gender identity of person 4",
+ field_49: "Working situation of person 4",
+ field_50: "Is person 5 the partner of buyer 1?",
+ field_51: "Age of person 5",
+ field_52: "Gender identity of person 5",
+ field_53: "Working situation of person 5",
+ field_54: "Is person 6 the partner of buyer 1?",
+ field_55: "Age of person 6",
+ field_56: "Gender identity of person 6",
+ field_57: "Working situation of person 6",
+
+ field_58: "What was buyer 1's previous tenure?",
+ field_59: "Do you know the postcode of buyer 1's last settled home?",
+ field_60: "Part 1 of postcode of buyer 1's last settled home",
+ field_61: "Part 2 of postcode of buyer 1's last settled home",
+ field_62: "What is the local authority of buyer 1's last settled home?",
+ field_63: "At the time of purchase, was buyer 2 living at the same address as buyer 1?",
+ field_64: "What was buyer 2's previous tenure?",
+
+ field_65: "Has the buyer ever served in the UK Armed Forces and for how long?",
+ field_66: "Is the buyer still serving in the UK armed forces?",
+ field_67: "Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years?",
+ field_68: "Does anyone in the household consider themselves to have a disability?",
+ field_69: "Does anyone in the household use a wheelchair?",
+
+ field_70: "What is buyer 1's gross annual income?",
+ field_71: "Was buyer 1's income used for a mortgage application?",
+ field_72: "What is buyer 2's gross annual income?",
+ field_73: "Was buyer 2's income used for a mortgage application?",
+ field_74: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?",
+ field_75: "What is the total amount the buyers had in savings before they paid any deposit for the property?",
+ field_76: "Have any of the purchasers previously owned a property?",
+ field_77: "Was the previous property under shared ownership?",
+
+ field_78: "Is this a resale?",
+ field_79: "How long have the buyers been living in the property before the purchase? - Shared ownership",
+ field_80: "What is the day of the practical completion or handover date?",
+ field_81: "What is the month of the practical completion or handover date?",
+ field_82: "What is the year of the practical completion or handover date?",
+ field_83: "How many bedrooms did the buyer's previous property have?",
+ field_84: "What was the type of the buyer's previous property?",
+ field_85: "What was the rent type of the buyer's previous property?",
+ field_86: "What was the full purchase price?",
+ field_87: "What was the initial percentage share purchased?",
+ field_88: "Was a mortgage used for the purchase of this property? - Shared ownership",
+ field_89: "What is the mortgage amount?",
+ field_90: "What is the length of the mortgage in years? - Shared ownership",
+ field_91: "How much was the cash deposit paid on the property?",
+ field_92: "How much cash discount was given through Social Homebuy?",
+ field_93: "What is the basic monthly rent?",
+ field_94: "What are the total monthly service charges for the property?",
+ field_95: "What are the total monthly estate management fees for the property?",
+
+ field_96: "What percentage of the property has been bought in this staircasing transaction?",
+ field_97: "What percentage of the property does the buyer now own in total?",
+ field_98: "Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?",
+ field_99: "Is this the first time the buyer has engaged in staircasing in the home?",
+ field_100: "What was the day of the initial purchase of a share in the property? DD",
+ field_101: "What was the month of the initial purchase of a share in the property? MM",
+ field_102: "What was the year of the initial purchase of a share in the property? YYYY",
+ field_103: "Including this time, how many times has the shared owner engaged in staircasing in the home?",
+ field_104: "What was the day of the last staircasing transaction? DD",
+ field_105: "What was the month of the last staircasing transaction? MM",
+ field_106: "What was the year of the last staircasing transaction? YYYY",
+ field_107: "What is the full purchase price for this staircasing transaction?",
+ field_108: "What was the percentage share purchased in the initial transaction?",
+ field_109: "Was a mortgage used for this staircasing transaction?",
+ field_110: "What was the basic monthly rent prior to staircasing?",
+ field_111: "What is the basic monthly rent after staircasing?",
+
+ field_112: "How long have the buyers been living in the property before the purchase? - Discounted ownership",
+ field_113: "What was the full purchase price?",
+ field_114: "What was the amount of any loan, grant, discount or subsidy given?",
+ field_115: "What was the percentage discount?",
+ field_116: "Was a mortgage used for the purchase of this property? - Discounted ownership",
+ field_117: "What is the mortgage amount?",
+ field_118: "What is the length of the mortgage in years? - Discounted ownership",
+ field_119: "Does this include any extra borrowing?",
+ field_120: "How much was the cash deposit paid on the property?",
+ field_121: "What are the total monthly leasehold charges for the property?",
+ }.freeze
+
+ ERROR_BASE_KEY = "validations.sales.2025.bulk_upload".freeze
+
+ attribute :bulk_upload
+ attribute :block_log_creation, :boolean, default: -> { false }
+
+ attribute :field_blank
+
+ attribute :field_1, :integer
+ attribute :field_2, :integer
+ attribute :field_3, :integer
+ attribute :field_4, :string
+ attribute :field_5, :string
+ attribute :field_6, :string
+ attribute :field_7, :string
+ attribute :field_8, :integer
+ attribute :field_9, :integer
+ attribute :field_10, :integer
+ attribute :field_11, :integer
+ attribute :field_12, :integer
+ attribute :field_13, :integer
+ attribute :field_14, :integer
+ attribute :field_15, :integer
+
+ attribute :field_16, :string
+ attribute :field_17, :string
+ attribute :field_18, :string
+ attribute :field_19, :string
+ attribute :field_20, :string
+ attribute :field_21, :string
+ attribute :field_22, :string
+ attribute :field_23, :string
+ attribute :field_24, :integer
+ attribute :field_25, :integer
+ attribute :field_26, :integer
+ attribute :field_27, :integer
+
+ attribute :field_28, :string
+ attribute :field_29, :string
+ attribute :field_30, :integer
+ attribute :field_31, :integer
+ attribute :field_32, :integer
+ attribute :field_33, :integer
+ attribute :field_34, :integer
+ attribute :field_35, :string
+ attribute :field_36, :string
+ attribute :field_37, :integer
+ attribute :field_38, :integer
+ attribute :field_39, :integer
+ attribute :field_40, :integer
+ attribute :field_41, :integer
+
+ attribute :field_42, :integer
+ attribute :field_43, :string
+ attribute :field_44, :string
+ attribute :field_45, :integer
+ attribute :field_46, :integer
+ attribute :field_47, :string
+ attribute :field_48, :string
+ attribute :field_49, :integer
+ attribute :field_50, :integer
+ attribute :field_51, :string
+ attribute :field_52, :string
+ attribute :field_53, :integer
+ attribute :field_54, :integer
+ attribute :field_55, :string
+ attribute :field_56, :string
+ attribute :field_57, :integer
+
+ attribute :field_58, :integer
+ attribute :field_59, :integer
+ attribute :field_60, :string
+ attribute :field_61, :string
+ attribute :field_62, :string
+ attribute :field_63, :integer
+ attribute :field_64, :string
+
+ attribute :field_65, :integer
+ attribute :field_66, :integer
+ attribute :field_67, :integer
+ attribute :field_68, :integer
+ attribute :field_69, :integer
+
+ attribute :field_70, :string
+ attribute :field_71, :integer
+ attribute :field_72, :string
+ attribute :field_73, :integer
+ attribute :field_74, :integer
+ attribute :field_75, :string
+ attribute :field_76, :integer
+ attribute :field_77, :integer
+
+ attribute :field_78, :integer
+ attribute :field_79, :integer
+ attribute :field_80, :integer
+ attribute :field_81, :integer
+ attribute :field_82, :integer
+ attribute :field_83, :integer
+ attribute :field_84, :integer
+ attribute :field_85, :integer
+ attribute :field_86, :integer
+ attribute :field_87, :integer
+ attribute :field_88, :integer
+ attribute :field_89, :integer
+ attribute :field_90, :integer
+ attribute :field_91, :integer
+ attribute :field_92, :integer
+ attribute :field_93, :decimal
+ attribute :field_94, :decimal
+ attribute :field_95, :decimal
+
+ attribute :field_96, :integer
+ attribute :field_97, :integer
+ attribute :field_98, :integer
+ attribute :field_99, :integer
+ attribute :field_100, :integer
+ attribute :field_101, :integer
+ attribute :field_102, :integer
+ attribute :field_103, :integer
+ attribute :field_104, :integer
+ attribute :field_105, :integer
+ attribute :field_106, :integer
+ attribute :field_107, :integer
+ attribute :field_108, :integer
+ attribute :field_109, :integer
+ attribute :field_110, :integer
+ attribute :field_111, :decimal
+
+ attribute :field_112, :integer
+ attribute :field_113, :integer
+ attribute :field_114, :integer
+ attribute :field_115, :integer
+ attribute :field_116, :integer
+ attribute :field_117, :integer
+ attribute :field_118, :integer
+ attribute :field_119, :integer
+ attribute :field_120, :integer
+ attribute :field_121, :integer
+
+ validates :field_1,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."),
+ category: :setup,
+ },
+ on: :after_log
+
+ validates :field_2,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (month)."),
+ category: :setup,
+ }, on: :after_log
+
+ validates :field_3,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (year)."),
+ category: :setup,
+ },
+ format: {
+ with: /\A(\d{2}|\d{4})\z/,
+ message: I18n.t("#{ERROR_BASE_KEY}.saledate.year_not_two_or_four_digits"),
+ category: :setup,
+ if: proc { field_3.present? },
+ }, on: :after_log
+
+ validates :field_8,
+ inclusion: {
+ in: [1, 2],
+ if: proc { field_8.present? },
+ category: :setup,
+ question: QUESTIONS[:field_8].downcase,
+ },
+ on: :before_log
+
+ validates :field_8,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "shared ownership sale type."),
+ category: :setup,
+ },
+ on: :after_log
+
+ validates :field_9,
+ inclusion: {
+ in: [2, 30, 18, 16, 24, 28, 31, 32],
+ if: proc { field_9.present? },
+ category: :setup,
+ question: QUESTIONS[:field_9].downcase,
+ },
+ on: :before_log
+
+ validates :field_9,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "type of shared ownership sale."),
+ category: :setup,
+ if: :shared_ownership?,
+ },
+ on: :after_log
+
+ validates :field_10,
+ inclusion: {
+ in: [1, 2],
+ if: proc { field_10.present? },
+ category: :setup,
+ question: QUESTIONS[:field_10].downcase,
+ },
+ on: :before_log
+
+ validates :field_10,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "staircasing transaction."),
+ category: :setup,
+ if: :shared_ownership?,
+ },
+ on: :after_log
+
+ validates :field_11,
+ inclusion: {
+ in: [8, 9, 14, 21, 22, 27, 29],
+ if: proc { field_11.present? },
+ category: :setup,
+ question: QUESTIONS[:field_11].downcase,
+ },
+ on: :before_log
+
+ validates :field_11,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "type of discounted ownership sale."),
+ category: :setup,
+ if: :discounted_ownership?,
+ },
+ on: :after_log
+
+ validates :field_115,
+ numericality: {
+ message: I18n.t("#{ERROR_BASE_KEY}.numeric.within_range", field: "Percentage discount", min: "0%", max: "70%"),
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 70,
+ if: :discounted_ownership?,
+ allow_blank: true,
+ },
+ on: :before_log
+
+ validates :field_12,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "joint purchase."),
+ category: :setup,
+ if: :joint_purchase_asked?,
+ },
+ on: :after_log
+
+ validates :field_13,
+ presence: {
+ message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "more than 2 joint buyers."),
+ category: :setup,
+ if: :joint_purchase?,
+ },
+ on: :after_log
+
+ validate :validate_buyer1_economic_status, on: :before_log
+ validate :validate_address_option_found, on: :after_log
+ validate :validate_buyer2_economic_status, on: :before_log
+ validate :validate_valid_radio_option, on: :before_log
+
+ validate :validate_owning_org_data_given, on: :after_log
+ validate :validate_owning_org_exists, on: :after_log
+ validate :validate_owning_org_owns_stock, on: :after_log
+ validate :validate_owning_org_permitted, on: :after_log
+
+ validate :validate_assigned_to_exists, on: :after_log
+ validate :validate_assigned_to_related, on: :after_log
+ validate :validate_assigned_to_when_support, on: :after_log
+ validate :validate_managing_org_related, on: :after_log
+ validate :validate_relevant_collection_window, on: :after_log
+ validate :validate_incomplete_soft_validations, on: :after_log
+
+ validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log
+ validate :validate_address_fields, on: :after_log
+ validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? }
+
+ validate :validate_nationality, on: :after_log
+ validate :validate_buyer_2_nationality, on: :after_log
+
+ validate :validate_nulls, on: :after_log
+
+ def self.question_for_field(field)
+ QUESTIONS[field]
+ end
+
+ def attribute_set
+ @attribute_set ||= instance_variable_get(:@attributes)
+ end
+
+ def blank_row?
+ attribute_set
+ .to_hash
+ .reject { |k, _| %w[bulk_upload block_log_creation].include?(k) }
+ .values
+ .reject(&:blank?)
+ .compact
+ .empty?
+ end
+
+ def log
+ @log ||= SalesLog.new(attributes_for_log)
+ end
+
+ def valid?
+ errors.clear
+
+ return true if blank_row?
+
+ super(:before_log)
+ @before_errors = errors.dup
+
+ log.valid?
+
+ super(:after_log)
+ errors.merge!(@before_errors)
+
+ log.errors.each do |error|
+ fields = field_mapping_for_errors[error.attribute] || []
+
+ fields.each do |field|
+ next if errors.include?(field)
+ next if error.type == :skip_bu_error
+
+ question = log.form.get_question(error.attribute, log)
+
+ if question.present? && setup_question?(question)
+ errors.add(field, error.message, category: :setup)
+ else
+ errors.add(field, error.message)
+ end
+ end
+ end
+
+ errors.blank?
+ end
+
+ def block_log_creation?
+ block_log_creation
+ end
+
+ def inspect
+ "#"
+ end
+
+ def log_already_exists?
+ return false if blank_row?
+
+ @log_already_exists ||= SalesLog
+ .where(status: %w[not_started in_progress completed])
+ .exists?(duplicate_check_fields.index_with { |field| log.public_send(field) })
+ end
+
+ def purchaser_code
+ field_7
+ end
+
+ def spreadsheet_duplicate_hash
+ attributes.slice(
+ "field_4", # owning org
+ "field_1", # saledate
+ "field_2", # saledate
+ "field_3", # saledate
+ "field_7", # purchaser_code
+ "field_21", # postcode
+ "field_22", # postcode
+ "field_28", # age1
+ "field_29", # sex1
+ "field_32", # ecstat1
+ )
+ end
+
+ def add_duplicate_found_in_spreadsheet_errors
+ spreadsheet_duplicate_hash.each_key do |field|
+ errors.add(field, I18n.t("#{ERROR_BASE_KEY}.spreadsheet_dupe"), category: :setup)
+ end
+ end
+
+private
+
+ def prevtenbuy2
+ case field_64
+ when "R"
+ 0
+ else
+ field_64
+ end
+ end
+
+ def infer_buyer2_ethnic_group_from_ethnic
+ case field_37
+ when 1, 2, 3, 18, 20
+ 0
+ when 4, 5, 6, 7
+ 1
+ when 8, 9, 10, 11, 15
+ 2
+ when 12, 13, 14
+ 3
+ when 16, 19
+ 4
+ else
+ field_37
+ end
+ end
+
+ def validate_uprn_exists_if_any_key_address_fields_are_blank
+ if field_16.blank? && !key_address_fields_provided?
+ %i[field_17 field_19 field_21 field_22].each do |field|
+ errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank?
+ end
+ errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))
+ end
+ end
+
+ def validate_address_option_found
+ if log.uprn.nil? && field_16.blank? && key_address_fields_provided?
+ error_message = if log.address_options_present?
+ I18n.t("#{ERROR_BASE_KEY}.address.not_determined")
+ else
+ I18n.t("#{ERROR_BASE_KEY}.address.not_found")
+ end
+ %i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field|
+ errors.add(field, error_message) if errors[field].blank?
+ end
+ end
+ end
+
+ def key_address_fields_provided?
+ field_17.present? && field_19.present? && postcode_full.present?
+ end
+
+ def validate_address_fields
+ if field_16.blank? || log.errors.attribute_names.include?(:uprn)
+ if field_17.blank? && errors[:field_17].blank?
+ errors.add(:field_17, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1."))
+ end
+
+ if field_19.blank? && errors[:field_19].blank?
+ errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city."))
+ end
+
+ if field_21.blank? && errors[:field_21].blank?
+ errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode."))
+ end
+
+ if field_22.blank? && errors[:field_22].blank?
+ errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))
+ end
+ end
+ end
+
+ def shared_ownership?
+ field_8 == 1
+ end
+
+ def discounted_ownership?
+ field_8 == 2
+ end
+
+ def joint_purchase?
+ field_12 == 1
+ end
+
+ def joint_purchase_asked?
+ shared_ownership? || discounted_ownership? || field_13 == 2
+ end
+
+ def shared_or_discounted_but_not_staircasing?
+ (shared_ownership? || discounted_ownership?) && field_10 != 1
+ end
+
+ def shared_ownership_initial_purchase?
+ field_8 == 1 && field_10 != 1
+ end
+
+ def staircasing?
+ field_8 == 1 && field_10 == 1
+ end
+
+ def two_buyers_share_address?
+ field_63 == 2
+ end
+
+ def not_resale?
+ field_78 == 2
+ end
+
+ def buyer_1_previous_tenure_not_1_or_2?
+ field_58 != 1 && field_58 != 2
+ end
+
+ def mortgage_used?
+ field_88 == 2
+ end
+
+ def social_homebuy?
+ field_9 == 18
+ end
+
+ def buyers_own_all?
+ field_97 == 100
+ end
+
+ def buyer_staircased_before?
+ field_99 == 1
+ end
+
+ def rtb_like_sale_type?
+ [9, 14, 27, 29].include?(field_11)
+ end
+
+ def field_mapping_for_errors
+ {
+ purchid: %i[field_7],
+ saledate: %i[field_1 field_2 field_3],
+ noint: %i[field_14],
+ age1_known: %i[field_28],
+ age1: %i[field_28],
+ age2_known: %i[field_35],
+ age2: %i[field_35],
+ age3_known: %i[field_43],
+ age3: %i[field_43],
+ age4_known: %i[field_47],
+ age4: %i[field_47],
+ age5_known: %i[field_51],
+ age5: %i[field_51],
+ age6_known: %i[field_55],
+ age6: %i[field_55],
+ sex1: %i[field_29],
+ sex2: %i[field_36],
+ sex3: %i[field_44],
+
+ sex4: %i[field_48],
+ sex5: %i[field_52],
+ sex6: %i[field_56],
+ relat2: %i[field_34],
+ relat3: %i[field_42],
+ relat4: %i[field_46],
+ relat5: %i[field_49],
+ relat6: %i[field_54],
+
+ ecstat1: %i[field_32],
+ ecstat2: %i[field_39],
+ ecstat3: %i[field_45],
+
+ ecstat4: %i[field_49],
+ ecstat5: %i[field_53],
+ ecstat6: %i[field_57],
+ ethnic_group: %i[field_30],
+ ethnic: %i[field_30],
+ nationality_all: %i[field_31],
+ nationality_all_group: %i[field_31],
+ income1nk: %i[field_70],
+ income1: %i[field_70],
+ income2nk: %i[field_72],
+ income2: %i[field_72],
+ inc1mort: %i[field_71],
+ inc2mort: %i[field_73],
+ savingsnk: %i[field_75],
+ savings: %i[field_75],
+ prevown: %i[field_76],
+ prevten: %i[field_58],
+ prevloc: %i[field_62],
+ previous_la_known: %i[field_62],
+ ppcodenk: %i[field_59],
+ ppostcode_full: %i[field_60 field_61],
+ disabled: %i[field_68],
+
+ wheel: %i[field_69],
+ beds: %i[field_25],
+ proptype: %i[field_24],
+ builtype: %i[field_26],
+ la_known: %i[field_23],
+ la: %i[field_23],
+
+ is_la_inferred: %i[field_23],
+ pcodenk: %i[field_21 field_22],
+ postcode_full: %i[field_21 field_22],
+ wchair: %i[field_27],
+
+ type: %i[field_9 field_11 field_8],
+ resale: %i[field_78],
+ hodate: %i[field_80 field_81 field_82],
+
+ frombeds: %i[field_83],
+ fromprop: %i[field_84],
+ value: value_fields,
+ equity: equity_fields,
+ mortgage: mortgage_fields,
+ extrabor: extrabor_fields,
+ deposit: deposit_fields,
+ cashdis: %i[field_92],
+ mrent: mrent_fields,
+
+ has_mscharge: mscharge_fields,
+ mscharge: mscharge_fields,
+ grant: %i[field_114],
+ discount: %i[field_115],
+ owning_organisation_id: %i[field_4],
+ managing_organisation_id: [:field_5],
+ assigned_to: %i[field_6],
+ hhregres: %i[field_65],
+ hhregresstill: %i[field_66],
+ armedforcesspouse: %i[field_67],
+
+ hb: %i[field_74],
+ mortlen: mortlen_fields,
+ proplen: proplen_fields,
+
+ jointmore: %i[field_13],
+ staircase: %i[field_10],
+ privacynotice: %i[field_15],
+ ownershipsch: %i[field_8],
+
+ jointpur: %i[field_12],
+ buy1livein: %i[field_33],
+ buy2livein: %i[field_40],
+ hholdcount: %i[field_41],
+ stairbought: %i[field_96],
+ stairowned: %i[field_97],
+ socprevten: %i[field_85],
+ mortgageused: mortgageused_fields,
+
+ uprn: %i[field_16],
+ address_line1: %i[field_17],
+ address_line2: %i[field_18],
+ town_or_city: %i[field_19],
+ county: %i[field_20],
+ uprn_selection: [:field_17],
+
+ ethnic_group2: %i[field_37],
+ ethnicbuy2: %i[field_37],
+ nationality_all_buyer2: %i[field_38],
+ nationality_all_buyer2_group: %i[field_38],
+
+ buy2living: %i[field_63],
+ prevtenbuy2: %i[field_64],
+
+ prevshared: %i[field_77],
+
+ staircasesale: %i[field_98],
+ firststair: %i[field_99],
+ numstair: %i[field_103],
+ mrentprestaircasing: %i[field_110],
+ lasttransaction: %i[field_104 field_105 field_106],
+ initialpurchase: %i[field_100 field_101 field_102],
+
+ }
+ end
+
+ def attributes_for_log
+ attributes = {}
+
+ attributes["purchid"] = purchaser_code
+ attributes["saledate"] = saledate
+ attributes["noint"] = field_14
+
+ attributes["age1_known"] = age1_known?
+ attributes["age1"] = field_28 if attributes["age1_known"]&.zero? && field_28&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["age2_known"] = age2_known?
+ attributes["age2"] = field_35 if attributes["age2_known"]&.zero? && field_35&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["age3_known"] = age3_known?
+ attributes["age3"] = field_43 if attributes["age3_known"]&.zero? && field_43&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["age4_known"] = age4_known?
+ attributes["age4"] = field_47 if attributes["age4_known"]&.zero? && field_47&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["age5_known"] = age5_known?
+ attributes["age5"] = field_51 if attributes["age5_known"]&.zero? && field_51&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["age6_known"] = age6_known?
+ attributes["age6"] = field_55 if attributes["age6_known"]&.zero? && field_55&.match(/\A\d{1,3}\z|\AR\z/)
+
+ attributes["sex1"] = field_29
+ attributes["sex2"] = field_36
+ attributes["sex3"] = field_44
+ attributes["sex4"] = field_48
+ attributes["sex5"] = field_52
+ attributes["sex6"] = field_56
+
+ attributes["relat2"] = if field_34 == 1
+ "P"
+ else
+ (field_34 == 2 ? "X" : "R")
+ end
+ attributes["relat3"] = if field_42 == 1
+ "P"
+ else
+ (field_42 == 2 ? "X" : "R")
+ end
+ attributes["relat4"] = if field_46 == 1
+ "P"
+ else
+ (field_46 == 2 ? "X" : "R")
+ end
+ attributes["relat5"] = if field_49 == 1
+ "P"
+ else
+ (field_49 == 2 ? "X" : "R")
+ end
+ attributes["relat6"] = if field_54 == 1
+ "P"
+ else
+ (field_54 == 2 ? "X" : "R")
+ end
+
+ attributes["ecstat1"] = field_32
+ attributes["ecstat2"] = field_39
+ attributes["ecstat3"] = field_45
+ attributes["ecstat4"] = field_49
+ attributes["ecstat5"] = field_53
+ attributes["ecstat6"] = field_57
+
+ attributes["details_known_2"] = details_known?(2)
+ attributes["details_known_3"] = details_known?(3)
+ attributes["details_known_4"] = details_known?(4)
+ attributes["details_known_5"] = details_known?(5)
+ attributes["details_known_6"] = details_known?(6)
+
+ attributes["ethnic_group"] = ethnic_group_from_ethnic
+ attributes["ethnic"] = field_30
+ attributes["nationality_all"] = field_31 if field_31.present? && valid_nationality_options.include?(field_31.to_s)
+ attributes["nationality_all_group"] = nationality_group(attributes["nationality_all"])
+
+ attributes["income1nk"] = field_70 == "R" ? 1 : 0
+ attributes["income1"] = field_70.to_i if attributes["income1nk"]&.zero? && field_70&.match(/\A\d+\z/)
+
+ attributes["income2nk"] = field_72 == "R" ? 1 : 0
+ attributes["income2"] = field_72.to_i if attributes["income2nk"]&.zero? && field_72&.match(/\A\d+\z/)
+
+ attributes["inc1mort"] = field_71
+ attributes["inc2mort"] = field_73
+
+ attributes["savingsnk"] = field_75 == "R" ? 1 : 0
+ attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/)
+ attributes["prevown"] = field_76
+
+ attributes["prevten"] = field_58
+ attributes["prevloc"] = field_62
+ attributes["previous_la_known"] = previous_la_known
+ attributes["ppcodenk"] = previous_postcode_known
+ attributes["ppostcode_full"] = ppostcode_full
+
+ attributes["disabled"] = field_68
+ attributes["wheel"] = field_69
+ attributes["beds"] = field_25
+ attributes["proptype"] = field_24
+ attributes["builtype"] = field_26
+ attributes["la_known"] = field_23.present? ? 1 : 0
+ attributes["la"] = field_23
+ attributes["la_as_entered"] = field_23
+ attributes["is_la_inferred"] = false
+ attributes["pcodenk"] = 0 if postcode_full.present?
+ attributes["postcode_full"] = postcode_full
+ attributes["postcode_full_as_entered"] = postcode_full
+ attributes["wchair"] = field_27
+
+ attributes["type"] = sale_type
+ attributes["resale"] = field_78
+
+ attributes["hodate"] = hodate
+
+ attributes["frombeds"] = field_83
+ attributes["fromprop"] = field_84
+
+ attributes["value"] = value
+ attributes["equity"] = equity
+ attributes["mortgage"] = mortgage
+ attributes["extrabor"] = extrabor
+ attributes["deposit"] = deposit
+
+ attributes["cashdis"] = field_92
+ attributes["mrent"] = mrent
+ attributes["mscharge"] = mscharge if mscharge&.positive?
+ attributes["has_mscharge"] = attributes["mscharge"].present? ? 1 : 0
+ attributes["grant"] = field_114
+ attributes["discount"] = field_115
+
+ attributes["owning_organisation"] = owning_organisation
+ attributes["managing_organisation"] = managing_organisation
+ attributes["assigned_to"] = assigned_to || (bulk_upload.user.support? ? nil : bulk_upload.user)
+ attributes["created_by"] = bulk_upload.user
+ attributes["hhregres"] = field_65
+ attributes["hhregresstill"] = field_66
+ attributes["armedforcesspouse"] = field_67
+
+ attributes["hb"] = field_74
+
+ attributes["mortlen"] = mortlen
+
+ attributes["proplen"] = proplen if proplen&.positive?
+ attributes["proplen_asked"] = attributes["proplen"]&.present? ? 0 : 1
+ attributes["jointmore"] = field_13
+ attributes["staircase"] = field_10
+ attributes["privacynotice"] = field_15
+ attributes["ownershipsch"] = field_8
+ attributes["jointpur"] = field_12
+ attributes["buy1livein"] = field_33
+ attributes["buy2livein"] = field_40
+ attributes["hholdcount"] = field_41
+ attributes["stairbought"] = field_96
+ attributes["stairowned"] = field_97
+ attributes["socprevten"] = field_85
+ attributes["soctenant"] = infer_soctenant_from_prevten_and_prevtenbuy2
+ attributes["mortgageused"] = mortgageused
+
+ attributes["uprn"] = field_16
+ attributes["uprn_known"] = field_16.present? ? 1 : 0
+ attributes["uprn_confirmed"] = 1 if field_16.present?
+ attributes["skip_update_uprn_confirmed"] = true
+ attributes["address_line1"] = field_17
+ attributes["address_line1_as_entered"] = field_17
+ attributes["address_line2"] = field_18
+ attributes["address_line2_as_entered"] = field_18
+ attributes["town_or_city"] = field_19
+ attributes["town_or_city_as_entered"] = field_19
+ attributes["county"] = field_20
+ attributes["county_as_entered"] = field_20
+ attributes["address_line1_input"] = address_line1_input
+ attributes["postcode_full_input"] = postcode_full
+ attributes["select_best_address_match"] = true if field_16.blank?
+
+ attributes["ethnic_group2"] = infer_buyer2_ethnic_group_from_ethnic
+ attributes["ethnicbuy2"] = field_37
+ attributes["nationality_all_buyer2"] = field_38 if field_38.present? && valid_nationality_options.include?(field_38.to_s)
+ attributes["nationality_all_buyer2_group"] = nationality_group(attributes["nationality_all_buyer2"])
+
+ attributes["buy2living"] = field_63
+ attributes["prevtenbuy2"] = prevtenbuy2
+
+ attributes["prevshared"] = field_77
+
+ attributes["staircasesale"] = field_98
+
+ attributes["firststair"] = field_99
+ attributes["numstair"] = field_103
+ attributes["mrentprestaircasing"] = field_110
+ attributes["lasttransaction"] = lasttransaction
+ attributes["initialpurchase"] = initialpurchase
+
+ attributes["management_fee"] = field_95
+ attributes["has_management_fee"] = field_95.present? && field_95.positive? ? 1 : 0
+
+ attributes
+ end
+
+ def address_line1_input
+ [field_17, field_18, field_19].compact.join(", ")
+ end
+
+ def saledate
+ year = field_3.to_s.strip.length.between?(1, 2) ? field_3 + 2000 : field_3
+ Date.new(year, field_2, field_1) if field_3.present? && field_2.present? && field_1.present?
+ rescue Date::Error
+ Date.new
+ end
+
+ def hodate
+ year = field_82.to_s.strip.length.between?(1, 2) ? field_82 + 2000 : field_82
+ Date.new(year, field_81, field_80) if field_82.present? && field_81.present? && field_80.present?
+ rescue Date::Error
+ Date.new
+ end
+
+ def lasttransaction
+ year = field_106.to_s.strip.length.between?(1, 2) ? field_106 + 2000 : field_106
+ Date.new(year, field_105, field_104) if field_106.present? && field_105.present? && field_104.present?
+ rescue Date::Error
+ Date.new
+ end
+
+ def initialpurchase
+ year = field_102.to_s.strip.length.between?(1, 2) ? field_102 + 2000 : field_102
+ Date.new(year, field_101, field_100) if field_102.present? && field_101.present? && field_100.present?
+ rescue Date::Error
+ Date.new
+ end
+
+ def age1_known?
+ return 1 if field_28 == "R"
+
+ 0
+ end
+
+ [
+ { person: 2, field: :field_35 },
+ { person: 3, field: :field_43 },
+ { person: 4, field: :field_47 },
+ { person: 5, field: :field_51 },
+ { person: 6, field: :field_55 },
+ ].each do |hash|
+ define_method("age#{hash[:person]}_known?") do
+ return 1 if public_send(hash[:field]) == "R"
+ return 0 if send("person_#{hash[:person]}_present?")
+ end
+ end
+
+ def person_2_present?
+ field_35.present? || field_36.present? || field_34.present?
+ end
+
+ def person_3_present?
+ field_43.present? || field_44.present? || field_42.present?
+ end
+
+ def person_4_present?
+ field_47.present? || field_48.present? || field_46.present?
+ end
+
+ def person_5_present?
+ field_51.present? || field_52.present? || field_49.present?
+ end
+
+ def person_6_present?
+ field_55.present? || field_56.present? || field_54.present?
+ end
+
+ def details_known?(person_n)
+ send("person_#{person_n}_present?") ? 1 : 2
+ end
+
+ def ethnic_group_from_ethnic
+ return nil if field_30.blank?
+
+ case field_30
+ when 1, 2, 3, 18, 20
+ 0
+ when 4, 5, 6, 7
+ 1
+ when 8, 9, 10, 11, 15
+ 2
+ when 12, 13, 14
+ 3
+ when 16, 19
+ 4
+ when 17
+ 17
+ end
+ end
+
+ def postcode_full
+ [field_21, field_22].compact_blank.join(" ") if field_21 || field_22
+ end
+
+ def ppostcode_full
+ "#{field_60} #{field_61}" if field_60 && field_61
+ end
+
+ def sale_type
+ return field_9 if shared_ownership?
+ return field_11 if discounted_ownership?
+ end
+
+ def value
+ return field_86 if shared_ownership_initial_purchase?
+ return field_113 if discounted_ownership?
+ return field_107 if staircasing?
+ end
+
+ def equity
+ return field_87 if shared_ownership_initial_purchase?
+ return field_108 if staircasing?
+ end
+
+ def mortgage
+ return field_89 if shared_ownership?
+ return field_117 if discounted_ownership?
+ end
+
+ def extrabor
+ return field_119 if discounted_ownership?
+ end
+
+ def deposit
+ return field_91 if shared_ownership?
+ return field_120 if discounted_ownership?
+ end
+
+ def mrent
+ return field_93 if shared_ownership_initial_purchase?
+ return field_111 if staircasing?
+ end
+
+ def mscharge
+ return field_94 if shared_ownership?
+ return field_121 if discounted_ownership?
+ end
+
+ def mortlen
+ return field_90 if shared_ownership?
+ return field_118 if discounted_ownership?
+ end
+
+ def proplen
+ return field_79 if shared_ownership?
+ return field_112 if discounted_ownership?
+ end
+
+ def mortgageused
+ return field_88 if shared_ownership_initial_purchase?
+ return field_116 if discounted_ownership?
+ return field_109 if staircasing?
+ end
+
+ def value_fields
+ return [:field_86] if shared_ownership_initial_purchase?
+ return [:field_113] if discounted_ownership?
+ return [:field_107] if staircasing?
+
+ %i[field_86 field_113 field_107]
+ end
+
+ def equity_fields
+ return [:field_87] if shared_ownership_initial_purchase?
+ return [:field_108] if staircasing?
+
+ %i[field_87 field_108]
+ end
+
+ def mortgage_fields
+ return [:field_89] if shared_ownership?
+ return [:field_117] if discounted_ownership?
+
+ %i[field_89 field_117]
+ end
+
+ def extrabor_fields
+ return [:field_119] if discounted_ownership?
+
+ %i[field_119]
+ end
+
+ def deposit_fields
+ return [:field_91] if shared_ownership?
+ return [:field_120] if discounted_ownership?
+
+ %i[field_91 field_120]
+ end
+
+ def mrent_fields
+ return [:field_93] if shared_ownership_initial_purchase?
+ return [:field_111] if staircasing?
+
+ %i[field_93 field_111]
+ end
+
+ def mscharge_fields
+ return [:field_94] if shared_ownership?
+ return [:field_121] if discounted_ownership?
+
+ %i[field_94 field_121]
+ end
+
+ def mortlen_fields
+ return [:field_90] if shared_ownership?
+ return [:field_118] if discounted_ownership?
+
+ %i[field_90 field_118]
+ end
+
+ def proplen_fields
+ return [:field_79] if shared_ownership?
+ return [:field_112] if discounted_ownership?
+
+ %i[field_79 field_112]
+ end
+
+ def mortgageused_fields
+ return [:field_88] if shared_ownership_initial_purchase?
+ return [:field_116] if discounted_ownership?
+ return [:field_109] if staircasing?
+
+ %i[field_88 field_116 field_109]
+ end
+
+ def owning_organisation
+ @owning_organisation ||= Organisation.find_by_id_on_multiple_fields(field_4)
+ end
+
+ def assigned_to
+ @assigned_to ||= User.where("lower(email) = ?", field_6&.downcase).first
+ end
+
+ def previous_la_known
+ field_62.present? ? 1 : 0
+ end
+
+ def previous_postcode_known
+ return 1 if field_59 == 2
+
+ 0 if field_59 == 1
+ end
+
+ def infer_soctenant_from_prevten_and_prevtenbuy2
+ return unless shared_ownership?
+
+ if [1, 2].include?(field_58) || [1, 2].include?(field_64.to_i)
+ 1
+ else
+ 2
+ end
+ end
+
+ def block_log_creation!
+ self.block_log_creation = true
+ end
+
+ def questions
+ @questions ||= log.form.subsections.flat_map { |ss| ss.applicable_questions(log) }
+ end
+
+ def duplicate_check_fields
+ %w[
+ saledate
+ age1
+ sex1
+ ecstat1
+ owning_organisation
+ postcode_full
+ purchid
+ ]
+ end
+
+ def validate_owning_org_data_given
+ if field_4.blank?
+ block_log_creation!
+
+ if errors[:field_4].blank?
+ errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "owning organisation."), category: :setup)
+ end
+ end
+ end
+
+ def validate_owning_org_exists
+ if owning_organisation.nil?
+ block_log_creation!
+
+ if field_4.present? && errors[:field_4].blank?
+ errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_found"), category: :setup)
+ end
+ end
+ end
+
+ def validate_owning_org_owns_stock
+ if owning_organisation && !owning_organisation.holds_own_stock?
+ block_log_creation!
+
+ if errors[:field_4].blank?
+ errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_stock_owner"), category: :setup)
+ end
+ end
+ end
+
+ def validate_owning_org_permitted
+ return unless owning_organisation
+ return if bulk_upload_organisation.affiliated_stock_owners.include?(owning_organisation)
+
+ block_log_creation!
+
+ return if errors[:field_4].present?
+
+ if bulk_upload.user.support?
+ errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.support", name: bulk_upload_organisation.name), category: :setup)
+ else
+ errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.not_support"), category: :setup)
+ end
+ end
+
+ def validate_assigned_to_exists
+ return if field_6.blank?
+
+ unless assigned_to
+ errors.add(:field_6, I18n.t("#{ERROR_BASE_KEY}.assigned_to.not_found"))
+ end
+ end
+
+ def validate_assigned_to_when_support
+ if field_6.blank? && bulk_upload.user.support?
+ errors.add(:field_6, category: :setup, message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "what is the CORE username of the account this sales log should be assigned to?"))
+ end
+ end
+
+ def validate_assigned_to_related
+ return unless assigned_to
+ return if assigned_to.organisation == owning_organisation || assigned_to.organisation == managing_organisation
+ return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation
+
+ block_log_creation!
+ errors.add(:field_6, I18n.t("#{ERROR_BASE_KEY}.assigned_to.organisation_not_related"), category: :setup)
+ end
+
+ def managing_organisation
+ Organisation.find_by_id_on_multiple_fields(field_5)
+ end
+
+ def nationality_group(nationality_value)
+ return unless nationality_value
+ return 0 if nationality_value.zero?
+ return 826 if nationality_value == 826
+
+ 12
+ end
+
+ def validate_managing_org_related
+ if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation)
+ block_log_creation!
+
+ if errors[:field_5].blank?
+ errors.add(:field_5, I18n.t("#{ERROR_BASE_KEY}.assigned_to.managing_organisation_not_related"), category: :setup)
+ end
+ end
+ end
+
+ def setup_question?(question)
+ log.form.setup_sections[0].subsections[0].questions.include?(question)
+ end
+
+ def validate_nulls
+ field_mapping_for_errors.each do |error_key, fields|
+ question_id = error_key.to_s
+ question = questions.find { |q| q.id == question_id }
+
+ next unless question
+ next if log.optional_fields.include?(question.id)
+ next if question.completed?(log)
+
+ if setup_question?(question)
+ fields.each do |field|
+ if errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) }
+ errors.add(field, question.unanswered_error_message, category: :setup)
+ end
+ end
+ else
+ fields.each do |field|
+ if errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) }
+ errors.add(field, question.unanswered_error_message)
+ end
+ end
+ end
+ end
+ end
+
+ def validate_valid_radio_option
+ log.attributes.each do |question_id, _v|
+ question = log.form.get_question(question_id, log)
+
+ next if question_id == "type"
+
+ next unless question&.type == "radio"
+ next if log[question_id].blank? || question.answer_options.key?(log[question_id].to_s) || !question.page.routed_to?(log, nil)
+
+ fields = field_mapping_for_errors[question_id.to_sym] || []
+
+ if setup_question?(question)
+ fields.each do |field|
+ if errors[field].none?
+ block_log_creation!
+ errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field])), category: :setup)
+ end
+ end
+ else
+ fields.each do |field|
+ unless errors.any? { |e| fields.include?(e.attribute) }
+ errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field])))
+ end
+ end
+ end
+ end
+ end
+
+ def validate_relevant_collection_window
+ return if saledate.blank? || bulk_upload.form.blank?
+ return if errors.key?(:field_1) || errors.key?(:field_2) || errors.key?(:field_3)
+
+ unless bulk_upload.form.valid_start_date_for_form?(saledate)
+ errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup)
+ errors.add(:field_2, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup)
+ errors.add(:field_3, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup)
+ end
+ end
+
+ def validate_if_log_already_exists
+ if log_already_exists?
+ error_message = I18n.t("#{ERROR_BASE_KEY}.duplicate")
+
+ errors.add(:field_4, error_message) # Owning org
+ errors.add(:field_1, 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_21, error_message) # Postcode
+ errors.add(:field_22, error_message) # Postcode
+ errors.add(:field_28, error_message) # Buyer 1 age
+ errors.add(:field_29, error_message) # Buyer 1 gender
+ errors.add(:field_32, error_message) # Buyer 1 working situation
+ errors.add(:field_7, error_message) # Purchaser code
+ end
+ end
+
+ def validate_incomplete_soft_validations
+ routed_to_soft_validation_questions = log.form.questions.filter { |q| q.type == "interruption_screen" && q.page.routed_to?(log, nil) }.compact
+ routed_to_soft_validation_questions.each do |question|
+ next if question.completed?(log)
+
+ question.page.interruption_screen_question_ids.each do |interruption_screen_question_id|
+ next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) }
+
+ field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field|
+ if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) }
+ error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ")
+ errors.add(field, message: error_message, category: :soft_validation)
+ end
+ end
+ end
+ end
+ end
+
+ def validate_buyer1_economic_status
+ if field_32 == 9
+ if field_28.present? && field_28.to_i >= 16
+ errors.add(:field_32, I18n.t("#{ERROR_BASE_KEY}.ecstat1.buyer_cannot_be_over_16_and_child"))
+ errors.add(:field_28, I18n.t("#{ERROR_BASE_KEY}.age1.buyer_cannot_be_over_16_and_child"))
+ else
+ errors.add(:field_32, I18n.t("#{ERROR_BASE_KEY}.ecstat1.buyer_cannot_be_child"))
+ end
+ end
+ end
+
+ def validate_buyer2_economic_status
+ return unless joint_purchase?
+
+ if field_39 == 9
+ if field_35.present? && field_35.to_i >= 16
+ errors.add(:field_39, I18n.t("#{ERROR_BASE_KEY}.ecstat2.buyer_cannot_be_over_16_and_child"))
+ errors.add(:field_35, I18n.t("#{ERROR_BASE_KEY}.age2.buyer_cannot_be_over_16_and_child"))
+ else
+ errors.add(:field_39, I18n.t("#{ERROR_BASE_KEY}.ecstat2.buyer_cannot_be_child"))
+ end
+ end
+ end
+
+ def validate_nationality
+ if field_31.present? && !valid_nationality_options.include?(field_31.to_s)
+ errors.add(:field_31, I18n.t("#{ERROR_BASE_KEY}.nationality.invalid"))
+ end
+ end
+
+ def validate_buyer_2_nationality
+ if field_38.present? && !valid_nationality_options.include?(field_38.to_s)
+ errors.add(:field_38, I18n.t("#{ERROR_BASE_KEY}.nationality.invalid"))
+ end
+ end
+
+ def valid_nationality_options
+ %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say"
+ end
+
+ def bulk_upload_organisation
+ Organisation.find(bulk_upload.organisation_id)
+ end
+end
diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
similarity index 88%
rename from app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb
rename to app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
index ce873b6d7..887b81554 100644
--- a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb
+++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
@@ -14,7 +14,7 @@
Download template
- <%= govuk_link_to "Download the lettings bulk upload template (2024 to 2025)", @form.template_path %>
+ <%= govuk_link_to "Download the lettings bulk upload template (#{@form.year_combo})", @form.template_path %>
There are 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.
Create your file
@@ -22,7 +22,7 @@
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. Leave column A blank - the bulk upload fields start in column B.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
- "Use the #{govuk_link_to 'Lettings bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
+ "Use the #{govuk_link_to "Lettings bulk upload Specification (#{@form.year_combo})", @form.specification_path} to check your data is in the correct format.".html_safe,
"Username field: To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>
diff --git a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
similarity index 82%
rename from app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb
rename to app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
index 723ae5314..00c693cd4 100644
--- a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb
+++ b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
@@ -14,15 +14,15 @@
Download template
- Use one of these templates to upload logs for 2024/25:
- <%= govuk_link_to "Download the sales bulk upload template (2024 to 2025)", @form.template_path %>: In this template, the questions are in the same order as the 2024/25 paper form and web form.
+ Use one of these templates to upload logs for <%= @form.slash_year_combo %>:
+ <%= govuk_link_to "Download the sales bulk upload template (#{@form.year_combo})", @form.template_path %>: In this template, the questions are in the same order as the <%= @form.slash_year_combo %> paper form and web form.
There are 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.
Create your file
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. The bulk upload fields start at column B. Leave column A blank.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
- "Use the #{govuk_link_to 'Sales bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
+ "Use the #{govuk_link_to "Sales bulk upload Specification (#{@form.year_combo})", @form.specification_path} to check your data is in the correct format.".html_safe,
"Username field: To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6ca3ea322..3e35a32ce 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -57,6 +57,8 @@ en:
<<: *bulk_upload__row_parser__base
bulk_upload/lettings/year2023/row_parser:
<<: *bulk_upload__row_parser__base
+ bulk_upload/sales/year2025/row_parser:
+ <<: *bulk_upload__row_parser__base
bulk_upload/sales/year2024/row_parser:
<<: *bulk_upload__row_parser__base
bulk_upload/sales/year2023/row_parser:
diff --git a/config/locales/validations/sales/2025/bulk_upload.en.yml b/config/locales/validations/sales/2025/bulk_upload.en.yml
new file mode 100644
index 000000000..c9d194fd0
--- /dev/null
+++ b/config/locales/validations/sales/2025/bulk_upload.en.yml
@@ -0,0 +1,46 @@
+en:
+ validations:
+ sales:
+ 2025:
+ bulk_upload:
+ not_answered: "You must answer %{question}"
+ invalid_option: "Enter a valid value for %{question}"
+ spreadsheet_dupe: "This is a duplicate of a log in your file."
+ duplicate: "This is a duplicate log."
+ blank_file: "Template is blank - The template must be filled in for us to create the logs and check if data is correct."
+ wrong_template:
+ over_max_column_count: "Too many columns, please ensure you have used the correct template."
+ no_headers: "Your file does not contain the required header rows. Add or check the header rows and upload your file again. [Read more about using the template headers](%{guidance_link})."
+ wrong_field_numbers_count: "Incorrect number of fields, please ensure you have used the correct template."
+ wrong_template: "Incorrect sale dates, please ensure you have used the correct template."
+ numeric:
+ within_range: "%{field} must be between %{min} and %{max}."
+ owning_organisation:
+ not_found: "The owning organisation code is incorrect."
+ not_stock_owner: "The owning organisation code provided is for an organisation that does not own stock."
+ not_permitted:
+ support: "This owning organisation is not affiliated with %{name}."
+ not_support: "You do not have permission to add logs for this owning organisation."
+ assigned_to:
+ not_found: "User with the specified email could not be found."
+ organisation_not_related: "User must be related to owning organisation or managing organisation."
+ managing_organisation_not_related: "This organisation does not have a relationship with the owning organisation."
+ saledate:
+ outside_collection_window: "Enter a date within the %{year_combo} collection year, which is between 1st April %{start_year} and 31st March %{end_year}."
+ year_not_two_or_four_digits: "Sale completion year must be 2 or 4 digits."
+ ecstat1:
+ buyer_cannot_be_over_16_and_child: "Buyer 1's age cannot be 16 or over if their working situation is child under 16."
+ buyer_cannot_be_child: "Buyer 1 cannot have a working situation of child under 16."
+ age1:
+ buyer_cannot_be_over_16_and_child: "Buyer 1's age cannot be 16 or over if their working situation is child under 16."
+ ecstat2:
+ buyer_cannot_be_over_16_and_child: "Buyer 2's age cannot be 16 or over if their working situation is child under 16."
+ buyer_cannot_be_child: "Buyer 2 cannot have a working situation of child under 16."
+ age2:
+ buyer_cannot_be_over_16_and_child: "Buyer 2's age cannot be 16 or over if their working situation is child under 16."
+ address:
+ not_found: "We could not find this address. Check the address data in your CSV file is correct and complete, or select the correct address using the CORE site."
+ not_determined: "There are multiple matches for this address. Either select the correct address manually or correct the UPRN in the CSV file."
+ not_answered: "Enter either the UPRN or the full address."
+ nationality:
+ invalid: "Select a valid nationality."
diff --git a/session-manager-plugin.deb b/session-manager-plugin.deb
new file mode 100644
index 0000000000000000000000000000000000000000..3befd8f9cc07016655f23aae9612305433c6f271
GIT binary patch
literal 3908056
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 |