Browse Source

Merge branch 'main' into CLDC-3361-scheme-back-link

pull/2802/head
kosiakkatrina 1 year ago committed by GitHub
parent
commit
01e9274408
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      Gemfile
  2. 5
      app/components/create_log_actions_component.html.erb
  3. 8
      app/components/create_log_actions_component.rb
  4. 27
      app/controllers/csv_downloads_controller.rb
  5. 14
      app/controllers/lettings_logs_controller.rb
  6. 1
      app/controllers/merge_requests_controller.rb
  7. 2
      app/controllers/organisations_controller.rb
  8. 14
      app/controllers/sales_logs_controller.rb
  9. 6
      app/controllers/schemes_controller.rb
  10. 1
      app/controllers/users_controller.rb
  11. 8
      app/helpers/filters_helper.rb
  12. 4
      app/helpers/merge_requests_helper.rb
  13. 20
      app/helpers/schemes_helper.rb
  14. 13
      app/jobs/email_csv_job.rb
  15. 13
      app/jobs/scheme_email_csv_job.rb
  16. 10
      app/models/csv_download.rb
  17. 5
      app/models/derived_variables/lettings_log_variables.rb
  18. 5
      app/models/derived_variables/sales_log_variables.rb
  19. 1
      app/models/form/lettings/subsections/household_needs.rb
  20. 2
      app/models/form/page.rb
  21. 2
      app/models/form/question.rb
  22. 2
      app/models/form/sales/pages/buyer_interview.rb
  23. 1
      app/models/form/sales/pages/deposit.rb
  24. 1
      app/models/form/sales/pages/deposit_discount.rb
  25. 1
      app/models/form/sales/pages/discount.rb
  26. 1
      app/models/form/sales/pages/equity.rb
  27. 13
      app/models/form/sales/pages/estate_management_fee.rb
  28. 1
      app/models/form/sales/pages/extra_borrowing.rb
  29. 1
      app/models/form/sales/pages/grant.rb
  30. 12
      app/models/form/sales/pages/living_before_purchase.rb
  31. 1
      app/models/form/sales/pages/monthly_rent.rb
  32. 1
      app/models/form/sales/pages/mortgage_amount.rb
  33. 1
      app/models/form/sales/pages/mortgage_lender.rb
  34. 1
      app/models/form/sales/pages/mortgage_lender_other.rb
  35. 1
      app/models/form/sales/pages/mortgage_length.rb
  36. 1
      app/models/form/sales/pages/mortgageused.rb
  37. 1
      app/models/form/sales/pages/previous_bedrooms.rb
  38. 1
      app/models/form/sales/pages/previous_property_type.rb
  39. 1
      app/models/form/sales/pages/previous_tenure.rb
  40. 2
      app/models/form/sales/pages/privacy_notice.rb
  41. 1
      app/models/form/sales/pages/resale.rb
  42. 2
      app/models/form/sales/pages/staircase.rb
  43. 1
      app/models/form/sales/pages/value_shared_ownership.rb
  44. 2
      app/models/form/sales/questions/buyer_interview.rb
  45. 1
      app/models/form/sales/questions/deposit_amount.rb
  46. 1
      app/models/form/sales/questions/deposit_discount.rb
  47. 1
      app/models/form/sales/questions/discount.rb
  48. 1
      app/models/form/sales/questions/equity.rb
  49. 1
      app/models/form/sales/questions/extra_borrowing.rb
  50. 1
      app/models/form/sales/questions/fromprop.rb
  51. 1
      app/models/form/sales/questions/grant.rb
  52. 24
      app/models/form/sales/questions/has_management_fee.rb
  53. 12
      app/models/form/sales/questions/management_fee.rb
  54. 1
      app/models/form/sales/questions/monthly_rent.rb
  55. 1
      app/models/form/sales/questions/mortgage_amount.rb
  56. 1
      app/models/form/sales/questions/mortgage_lender.rb
  57. 1
      app/models/form/sales/questions/mortgage_lender_other.rb
  58. 1
      app/models/form/sales/questions/mortgage_length.rb
  59. 1
      app/models/form/sales/questions/mortgageused.rb
  60. 1
      app/models/form/sales/questions/previous_bedrooms.rb
  61. 1
      app/models/form/sales/questions/previous_tenure.rb
  62. 2
      app/models/form/sales/questions/privacy_notice.rb
  63. 1
      app/models/form/sales/questions/resale.rb
  64. 1
      app/models/form/sales/questions/value.rb
  65. 10
      app/models/form/sales/sections/sale_information.rb
  66. 1
      app/models/form/sales/subsections/discounted_ownership_scheme.rb
  67. 1
      app/models/form/sales/subsections/outright_sale.rb
  68. 48
      app/models/form/sales/subsections/shared_ownership_initial_purchase.rb
  69. 2
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  70. 8
      app/models/location.rb
  71. 1
      app/models/log.rb
  72. 7
      app/models/merge_request_organisation.rb
  73. 8
      app/models/scheme.rb
  74. 16
      app/policies/csv_download_policy.rb
  75. 15
      app/policies/location_policy.rb
  76. 15
      app/policies/scheme_policy.rb
  77. 2
      app/services/bulk_upload/lettings/year2024/csv_parser.rb
  78. 2
      app/services/bulk_upload/sales/year2024/csv_parser.rb
  79. 50
      app/services/csv/downloader.rb
  80. 4
      app/services/feature_toggle.rb
  81. 2
      app/services/mandatory_collection_resources_service.rb
  82. 10
      app/views/csv_downloads/show.html.erb
  83. 8
      app/views/errors/download_link_expired.html.erb
  84. 6
      app/views/form/guidance/_financial_calculations_shared_ownership.html.erb
  85. 15
      app/views/locations/index.html.erb
  86. 2
      app/views/locations/show.html.erb
  87. 8
      app/views/merge_requests/_notification_banners.html.erb
  88. 2
      app/views/merge_requests/show.html.erb
  89. 6
      app/views/schemes/details.html.erb
  90. 2
      app/views/schemes/show.html.erb
  91. 4
      config/locales/en.yml
  92. 2
      config/locales/forms/2024/lettings/setup.en.yml
  93. 21
      config/locales/forms/2025/sales/sale_information.en.yml
  94. 12
      config/routes.rb
  95. 8
      db/migrate/20241114154215_add_management_fee_fields.rb
  96. 12
      db/migrate/20241118104046_add_csv_download_table.rb
  97. 16
      db/schema.rb
  98. 2
      db/seeds.rb
  99. 1
      docs/Gemfile.lock
  100. 2
      docs/app_api.md
  101. Some files were not shown because too many files have changed in this diff Show More

4
Gemfile

@ -62,6 +62,8 @@ gem "possessive"
# Strip whitespace from active record attributes
gem "auto_strip_attributes"
# Use sidekiq for background processing
gem "factory_bot_rails"
gem "faker"
gem "method_source", "~> 1.1"
gem "rails_admin", "~> 3.1"
gem "ruby-openai"
@ -75,8 +77,6 @@ group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem "byebug", platforms: %i[mri mingw x64_mingw]
gem "dotenv-rails"
gem "factory_bot_rails"
gem "faker"
gem "pry-byebug"
gem "parallel_tests"

5
app/components/create_log_actions_component.html.erb

@ -7,5 +7,10 @@
<% if user.support? %>
<%= govuk_button_link_to view_uploads_button_copy, view_uploads_button_href, secondary: true %>
<% end %>
<% if FeatureToggle.create_test_logs_enabled? %>
<%= govuk_button_link_to "Create test log", create_test_log_href, secondary: true %>
<%= govuk_button_link_to "Create test log (setup only)", create_setup_test_log_href, secondary: true %>
<% end %>
<% end %>
</div>

8
app/components/create_log_actions_component.rb

@ -34,6 +34,14 @@ class CreateLogActionsComponent < ViewComponent::Base
send("bulk_upload_#{log_type}_log_path", id: "start")
end
def create_test_log_href
send("create_test_#{log_type}_log_path")
end
def create_setup_test_log_href
send("create_setup_test_#{log_type}_log_path")
end
def view_uploads_button_copy
"View #{log_type} bulk uploads"
end

27
app/controllers/csv_downloads_controller.rb

@ -0,0 +1,27 @@
class CsvDownloadsController < ApplicationController
before_action :authenticate_user!
def show
@csv_download = CsvDownload.find(params[:id])
authorize @csv_download
return render "errors/download_link_expired" if @csv_download.expired?
end
def download
csv_download = CsvDownload.find(params[:id])
authorize csv_download
return render "errors/download_link_expired" if csv_download.expired?
downloader = Csv::Downloader.new(csv_download:)
if Rails.env.development?
downloader.call
send_file downloader.path, filename: csv_download.filename, type: "text/csv"
else
presigned_url = downloader.presigned_url
redirect_to presigned_url, allow_other_host: true
end
end
end

14
app/controllers/lettings_logs_controller.rb

@ -149,6 +149,20 @@ class LettingsLogsController < LogsController
end
end
def create_test_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA")
redirect_to lettings_log_path(log)
end
def create_setup_test_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:lettings_log, :setup_completed, assigned_to: current_user)
redirect_to lettings_log_path(log)
end
private
def session_filters

1
app/controllers/merge_requests_controller.rb

@ -143,6 +143,7 @@ private
if [day, month, year].none?(&:blank?) && Date.valid_date?(year.to_i, month.to_i, day.to_i)
merge_request_params["merge_date"] = Time.zone.local(year.to_i, month.to_i, day.to_i)
@merge_request.errors.add(:merge_date, :more_than_year_from_today) if Time.zone.local(year.to_i, month.to_i, day.to_i) - 1.year > Time.zone.today
else
@merge_request.errors.add(:merge_date, :invalid)
end

2
app/controllers/organisations_controller.rb

@ -24,7 +24,7 @@ class OrganisationsController < ApplicationController
end
def schemes
organisation_schemes = Scheme.visible.where(owning_organisation: [@organisation] + @organisation.parent_organisations)
organisation_schemes = Scheme.visible.where(owning_organisation: [@organisation] + @organisation.parent_organisations + @organisation.absorbed_organisations.visible.merged_during_open_collection_period)
@pagy, @schemes = pagy(filter_manager.filtered_schemes(organisation_schemes, search_term, session_filters))
@searched = search_term.presence

14
app/controllers/sales_logs_controller.rb

@ -119,6 +119,20 @@ class SalesLogsController < LogsController
end
end
def create_test_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user)
redirect_to sales_log_path(log)
end
def create_setup_test_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:sales_log, :shared_ownership_setup_complete, assigned_to: current_user)
redirect_to sales_log_path(log)
end
private
def session_filters

6
app/controllers/schemes_controller.rb

@ -118,6 +118,10 @@ class SchemesController < ApplicationController
validation_errors scheme_params
if @scheme.errors.empty? && @scheme.save
if @scheme.owning_organisation.merge_date.present?
deactivation = SchemeDeactivationPeriod.new(scheme: @scheme, deactivation_date: @scheme.owning_organisation.merge_date)
deactivation.save!(validate: false)
end
redirect_to scheme_primary_client_group_path(@scheme)
else
if @scheme.errors.any? { |error| error.attribute == :owning_organisation }
@ -152,7 +156,7 @@ class SchemesController < ApplicationController
flash[:notice] = if scheme_previously_confirmed
"#{@scheme.service_name} has been updated."
else
"#{@scheme.service_name} has been created. It does not require helpdesk approval."
"#{@scheme.service_name} has been created."
end
redirect_to scheme_path(@scheme)
end

1
app/controllers/users_controller.rb

@ -114,6 +114,7 @@ class UsersController < ApplicationController
validate_attributes
if @user.errors.empty? && @user.save
flash[:notice] = "Invitation sent to #{@user.email}"
redirect_to created_user_redirect_path
else
unless @user.errors[:organisation].empty?

8
app/helpers/filters_helper.rb

@ -192,9 +192,15 @@ module FiltersHelper
end
def show_scheme_managing_org_filter?(user)
return true if user.support?
org = user.organisation
stock_owners = org.stock_owners.count
recently_absorbed_with_stock = org.absorbed_organisations.visible.merged_during_open_collection_period.where(holds_own_stock: true).count
relevant_orgs_count = stock_owners + recently_absorbed_with_stock + (org.holds_own_stock? ? 1 : 0)
user.support? || org.stock_owners.count > 1 || (org.holds_own_stock? && org.stock_owners.count.positive?)
relevant_orgs_count > 1
end
def logs_for_both_needstypes_present?(organisation)

4
app/helpers/merge_requests_helper.rb

@ -276,4 +276,8 @@ module MergeRequestsHelper
def any_organisations_share_logs?(organisations, type)
organisations.any? { |organisation| organisation.send("#{type}_logs").filter_by_managing_organisation(organisations.where.not(id: organisation.id)).exists? }
end
def begin_merge_disabled?(merge_request)
merge_request.status != "ready_to_merge" || merge_request.merge_date.future?
end
end

20
app/helpers/schemes_helper.rb

@ -20,11 +20,14 @@ module SchemesHelper
end
def owning_organisation_options(current_user)
all_orgs = Organisation.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) }
user_org = [OpenStruct.new(id: current_user.organisation_id, name: current_user.organisation.name)]
stock_owners = current_user.organisation.stock_owners.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) }
merged_organisations = current_user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.map { |org| OpenStruct.new(id: org.id, name: org.name) }
current_user.support? ? all_orgs : user_org + stock_owners + merged_organisations
if current_user.support?
Organisation.visible.map { |org| OpenStruct.new(id: org.id, name: org.name) }
else
user_org = [current_user.organisation]
stock_owners = current_user.organisation.stock_owners.visible.filter { |org| org.status == :active || (org.status == :merged && org.merge_date >= FormHandler.instance.start_date_of_earliest_open_for_editing_collection_period) }
merged_organisations = current_user.organisation.absorbed_organisations.visible.merged_during_open_collection_period
(user_org + stock_owners + merged_organisations).map { |org| OpenStruct.new(id: org.id, name: org.name) }
end
end
def null_option
@ -81,7 +84,12 @@ module SchemesHelper
when :deactivating_soon
"This scheme deactivates on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date."
when :deactivated
"This scheme deactivated on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date."
case scheme.owning_organisation.status
when :active
"This scheme deactivated on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date."
when :merged
"This scheme has been deactivated due to #{scheme.owning_organisation.name} merging into #{scheme.owning_organisation.absorbing_organisation.name} on #{scheme.owning_organisation.merge_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Use the after merge organisation for schemes and locations active after this date."
end
end
end

13
app/jobs/email_csv_job.rb

@ -1,9 +1,10 @@
class EmailCsvJob < ApplicationJob
include Rails.application.routes.url_helpers
queue_as :default
BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8
EXPIRATION_TIME = 24.hours.to_i
EXPIRATION_TIME = 48.hours.to_i
def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, codes_only_export = false, log_type = "lettings", year = nil) # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params
export_type = codes_only_export ? "codes" : "labels"
@ -20,10 +21,16 @@ class EmailCsvJob < ApplicationJob
filename = "#{[log_type, 'logs', organisation&.name, Time.zone.now].compact.join('-')}.csv"
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"])
storage_service = if FeatureToggle.upload_enabled?
Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"])
else
Storage::LocalDiskService.new
end
storage_service.write_file(filename, BYTE_ORDER_MARK + csv_string)
csv_download = CsvDownload.create!(user:, organisation: user.organisation, filename:, download_type: log_type, expiration_time: EXPIRATION_TIME)
url = storage_service.get_presigned_url(filename, EXPIRATION_TIME)
url = csv_download_url(csv_download.id, host: ENV["APP_HOST"])
CsvDownloadMailer.new.send_csv_download_mail(user, url, EXPIRATION_TIME)
end

13
app/jobs/scheme_email_csv_job.rb

@ -1,9 +1,10 @@
class SchemeEmailCsvJob < ApplicationJob
include Rails.application.routes.url_helpers
queue_as :default
BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8
EXPIRATION_TIME = 24.hours.to_i
EXPIRATION_TIME = 48.hours.to_i
def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, download_type = "combined") # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params
unfiltered_schemes = if organisation.present? && user.support?
@ -23,10 +24,16 @@ class SchemeEmailCsvJob < ApplicationJob
filename = "#{['schemes-and-locations', organisation&.name, Time.zone.now].compact.join('-')}.csv"
end
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"])
storage_service = if FeatureToggle.upload_enabled?
Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"])
else
Storage::LocalDiskService.new
end
storage_service.write_file(filename, BYTE_ORDER_MARK + csv_string)
csv_download = CsvDownload.create!(user:, organisation: user.organisation, filename:, download_type:, expiration_time: EXPIRATION_TIME)
url = storage_service.get_presigned_url(filename, EXPIRATION_TIME)
url = csv_download_url(csv_download.id, host: ENV["APP_HOST"])
CsvDownloadMailer.new.send_csv_download_mail(user, url, EXPIRATION_TIME)
end

10
app/models/csv_download.rb

@ -0,0 +1,10 @@
class CsvDownload < ApplicationRecord
enum download_type: { lettings: "lettings", sales: "sales", schemes: "schemes", locations: "locations", combined: "combined" }
belongs_to :user
belongs_to :organisation
def expired?
created_at < expiration_time.seconds.ago
end
end

5
app/models/derived_variables/lettings_log_variables.rb

@ -124,6 +124,11 @@ module DerivedVariables::LettingsLogVariables
self.nationality_all = nationality_all_group if nationality_uk_or_prefers_not_to_say?
if startdate_changed? && !LocalAuthority.active(startdate).where(code: la).exists?
self.la = nil
self.is_la_inferred = false
end
reset_address_fields! if is_supported_housing?
end

5
app/models/derived_variables/sales_log_variables.rb

@ -75,6 +75,11 @@ module DerivedVariables::SalesLogVariables
self.nationality_all = nationality_all_group if nationality_uk_or_prefers_not_to_say?
self.nationality_all_buyer2 = nationality_all_buyer2_group if nationality2_uk_or_prefers_not_to_say?
if saledate_changed? && !LocalAuthority.active(saledate).where(code: la).exists?
self.la = nil
self.is_la_inferred = false
end
set_encoded_derived_values!(DEPENDENCIES)
end

1
app/models/form/lettings/subsections/household_needs.rb

@ -2,7 +2,6 @@ class Form::Lettings::Subsections::HouseholdNeeds < ::Form::Subsection
def initialize(id, hsh, section)
super
@id = "household_needs"
@copy_key = "lettings.household_needs.housingneeds_type"
@label = "Household needs"
@depends_on = [{ "non_location_setup_questions_completed?" => true }]
end

2
app/models/form/page.rb

@ -25,7 +25,7 @@ class Form::Page
delegate :form, to: :subsection
def copy_key
@copy_key ||= "#{form.type}.#{subsection.id}.#{questions[0].id}"
@copy_key ||= "#{form.type}.#{subsection.copy_key}.#{questions[0].id}"
end
def header

2
app/models/form/question.rb

@ -51,7 +51,7 @@ class Form::Question
delegate :form, to: :subsection
def copy_key
@copy_key ||= "#{form.type}.#{subsection.id}.#{id}"
@copy_key ||= "#{form.type}.#{subsection.copy_key}.#{id}"
end
def check_answer_label

2
app/models/form/sales/pages/buyer_interview.rb

@ -2,7 +2,7 @@ class Form::Sales::Pages::BuyerInterview < ::Form::Page
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@joint_purchase = joint_purchase
@copy_key = "sales.#{subsection.id}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@copy_key = "sales.#{subsection.copy_key}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
end
def questions

1
app/models/form/sales/pages/deposit.rb

@ -3,7 +3,6 @@ class Form::Sales::Pages::Deposit < ::Form::Page
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@optional = optional
@copy_key = "sales.sale_information.deposit"
end
def questions

1
app/models/form/sales/pages/deposit_discount.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::DepositDiscount < ::Form::Page
def initialize(id, hsh, subsection, optional:)
super(id, hsh, subsection)
@optional = optional
@copy_key = "sales.sale_information.cashdis"
end
def questions

1
app/models/form/sales/pages/discount.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::Discount < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "discount"
@copy_key = "sales.sale_information.discount"
@depends_on = [{
"right_to_buy?" => true,
}]

1
app/models/form/sales/pages/equity.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::Equity < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "equity"
@copy_key = "sales.sale_information.equity"
end
def questions

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

@ -0,0 +1,13 @@
class Form::Sales::Pages::EstateManagementFee < ::Form::Page
def initialize(id, hsh, subsection)
super
@copy_key = "sales.sale_information.management_fee"
end
def questions
@questions ||= [
Form::Sales::Questions::HasManagementFee.new(nil, nil, self),
Form::Sales::Questions::ManagementFee.new(nil, nil, self),
]
end
end

1
app/models/form/sales/pages/extra_borrowing.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::ExtraBorrowing < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@copy_key = "sales.sale_information.extrabor"
@description = ""
@subsection = subsection
@depends_on = [{

1
app/models/form/sales/pages/grant.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::Grant < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "grant"
@copy_key = "sales.sale_information.grant"
@depends_on = [{
"right_to_buy?" => false,
"rent_to_buy_full_ownership?" => false,

12
app/models/form/sales/pages/living_before_purchase.rb

@ -19,11 +19,17 @@ class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page
end
end
def depends_on
def routed_to?(log, _user)
super && page_routed_to?(log)
end
def page_routed_to?(log)
return false if form.start_year_2025_or_later? && log.resale != 2
if @joint_purchase
[{ "joint_purchase?" => true }]
log.joint_purchase?
else
[{ "not_joint_purchase?" => true }, { "jointpur" => nil }]
log.not_joint_purchase? || log.jointpur.nil?
end
end
end

1
app/models/form/sales/pages/monthly_rent.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::MonthlyRent < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "monthly_rent"
@copy_key = "sales.sale_information.mrent"
end
def questions

1
app/models/form/sales/pages/mortgage_amount.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageAmount < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@copy_key = "sales.sale_information.mortgage"
@depends_on = [{ "mortgage_used?" => true }]
end

1
app/models/form/sales/pages/mortgage_lender.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLender < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@copy_key = "sales.sale_information.mortgagelender"
@description = ""
@subsection = subsection
@depends_on = [{

1
app/models/form/sales/pages/mortgage_lender_other.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLenderOther < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@copy_key = "sales.sale_information.mortgagelenderother"
@description = ""
@subsection = subsection
@depends_on = [{

1
app/models/form/sales/pages/mortgage_length.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::MortgageLength < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@ownershipsch = ownershipsch
@copy_key = "sales.sale_information.mortlen"
@depends_on = [{
"mortgageused" => 1,
}]

1
app/models/form/sales/pages/mortgageused.rb

@ -1,7 +1,6 @@
class Form::Sales::Pages::Mortgageused < ::Form::Page
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@copy_key = "sales.sale_information.mortgageused"
@ownershipsch = ownershipsch
end

1
app/models/form/sales/pages/previous_bedrooms.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousBedrooms < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_bedrooms"
@copy_key = "sales.sale_information.frombeds"
@depends_on = [
{
"soctenant" => 1,

1
app/models/form/sales/pages/previous_property_type.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousPropertyType < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_property_type"
@copy_key = "sales.sale_information.fromprop"
@description = ""
@subsection = subsection
@depends_on = [

1
app/models/form/sales/pages/previous_tenure.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::PreviousTenure < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "shared_ownership_previous_tenure"
@copy_key = "sales.sale_information.socprevten"
@header = ""
@description = ""
@subsection = subsection

2
app/models/form/sales/pages/privacy_notice.rb

@ -1,7 +1,7 @@
class Form::Sales::Pages::PrivacyNotice < ::Form::Page
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@copy_key = "sales.#{subsection.id}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@copy_key = "sales.#{subsection.copy_key}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@joint_purchase = joint_purchase
end

1
app/models/form/sales/pages/resale.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::Resale < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "resale"
@copy_key = "sales.sale_information.resale"
@depends_on = [
{
"staircase" => 2,

2
app/models/form/sales/pages/staircase.rb

@ -3,7 +3,7 @@ class Form::Sales::Pages::Staircase < ::Form::Page
super
@id = "staircasing"
@depends_on = [{ "ownershipsch" => 1 }]
@copy_key = "sales.#{subsection.id}.staircasing"
@copy_key = "sales.#{subsection.copy_key}.staircasing"
end
def questions

1
app/models/form/sales/pages/value_shared_ownership.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::ValueSharedOwnership < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "value_shared_ownership"
@copy_key = "sales.sale_information.value"
end
def questions

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::BuyerInterview < ::Form::Question
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "noint"
@copy_key = "sales.#{subsection.id}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@copy_key = "sales.#{subsection.copy_key}.noint.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]

1
app/models/form/sales/questions/deposit_amount.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::DepositAmount < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:, optional:)
super(id, hsh, subsection)
@id = "deposit"
@copy_key = "sales.sale_information.deposit"
@type = "numeric"
@min = 0
@max = 999_999

1
app/models/form/sales/questions/deposit_discount.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::DepositDiscount < ::Form::Question
def initialize(id, hsh, page)
super
@id = "cashdis"
@copy_key = "sales.sale_information.cashdis"
@type = "numeric"
@min = 0
@max = 999_999

1
app/models/form/sales/questions/discount.rb

@ -3,7 +3,6 @@ class Form::Sales::Questions::Discount < ::Form::Question
super
@id = "discount"
@type = "numeric"
@copy_key = "sales.sale_information.discount"
@min = 0
@max = form.start_year_2024_or_later? ? 70 : 100
@step = 0.1

1
app/models/form/sales/questions/equity.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::Equity < ::Form::Question
def initialize(id, hsh, page)
super
@id = "equity"
@copy_key = "sales.sale_information.equity"
@type = "numeric"
@min = 0
@max = 100

1
app/models/form/sales/questions/extra_borrowing.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::ExtraBorrowing < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "extrabor"
@copy_key = "sales.sale_information.extrabor"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@page = page

1
app/models/form/sales/questions/fromprop.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::Fromprop < ::Form::Question
def initialize(id, hsh, page)
super
@id = "fromprop"
@copy_key = "sales.sale_information.fromprop"
@type = "radio"
@page = page
@answer_options = ANSWER_OPTIONS

1
app/models/form/sales/questions/grant.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::Grant < ::Form::Question
def initialize(id, hsh, page)
super
@id = "grant"
@copy_key = "sales.sale_information.grant"
@type = "numeric"
@min = 0
@max = 999_999

24
app/models/form/sales/questions/has_management_fee.rb

@ -0,0 +1,24 @@
class Form::Sales::Questions::HasManagementFee < ::Form::Question
def initialize(id, hsh, subsection)
super
@id = "has_management_fee"
@copy_key = "sales.sale_information.management_fee.has_management_fee"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = {
"management_fee" => [1],
}
@hidden_in_check_answers = {
"depends_on" => [
{
"has_management_fee" => 1,
},
],
}
end
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"0" => { "value" => "No" },
}.freeze
end

12
app/models/form/sales/questions/management_fee.rb

@ -0,0 +1,12 @@
class Form::Sales::Questions::ManagementFee < ::Form::Question
def initialize(id, hsh, subsection)
super
@id = "management_fee"
@copy_key = "sales.sale_information.management_fee.management_fee"
@type = "numeric"
@min = 1
@step = 0.01
@width = 5
@prefix = "£"
end
end

1
app/models/form/sales/questions/monthly_rent.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::MonthlyRent < ::Form::Question
def initialize(id, hsh, page)
super
@id = "mrent"
@copy_key = "sales.sale_information.mrent"
@type = "numeric"
@min = 0
@step = 0.01

1
app/models/form/sales/questions/mortgage_amount.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageAmount < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "mortgage"
@copy_key = "sales.sale_information.mortgage"
@type = "numeric"
@min = 1
@step = 1

1
app/models/form/sales/questions/mortgage_lender.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLender < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "mortgagelender"
@copy_key = "sales.sale_information.mortgagelender"
@type = "select"
@page = page
@bottom_guidance_partial = "mortgage_lender"

1
app/models/form/sales/questions/mortgage_lender_other.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLenderOther < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "mortgagelenderother"
@copy_key = "sales.sale_information.mortgagelenderother"
@type = "text"
@page = page
@ownershipsch = ownershipsch

1
app/models/form/sales/questions/mortgage_length.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::MortgageLength < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "mortlen"
@copy_key = "sales.sale_information.mortlen"
@type = "numeric"
@min = 0
@max = 60

1
app/models/form/sales/questions/mortgageused.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::Mortgageused < ::Form::Question
def initialize(id, hsh, subsection, ownershipsch:)
super(id, hsh, subsection)
@id = "mortgageused"
@copy_key = "sales.sale_information.mortgageused"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@ownershipsch = ownershipsch

1
app/models/form/sales/questions/previous_bedrooms.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::PreviousBedrooms < ::Form::Question
def initialize(id, hsh, page)
super
@id = "frombeds"
@copy_key = "sales.sale_information.frombeds"
@type = "numeric"
@width = 5
@min = 1

1
app/models/form/sales/questions/previous_tenure.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::PreviousTenure < ::Form::Question
def initialize(id, hsh, page)
super
@id = "socprevten"
@copy_key = "sales.sale_information.socprevten"
@type = "radio"
@page = page
@answer_options = ANSWER_OPTIONS

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::PrivacyNotice < ::Form::Question
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "privacynotice"
@copy_key = "sales.#{subsection.id}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@copy_key = "sales.#{subsection.copy_key}.privacynotice.#{joint_purchase ? 'joint_purchase' : 'not_joint_purchase'}"
@type = "checkbox"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@joint_purchase = joint_purchase

1
app/models/form/sales/questions/resale.rb

@ -2,7 +2,6 @@ class Form::Sales::Questions::Resale < ::Form::Question
def initialize(id, hsh, page)
super
@id = "resale"
@copy_key = "sales.sale_information.resale"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]

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

@ -2,7 +2,6 @@ class Form::Sales::Questions::Value < ::Form::Question
def initialize(id, hsh, page)
super
@id = "value"
@copy_key = "sales.sale_information.value"
@type = "numeric"
@min = 0
@step = 1

10
app/models/form/sales/sections/sale_information.rb

@ -5,9 +5,17 @@ class Form::Sales::Sections::SaleInformation < ::Form::Section
@label = "Sale information"
@description = ""
@subsections = [
Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self),
shared_ownership_scheme_subsection,
Form::Sales::Subsections::DiscountedOwnershipScheme.new(nil, nil, self),
Form::Sales::Subsections::OutrightSale.new(nil, nil, self),
] || []
end
def shared_ownership_scheme_subsection
if form.start_year_2025_or_later?
Form::Sales::Subsections::SharedOwnershipInitialPurchase.new(nil, nil, self)
else
Form::Sales::Subsections::SharedOwnershipScheme.new(nil, nil, self)
end
end
end

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

@ -4,6 +4,7 @@ class Form::Sales::Subsections::DiscountedOwnershipScheme < ::Form::Subsection
@id = "discounted_ownership_scheme"
@label = "Discounted ownership scheme"
@depends_on = [{ "ownershipsch" => 2, "setup_completed?" => true }]
@copy_key = "sale_information"
end
def pages

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

@ -4,6 +4,7 @@ class Form::Sales::Subsections::OutrightSale < ::Form::Subsection
@id = "outright_sale"
@label = "Outright sale"
@depends_on = [{ "ownershipsch" => 3, "setup_completed?" => true }]
@copy_key = "sale_information"
end
def pages

48
app/models/form/sales/subsections/shared_ownership_initial_purchase.rb

@ -0,0 +1,48 @@
class Form::Sales::Subsections::SharedOwnershipInitialPurchase < ::Form::Subsection
def initialize(id, hsh, section)
super
@id = "shared_ownership_initial_purchase"
@label = "Shared ownership - initial purchase"
@depends_on = [{ "ownershipsch" => 1, "setup_completed?" => true, "staircase" => 2 }]
end
def pages
@pages ||= [
Form::Sales::Pages::Resale.new(nil, nil, self),
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership_joint_purchase", nil, self, ownershipsch: 1, joint_purchase: true),
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1, joint_purchase: false),
Form::Sales::Pages::HandoverDate.new(nil, nil, self),
Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self),
Form::Sales::Pages::BuyerPrevious.new("buyer_previous_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::BuyerPrevious.new("buyer_previous_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.new(nil, nil, self),
Form::Sales::Pages::ValueSharedOwnership.new(nil, nil, self),
Form::Sales::Pages::AboutPriceValueCheck.new("about_price_shared_ownership_value_check", nil, self),
Form::Sales::Pages::Equity.new(nil, nil, self),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_equity_value_check", nil, self),
Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_used_mortgage_value_check", nil, self),
Form::Sales::Pages::MortgageAmount.new("mortgage_amount_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_mortgage_amount_value_check", nil, self),
Form::Sales::Pages::MortgageValueCheck.new("mortgage_amount_mortgage_value_check", nil, self),
Form::Sales::Pages::MortgageLength.new("mortgage_length_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::Deposit.new("deposit_shared_ownership", nil, self, ownershipsch: 1, optional: false),
Form::Sales::Pages::Deposit.new("deposit_shared_ownership_optional", nil, self, ownershipsch: 1, optional: true),
Form::Sales::Pages::DepositValueCheck.new("deposit_joint_purchase_value_check", nil, self, joint_purchase: true),
Form::Sales::Pages::DepositValueCheck.new("deposit_value_check", nil, self, joint_purchase: false),
Form::Sales::Pages::DepositDiscount.new("deposit_discount", nil, self, optional: false),
Form::Sales::Pages::DepositDiscount.new("deposit_discount_optional", nil, self, optional: true),
Form::Sales::Pages::SharedOwnershipDepositValueCheck.new("shared_ownership_deposit_value_check", nil, self),
Form::Sales::Pages::MonthlyRent.new(nil, nil, self),
Form::Sales::Pages::LeaseholdCharges.new("leasehold_charges_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_shared_ownership_value_check", nil, self),
Form::Sales::Pages::EstateManagementFee.new("estate_management_fee", nil, self),
].compact
end
def displayed_in_tasklist?(log)
log.staircase == 2 && (log.ownershipsch.nil? || log.ownershipsch == 1)
end
end

2
app/models/form/sales/subsections/shared_ownership_scheme.rb

@ -11,7 +11,7 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection
@pages ||= [
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership_joint_purchase", nil, self, ownershipsch: 1, joint_purchase: true),
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1, joint_purchase: false),
(Form::Sales::Pages::Staircase.new(nil, nil, self) unless form.start_year_2025_or_later?),
Form::Sales::Pages::Staircase.new(nil, nil, self),
Form::Sales::Pages::AboutStaircase.new("about_staircasing_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::AboutStaircase.new("about_staircasing_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::StaircaseBoughtValueCheck.new(nil, nil, self),

8
app/models/location.rb

@ -54,13 +54,13 @@ class Location < ApplicationRecord
}
scope :deactivated, lambda { |date = Time.zone.now|
deactivated_by_organisation
deactivated_by_organisation(date)
.or(deactivated_directly(date))
.or(deactivated_by_scheme(date))
}
scope :deactivated_by_organisation, lambda {
merge(Organisation.filter_by_inactive)
scope :deactivated_by_organisation, lambda { |date = Time.zone.now|
merge(Organisation.filter_by_inactive.or(Organisation.where("merge_date <= ?", date)))
}
scope :deactivated_by_scheme, lambda { |date = Time.zone.now|
@ -206,7 +206,7 @@ class Location < ApplicationRecord
def status_at(date)
return :deleted if discarded_at.present?
return :incomplete unless confirmed
return :deactivated if scheme.owning_organisation.status_at(date) == :deactivated ||
return :deactivated if scheme.owning_organisation.status_at(date) == :deactivated || scheme.owning_organisation.status_at(date) == :merged ||
open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date || scheme.status_at(date) == :deactivated
return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date || scheme.status_at(date) == :deactivating_soon
return :activating_soon if startdate.present? && date < startdate

1
app/models/log.rb

@ -111,6 +111,7 @@ class Log < ApplicationRecord
self.town_or_city = nil
self.county = nil
self.postcode_full = postcode_full_input
process_postcode_changes!
else
self.uprn = uprn_selection
self.uprn_confirmed = 1

7
app/models/merge_request_organisation.rb

@ -29,5 +29,12 @@ private
if merging_organisation_id.blank? || !Organisation.where(id: merging_organisation_id).exists?
merge_request.errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_not_selected"))
end
existing_merges = MergeRequestOrganisation.with_merging_organisation(merging_organisation)
if existing_merges.count.positive?
existing_merge_request = existing_merges.first.merge_request
errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_part_of_another_merge"))
merge_request.errors.add(:merging_organisation, I18n.t("validations.merge_request.organisation_part_of_another_incomplete_merge", organisation: merging_organisation.name, absorbing_organisation: existing_merge_request.absorbing_organisation&.name, merge_date: existing_merge_request.merge_date&.to_fs(:govuk_date)))
end
end
end

8
app/models/scheme.rb

@ -57,8 +57,8 @@ class Scheme < ApplicationRecord
.or(deactivated_directly)
}
scope :deactivated_by_organisation, lambda {
merge(Organisation.filter_by_inactive)
scope :deactivated_by_organisation, lambda { |date = Time.zone.now|
merge(Organisation.filter_by_inactive.or(Organisation.where("merge_date <= ?", date)))
}
scope :deactivated_directly, lambda { |date = Time.zone.now|
@ -96,7 +96,7 @@ class Scheme < ApplicationRecord
scope :active, lambda { |date = Time.zone.now|
where.not(id: joins(:scheme_deactivation_periods).reactivating_soon(date).pluck(:id))
.where.not(id: incomplete.pluck(:id))
.where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id))
.where.not(id: joins(:owning_organisation).deactivated_by_organisation(date).pluck(:id))
.where.not(id: joins(:owning_organisation).joins(:scheme_deactivation_periods).deactivated_directly(date).pluck(:id))
.where.not(id: activating_soon(date).pluck(:id))
}
@ -314,7 +314,7 @@ class Scheme < ApplicationRecord
def status_at(date)
return :deleted if discarded_at.present?
return :incomplete unless confirmed && locations.confirmed.any?
return :deactivated if owning_organisation.status_at(date) == :deactivated ||
return :deactivated if owning_organisation.status_at(date) == :deactivated || owning_organisation.status_at(date) == :merged ||
(open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date)
return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date
return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date

16
app/policies/csv_download_policy.rb

@ -0,0 +1,16 @@
class CsvDownloadPolicy
attr_reader :current_user, :csv_download
def initialize(current_user, csv_download)
@current_user = current_user
@csv_download = csv_download
end
def show?
@current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation
end
def download?
@current_user == @csv_download.user || @current_user.support? || @current_user.organisation == @csv_download.organisation
end
end

15
app/policies/location_policy.rb

@ -16,14 +16,14 @@ class LocationPolicy
if location == Location
user.data_coordinator?
else
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
def update?
return true if user.support?
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
def delete_confirmation?
@ -62,7 +62,7 @@ class LocationPolicy
define_method method_name do
return true if user.support?
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
@ -73,7 +73,7 @@ class LocationPolicy
define_method method_name do
return true if user.support?
scheme_owned_by_user_org_or_stock_owner
scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
@ -83,8 +83,11 @@ private
location.scheme
end
def scheme_owned_by_user_org_or_stock_owner
scheme&.owning_organisation == user.organisation || user.organisation.stock_owners.exists?(scheme&.owning_organisation_id)
def scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
scheme_owned_by_user_org = scheme&.owning_organisation == user.organisation
scheme_owned_by_stock_owner = user.organisation.stock_owners.exists?(scheme&.owning_organisation_id)
scheme_owned_by_recently_absorbed_org = user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.exists?(scheme&.owning_organisation_id)
scheme_owned_by_user_org || scheme_owned_by_stock_owner || scheme_owned_by_recently_absorbed_org
end
def has_any_logs_in_editable_collection_period

15
app/policies/scheme_policy.rb

@ -12,7 +12,7 @@ class SchemePolicy
if scheme == Scheme
true
else
scheme_owned_by_user_org_or_stock_owner
scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
@ -27,7 +27,7 @@ class SchemePolicy
def update?
return true if user.support?
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
def changes?
@ -41,7 +41,7 @@ class SchemePolicy
define_method method_name do
return true if user.support?
scheme_owned_by_user_org_or_stock_owner
scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
@ -61,7 +61,7 @@ class SchemePolicy
define_method method_name do
return true if user.support?
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner
user.data_coordinator? && scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
end
end
@ -78,8 +78,11 @@ class SchemePolicy
private
def scheme_owned_by_user_org_or_stock_owner
scheme&.owning_organisation == user.organisation || user.organisation.stock_owners.exists?(scheme&.owning_organisation_id)
def scheme_owned_by_user_org_or_stock_owner_or_recently_absorbed_org
scheme_owned_by_user_org = scheme&.owning_organisation == user.organisation
scheme_owned_by_stock_owner = user.organisation.stock_owners.exists?(scheme&.owning_organisation_id)
scheme_owned_by_recently_absorbed_org = user.organisation.absorbed_organisations.visible.merged_during_open_collection_period.exists?(scheme&.owning_organisation_id)
scheme_owned_by_user_org || scheme_owned_by_stock_owner || scheme_owned_by_recently_absorbed_org
end
def has_any_logs_in_editable_collection_period

2
app/services/bulk_upload/lettings/year2024/csv_parser.rb

@ -15,7 +15,7 @@ class BulkUpload::Lettings::Year2024::CsvParser
def row_offset
if with_headers?
rows.find_index { |row| row[0].match(/field number/i) } + 1
rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1
else
0
end

2
app/services/bulk_upload/sales/year2024/csv_parser.rb

@ -15,7 +15,7 @@ class BulkUpload::Sales::Year2024::CsvParser
def row_offset
if with_headers?
rows.find_index { |row| row[0].match(/field number/i) } + 1
rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1
else
0
end

50
app/services/csv/downloader.rb

@ -0,0 +1,50 @@
class Csv::Downloader
attr_reader :csv_download
delegate :path, to: :file
def initialize(csv_download:)
@csv_download = csv_download
end
def call
download
end
def delete_local_file!
file.unlink
end
def presigned_url
s3_storage_service.get_presigned_url(csv_download.filename, 60, response_content_disposition: "attachment; filename=#{csv_download.filename}")
end
private
def download
io = storage_service.get_file_io(csv_download.filename)
file.write(io.read)
io.close
file.close
end
def file
@file ||= Tempfile.new
end
def storage_service
@storage_service ||= if FeatureToggle.upload_enabled?
s3_storage_service
else
local_disk_storage_service
end
end
def s3_storage_service
Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["BULK_UPLOAD_BUCKET"])
end
def local_disk_storage_service
Storage::LocalDiskService.new
end
end

4
app/services/feature_toggle.rb

@ -46,4 +46,8 @@ class FeatureToggle
def self.managing_resources_enabled?
!Rails.env.production?
end
def self.create_test_logs_enabled?
Rails.env.development? || Rails.env.review?
end
end

2
app/services/mandatory_collection_resources_service.rb

@ -46,7 +46,7 @@ class MandatoryCollectionResourcesService
year_range = "#{year} to #{year + 1}"
case resource
when "paper_form"
"#{log_type} log for tenants (#{year_range})"
"#{log_type} paper form (#{year_range})"
when "bulk_upload_template"
"#{log_type} bulk upload template (#{year_range})"
when "bulk_upload_specification"

10
app/views/csv_downloads/show.html.erb

@ -0,0 +1,10 @@
<% title = "Downlaod CSV file" %>
<% content_for :title, title %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-l govuk-!-margin-bottom-6">You are about to download a CSV file</h1>
<p class="govuk-body-m">Filename: <%= @csv_download.filename %></p>
<%= govuk_button_link_to "Download CSV", download_csv_download_path(@csv_download) %>
</div>
</div>

8
app/views/errors/download_link_expired.html.erb

@ -0,0 +1,8 @@
<% content_for :title, "This link has expired" %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl govuk-!-margin-bottom-6">This link has expired.</h1>
<p class="govuk-body-m">Download the logs again to get a new link.</p>
</div>
</div>

6
app/views/form/guidance/_financial_calculations_shared_ownership.html.erb

@ -20,11 +20,11 @@
<% end %>
must equal
the purchase price <%= question_link("value", log, current_user) %>
<% stairbought_page = log.form.get_question("stairbought", log).page %>
<% if stairbought_page.routed_to?(log, current_user) %>
<% stairbought_page = log.form.get_question("stairbought", log)&.page %>
<% if stairbought_page&.routed_to?(log, current_user) %>
multiplied by the percentage bought <%= question_link("stairbought", log, current_user) %>
<% else %>
multiplied by the percentage equity stake <%= question_link("equity", log, current_user) %>
multiplied by the percentage equity share <%= question_link("equity", log, current_user) %>
<% end %>
</p>
<% end %>

15
app/views/locations/index.html.erb

@ -56,14 +56,13 @@
<% end %>
<% end %>
<% if status_hint_message = scheme_status_hint(@scheme) %>
<div class="govuk-hint">
<%= status_hint_message %>
</div>
<br>
<% end %>
<% if LocationPolicy.new(current_user, @scheme.locations.new).create? %>
<% if LocationPolicy.new(current_user, @scheme.locations.new).create? && [:active, :merged].include?(@scheme.owning_organisation.status) %>
<% if status_hint_message = scheme_status_hint(@scheme) %>
<div class="govuk-hint">
<%= status_hint_message %>
</div>
<br>
<% end %>
<%= govuk_button_to "Add a location", scheme_locations_path(@scheme), method: "post" %>
<% end %>
<br>

2
app/views/locations/show.html.erb

@ -47,7 +47,7 @@
</div>
</div>
<% if @location.scheme.owning_organisation.active? && LocationPolicy.new(current_user, @location).deactivate? %>
<% if @location.scheme.owning_organisation.status == :active && LocationPolicy.new(current_user, @location).deactivate? %>
<%= toggle_location_link(@location) %>
<% end %>

8
app/views/merge_requests/_notification_banners.html.erb

@ -19,3 +19,11 @@
No changes have been made. Try beginning the merge again.
<% end %>
<% end %>
<% if @merge_request.merge_date&.future? %>
<%= govuk_notification_banner(title_text: "Important") do %>
<p class="govuk-notification-banner__heading govuk-!-width-full" style="max-width: fit-content">
This merge is happening in the future. Wait until the merge date to begin this merge.
</p>
<% end %>
<% end %>

2
app/views/merge_requests/show.html.erb

@ -12,7 +12,7 @@
</h1>
<% unless @merge_request.status == "request_merged" || @merge_request.status == "processing" %>
<div class="govuk-button-group">
<%= govuk_button_link_to "Begin merge", merge_start_confirmation_merge_request_path(@merge_request), disabled: @merge_request.status != "ready_to_merge" %>
<%= govuk_button_link_to "Begin merge", merge_start_confirmation_merge_request_path(@merge_request), disabled: begin_merge_disabled?(@merge_request) %>
<%= govuk_button_link_to "Delete merge request", delete_confirmation_merge_request_path(@merge_request), warning: true %>
</div>
<% end %>

6
app/views/schemes/details.html.erb

@ -49,11 +49,13 @@
:description,
legend: { text: "Is this scheme registered under the Care Standards Act 2000?", size: "m" } %>
<% if current_user.data_coordinator? && current_user.organisation.stock_owners.count.zero? && !current_user.organisation.has_recent_absorbed_organisations? %>
<% scheme_owning_organisation_options = owning_organisation_options(current_user) %>
<% if scheme_owning_organisation_options.count == 1 %>
<%= f.hidden_field :owning_organisation_id, value: current_user.organisation.id %>
<% else %>
<%= f.govuk_collection_select :owning_organisation_id,
owning_organisation_options(current_user),
scheme_owning_organisation_options,
:id,
:name,
label: { text: "Which organisation owns the housing stock for this scheme?", size: "m" },

2
app/views/schemes/show.html.erb

@ -52,7 +52,7 @@
</div>
</div>
<% if @scheme.owning_organisation.active? && SchemePolicy.new(current_user, @scheme).deactivate? %>
<% if @scheme.owning_organisation.status == :active && SchemePolicy.new(current_user, @scheme).deactivate? %>
<%= toggle_scheme_link(@scheme) %>
<% end %>

4
config/locales/en.yml

@ -183,8 +183,11 @@ en:
merge_date:
blank: "Enter a merge date."
invalid: "Enter a valid merge date."
more_than_year_from_today: "The merge date must not be later than a year from today’s date."
existing_absorbing_organisation:
blank: "You must answer absorbing organisation already active?"
merging_organisation_id:
part_of_another_merge: "Another merge request records %{organisation} as merging into %{absorbing_organisation} on %{merge_date}. Select another organisation or remove this organisation from the other merge request."
notification:
attributes:
title:
@ -370,6 +373,7 @@ en:
during_deactivated_period: "The location is already deactivated during this date, please enter a different date."
merge_request:
organisation_part_of_another_merge: "This organisation is part of another merge - select a different one."
organisation_part_of_another_incomplete_merge: "Another merge request records %{organisation} as merging into %{absorbing_organisation} on %{merge_date}. Select another organisation or remove this organisation from the other merge request."
organisation_not_selected: "Select an organisation from the search list."
soft_validations:

2
config/locales/forms/2024/lettings/setup.en.yml

@ -30,7 +30,7 @@ en:
scheme_id:
page_header: "Scheme"
check_answer_label: "Scheme name"
hint_text: "Enter postcode or scheme name.<br><br>A supported housing scheme provides shared or self-contained housing for a particular client group, for example younger or vulnerable people."
hint_text: "Enter postcode, scheme name, or scheme code (for example, S123).<br><br>A supported housing scheme provides shared or self-contained housing for a particular client group, for example younger or vulnerable people."
question_text: "What scheme is this log for?"
location_id:

21
config/locales/forms/2025/sales/sale_information.en.yml

@ -107,9 +107,9 @@ en:
equity:
page_header: "About the price of the property"
check_answer_label: "Initial percentage equity stake"
hint_text: "Enter the amount of initial equity held by the purchaser (for example, 25% or 50%)"
question_text: "What was the initial percentage equity stake purchased?"
check_answer_label: "Initial percentage equity share"
hint_text: "Enter the amount of initial equity share held by the purchaser (for example, 25% or 50%)"
question_text: "What was the initial percentage share purchased?"
mortgageused:
page_header: "Mortgage Amount"
@ -168,9 +168,9 @@ en:
leaseholdcharges:
page_header: ""
has_mscharge:
check_answer_label: "Does the property have any monthly leasehold charges?"
check_answer_label: "Does the property have any service charges?"
hint_text: "For example, service and management charges"
question_text: "Does the property have any monthly leasehold charges?"
question_text: "Does the property have any service charges?"
mscharge:
check_answer_label: "Monthly leasehold charges"
hint_text: ""
@ -199,3 +199,14 @@ en:
check_answer_label: "Amount of any loan, grant or subsidy"
hint_text: "For all schemes except Right to Buy (RTB), Preserved Right to Buy (PRTB), Voluntary Right to Buy (VRTB) and Rent to Buy"
question_text: "What was the amount of any loan, grant, discount or subsidy given?"
management_fee:
page_header: ""
has_management_fee:
check_answer_label: "Does the property have an estate management fee?"
hint_text: "Estate management fees are typically used for the maintenance of communal gardens, payments, private roads, car parks and/or play areas within new build estates."
question_text: "Does the property have an estate management fee?"
management_fee:
check_answer_label: "Monthly estate management fee"
hint_text: ""
question_text: "Enter the total monthly management fee"

12
config/routes.rb

@ -382,6 +382,18 @@ Rails.application.routes.draw do
end
end
resources :csv_downloads, path: "csv-downloads" do
member do
get "/", to: "csv_downloads#show", as: "show"
get "download", to: "csv_downloads#download"
end
end
get "create-test-lettings-log", to: "lettings_logs#create_test_log"
get "create-test-sales-log", to: "sales_logs#create_test_log"
get "create-setup-test-lettings-log", to: "lettings_logs#create_setup_test_log"
get "create-setup-test-sales-log", to: "sales_logs#create_setup_test_log"
scope via: :all do
match "/404", to: "errors#not_found"
match "/429", to: "errors#too_many_requests", status: 429

8
db/migrate/20241114154215_add_management_fee_fields.rb

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

12
db/migrate/20241118104046_add_csv_download_table.rb

@ -0,0 +1,12 @@
class AddCsvDownloadTable < ActiveRecord::Migration[7.0]
def change
create_table :csv_downloads do |t|
t.column :download_type, :string
t.column :filename, :string
t.column :expiration_time, :integer
t.timestamps
t.references :user
t.references :organisation
end
end
end

16
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do
ActiveRecord::Schema[7.0].define(version: 2024_11_18_104046) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -64,6 +64,18 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do
t.datetime "discarded_at"
end
create_table "csv_downloads", force: :cascade do |t|
t.string "download_type"
t.string "filename"
t.integer "expiration_time"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "user_id"
t.bigint "organisation_id"
t.index ["organisation_id"], name: "index_csv_downloads_on_organisation_id"
t.index ["user_id"], name: "index_csv_downloads_on_user_id"
end
create_table "csv_variable_definitions", force: :cascade do |t|
t.string "variable", null: false
t.string "definition", null: false
@ -746,6 +758,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_31_102744) do
t.integer "partner_under_16_value_check"
t.integer "multiple_partners_value_check"
t.bigint "created_by_id"
t.integer "has_management_fee"
t.decimal "management_fee", precision: 10, scale: 2
t.index ["assigned_to_id"], name: "index_sales_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"

2
db/seeds.rb

@ -15,6 +15,8 @@ def create_data_protection_confirmation(user)
signed_at: Time.zone.local(2019, 1, 1),
data_protection_officer_email: user.email,
data_protection_officer_name: user.name,
organisation_name: user.organisation.name,
organisation_address: user.organisation.address_row,
)
end

1
docs/Gemfile.lock

@ -255,6 +255,7 @@ GEM
PLATFORMS
arm64-darwin-21
arm64-darwin-23
x86_64-darwin-22
x86_64-linux

2
docs/app_api.md

@ -12,4 +12,4 @@ In order to use the app as an API, you will need to configure requests to the AP
- `Content-Type = application/json`
- `Action = application/json` N.B. If you use `*/*` instead, the request won't be recognised as an API request`
Currently only the logs controller is configured to accept and authenticate API requests, when the above API environment variables are set.
Currently, only the Logs Controller is configured to accept and authenticate API requests, provided that the specified API environment variables are set. Please note that the API has not been actively maintained for an extended period and may not function as expected. Additionally, the required environment variables are not configured on any of the environments deployed on AWS, rendering API requests to those environments non-functional.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save