From 04f3285bbfe32a3d171d85af2d792f44a3d62e48 Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:18:50 +0000 Subject: [PATCH 1/5] CLDC-4354: Return staging to real time post-testing (#3273) * Revert "CLDC-4235, CLDC-4280: staging go-live testing prep (#3262)" This reverts commit 21367a7e3e5c458b26abe750dc50ff042ab749e0. * CLDC-4280: continue to block future form use on staging --- Gemfile | 5 +- app/helpers/timecop_helper.rb | 9 ---- app/mailers/devise_notify_mailer.rb | 13 ++--- app/mailers/notify_mailer.rb | 13 ++--- app/services/storage/s3_service.rb | 76 +++++++++++------------------ config/initializers/timecop.rb | 4 -- 6 files changed, 39 insertions(+), 81 deletions(-) delete mode 100644 app/helpers/timecop_helper.rb delete mode 100644 config/initializers/timecop.rb diff --git a/Gemfile b/Gemfile index d7f1db035..91ed6c8a8 100644 --- a/Gemfile +++ b/Gemfile @@ -103,10 +103,6 @@ group :development do gem "rubocop-rails", require: false end -group :test, :staging do - gem "timecop", "~> 0.9.4" -end - group :test do gem "axe-core-rspec" gem "capybara", require: false @@ -115,6 +111,7 @@ group :test do gem "rspec-rails", require: false gem "selenium-webdriver", require: false gem "simplecov", require: false + gem "timecop", "~> 0.9.4" gem "webmock", require: false end diff --git a/app/helpers/timecop_helper.rb b/app/helpers/timecop_helper.rb deleted file mode 100644 index 2780cc918..000000000 --- a/app/helpers/timecop_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -module TimecopHelper - def without_timecop(&block) - if defined?(Timecop) - Timecop.return(&block) - else - yield - end - end -end diff --git a/app/mailers/devise_notify_mailer.rb b/app/mailers/devise_notify_mailer.rb index d023ca843..4065e3aa9 100644 --- a/app/mailers/devise_notify_mailer.rb +++ b/app/mailers/devise_notify_mailer.rb @@ -1,5 +1,4 @@ class DeviseNotifyMailer < Devise::Mailer - include TimecopHelper require "notifications/client" def notify_client @@ -9,13 +8,11 @@ class DeviseNotifyMailer < Devise::Mailer def send_email(email_address, template_id, personalisation) return true if intercept_send?(email_address) - without_timecop do - notify_client.send_email( - email_address:, - template_id:, - personalisation:, - ) - end + notify_client.send_email( + email_address:, + template_id:, + personalisation:, + ) rescue Notifications::Client::BadRequestError => e Sentry.capture_exception(e) diff --git a/app/mailers/notify_mailer.rb b/app/mailers/notify_mailer.rb index 93187a749..21a6e0270 100644 --- a/app/mailers/notify_mailer.rb +++ b/app/mailers/notify_mailer.rb @@ -1,5 +1,4 @@ class NotifyMailer < ApplicationMailer - include TimecopHelper require "notifications/client" def notify_client @@ -9,13 +8,11 @@ class NotifyMailer < ApplicationMailer def send_email(email, template_id, personalisation) return true if intercept_send?(email) - without_timecop do - notify_client.send_email( - email_address: email, - template_id:, - personalisation:, - ) - end + notify_client.send_email( + email_address: email, + template_id:, + personalisation:, + ) end def personalisation(record, token, url, username: false) diff --git a/app/services/storage/s3_service.rb b/app/services/storage/s3_service.rb index c94a418b7..a6eef7d49 100644 --- a/app/services/storage/s3_service.rb +++ b/app/services/storage/s3_service.rb @@ -1,7 +1,5 @@ module Storage class S3Service < StorageService - include TimecopHelper - attr_reader :configuration def initialize(config_service, instance_name) @@ -13,79 +11,61 @@ module Storage end def list_files(folder) - without_timecop do - @client.list_objects_v2(bucket: @configuration.bucket_name, prefix: folder) - .flat_map { |response| response.contents.map(&:key) } - end + @client.list_objects_v2(bucket: @configuration.bucket_name, prefix: folder) + .flat_map { |response| response.contents.map(&:key) } end def folder_present?(folder) - without_timecop do - response = @client.list_objects_v2(bucket: @configuration.bucket_name, prefix: folder, max_keys: 1) - response.key_count == 1 - end + response = @client.list_objects_v2(bucket: @configuration.bucket_name, prefix: folder, max_keys: 1) + response.key_count == 1 end def get_presigned_url(file_name, duration, response_content_disposition: nil) - without_timecop do - Aws::S3::Presigner - .new({ client: @client }) - .presigned_url(:get_object, bucket: @configuration.bucket_name, key: file_name, expires_in: duration, response_content_disposition:) - end + Aws::S3::Presigner + .new({ client: @client }) + .presigned_url(:get_object, bucket: @configuration.bucket_name, key: file_name, expires_in: duration, response_content_disposition:) end def get_file_io(file_name) - without_timecop do - @client.get_object(bucket: @configuration.bucket_name, key: file_name) - .body - end + @client.get_object(bucket: @configuration.bucket_name, key: file_name) + .body end def get_file(file_name) - without_timecop do - @client.get_object(bucket: @configuration.bucket_name, key: file_name) - .body.read - end + @client.get_object(bucket: @configuration.bucket_name, key: file_name) + .body.read end def write_file(file_name, data, content_type: nil) - without_timecop do - if content_type.nil? - @client.put_object( - body: data, - bucket: @configuration.bucket_name, - key: file_name, - ) - else - @client.put_object( - body: data, - bucket: @configuration.bucket_name, - key: file_name, - content_type:, - ) - end + if content_type.nil? + @client.put_object( + body: data, + bucket: @configuration.bucket_name, + key: file_name, + ) + else + @client.put_object( + body: data, + bucket: @configuration.bucket_name, + key: file_name, + content_type:, + ) end end def get_file_metadata(file_name) - without_timecop do - @client.head_object(bucket: @configuration.bucket_name, key: file_name) - end + @client.head_object(bucket: @configuration.bucket_name, key: file_name) end def file_exists?(file_name) - without_timecop do - @client.head_object(bucket: @configuration.bucket_name, key: file_name) - true - end + @client.head_object(bucket: @configuration.bucket_name, key: file_name) + true rescue Aws::S3::Errors::NotFound false end def delete_file(file_name) - without_timecop do - @client.delete_object(bucket: @configuration.bucket_name, key: file_name) - end + @client.delete_object(bucket: @configuration.bucket_name, key: file_name) end private diff --git a/config/initializers/timecop.rb b/config/initializers/timecop.rb deleted file mode 100644 index 6e1f6fdf7..000000000 --- a/config/initializers/timecop.rb +++ /dev/null @@ -1,4 +0,0 @@ -if Rails.env.staging? - require "timecop" - Timecop.travel(Time.zone.local(2026, 4, 1)) -end From dc1f25f72254032ad9ef67678edaf8ae1b37ef5f Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 26 Mar 2026 12:54:35 +0000 Subject: [PATCH 2/5] CLDC-4340: Account for nil location name on location select (#3268) * CLDC-4340: Account for nil location name on location select use postcode as a fallback, which should never be nil * CLDC-4340: Add a verifying test * CLDC-4340: Put all postcode locations at the end * CLDC-4340: Sort locations by postcode --- .../form/lettings/questions/location_id.rb | 7 ++--- .../lettings/questions/location_id_spec.rb | 26 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/models/form/lettings/questions/location_id.rb b/app/models/form/lettings/questions/location_id.rb index 80e34aac0..2964298ed 100644 --- a/app/models/form/lettings/questions/location_id.rb +++ b/app/models/form/lettings/questions/location_id.rb @@ -30,11 +30,8 @@ class Form::Lettings::Questions::LocationId < ::Form::Question scheme_location_ids = lettings_log.scheme.locations.visible.confirmed.pluck(:id) answer_options.select { |k, _v| scheme_location_ids.include?(k.to_i) } - .sort_by { |_, v| - name = v["hint"].match(/[a-zA-Z].*/).to_s - number = v["hint"].match(/\d+/).to_s.to_i - [name, number] - }.to_h + .sort_by { |_, v| v["value"] } + .to_h end def hidden_in_check_answers?(lettings_log, _current_user = nil) diff --git a/spec/models/form/lettings/questions/location_id_spec.rb b/spec/models/form/lettings/questions/location_id_spec.rb index 2ea72c705..755c1f1a6 100644 --- a/spec/models/form/lettings/questions/location_id_spec.rb +++ b/spec/models/form/lettings/questions/location_id_spec.rb @@ -142,27 +142,27 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do context "and some locations start with numbers" do before do - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road", postcode: "AA1 1AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road", postcode: "AA1 2AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane", postcode: "AA1 3AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road", postcode: "AA1 4AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane", postcode: "AA1 5AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue", postcode: "AA1 6AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road", postcode: "AA1 7AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road", postcode: "AA1 8AA") lettings_log.update!(scheme:) end - it "orders the locations by name then numerically" do + it "orders the locations by postcode" do expect(question.displayed_answer_options(lettings_log).values.map { |v| v["hint"] }).to eq([ - "Abacus Road", - "1 Abe Road", "2 Abe Road", - "3 Abe Road", - "Hawthorne Road", + "1 Abe Road", "1 Lake Lane", + "3 Abe Road", "2 Lake Lane", "Smith Avenue", + "Abacus Road", + "Hawthorne Road", ]) end end From 0a10512dc664f6a091beb4432303b868f20335d0 Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 26 Mar 2026 12:56:07 +0000 Subject: [PATCH 3/5] Revert "CLDC-4340: Account for nil location name on location select (#3268)" (#3280) This reverts commit dc1f25f72254032ad9ef67678edaf8ae1b37ef5f. --- .../form/lettings/questions/location_id.rb | 7 +++-- .../lettings/questions/location_id_spec.rb | 26 +++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/models/form/lettings/questions/location_id.rb b/app/models/form/lettings/questions/location_id.rb index 2964298ed..80e34aac0 100644 --- a/app/models/form/lettings/questions/location_id.rb +++ b/app/models/form/lettings/questions/location_id.rb @@ -30,8 +30,11 @@ class Form::Lettings::Questions::LocationId < ::Form::Question scheme_location_ids = lettings_log.scheme.locations.visible.confirmed.pluck(:id) answer_options.select { |k, _v| scheme_location_ids.include?(k.to_i) } - .sort_by { |_, v| v["value"] } - .to_h + .sort_by { |_, v| + name = v["hint"].match(/[a-zA-Z].*/).to_s + number = v["hint"].match(/\d+/).to_s.to_i + [name, number] + }.to_h end def hidden_in_check_answers?(lettings_log, _current_user = nil) diff --git a/spec/models/form/lettings/questions/location_id_spec.rb b/spec/models/form/lettings/questions/location_id_spec.rb index 755c1f1a6..2ea72c705 100644 --- a/spec/models/form/lettings/questions/location_id_spec.rb +++ b/spec/models/form/lettings/questions/location_id_spec.rb @@ -142,27 +142,27 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do context "and some locations start with numbers" do before do - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road", postcode: "AA1 1AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road", postcode: "AA1 2AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane", postcode: "AA1 3AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road", postcode: "AA1 4AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane", postcode: "AA1 5AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue", postcode: "AA1 6AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road", postcode: "AA1 7AA") - FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road", postcode: "AA1 8AA") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 5), name: "2 Abe Road") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 6), name: "1 Abe Road") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 7), name: "1 Lake Lane") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 8), name: "3 Abe Road") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 9), name: "2 Lake Lane") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 10), name: "Smith Avenue") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 11), name: "Abacus Road") + FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12), name: "Hawthorne Road") lettings_log.update!(scheme:) end - it "orders the locations by postcode" do + it "orders the locations by name then numerically" do expect(question.displayed_answer_options(lettings_log).values.map { |v| v["hint"] }).to eq([ - "2 Abe Road", + "Abacus Road", "1 Abe Road", - "1 Lake Lane", + "2 Abe Road", "3 Abe Road", + "Hawthorne Road", + "1 Lake Lane", "2 Lake Lane", "Smith Avenue", - "Abacus Road", - "Hawthorne Road", ]) end end From eda08bea083423cf155930cfd1288f8aed46be3a Mon Sep 17 00:00:00 2001 From: Samuel Young Date: Thu, 26 Mar 2026 12:57:33 +0000 Subject: [PATCH 4/5] CLDC-NONE: Turn future forms back on for staging (#3279) --- app/services/feature_toggle.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index fd9c2cf1c..6f557722c 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -1,6 +1,6 @@ class FeatureToggle def self.allow_future_form_use? - Rails.env.development? || Rails.env.review? + Rails.env.development? || Rails.env.review? || Rails.env.staging? end def self.bulk_upload_duplicate_log_check_enabled? From 3124107db87ba71916915daa2b030f4cc1e45107 Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:39:32 +0000 Subject: [PATCH 5/5] CLDC-4333: staircasing value hint text/validation fix (#3270) * CLDC-4333: update purchase price soft vals to take stairbought into account * CLDC-4333: set hard min to 0 for staircasing * CLDC-4333: add dynamic hard min based on stairbought * CLDC-4333: step value input width up to next option * CLDC-4333: update value spec * CLDC-4333: update hint text instead to ensure value is 100% of equity * CLDC-4333: update hint text instead to ensure value is 100% of equity * CLDC-4333: revert q text change to align with spec * CLDC-4333: revert spec changes * CLDC-4333: revert 2025 changes * CLDC-4333: clarify 2026 question * CLDC-4333: revert question fix * CLDC-4333: revert initial purchase hint text update --- app/models/form/sales/questions/value.rb | 2 +- config/locales/forms/2026/sales/sale_information.en.yml | 2 +- config/locales/validations/sales/sale_information.en.yml | 2 +- spec/models/form/sales/questions/value_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/form/sales/questions/value.rb b/app/models/form/sales/questions/value.rb index e731fda8e..0884a3fb6 100644 --- a/app/models/form/sales/questions/value.rb +++ b/app/models/form/sales/questions/value.rb @@ -6,7 +6,7 @@ class Form::Sales::Questions::Value < ::Form::Question @type = "numeric" @min = form.start_year_2026_or_later? ? 15_000 : 0 @step = 1 - @width = 5 + @width = 10 @prefix = "£" @question_number = get_question_number_from_hash(QUESTION_NUMBER_FROM_YEAR_AND_SECTION, value_key: subsection.id) @top_guidance_partial = "financial_calculations_shared_ownership" diff --git a/config/locales/forms/2026/sales/sale_information.en.yml b/config/locales/forms/2026/sales/sale_information.en.yml index b9c3bc8e9..f7edff900 100644 --- a/config/locales/forms/2026/sales/sale_information.en.yml +++ b/config/locales/forms/2026/sales/sale_information.en.yml @@ -153,7 +153,7 @@ en: value_shared_ownership_staircase: check_answer_label: "Full purchase price" check_answer_prompt: "" - hint_text: "Enter the full purchase price paid for the equity bought in this staircasing transaction (this is equal to the value of the share bought by the purchaser)." + 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 in the current and all previous transactions)." question_text: "What was the full purchase price for this staircasing transaction?" equity: diff --git a/config/locales/validations/sales/sale_information.en.yml b/config/locales/validations/sales/sale_information.en.yml index 3c39608cc..ffdf1a874 100644 --- a/config/locales/validations/sales/sale_information.en.yml +++ b/config/locales/validations/sales/sale_information.en.yml @@ -76,7 +76,7 @@ en: mortgage_not_used: "The cash deposit is %{deposit}.

The full purchase price (%{value}) multiplied by the percentage bought is %{expected_shared_ownership_deposit_value}.

These two amounts should be the same." mortgage_used_socialhomebuy: "The mortgage amount (%{mortgage}), cash deposit (%{deposit}), and cash discount (%{cashdis}) added together is %{mortgage_deposit_and_discount_total}.

The full purchase price (%{value}) multiplied by the percentage equity stake purchased (%{equity}) is %{expected_shared_ownership_deposit_value}.

These two amounts should be the same." mortgage_not_used_socialhomebuy: "The cash deposit (%{deposit}) and cash discount (%{cashdis}) added together is %{deposit_and_discount_total}.

The full purchase price (%{value}) multiplied by the percentage bought (%{equity}) is %{expected_shared_ownership_deposit_value}.

These two amounts should be the same." - staircasing_mortgage: # this key + staircasing_mortgage: mortgage_used: "The mortgage (%{mortgage}) and cash deposit (%{deposit}) added together is %{mortgage_and_deposit_total}.

The full purchase price (%{value}) multiplied by the percentage bought is %{stairbought_part_of_value}.

These two amounts should be the same." mortgage_not_used: "The cash deposit is %{deposit}.

The full purchase price (%{value}) multiplied by the percentage bought is %{stairbought_part_of_value}.

These two amounts should be the same." mortgage_used_socialhomebuy: "The mortgage amount (%{mortgage}), cash deposit (%{deposit}), and cash discount (%{cashdis}) added together is %{mortgage_deposit_and_discount_total}.

The full purchase price (%{value}) multiplied by the percentage bought (%{stairbought}) is %{stairbought_part_of_value}.

These two amounts should be the same." diff --git a/spec/models/form/sales/questions/value_spec.rb b/spec/models/form/sales/questions/value_spec.rb index 2ebbf6d89..952b02e7c 100644 --- a/spec/models/form/sales/questions/value_spec.rb +++ b/spec/models/form/sales/questions/value_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Form::Sales::Questions::Value, type: :model do end it "has correct width" do - expect(question.width).to eq(5) + expect(question.width).to eq(10) end it "has correct prefix" do