Compare commits

...

14 Commits

Author SHA1 Message Date
SuperrrrrFrank c92f35f0c4 CLDC-4029: Add filters for roles and responsibilities 3 weeks ago
Samuel Young cede11dd3c
CLDC-4028: Ensure logs will update when an organisation name change is created (#3109) 3 weeks ago
Samuel Young 6a40c98f63
Reapply "CLDC-4028: Ensure changes to dependent objects are included in export (#3105)" (#3115) (#3117) 3 weeks ago
Samuel Young 138d538179
CLDC-4013: Update sale type bulk uploading wording for 25/26 (#3110) 4 weeks ago
Samuel Young 99c4e78f88
CLDC-4094: Fix associating log locations after merge (#3112) 4 weeks ago
Samuel Young 0f743f898c
CLDC-4086: Replace customer satisfaction link (#3111) 4 weeks ago
Samuel Young e5f0eb6d24
CLDC-NONE: Improve README (#3106) 1 month ago
dependabot[bot] dcacfe9f04
Bump rack from 3.1.17 to 3.1.18 (#3118) 1 month ago
Samuel Young 07e230ff1d
Revert "CLDC-4028: Ensure changes to dependent objects are included in export (#3105)" (#3115) 2 months ago
dependabot[bot] 1b41187a03
Bump rack from 3.1.16 to 3.1.17 uri from 1.0.3 to 1.0.4 (#3113) 2 months ago
Samuel Young 45f6855079
CLDC-4044: Allow unconfirmed but logged in (reconfirmed) users to be reinvited (#3103) 2 months ago
Samuel Young 351b6013c1
CLDC-4078: Optimise OS API queries for records that are validated a lot (#3108) 2 months ago
Samuel Young 9db4ccb474
CLDC-4033: Improve flow for unconfirmed users (#3089) 2 months ago
Samuel Young ca72558c7c
CLDC-4046: Remove forcing on Q84 based on Q76 answer (#3094) 2 months ago
  1. 4
      Gemfile.lock
  2. 5
      app/controllers/auth/passwords_controller.rb
  3. 18
      app/helpers/filters_helper.rb
  4. 2
      app/models/lettings_log.rb
  5. 12
      app/models/log.rb
  6. 2
      app/models/sales_log.rb
  7. 31
      app/models/user.rb
  8. 11
      app/models/validations/household_validations.rb
  9. 4
      app/services/bulk_upload/sales/year2025/row_parser.rb
  10. 11
      app/services/exports/lettings_log_export_service.rb
  11. 8
      app/services/exports/sales_log_export_service.rb
  12. 2
      app/services/exports/user_export_service.rb
  13. 8
      app/services/filter_manager.rb
  14. 8
      app/services/merge/merge_organisations_service.rb
  15. 6
      app/views/devise/passwords/reset_resend_confirmation.html.erb
  16. 2
      app/views/layouts/_feedback.html.erb
  17. 30
      app/views/users/_user_filters.html.erb
  18. 5
      app/views/users/show.html.erb
  19. 2
      config/locales/en.yml
  20. 7
      config/locales/validations/lettings/household.en.yml
  21. 67
      docs/setup.md
  22. 13
      spec/features/user_spec.rb
  23. 8
      spec/models/validations/household_validations_spec.rb
  24. 16
      spec/services/exports/lettings_log_export_service_spec.rb
  25. 16
      spec/services/exports/sales_log_export_service_spec.rb
  26. 25
      spec/services/merge/merge_organisations_service_spec.rb

4
Gemfile.lock

@ -343,7 +343,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.16) rack (3.1.18)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-mini-profiler (3.3.1) rack-mini-profiler (3.3.1)
@ -511,7 +511,7 @@ GEM
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
unread (0.14.0) unread (0.14.0)
activerecord (>= 6.1) activerecord (>= 6.1)
uri (1.0.3) uri (1.0.4)
useragent (0.16.11) useragent (0.16.11)
view_component (3.10.0) view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0) activesupport (>= 5.2.0, < 8.0)

5
app/controllers/auth/passwords_controller.rb

@ -4,6 +4,7 @@ class Auth::PasswordsController < Devise::PasswordsController
def reset_confirmation def reset_confirmation
self.resource = resource_class.new self.resource = resource_class.new
@email = params["email"] @email = params["email"]
@unconfirmed = params["unconfirmed"] == "true"
if @email.blank? if @email.blank?
resource.errors.add :email, I18n.t("validations.email.blank") resource.errors.add :email, I18n.t("validations.email.blank")
render "devise/passwords/new", status: :unprocessable_entity render "devise/passwords/new", status: :unprocessable_entity
@ -65,8 +66,8 @@ protected
resource.need_two_factor_authentication?(request) ? :updated_2FA : :updated resource.need_two_factor_authentication?(request) ? :updated_2FA : :updated
end end
def after_sending_reset_password_instructions_path_for(_resource) def after_sending_reset_password_instructions_path_for(_resource_name)
account_password_reset_confirmation_path(email: params.dig("user", "email")) account_password_reset_confirmation_path(email: params.dig("user", "email"), unconfirmed: resource.initial_confirmation_sent && !resource.confirmed?)
end end
def after_resetting_password_path_for(resource) def after_resetting_password_path_for(resource)

18
app/helpers/filters_helper.rb

@ -52,6 +52,22 @@ module FiltersHelper
}.freeze }.freeze
end end
def user_role_type_filters(include_support = false)
roles = {
"1" => "Data provider",
"2" => "Coordinator",
}
roles["99"] = "Support" if include_support
roles.freeze
end
def user_additional_responsibilities_filters
{
"data_protection_officer" => "Data protection officer",
"key_contact" => "Key contact",
}.freeze
end
def scheme_status_filters def scheme_status_filters
{ {
"incomplete" => "Incomplete", "incomplete" => "Incomplete",
@ -306,7 +322,7 @@ private
def filters_count(filters) def filters_count(filters)
filters.each.sum do |category, category_filters| filters.each.sum do |category, category_filters|
if %w[years status needstypes bulk_upload_id].include?(category) if %w[years status needstypes bulk_upload_id role additional_responsibilities].include?(category)
category_filters.count(&:present?) category_filters.count(&:present?)
elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category) elsif %w[user owning_organisation managing_organisation user_text_search owning_organisation_text_search managing_organisation_text_search uploading_organisation].include?(category)
1 1

2
app/models/lettings_log.rb

@ -915,6 +915,7 @@ private
def should_process_uprn_change? def should_process_uprn_change?
return unless uprn return unless uprn
return unless startdate return unless startdate
return if skip_uprn_lookup
uprn_changed? || startdate_changed? uprn_changed? || startdate_changed?
end end
@ -923,6 +924,7 @@ private
return unless uprn_selection || select_best_address_match return unless uprn_selection || select_best_address_match
return unless startdate return unless startdate
return unless form.start_year_2024_or_later? return unless form.start_year_2024_or_later?
return if skip_address_lookup
if select_best_address_match if select_best_address_match
address_line1_input.present? && postcode_full_input.present? address_line1_input.present? && postcode_full_input.present?

12
app/models/log.rb

@ -57,7 +57,7 @@ class Log < ApplicationRecord
scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) } scope :filter_by_owning_organisation_text_search, ->(param, _user) { where(owning_organisation: Organisation.search_by(param)) }
scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) } scope :filter_by_managing_organisation_text_search, ->(param, _user) { where(managing_organisation: Organisation.search_by(param)) }
attr_accessor :skip_update_status, :skip_update_uprn_confirmed, :select_best_address_match, :skip_dpo_validation attr_accessor :skip_update_status, :skip_update_uprn_confirmed, :select_best_address_match, :skip_dpo_validation, :skip_uprn_lookup, :skip_address_lookup
delegate :present?, to: :address_options, prefix: true delegate :present?, to: :address_options, prefix: true
@ -74,6 +74,9 @@ class Log < ApplicationRecord
presenter = UprnDataPresenter.new(service.result) presenter = UprnDataPresenter.new(service.result)
# the address for this uprn is already known, skip further lookups for this object
self.skip_uprn_lookup = true if address_line1 == presenter.address_line1 && address_line2 == presenter.address_line2 && town_or_city == presenter.town_or_city && postcode_full == presenter.postcode
self.uprn_known = 1 self.uprn_known = 1
self.uprn_selection = uprn self.uprn_selection = uprn
self.address_line1 = presenter.address_line1 self.address_line1 = presenter.address_line1
@ -98,9 +101,14 @@ class Log < ApplicationRecord
presenter = AddressDataPresenter.new(service.result.first) presenter = AddressDataPresenter.new(service.result.first)
os_match_threshold_for_bulk_upload = 0.7 os_match_threshold_for_bulk_upload = 0.7
if presenter.match >= os_match_threshold_for_bulk_upload if presenter.match >= os_match_threshold_for_bulk_upload
# the address for this uprn is already known, skip further lookups for this object
self.skip_address_lookup = true if uprn_selection == presenter.uprn
self.uprn_selection = presenter.uprn self.uprn_selection = presenter.uprn
else else
select_manual_address_entry! select_manual_address_entry!
# this uprn cannot be used for lookup
self.skip_address_lookup = true
return nil return nil
end end
end end
@ -119,7 +127,7 @@ class Log < ApplicationRecord
self.uprn = uprn_selection self.uprn = uprn_selection
self.uprn_confirmed = 1 self.uprn_confirmed = 1
self.skip_update_uprn_confirmed = true self.skip_update_uprn_confirmed = true
process_uprn_change! process_uprn_change! unless skip_uprn_lookup
end end
end end
end end

2
app/models/sales_log.rb

@ -435,6 +435,7 @@ class SalesLog < Log
def should_process_uprn_change? def should_process_uprn_change?
return unless uprn return unless uprn
return unless saledate return unless saledate
return if skip_uprn_lookup
uprn_changed? || saledate_changed? uprn_changed? || saledate_changed?
end end
@ -443,6 +444,7 @@ class SalesLog < Log
return unless uprn_selection || select_best_address_match return unless uprn_selection || select_best_address_match
return unless saledate return unless saledate
return unless form.start_year_2024_or_later? return unless form.start_year_2024_or_later?
return if skip_address_lookup
if select_best_address_match if select_best_address_match
address_line1_input.present? && postcode_full_input.present? address_line1_input.present? && postcode_full_input.present?

31
app/models/user.rb

@ -81,6 +81,29 @@ class User < ApplicationRecord
filtered_records filtered_records
} }
scope :filter_by_role, ->(role, _user = nil) { where(role:) }
scope :filter_by_additional_responsibilities, lambda { |additional_responsibilities, _user|
filtered_records = all
scopes = []
additional_responsibilities.each do |responsibility|
case responsibility
when "key_contact"
scopes << send("is_key_contact")
when "data_protection_officer"
scopes << send("is_data_protection_officer")
end
end
if scopes.any?
filtered_records = filtered_records.merge(scopes.reduce(&:or))
end
filtered_records
}
scope :is_key_contact, -> { where(is_key_contact: true) }
scope :is_data_protection_officer, -> { where(is_dpo: true) }
scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) } scope :not_signed_in, -> { where(last_sign_in_at: nil, active: true) }
scope :deactivated, -> { where(active: false) } scope :deactivated, -> { where(active: false) }
scope :activated, -> { where(active: true) } scope :activated, -> { where(active: true) }
@ -358,6 +381,14 @@ class User < ApplicationRecord
end end
end end
def send_reset_password_instructions
if confirmed?
super
else
send_confirmation_instructions
end
end
protected protected
# Checks whether a password is needed or not. For validations only. # Checks whether a password is needed or not. For validations only.

11
app/models/validations/household_validations.rb

@ -30,17 +30,6 @@ module Validations::HouseholdValidations
end end
validate_other_field(record, 20, :reason, :reasonother) validate_other_field(record, 20, :reason, :reasonother)
if record.is_reason_permanently_decanted?
if record.referral_type.present? && !record.is_from_prp_only_housing_register_or_waiting_list?
record.errors.add :referral_type, I18n.t("validations.lettings.household.referral_type.leaving_last_settled_home.reason_permanently_decanted")
end
if record.referral.present? && !record.is_internal_transfer?
record.errors.add :referral, I18n.t("validations.lettings.household.referral.leaving_last_settled_home.reason_permanently_decanted")
record.errors.add :reason, I18n.t("validations.lettings.household.reason.leaving_last_settled_home.not_internal_transfer")
end
end
return unless record.form.start_year_2024_or_later? return unless record.form.start_year_2024_or_later?
if record.reason == 20 && PHRASES_INDICATING_HOMELESSNESS_REGEX.match?(record.reasonother) if record.reason == 20 && PHRASES_INDICATING_HOMELESSNESS_REGEX.match?(record.reasonother)

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

@ -12,7 +12,7 @@ class BulkUpload::Sales::Year2025::RowParser
field_5: "Which organisation is reporting this sale?", field_5: "Which organisation is reporting this sale?",
field_6: "Username", field_6: "Username",
field_7: "What is the purchaser code?", field_7: "What is the purchaser code?",
field_8: "What is the sale type?", field_8: "Is this a shared ownership or discounted ownership sale?",
field_9: "What is the type of shared ownership sale?", field_9: "What is the type of shared ownership sale?",
field_10: "Is this a staircasing transaction?", field_10: "Is this a staircasing transaction?",
field_11: "What is the type of discounted ownership sale?", field_11: "What is the type of discounted ownership sale?",
@ -311,7 +311,7 @@ class BulkUpload::Sales::Year2025::RowParser
validates :field_8, validates :field_8,
presence: { presence: {
message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "shared ownership sale type."), message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale type."),
category: :setup, category: :setup,
}, },
on: :after_log on: :after_log

11
app/services/exports/lettings_log_export_service.rb

@ -31,7 +31,8 @@ module Exports
end end
def retrieve_resources_from_range(range, year) def retrieve_resources_from_range(range, year)
relation = LettingsLog.exportable.filter_by_year(year).left_joins(:created_by, :updated_by, :assigned_to, :owning_organisation, :managing_organisation) relation = LettingsLog.exportable.filter_by_year(year)
.left_joins(:created_by, :updated_by, :assigned_to, :owning_organisation, :managing_organisation)
ids = relation ids = relation
.where({ updated_at: range }) .where({ updated_at: range })
@ -55,6 +56,14 @@ module Exports
) )
.pluck(:id) .pluck(:id)
# these must be separate as activerecord struggles to join to two different name change tables in the same query
ids.concat(
relation.left_joins(owning_organisation: :organisation_name_changes).where(owning_organisation: { organisation_name_changes: { updated_at: range } }).pluck(:id),
)
ids.concat(
relation.left_joins(managing_organisation: :organisation_name_changes).where(managing_organisation: { organisation_name_changes: { updated_at: range } }).pluck(:id),
)
LettingsLog.where(id: ids) LettingsLog.where(id: ids)
end end

8
app/services/exports/sales_log_export_service.rb

@ -55,6 +55,14 @@ module Exports
) )
.pluck(:id) .pluck(:id)
# these must be separate as activerecord struggles to join to two different name change tables in the same query
ids.concat(
relation.left_joins(owning_organisation: :organisation_name_changes).where(owning_organisation: { organisation_name_changes: { updated_at: range } }).pluck(:id),
)
ids.concat(
relation.left_joins(managing_organisation: :organisation_name_changes).where(managing_organisation: { organisation_name_changes: { updated_at: range } }).pluck(:id),
)
SalesLog.where(id: ids) SalesLog.where(id: ids)
end end

2
app/services/exports/user_export_service.rb

@ -33,7 +33,7 @@ module Exports
relation.where.not(organisations: { updated_at: nil }).where(organisations: { updated_at: range }), relation.where.not(organisations: { updated_at: nil }).where(organisations: { updated_at: range }),
) )
.or( .or(
relation.where(organisation_name_changes: { created_at: range }), relation.where(organisation_name_changes: { updated_at: range }),
) )
.pluck(:id) .pluck(:id)

8
app/services/filter_manager.rb

@ -130,6 +130,14 @@ class FilterManager
new_filters["status"] = params["status"] new_filters["status"] = params["status"]
end end
if filter_type.include?("users") && params["role"].present?
new_filters["role"] = params["role"]
end
if filter_type.include?("users") && params["additional_responsibilities"].present?
new_filters["additional_responsibilities"] = params["additional_responsibilities"]
end
if filter_type.include?("schemes") if filter_type.include?("schemes")
current_user.scheme_filters(specific_org:).each do |filter| current_user.scheme_filters(specific_org:).each do |filter|
new_filters[filter] = params[filter] if params[filter].present? new_filters[filter] = params[filter] if params[filter].present?

8
app/services/merge/merge_organisations_service.rb

@ -4,6 +4,8 @@ class Merge::MergeOrganisationsService
@merging_organisations = Organisation.find(merging_organisation_ids) @merging_organisations = Organisation.find(merging_organisation_ids)
@merge_date = merge_date || Time.zone.today @merge_date = merge_date || Time.zone.today
@absorbing_organisation_active_from_merge_date = absorbing_organisation_active_from_merge_date @absorbing_organisation_active_from_merge_date = absorbing_organisation_active_from_merge_date
@pre_to_post_merge_scheme_ids = {}
@pre_to_post_merge_location_ids = {}
end end
def call def call
@ -70,12 +72,14 @@ private
merging_organisation.owned_schemes.each do |scheme| merging_organisation.owned_schemes.each do |scheme|
new_scheme = Scheme.new(scheme.attributes.except("id", "owning_organisation_id", "old_id", "old_visible_id").merge(owning_organisation: @absorbing_organisation, startdate: [scheme&.startdate, @merge_date].compact.max)) new_scheme = Scheme.new(scheme.attributes.except("id", "owning_organisation_id", "old_id", "old_visible_id").merge(owning_organisation: @absorbing_organisation, startdate: [scheme&.startdate, @merge_date].compact.max))
new_scheme.save!(validate: false) new_scheme.save!(validate: false)
@pre_to_post_merge_scheme_ids[scheme.id] = new_scheme.id
scheme.scheme_deactivation_periods.each do |deactivation_period| scheme.scheme_deactivation_periods.each do |deactivation_period|
split_scheme_deactivation_period_between_organisations(deactivation_period, new_scheme) split_scheme_deactivation_period_between_organisations(deactivation_period, new_scheme)
end end
scheme.locations.each do |location| scheme.locations.each do |location|
new_location = Location.new(location.attributes.except("id", "scheme_id", "old_id", "old_visible_id").merge(scheme: new_scheme, startdate: [location&.startdate, @merge_date].compact.max)) new_location = Location.new(location.attributes.except("id", "scheme_id", "old_id", "old_visible_id").merge(scheme: new_scheme, startdate: [location&.startdate, @merge_date].compact.max))
new_location.save!(validate: false) new_location.save!(validate: false)
@pre_to_post_merge_location_ids[location.id] = new_location.id
location.location_deactivation_periods.each do |deactivation_period| location.location_deactivation_periods.each do |deactivation_period|
split_location_deactivation_period_between_organisations(deactivation_period, new_location) split_location_deactivation_period_between_organisations(deactivation_period, new_location)
end end
@ -98,8 +102,8 @@ private
lettings_log.managing_organisation = @absorbing_organisation lettings_log.managing_organisation = @absorbing_organisation
end end
if lettings_log.scheme.present? if lettings_log.scheme.present?
scheme_to_set = @absorbing_organisation.owned_schemes.find_by(service_name: lettings_log.scheme.service_name) scheme_to_set = @absorbing_organisation.owned_schemes.find_by(id: @pre_to_post_merge_scheme_ids[lettings_log.scheme.id])
location_to_set = scheme_to_set.locations.find_by(name: lettings_log.location&.name, postcode: lettings_log.location&.postcode) location_to_set = scheme_to_set.locations.find_by(id: @pre_to_post_merge_location_ids[lettings_log.location&.id])
lettings_log.scheme = scheme_to_set if scheme_to_set.present? lettings_log.scheme = scheme_to_set if scheme_to_set.present?
# in some cases the lettings_log location is nil even if scheme is present (they're two different questions). # in some cases the lettings_log location is nil even if scheme is present (they're two different questions).

6
app/views/devise/passwords/reset_resend_confirmation.html.erb

@ -6,7 +6,11 @@
<%= content_for(:title) %> <%= content_for(:title) %>
</h1> </h1>
<p class="govuk-body">We’ve sent a link to reset your password to <strong><%= @email %></strong>.</p> <% if @unconfirmed %>
<p class="govuk-body">We’ve sent a link to confirm your email address to <strong><%= @email %></strong>. This will complete your registration onto the CORE service.</p>
<% else %>
<p class="govuk-body">We’ve sent a link to reset your password to <strong><%= @email %></strong>.</p>
<% end %>
<p class="govuk-body">You’ll only receive this link if your email address already exists in our system.</p> <p class="govuk-body">You’ll only receive this link if your email address already exists in our system.</p>
<p class="govuk-body">If you don’t receive the email within 5 minutes, check your spam or junk folders. Try again if you still haven’t received the email.</p> <p class="govuk-body">If you don’t receive the email within 5 minutes, check your spam or junk folders. Try again if you still haven’t received the email.</p>
</div> </div>

2
app/views/layouts/_feedback.html.erb

@ -10,7 +10,7 @@
</div> </div>
</div> </div>
<div class="gem-c-feedback__prompt-questions"> <div class="gem-c-feedback__prompt-questions">
<a class="govuk-button gem-c-feedback__prompt-link" target="_blank" href="https://forms.office.com/Pages/ResponsePage.aspx?id=EGg0v32c3kOociSi7zmVqC4YDsCJ3llAvEZelBFBLUBURFVUTzFDTUJPQlM4M0laTE5DTlNFSjJBQi4u"> <a class="govuk-button gem-c-feedback__prompt-link" target="_blank" href="https://forms.office.com/e/thT1Vm7edm">
Give feedback (opens in a new tab) Give feedback (opens in a new tab)
</a> </a>
</div> </div>

30
app/views/users/_user_filters.html.erb

@ -17,12 +17,30 @@
<%= render partial: "filters/checkbox_filter", <%= render partial: "filters/checkbox_filter",
locals: { locals: {
f:, f:,
options: user_status_filters, options: user_status_filters,
label: "Status", label: "Status",
category: "status", category: "status",
size: "s", size: "s",
} %> } %>
<%= render partial: "filters/checkbox_filter",
locals: {
f:,
options: user_role_type_filters(current_user.support?),
label: "Role type",
category: "role",
size: "s",
} %>
<%= render partial: "filters/checkbox_filter",
locals: {
f:,
options: user_additional_responsibilities_filters,
label: "Additional responsibilities",
category: "additional_responsibilities",
size: "s",
} %>
<% if request.params["search"].present? %> <% if request.params["search"].present? %>
<%= f.hidden_field :search, value: request.params["search"] %> <%= f.hidden_field :search, value: request.params["search"] %>

5
app/views/users/show.html.erb

@ -154,7 +154,10 @@
<div class="govuk-button-group"> <div class="govuk-button-group">
<% if @user.active? %> <% if @user.active? %>
<%= govuk_button_link_to "Deactivate user", deactivate_user_path(@user), warning: true %> <%= govuk_button_link_to "Deactivate user", deactivate_user_path(@user), warning: true %>
<% if current_user.support? && @user.last_sign_in_at.nil? %> <%# Some users are confirmed but have no sign in date, since logging in is a separate step that happens after confirmation %>
<%# Some users are unconfirmed but have a sign in date, since deactivating an account will unconfirm but not reset login date %>
<%# So, allow both cases to receive invite links %>
<% if current_user.support? && (@user.last_sign_in_at.nil? || !@user.confirmed?) %>
<%= govuk_button_to "Resend invite link", resend_invite_user_path(@user), secondary: true %> <%= govuk_button_to "Resend invite link", resend_invite_user_path(@user), secondary: true %>
<% end %> <% end %>
<% else %> <% else %>

2
config/locales/en.yml

@ -31,7 +31,7 @@
en: en:
service_name: "Submit social housing lettings and sales data (CORE)" service_name: "Submit social housing lettings and sales data (CORE)"
feedback_form: "https://forms.office.com/Pages/ResponsePage.aspx?id=EGg0v32c3kOociSi7zmVqC4YDsCJ3llAvEZelBFBLUBURFVUTzFDTUJPQlM4M0laTE5DTlNFSjJBQi4u" feedback_form: "https://forms.office.com/e/thT1Vm7edm"
organisation: organisation:
created: "%{organisation} was created." created: "%{organisation} was created."
updated: "Organisation details updated." updated: "Organisation details updated."

7
config/locales/validations/lettings/household.en.yml

@ -28,7 +28,6 @@ en:
reason: reason:
leaving_last_settled_home: leaving_last_settled_home:
dont_know_required: "Answer must be ‘don’t know’ as you told us you don’t know the tenant’s main reason for leaving." dont_know_required: "Answer must be ‘don’t know’ as you told us you don’t know the tenant’s main reason for leaving."
not_internal_transfer: "Answer cannot be ‘permanently decanted from another property owned by this landlord’ as you told us the source of referral for this tenancy was not an internal transfer."
other_not_settled: "Please give the reason for the tenant leaving their last settled home. This is where they were living before they became homeless, were living in temporary accommodation or sleeping rough." other_not_settled: "Please give the reason for the tenant leaving their last settled home. This is where they were living before they became homeless, were living in temporary accommodation or sleeping rough."
condition_effects: condition_effects:
@ -102,11 +101,5 @@ en:
referral: referral:
prevten_invalid: "Answer cannot be internal transfer as the household situation immediately before this letting was %{prevten}." prevten_invalid: "Answer cannot be internal transfer as the household situation immediately before this letting was %{prevten}."
leaving_last_settled_home:
reason_permanently_decanted: "Answer must be internal transfer as the tenant was permanently decanted from another property owned by this landlord."
la_general_needs: la_general_needs:
internal_transfer: "Answer cannot be internal transfer as it’s the same landlord on the tenancy agreement and the household had either a fixed-term or lifetime local authority general needs tenancy immediately before this letting." internal_transfer: "Answer cannot be internal transfer as it’s the same landlord on the tenancy agreement and the household had either a fixed-term or lifetime local authority general needs tenancy immediately before this letting."
referral_type:
leaving_last_settled_home:
reason_permanently_decanted: "Answer must be from a PRP-only housing register or waiting list (no local authority involvement) as the tenant was permanently decanted from another property owned by this landlord."

67
docs/setup.md

@ -22,6 +22,8 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage NodeJS version
1. Install PostgreSQL 1. Install PostgreSQL
If you already have a valid Postgres installation you can skip this step.
macOS: macOS:
```bash ```bash
@ -38,6 +40,8 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage NodeJS version
2. Create a Postgres user 2. Create a Postgres user
If you already have a valid Postgres installation you can skip this step.
```bash ```bash
sudo su - postgres -c "createuser <username> -s -P" sudo su - postgres -c "createuser <username> -s -P"
``` ```
@ -84,35 +88,22 @@ We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage NodeJS version
brew install yarn brew install yarn
``` ```
or you could run it without specifying the version and it should use the version from .nvmrc
```bash
nvm install
nvm use
brew install yarn
```
Linux (Debian): Linux (Debian):
```bash ```bash
curl -sL https://deb.nodesource.com/setup_20.x | sudo bash - npm install --global yarn
sudo apt -y install nodejs
mkdir -p ~/.npm-packages
npm config set prefix ~/.npm-packages
echo 'NPM_PACKAGES="~/.npm-packages"' >> ~/.bashrc
echo 'export PATH="$PATH:$NPM_PACKAGES/bin"' >> ~/.bashrc
source ~/.bashrc
npm install --location=global yarn
``` ```
6. (For running tests) Install Gecko Driver 6. (For running tests) Install Gecko Driver
Linux (Debian): Linux (Debian):
Find the latest version at https://github.com/mozilla/geckodriver/releases/, right click the linux64.tar.gz download and copy URL.
```bash ```bash
wget https://github.com/mozilla/geckodriver/releases/download/v0.31.0/geckodriver-v0.31.0-linux64.tar.gz wget <url copied>
tar -xvzf geckodriver-v0.31.0-linux64.tar.gz tar -xvzf <file downloaded>
rm geckodriver-v0.31.0-linux64.tar.gz rm <file downloaded>
chmod +x geckodriver chmod +x geckodriver
sudo mv geckodriver /usr/local/bin/ sudo mv geckodriver /usr/local/bin/
``` ```
@ -147,22 +138,30 @@ Also ensure you have firefox installed
bundle exec rake db:seed bundle exec rake db:seed
``` ```
5. For Ordinance Survey related functionality, such as using the UPRN, you will need to set `OS_DATA_KEY` in your .env file. This key is shared across the team and can be found in AWS Secrets Manager. 5. Build assets once before running the app for the first time:
6. For email functionality, you will need a GOV.UK Notify API key, which is individual to you. Ask an existing team member to invite you to the "CORE Helpdesk" Notify service. Once invited, sign in and go to "API integration" to generate an API key, and set this as `GOVUK_NOTIFY_API_KEY` in your .env file.
```bash
yarn build --mode=development
```
6. For Ordinance Survey related functionality, such as using the UPRN, you will need to set `OS_DATA_KEY` in your .env file. This key is shared across the team and can be found in AWS Secrets Manager.
7. For email functionality, you will need a GOV.UK Notify API key, which is individual to you. Ask an existing team member to invite you to the "CORE Helpdesk" Notify service. Once invited, sign in and go to "API integration" to generate an API key, and set this as `GOVUK_NOTIFY_API_KEY` in your .env file.
## Running Locally ## Running Locally
### Application ### Application
Start the dev servers Start the dev servers via one of the following methods:
a. Using Foreman: a. If using RubyMine, run the "submit-social-housing-lettings-and-sales-data" Rails configuration.
b. Using Foreman:
```bash ```bash
./bin/dev ./bin/dev
``` ```
b. Individually: c. Individually:
Rails: Rails:
@ -176,20 +175,18 @@ JavaScript (for hot reloading):
yarn build --mode=development --watch yarn build --mode=development --watch
``` ```
If you’re not modifying front end assets you can bundle them as a one off task:
```bash
yarn build --mode=development
```
Development mode will target the latest versions of Chrome, Firefox and Safari for transpilation while production mode will target older browsers. Development mode will target the latest versions of Chrome, Firefox and Safari for transpilation while production mode will target older browsers.
The Rails server will start on <http://localhost:3000>. The Rails server will start on <http://localhost:3000>.
To sign in locally, you can use any username and password from your local database. The seed task in `seeds.rb` creates users in various roles all with the password `REVIEW_APP_USER_PASSWORD` from your .env file (which has default value `password`). To sign in locally, you can use any username and password from your local database. The seed task in `seeds.rb` creates users in various roles all with the password `REVIEW_APP_USER_PASSWORD` from your .env file (which has default value `password`).
To create any other users, you can edit the seed commands, or run similar commands in the rails console. To create any other users, you can log in as a support user and create new users via the admin interface. Or, you can edit the seed commands, or run similar commands in the rails console.
You can also insert a new user row using SQL, but you will need to generate a correctly encrypted password. You can find the value to use for encrypted password which corresponds to the password `YOURPASSWORDHERE` using `User.new(:password => [YOURPASSWORDHERE]).encrypted_password`. You can also insert a new user row using SQL, but you will need to generate a correctly encrypted password. You can find the value to use for encrypted password which corresponds to the password `YOURPASSWORDHERE` using `User.new(:password => [YOURPASSWORDHERE]).encrypted_password`.
### rbenv
In general, whenever needing to run a Ruby command, use `bundle exec <command>` to ensure the correct Ruby version and gemset are used. Rbenv will automatically use the correct Ruby version.
### Debugging ### Debugging
Add the line `binding.pry` to the code to pause the execution of the code at that line and open a powerful interactive console for debugging. Add the line `binding.pry` to the code to pause the execution of the code at that line and open a powerful interactive console for debugging.
@ -219,6 +216,8 @@ More details on debugging in RubyMine can be found at <https://www.jetbrains.com
bundle exec rspec bundle exec rspec
``` ```
Or if using RubyMine, right click a spec file and select the 'Run' option.
To run a specific folder use To run a specific folder use
```bash ```bash
@ -233,6 +232,12 @@ bundle exec rspec ./spec/path/to/file.rb
or run individual files/tests from your IDE or run individual files/tests from your IDE
If you have made database changes, you may need to run the migrations for the test database:
```bash
bundle exec rake db:migrate RAILS_ENV=test
```
### Feature toggles ### Feature toggles
Feature toggles can be found in `app/services/feature_toggle.rb` Feature toggles can be found in `app/services/feature_toggle.rb`

13
spec/features/user_spec.rb

@ -608,6 +608,19 @@ RSpec.describe "User Features" do
click_button("Resend invite link") click_button("Resend invite link")
end end
end end
context "when reactivating a user" do
let!(:other_user) { create(:user, name: "Other name", active: false, organisation: user.organisation, last_sign_in_at: Time.zone.now, confirmed_at: nil) }
it "allows for reactivation email to be resent" do
allow(user).to receive(:need_two_factor_authentication?).and_return(false)
sign_in(user)
visit(user_path(other_user))
click_link("Reactivate user")
click_button("I’m sure – reactivate this user")
expect(page).to have_button("Resend invite link")
end
end
end end
context "when the user is a customer support person" do context "when the user is a customer support person" do

8
spec/models/validations/household_validations_spec.rb

@ -116,17 +116,17 @@ RSpec.describe Validations::HouseholdValidations do
end end
context "when referral is not internal transfer" do context "when referral is not internal transfer" do
it "cannot be permanently decanted from another property owned by this landlord" do it "can be permanently decanted from another property owned by this landlord" do
record.reason = 1 record.reason = 1
record.referral_type = 1 record.referral_type = 1
record.referral = 2 record.referral = 2
household_validator.validate_reason_for_leaving_last_settled_home(record) household_validator.validate_reason_for_leaving_last_settled_home(record)
expect(record.errors["reason"]) expect(record.errors["reason"])
.to include(match(I18n.t("validations.lettings.household.reason.leaving_last_settled_home.not_internal_transfer"))) .to be_empty
expect(record.errors["referral"]) expect(record.errors["referral"])
.to include(match(I18n.t("validations.lettings.household.referral.leaving_last_settled_home.reason_permanently_decanted"))) .to be_empty
expect(record.errors["referral_type"]) expect(record.errors["referral_type"])
.to include(match(I18n.t("validations.lettings.household.referral_type.leaving_last_settled_home.reason_permanently_decanted"))) .to be_empty
end end
end end

16
spec/services/exports/lettings_log_export_service_spec.rb

@ -550,6 +550,22 @@ RSpec.describe Exports::LettingsLogExportService do
export_service.export_xml_lettings_logs(collection_year: current_collection_start_year) export_service.export_xml_lettings_logs(collection_year: current_collection_start_year)
end end
it "does export the lettings log if owning_organisation name change is created" do
create(:organisation_name_change, organisation: owning_organisation)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
export_service.export_xml_lettings_logs(collection_year: current_collection_start_year)
end
it "does export the lettings log if managing_organisation name change is created" do
create(:organisation_name_change, organisation: managing_organisation)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
export_service.export_xml_lettings_logs(collection_year: current_collection_start_year)
end
end end
end end

16
spec/services/exports/sales_log_export_service_spec.rb

@ -491,6 +491,22 @@ RSpec.describe Exports::SalesLogExportService do
export_service.export_xml_sales_logs(collection_year: current_collection_start_year) export_service.export_xml_sales_logs(collection_year: current_collection_start_year)
end end
it "does export the sales log if owning_organisation name change is created" do
create(:organisation_name_change, organisation: owning_organisation)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
export_service.export_xml_sales_logs(collection_year: current_collection_start_year)
end
it "does export the sales log if managing_organisation name change is created" do
create(:organisation_name_change, organisation: managing_organisation)
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
export_service.export_xml_sales_logs(collection_year: current_collection_start_year)
end
end end
end end
end end

25
spec/services/merge/merge_organisations_service_spec.rb

@ -728,6 +728,31 @@ RSpec.describe Merge::MergeOrganisationsService do
expect(incomplete_lettings_log.location).to be_nil expect(incomplete_lettings_log.location).to be_nil
end end
it "associates correct lettings logs with correct locations which share a name and postcode" do
scheme = create(:scheme, owning_organisation: merging_organisation)
location_0 = create(:location, scheme:, name: "duplicate name", postcode: "EE1 1EE", location_admin_district: "dist1", startdate: Time.zone.today)
location_1 = create(:location, scheme:, name: "duplicate name", postcode: "EE1 1EE", location_admin_district: "dist2", startdate: Time.zone.today)
location_2 = create(:location, scheme:, name: "duplicate name", postcode: "EE1 1EE", location_admin_district: "dist3", startdate: Time.zone.today)
lettings_log_0 = build(:lettings_log, scheme:, owning_organisation: merging_organisation, startdate: Time.zone.today, location: location_0)
lettings_log_0.save!(validate: false)
lettings_log_1 = build(:lettings_log, scheme:, owning_organisation: merging_organisation, startdate: Time.zone.today, location: location_1)
lettings_log_1.save!(validate: false)
lettings_log_2 = build(:lettings_log, scheme:, owning_organisation: merging_organisation, startdate: Time.zone.today, location: location_2)
lettings_log_2.save!(validate: false)
expect(Rails.logger).not_to receive(:error)
merge_organisations_service.call
lettings_log_0.reload
lettings_log_1.reload
lettings_log_2.reload
expect(lettings_log_0.location.location_admin_district).to eq("dist1")
expect(lettings_log_1.location.location_admin_district).to eq("dist2")
expect(lettings_log_2.location.location_admin_district).to eq("dist3")
end
context "with merge date in closed collection year" do context "with merge date in closed collection year" do
subject(:merge_organisations_service) { described_class.new(absorbing_organisation_id: absorbing_organisation.id, merging_organisation_ids:, merge_date: Time.zone.local(2021, 3, 3)) } subject(:merge_organisations_service) { described_class.new(absorbing_organisation_id: absorbing_organisation.id, merging_organisation_ids:, merge_date: Time.zone.local(2021, 3, 3)) }

Loading…
Cancel
Save