Browse Source

Merge branch 'main' into CLDC-1917-allow-23-24-form

# Conflicts:
#	config/initializers/feature_toggle.rb
pull/1318/head
natdeanlewissoftwire 2 years ago
parent
commit
513e0c0b26
  1. 3
      Gemfile
  2. 276
      Gemfile.lock
  3. 16
      app/helpers/collection_time_helper.rb
  4. 33
      app/helpers/interruption_screen_helper.rb
  5. 21
      app/helpers/tasklist_helper.rb
  6. 6
      app/models/form.rb
  7. 10
      app/models/form/lettings/questions/location_id.rb
  8. 6
      app/models/form/sales/pages/about_price_shared_ownership_value_check.rb
  9. 15
      app/models/form/sales/pages/buyer1_income_value_check.rb
  10. 35
      app/models/form/sales/pages/buyer2_income_value_check.rb
  11. 11
      app/models/form/sales/pages/living_before_purchase.rb
  12. 14
      app/models/form/sales/pages/purchase_price.rb
  13. 2
      app/models/form/sales/questions/buyer1_income_value_check.rb
  14. 1
      app/models/form/sales/questions/buyer2_income.rb
  15. 25
      app/models/form/sales/questions/buyer2_income_value_check.rb
  16. 31
      app/models/form/sales/questions/living_before_purchase.rb
  17. 31
      app/models/form/sales/questions/living_before_purchase_years.rb
  18. 10
      app/models/form/sales/questions/previous_postcode_known.rb
  19. 1
      app/models/form/sales/subsections/discounted_ownership_scheme.rb
  20. 3
      app/models/form/sales/subsections/household_characteristics.rb
  21. 1
      app/models/form/sales/subsections/income_benefits_and_savings.rb
  22. 8
      app/models/form_handler.rb
  23. 6
      app/models/location.rb
  24. 4
      app/models/log.rb
  25. 27
      app/models/organisation.rb
  26. 18
      app/models/sales_log.rb
  27. 10
      app/models/scheme.rb
  28. 63
      app/models/validations/sales/financial_validations.rb
  29. 38
      app/models/validations/sales/setup_validations.rb
  30. 12
      app/models/validations/sales/soft_validations.rb
  31. 122
      app/services/bulk_upload/lettings/row_parser.rb
  32. 1
      app/services/bulk_upload/lettings/validator.rb
  33. 45
      app/views/content/privacy_notice.md
  34. 2
      config/forms/2022_2023.json
  35. 9
      config/initializers/feature_toggle.rb
  36. 19
      config/locales/en.yml
  37. 5
      db/migrate/20230105103134_add_income2_value_check.rb
  38. 5
      db/migrate/20230213140932_add_proplen_asked_to_sales_log.rb
  39. 4
      db/schema.rb
  40. 5
      spec/factories/location.rb
  41. 4
      spec/factories/organisation.rb
  42. 4
      spec/factories/scheme.rb
  43. 53
      spec/helpers/interruption_screen_helper_spec.rb
  44. 147
      spec/helpers/tasklist_helper_spec.rb
  45. 6
      spec/models/form/lettings/pages/location_spec.rb
  46. 18
      spec/models/form/lettings/questions/location_id_spec.rb
  47. 20
      spec/models/form/sales/pages/living_before_purchase_spec.rb
  48. 35
      spec/models/form/sales/pages/purchase_price_spec.rb
  49. 2
      spec/models/form/sales/questions/buyer1_income_value_check_spec.rb
  50. 31
      spec/models/form/sales/questions/living_before_purchase_spec.rb
  51. 114
      spec/models/form/sales/questions/living_before_purchase_years_spec.rb
  52. 9
      spec/models/form/sales/questions/previous_postcode_known_spec.rb
  53. 1
      spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb
  54. 6
      spec/models/form/sales/subsections/household_characteristics_spec.rb
  55. 2
      spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb
  56. 4
      spec/models/form_handler_spec.rb
  57. 2
      spec/models/form_spec.rb
  58. 23
      spec/models/sales_log_spec.rb
  59. 222
      spec/models/validations/sales/financial_validations_spec.rb
  60. 97
      spec/models/validations/sales/setup_validations_spec.rb
  61. 3
      spec/rails_helper.rb
  62. 226
      spec/services/bulk_upload/lettings/row_parser_spec.rb
  63. 20
      spec/services/bulk_upload/lettings/validator_spec.rb

3
Gemfile

@ -20,7 +20,7 @@ gem "bootsnap", ">= 1.4.4", require: false
# GOV UK frontend components
gem "govuk-components"
# GOV UK component form builder DSL
gem "govuk_design_system_formbuilder"
gem "govuk_design_system_formbuilder", "3.1.2"
# Convert Markdown into GOV.UK frontend-styled HTML
gem "govuk_markdown"
# GOV UK Notify
@ -91,6 +91,7 @@ end
group :test do
gem "capybara", require: false
gem "capybara-lockstep"
gem "capybara-screenshot"
gem "faker"
gem "rspec-rails", require: false
gem "selenium-webdriver", require: false

276
Gemfile.lock

@ -13,67 +13,67 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.4.1)
actionpack (= 7.0.4.1)
activesupport (= 7.0.4.1)
actioncable (7.0.4.2)
actionpack (= 7.0.4.2)
activesupport (= 7.0.4.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.4.1)
actionpack (= 7.0.4.1)
activejob (= 7.0.4.1)
activerecord (= 7.0.4.1)
activestorage (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionmailbox (7.0.4.2)
actionpack (= 7.0.4.2)
activejob (= 7.0.4.2)
activerecord (= 7.0.4.2)
activestorage (= 7.0.4.2)
activesupport (= 7.0.4.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.4.1)
actionpack (= 7.0.4.1)
actionview (= 7.0.4.1)
activejob (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionmailer (7.0.4.2)
actionpack (= 7.0.4.2)
actionview (= 7.0.4.2)
activejob (= 7.0.4.2)
activesupport (= 7.0.4.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.4.1)
actionview (= 7.0.4.1)
activesupport (= 7.0.4.1)
actionpack (7.0.4.2)
actionview (= 7.0.4.2)
activesupport (= 7.0.4.2)
rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.4.1)
actionpack (= 7.0.4.1)
activerecord (= 7.0.4.1)
activestorage (= 7.0.4.1)
activesupport (= 7.0.4.1)
actiontext (7.0.4.2)
actionpack (= 7.0.4.2)
activerecord (= 7.0.4.2)
activestorage (= 7.0.4.2)
activesupport (= 7.0.4.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.4.1)
activesupport (= 7.0.4.1)
actionview (7.0.4.2)
activesupport (= 7.0.4.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.4.1)
activesupport (= 7.0.4.1)
activejob (7.0.4.2)
activesupport (= 7.0.4.2)
globalid (>= 0.3.6)
activemodel (7.0.4.1)
activesupport (= 7.0.4.1)
activerecord (7.0.4.1)
activemodel (= 7.0.4.1)
activesupport (= 7.0.4.1)
activestorage (7.0.4.1)
actionpack (= 7.0.4.1)
activejob (= 7.0.4.1)
activerecord (= 7.0.4.1)
activesupport (= 7.0.4.1)
activemodel (7.0.4.2)
activesupport (= 7.0.4.2)
activerecord (7.0.4.2)
activemodel (= 7.0.4.2)
activesupport (= 7.0.4.2)
activestorage (7.0.4.2)
actionpack (= 7.0.4.2)
activejob (= 7.0.4.2)
activerecord (= 7.0.4.2)
activesupport (= 7.0.4.2)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.4.1)
activesupport (7.0.4.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -84,20 +84,20 @@ GEM
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
aws-eventstream (1.2.0)
aws-partitions (1.635.0)
aws-sdk-core (3.153.0)
aws-partitions (1.714.0)
aws-sdk-core (3.170.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (1.62.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-s3 (1.119.1)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.18)
better_html (2.0.1)
@ -108,14 +108,14 @@ GEM
parser (>= 2.4)
smart_properties
bindex (0.8.1)
bootsnap (1.13.0)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.4)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
capybara (3.37.1)
capybara (3.38.0)
addressable
matrix
mini_mime (>= 0.1.3)
@ -124,14 +124,17 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-lockstep (1.2.1)
capybara-lockstep (1.3.0)
activesupport (>= 3.2)
capybara (>= 2.0)
ruby2_keywords
selenium-webdriver (>= 3)
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
childprocess (4.1.0)
coderay (1.1.3)
concurrent-ruby (1.1.10)
concurrent-ruby (1.2.0)
connection_pool (2.3.0)
crack (0.4.5)
rexml
@ -150,7 +153,7 @@ GEM
dotenv (= 2.8.1)
railties (>= 3.2)
encryptor (3.0.0)
erb_lint (0.2.0)
erb_lint (0.3.1)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
@ -160,19 +163,19 @@ GEM
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
excon (0.92.5)
excon (0.99.0)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (2.23.0)
faker (3.1.1)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.0.1)
globalid (1.1.0)
activesupport (>= 5.0)
govuk-components (3.2.1)
actionpack (>= 6.1)
@ -186,7 +189,7 @@ GEM
activemodel (>= 6.1)
activesupport (>= 6.1)
html-attributes-utils (~> 0.9, >= 0.9.2)
govuk_markdown (1.0.0)
govuk_markdown (2.0.0)
activesupport
redcarpet
hashdiff (1.0.1)
@ -195,19 +198,21 @@ GEM
i18n (1.12.0)
concurrent-ruby (~> 1.0)
iniparse (1.5.0)
jmespath (1.6.1)
jsbundling-rails (1.0.3)
jmespath (1.6.2)
jsbundling-rails (1.1.1)
railties (>= 6.0.0)
json-schema (3.0.0)
addressable (>= 2.8)
jwt (2.5.0)
listen (3.7.1)
jwt (2.7.0)
launchy (2.5.2)
addressable (~> 2.8)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.8.0.1)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
@ -217,7 +222,7 @@ GEM
method_source (1.0.0)
mini_mime (1.1.2)
minitest (5.17.0)
msgpack (1.5.6)
msgpack (1.6.0)
net-imap (0.3.4)
date
net-protocol
@ -228,33 +233,33 @@ GEM
net-smtp (0.3.3)
net-protocol
nio4r (2.5.8)
nokogiri (1.14.0-arm64-darwin)
nokogiri (1.14.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-darwin)
nokogiri (1.14.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-linux)
nokogiri (1.14.2-x86_64-linux)
racc (~> 1.4)
notifications-ruby-client (5.3.0)
notifications-ruby-client (5.4.0)
jwt (>= 1.5, < 3)
orm_adapter (0.5.0)
overcommit (0.59.1)
overcommit (0.60.0)
childprocess (>= 0.6.3, < 5)
iniparse (~> 1.4)
rexml (~> 3.2)
pagy (5.10.1)
activesupport
paper_trail (13.0.0)
activerecord (>= 5.2)
request_store (~> 1.1)
paper_trail (14.0.0)
activerecord (>= 6.0)
request_store (~> 1.4)
paper_trail-globalid (0.2.0)
globalid
paper_trail (>= 3.0.0)
parallel (1.22.1)
parallel_tests (4.0.0)
parallel_tests (4.2.0)
parallel
parser (3.1.2.1)
parser (3.2.1.0)
ast (~> 2.4.1)
pg (1.4.3)
pg (1.4.5)
possessive (1.0.1)
postcodes_io (0.4.0)
excon (~> 0.39)
@ -263,13 +268,13 @@ GEM
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
pry (0.14.1)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
public_suffix (5.0.0)
public_suffix (5.0.1)
puma (5.6.5)
nio4r (~> 2.0)
raabro (1.4.0)
@ -281,28 +286,28 @@ GEM
rack (>= 1.2.0)
rack-test (2.0.2)
rack (>= 1.3)
rails (7.0.4.1)
actioncable (= 7.0.4.1)
actionmailbox (= 7.0.4.1)
actionmailer (= 7.0.4.1)
actionpack (= 7.0.4.1)
actiontext (= 7.0.4.1)
actionview (= 7.0.4.1)
activejob (= 7.0.4.1)
activemodel (= 7.0.4.1)
activerecord (= 7.0.4.1)
activestorage (= 7.0.4.1)
activesupport (= 7.0.4.1)
rails (7.0.4.2)
actioncable (= 7.0.4.2)
actionmailbox (= 7.0.4.2)
actionmailer (= 7.0.4.2)
actionpack (= 7.0.4.2)
actiontext (= 7.0.4.2)
actionview (= 7.0.4.2)
activejob (= 7.0.4.2)
activemodel (= 7.0.4.2)
activerecord (= 7.0.4.2)
activestorage (= 7.0.4.2)
activesupport (= 7.0.4.2)
bundler (>= 1.15.0)
railties (= 7.0.4.1)
railties (= 7.0.4.2)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (7.0.4.1)
actionpack (= 7.0.4.1)
activesupport (= 7.0.4.1)
railties (7.0.4.2)
actionpack (= 7.0.4.2)
activesupport (= 7.0.4.2)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -313,36 +318,38 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.5.1)
redis (4.8.0)
regexp_parser (2.5.0)
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.12.2)
connection_pool
regexp_parser (2.7.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.5)
roo (2.9.0)
roo (2.10.0)
nokogiri (~> 1)
rubyzip (>= 1.3.0, < 3.0.0)
rotp (6.2.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.1)
rotp (6.2.2)
rspec-core (3.12.1)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.1)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-rails (5.1.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
rspec-core (~> 3.10)
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.11.1)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
rubocop (1.25.0)
parallel (~> 1.10)
parser (>= 3.1.0.0)
@ -360,7 +367,7 @@ GEM
rubocop-rails (= 2.13.2)
rubocop-rake (= 0.6.0)
rubocop-rspec (= 2.7.0)
rubocop-performance (1.15.0)
rubocop-performance (1.16.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.13.2)
@ -374,39 +381,39 @@ GEM
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
selenium-webdriver (4.4.0)
childprocess (>= 0.5, < 5.0)
selenium-webdriver (4.8.1)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sentry-rails (5.4.2)
sentry-rails (5.8.0)
railties (>= 5.0)
sentry-ruby (~> 5.4.2)
sentry-ruby (5.4.2)
sentry-ruby (~> 5.8.0)
sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.5)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.5.0)
sidekiq-cron (1.8.0)
fugit (~> 1)
sidekiq (7.0.5)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.11.0)
sidekiq-cron (1.9.1)
fugit (~> 1.8)
sidekiq (>= 4.2.1)
simplecov (0.21.2)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
stimulus-rails (1.1.0)
stimulus-rails (1.2.1)
railties (>= 6.0.0)
thor (1.2.1)
timecop (0.9.5)
timeout (0.3.1)
tzinfo (2.0.5)
timecop (0.9.6)
timeout (0.3.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode-display_width (2.3.0)
unicode-display_width (2.4.2)
view_component (2.69.0)
activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0)
@ -428,7 +435,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.6)
zeitwerk (2.6.7)
PLATFORMS
arm64-darwin-21
@ -447,6 +454,7 @@ DEPENDENCIES
byebug
capybara
capybara-lockstep
capybara-screenshot
devise!
devise_two_factor_authentication
dotenv-rails
@ -454,7 +462,7 @@ DEPENDENCIES
factory_bot_rails
faker
govuk-components
govuk_design_system_formbuilder
govuk_design_system_formbuilder (= 3.1.2)
govuk_markdown
jsbundling-rails
json-schema

16
app/helpers/collection_time_helper.rb

@ -23,4 +23,20 @@ module CollectionTimeHelper
def current_collection_end_date
Time.zone.local(current_collection_start_year + 1, 3, 31)
end
def previous_collection_end_date
current_collection_end_date - 1.year
end
def next_collection_start_year
current_collection_start_year + 1
end
def previous_collection_start_year
current_collection_start_year - 1
end
def previous_collection_start_date
current_collection_start_date - 1.year
end
end

33
app/helpers/interruption_screen_helper.rb

@ -1,17 +1,10 @@
module InterruptionScreenHelper
def display_informative_text(informative_text, lettings_log)
def display_informative_text(informative_text, log)
return "" unless informative_text["arguments"]
translation_params = {}
informative_text["arguments"].each do |argument|
value = if argument["label"]
pre_casing_value = lettings_log.form.get_question(argument["key"], lettings_log).answer_label(lettings_log)
pre_casing_value.downcase
elsif argument["currency"]
number_to_currency(lettings_log.public_send(argument["key"]), delimiter: ",", format: "%n", unit: "£")
else
lettings_log.public_send(argument["key"])
end
value = get_value_from_argument(log, argument)
translation_params[argument["i18n_template"].to_sym] = value
end
@ -24,21 +17,27 @@ module InterruptionScreenHelper
end
end
def display_title_text(title_text, lettings_log)
def display_title_text(title_text, log)
return "" if title_text.nil?
translation_params = {}
arguments = title_text["arguments"] || {}
arguments.each do |argument|
value = if argument["label"]
lettings_log.form.get_question(argument["key"], lettings_log).answer_label(lettings_log).downcase
elsif argument["currency"]
number_to_currency(lettings_log.public_send(argument["key"]), delimiter: ",", format: "%n", unit: "£")
else
lettings_log.public_send(argument["key"])
end
value = get_value_from_argument(log, argument)
translation_params[argument["i18n_template"].to_sym] = value
end
I18n.t(title_text["translation"], **translation_params).to_s
end
private
def get_value_from_argument(log, argument)
if argument["label"]
log.form.get_question(argument["key"], log).answer_label(log).downcase
elsif argument["arguments_for_key"]
log.public_send(argument["key"], argument["arguments_for_key"])
else
log.public_send(argument["key"])
end
end
end

21
app/helpers/tasklist_helper.rb

@ -11,15 +11,6 @@ module TasklistHelper
log.form.subsections.count { |subsection| subsection.status(log) == status && subsection.applicable_questions(log).count.positive? }
end
def next_page_or_check_answers(subsection, log, current_user)
path = if subsection.is_started?(log)
"#{log.class.name.underscore}_#{subsection.id}_check_answers_path"
else
"#{log.class.name.underscore}_#{next_question_page(subsection, log, current_user)}_path"
end
send(path, log)
end
def next_question_page(subsection, log, current_user)
if subsection.pages.first.routed_to?(log, current_user)
subsection.pages.first.id
@ -46,4 +37,16 @@ module TasklistHelper
"This log is from the #{log.form.start_date.year}/#{log.form.start_date.year + 1} collection window, which is now closed."
end
end
private
def next_page_or_check_answers(subsection, log, current_user)
path = if subsection.is_started?(log)
"#{log.class.name.underscore}_#{subsection.id}_check_answers_path"
else
"#{log.class.name.underscore}_#{next_question_page(subsection, log, current_user)}_path"
end
send(path, log)
end
end

6
app/models/form.rb

@ -6,7 +6,11 @@ class Form
def initialize(form_path, start_year = "", sections_in_form = [], type = "lettings")
if sales_or_start_year_after_2022?(type, start_year)
@start_date = Time.zone.local(start_year, 4, 1)
@end_date = Time.zone.local(start_year + 1, 7, 1)
@end_date = if start_year && start_year.to_i > 2022
Time.zone.local(start_year + 1, 7, 9)
else
Time.zone.local(start_year + 1, 7, 7)
end
@setup_sections = type == "sales" ? [Form::Sales::Sections::Setup.new(nil, nil, self)] : [Form::Lettings::Sections::Setup.new(nil, nil, self)]
@form_sections = sections_in_form.map { |sec| sec.new(nil, nil, self) }
@type = type

10
app/models/form/lettings/questions/location_id.rb

@ -2,7 +2,7 @@ class Form::Lettings::Questions::LocationId < ::Form::Question
def initialize(_id, hsh, page)
super("location_id", hsh, page)
@check_answer_label = "Location"
@header = "Which location is this log for?"
@header = header_text
@type = "radio"
@answer_options = answer_options
@inferred_answers = {
@ -47,4 +47,12 @@ private
def selected_answer_option_is_derived?(_lettings_log)
false
end
def header_text
if form.start_date && form.start_date.year >= 2023
"Which location is this letting for?"
else
"Which location is this log for?"
end
end
end

6
app/models/form/sales/pages/about_price_shared_ownership_value_check.rb

@ -20,14 +20,12 @@ class Form::Sales::Pages::AboutPriceSharedOwnershipValueCheck < ::Form::Page
"translation" => "soft_validations.purchase_price.hint_text",
"arguments" => [
{
"key" => "purchase_price_soft_min_or_soft_max",
"label" => false,
"key" => "field_formatted_as_currency",
"arguments_for_key" => "purchase_price_soft_min_or_soft_max",
"i18n_template" => "soft_min_or_soft_max",
"currency" => true,
},
{
"key" => "purchase_price_min_or_max_text",
"label" => false,
"i18n_template" => "min_or_max",
},
],

15
app/models/form/sales/pages/buyer1_income_value_check.rb

@ -6,6 +6,21 @@ class Form::Sales::Pages::Buyer1IncomeValueCheck < ::Form::Page
"income1_under_soft_min?" => true,
},
]
@title_text = {
"translation" => "soft_validations.income.under_soft_min_for_economic_status",
"arguments" => [
{
"key" => "field_formatted_as_currency",
"arguments_for_key" => "income1",
"i18n_template" => "income",
},
{
"key" => "income_soft_min_for_ecstat",
"arguments_for_key" => "ecstat1",
"i18n_template" => "minimum",
},
],
}
@informative_text = {}
end

35
app/models/form/sales/pages/buyer2_income_value_check.rb

@ -0,0 +1,35 @@
class Form::Sales::Pages::Buyer2IncomeValueCheck < ::Form::Page
def initialize(id, hsh, subsection)
super
@header = ""
@description = ""
@subsection = subsection
@depends_on = [
{
"income2_under_soft_min?" => true,
},
]
@title_text = {
"translation" => "soft_validations.income.under_soft_min_for_economic_status",
"arguments" => [
{
"key" => "field_formatted_as_currency",
"arguments_for_key" => "income2",
"i18n_template" => "income",
},
{
"key" => "income_soft_min_for_ecstat",
"arguments_for_key" => "ecstat2",
"i18n_template" => "minimum",
},
],
}
@informative_text = {}
end
def questions
@questions ||= [
Form::Sales::Questions::Buyer2IncomeValueCheck.new(nil, nil, self),
]
end
end

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

@ -1,7 +1,14 @@
class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page
def questions
@questions ||= [
Form::Sales::Questions::LivingBeforePurchase.new(nil, nil, self),
]
living_before_purchase,
Form::Sales::Questions::LivingBeforePurchaseYears.new(nil, nil, self),
].compact
end
def living_before_purchase
if form.start_date.year >= 2023
Form::Sales::Questions::LivingBeforePurchase.new(nil, nil, self)
end
end
end

14
app/models/form/sales/pages/purchase_price.rb

@ -1,14 +0,0 @@
class Form::Sales::Pages::PurchasePrice < ::Form::Page
def initialize(id, hsh, subsection)
super
@depends_on = [
{ "ownershipsch" => 2, "rent_to_buy_full_ownership?" => false },
]
end
def questions
@questions ||= [
Form::Sales::Questions::PurchasePrice.new(nil, nil, self),
]
end
end

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

@ -3,7 +3,7 @@ class Form::Sales::Questions::Buyer1IncomeValueCheck < ::Form::Question
super
@id = "income1_value_check"
@check_answer_label = "Income confirmation"
@header = "Are you sure this income is correct?"
@header = "Are you sure this is correct?"
@type = "interruption_screen"
@answer_options = {
"0" => { "value" => "Yes" },

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

@ -7,6 +7,7 @@ class Form::Sales::Questions::Buyer2Income < ::Form::Question
@type = "numeric"
@hint_text = "Provide the gross annual income (i.e. salary before tax) plus the annual amount of benefits, Universal Credit or pensions, and income from investments."
@min = 0
@max = 999_999
@step = 1
@width = 5
@prefix = "£"

25
app/models/form/sales/questions/buyer2_income_value_check.rb

@ -0,0 +1,25 @@
class Form::Sales::Questions::Buyer2IncomeValueCheck < ::Form::Question
def initialize(id, hsh, page)
super
@id = "income2_value_check"
@check_answer_label = "Income confirmation"
@header = "Are you sure this is correct?"
@type = "interruption_screen"
@answer_options = {
"0" => { "value" => "Yes" },
"1" => { "value" => "No" },
}
@hidden_in_check_answers = {
"depends_on" => [
{
"income2_value_check" => 0,
},
{
"income2_value_check" => 1,
},
],
}
@check_answers_card_number = 2
@page = page
end
end

31
app/models/form/sales/questions/living_before_purchase.rb

@ -1,15 +1,26 @@
class Form::Sales::Questions::LivingBeforePurchase < ::Form::Question
def initialize(id, hsh, page)
super
@id = "proplen"
@check_answer_label = "Number of years living in the property before purchase"
@header = "How long did the buyer(s) live in the property before purchase?"
@hint_text = "You should round this up to the nearest year. If the buyers haven't been living in the property, enter '0'"
@type = "numeric"
@min = 0
@max = 80
@step = 1
@width = 5
@suffix = " years"
@id = "proplen_asked"
@check_answer_label = "Buyer lived in the property before purchasing"
@header = "Did the buyer live in the property before purchasing it?"
@hint_text = nil
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = {
"proplen" => [0],
}
@hidden_in_check_answers = {
"depends_on" => [
{
"proplen_asked" => 0,
},
],
}
end
ANSWER_OPTIONS = {
"0" => { "value" => "Yes" },
"1" => { "value" => "No" },
}.freeze
end

31
app/models/form/sales/questions/living_before_purchase_years.rb

@ -0,0 +1,31 @@
class Form::Sales::Questions::LivingBeforePurchaseYears < ::Form::Question
def initialize(id, hsh, page)
super
@id = "proplen"
@check_answer_label = "Number of years living in the property before purchase"
@header = header_text
@hint_text = hint_text
@type = "numeric"
@min = 0
@max = 80
@step = 1
@width = 5
@suffix = " years"
end
def header_text
if form.start_date.year >= 2023
"How long did they live there?"
else
"How long did the buyer(s) live in the property before purchase?"
end
end
def hint_text
if form.start_date.year >= 2023
"You should round up to the nearest year"
else
"You should round this up to the nearest year. If the buyers haven't been living in the property, enter '0'"
end
end
end

10
app/models/form/sales/questions/previous_postcode_known.rb

@ -10,6 +10,16 @@ class Form::Sales::Questions::PreviousPostcodeKnown < ::Form::Question
"ppostcode_full" => [0],
}
@hint_text = "This is also known as the household’s 'last settled home'"
@hidden_in_check_answers = {
"depends_on" => [
{
"ppcodenk" => 0,
},
{
"ppcodenk" => 1,
},
],
}
end
ANSWER_OPTIONS = {

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

@ -13,7 +13,6 @@ class Form::Sales::Subsections::DiscountedOwnershipScheme < ::Form::Subsection
Form::Sales::Pages::ExtraBorrowingValueCheck.new("extra_borrowing_price_value_check", nil, self),
Form::Sales::Pages::AboutPriceNotRtb.new(nil, nil, self),
Form::Sales::Pages::GrantValueCheck.new(nil, nil, self),
Form::Sales::Pages::PurchasePrice.new("purchase_price_discounted_ownership", nil, self),
Form::Sales::Pages::PurchasePriceOutrightOwnership.new("purchase_price_outright_ownership", nil, self),
Form::Sales::Pages::DepositAndMortgageValueCheck.new("discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount", nil, self),
Form::Sales::Pages::Mortgageused.new("mortgage_used_discounted_ownership", nil, self),

3
app/models/form/sales/subsections/household_characteristics.rb

@ -34,7 +34,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2),
ethnic_pages_for_buyer_2,
Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_buyer_retirement_value_check", nil, self, person_index: 2),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2),
Form::Sales::Pages::Buyer2IncomeValueCheck.new("working_situation_buyer_2_income_value_check", nil, self),
Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self),
Form::Sales::Pages::NumberOfOthersInProperty.new(nil, nil, self),
Form::Sales::Pages::PersonKnown.new("person_2_known", nil, self, person_index: 2),

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

@ -15,6 +15,7 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection
Form::Sales::Pages::MortgageValueCheck.new("buyer_1_mortgage_value_check", nil, self, 1),
Form::Sales::Pages::Buyer2Income.new(nil, nil, self),
Form::Sales::Pages::MortgageValueCheck.new("buyer_2_income_mortgage_value_check", nil, self, 2),
Form::Sales::Pages::Buyer2IncomeValueCheck.new("buyer_2_income_value_check", nil, self),
Form::Sales::Pages::Buyer2Mortgage.new(nil, nil, self),
Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2),
Form::Sales::Pages::HousingBenefits.new(nil, nil, self),

8
app/models/form_handler.rb

@ -35,8 +35,8 @@ class FormHandler
def sales_forms
{
"current_sales" => Form.new(nil, current_collection_start_year, SALES_SECTIONS, "sales"),
"previous_sales" => Form.new(nil, current_collection_start_year - 1, SALES_SECTIONS, "sales"),
"next_sales" => Form.new(nil, current_collection_start_year + 1, SALES_SECTIONS, "sales"),
"previous_sales" => Form.new(nil, previous_collection_start_year, SALES_SECTIONS, "sales"),
"next_sales" => Form.new(nil, next_collection_start_year, SALES_SECTIONS, "sales"),
}
end
@ -52,10 +52,10 @@ class FormHandler
end
if forms["previous_lettings"].blank? && current_collection_start_year >= 2022
forms["previous_lettings"] = Form.new(nil, current_collection_start_year - 1, LETTINGS_SECTIONS, "lettings")
forms["previous_lettings"] = Form.new(nil, previous_collection_start_year, LETTINGS_SECTIONS, "lettings")
end
forms["current_lettings"] = Form.new(nil, current_collection_start_year, LETTINGS_SECTIONS, "lettings") if forms["current_lettings"].blank?
forms["next_lettings"] = Form.new(nil, current_collection_start_year + 1, LETTINGS_SECTIONS, "lettings") if forms["next_lettings"].blank?
forms["next_lettings"] = Form.new(nil, next_collection_start_year, LETTINGS_SECTIONS, "lettings") if forms["next_lettings"].blank?
forms
end

6
app/models/location.rb

@ -366,6 +366,12 @@ class Location < ApplicationRecord
enum type_of_unit: TYPE_OF_UNIT
def self.find_by_id_on_mulitple_fields(id)
return if id.nil?
where(id:).or(where(old_visible_id: id)).first
end
def postcode=(postcode)
if postcode
super UKPostcode.parse(postcode).to_s

4
app/models/log.rb

@ -147,4 +147,8 @@ private
self[is_inferred_key] = false
self[postcode_key] = nil
end
def format_as_currency(num_string)
ActionController::Base.helpers.number_to_currency(num_string, unit: "£")
end
end

27
app/models/organisation.rb

@ -17,8 +17,21 @@ class Organisation < ApplicationRecord
has_many :managing_agent_relationships, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship"
has_many :managing_agents, through: :managing_agent_relationships, source: :child_organisation
def affiliated_stock_owners
ids = []
if holds_own_stock? && persisted?
ids << id
end
ids.concat(stock_owners.pluck(:id))
Organisation.where(id: ids)
end
scope :search_by_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
scope :search_by, ->(param) { search_by_name(param) }
has_paper_trail
auto_strip_attributes :name
@ -35,6 +48,20 @@ class Organisation < ApplicationRecord
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") }
validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_missing") }
def self.find_by_id_on_mulitple_fields(id)
return if id.nil?
if id.start_with?("ORG")
where(id: id[3..]).first
else
where(old_visible_id: id).first
end
end
def can_be_managed_by?(organisation:)
organisation == self || managing_agents.include?(organisation)
end
def lettings_logs
LettingsLog.filter_by_organisation(self)
end

18
app/models/sales_log.rb

@ -124,6 +124,10 @@ class SalesLog < Log
la && LONDON_BOROUGHS.include?(la)
end
def property_not_in_london?
!london_property?
end
def income1_used_for_mortgage?
inc1mort == 1
end
@ -256,4 +260,18 @@ class SalesLog < Log
def purchase_price_soft_max
LaSaleRange.find_by(start_year: collection_start_year, la:, bedrooms: beds).soft_max
end
def income_soft_min_for_ecstat(ecstat_field)
economic_status_code = public_send(ecstat_field)
return unless ALLOWED_INCOME_RANGES_SALES
soft_min = ALLOWED_INCOME_RANGES_SALES[economic_status_code]&.soft_min
format_as_currency(soft_min)
end
def field_formatted_as_currency(field_name)
field_value = public_send(field_name)
format_as_currency(field_value)
end
end

10
app/models/scheme.rb

@ -110,6 +110,16 @@ class Scheme < ApplicationRecord
enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true
def self.find_by_id_on_mulitple_fields(id)
return if id.nil?
if id.start_with?("S")
where(id: id[1..]).first
else
where(old_visible_id: id).first
end
end
def id_to_display
"S#{id}"
end

63
app/models/validations/sales/financial_validations.rb

@ -3,20 +3,36 @@ module Validations::Sales::FinancialValidations
# or 'validate_' to run on submit as well
def validate_income1(record)
if record.ecstat1 && record.income1 && record.la && record.ownershipsch == 1
if record.london_property?
record.errors.add :income1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000
record.errors.add :ecstat1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000
record.errors.add :ownershipsch, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000
record.errors.add :la, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000
record.errors.add :postcode_full, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000
elsif record.income1 > 80_000
record.errors.add :income1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)
record.errors.add :ecstat1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)
record.errors.add :ownershipsch, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000)
record.errors.add :la, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) if record.income1 > 80_000
record.errors.add :postcode_full, I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000) if record.income1 > 80_000
end
return unless record.income1 && record.la && record.shared_ownership_scheme?
relevant_fields = %i[income1 ownershipsch la postcode_full]
if record.london_property? && record.income1 > 90_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_london") }
elsif record.property_not_in_london? && record.income1 > 80_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_outside_london") }
end
end
def validate_income2(record)
return unless record.income2 && record.la && record.shared_ownership_scheme?
relevant_fields = %i[income2 ownershipsch la postcode_full]
if record.london_property? && record.income2 > 90_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_london") }
elsif record.property_not_in_london? && record.income2 > 80_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_outside_london") }
end
end
def validate_combined_income(record)
return unless record.income1 && record.income2 && record.la && record.shared_ownership_scheme?
combined_income = record.income1 + record.income2
relevant_fields = %i[income1 income2 ownershipsch la postcode_full]
if record.london_property? && combined_income > 90_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.combined_over_hard_max_for_london") }
elsif record.property_not_in_london? && combined_income > 80_000
relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london") }
end
end
@ -36,6 +52,15 @@ module Validations::Sales::FinancialValidations
end
end
def validate_child_income(record)
return unless record.income2 && record.ecstat2
if record.income2.positive? && is_economic_status_child?(record.ecstat2)
record.errors.add :ecstat2, I18n.t("validations.financial.income.child_has_income")
record.errors.add :income2, I18n.t("validations.financial.income.child_has_income")
end
end
def validate_percentage_owned_not_too_much_if_older_person(record)
return unless record.old_persons_shared_ownership? && record.stairowned
@ -44,4 +69,14 @@ module Validations::Sales::FinancialValidations
record.errors.add :type, I18n.t("validations.financial.staircasing.older_person_percentage_owned_maximum_75")
end
end
private
def is_relationship_child?(relationship)
relationship == "C"
end
def is_economic_status_child?(economic_status)
economic_status == 9
end
end

38
app/models/validations/sales/setup_validations.rb

@ -1,11 +1,45 @@
module Validations::Sales::SetupValidations
include Validations::SharedValidations
include CollectionTimeHelper
def validate_saledate(record)
return unless record.saledate && date_valid?("saledate", record)
unless record.saledate.between?(Time.zone.local(2022, 4, 1), Time.zone.local(2023, 3, 31)) || !FeatureToggle.saledate_collection_window_validation_enabled?
record.errors.add :saledate, I18n.t("validations.setup.saledate.financial_year")
unless record.saledate.between?(active_collection_start_date, current_collection_end_date) || !FeatureToggle.saledate_collection_window_validation_enabled?
record.errors.add :saledate, validation_error_message
end
end
private
def active_collection_start_date
if FormHandler.instance.sales_in_crossover_period?
previous_collection_start_date
else
current_collection_start_date
end
end
def validation_error_message
current_end_year_long = current_collection_end_date.strftime("#{current_collection_end_date.day.ordinalize} %B %Y")
if FormHandler.instance.sales_in_crossover_period?
I18n.t(
"validations.setup.saledate.previous_and_current_financial_year",
previous_start_year_short: previous_collection_start_date.strftime("%y"),
previous_end_year_short: previous_collection_end_date.strftime("%y"),
previous_start_year_long: previous_collection_start_date.strftime("#{previous_collection_start_date.day.ordinalize} %B %Y"),
current_end_year_short: current_collection_end_date.strftime("%y"),
current_end_year_long:,
)
else
I18n.t(
"validations.setup.saledate.current_financial_year",
current_start_year_short: current_collection_start_date.strftime("%y"),
current_end_year_short: current_collection_end_date.strftime("%y"),
current_start_year_long: current_collection_start_date.strftime("#{current_collection_start_date.day.ordinalize} %B %Y"),
current_end_year_long:,
)
end
end
end

12
app/models/validations/sales/soft_validations.rb

@ -1,5 +1,5 @@
module Validations::Sales::SoftValidations
ALLOWED_INCOME_RANGES = {
ALLOWED_INCOME_RANGES_SALES = {
1 => OpenStruct.new(soft_min: 5000),
2 => OpenStruct.new(soft_min: 1500),
3 => OpenStruct.new(soft_min: 1000),
@ -8,9 +8,15 @@ module Validations::Sales::SoftValidations
}.freeze
def income1_under_soft_min?
return false unless ecstat1 && income1 && ALLOWED_INCOME_RANGES[ecstat1]
return false unless ecstat1 && income1 && ALLOWED_INCOME_RANGES_SALES[ecstat1]
income1 < ALLOWED_INCOME_RANGES[ecstat1][:soft_min]
income1 < ALLOWED_INCOME_RANGES_SALES[ecstat1][:soft_min]
end
def income2_under_soft_min?
return false unless ecstat2 && income2 && ALLOWED_INCOME_RANGES_SALES[ecstat2]
income2 < ALLOWED_INCOME_RANGES_SALES[ecstat2][:soft_min]
end
def staircase_bought_above_fifty?

122
app/services/bulk_upload/lettings/row_parser.rb

@ -3,11 +3,12 @@ class BulkUpload::Lettings::RowParser
include ActiveModel::Attributes
attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
attribute :field_1, :integer
attribute :field_2
attribute :field_3
attribute :field_4, :integer
attribute :field_4, :string
attribute :field_5, :integer
attribute :field_6
attribute :field_7, :string
@ -114,9 +115,9 @@ class BulkUpload::Lettings::RowParser
attribute :field_108, :string
attribute :field_109, :string
attribute :field_110
attribute :field_111, :integer
attribute :field_111, :string
attribute :field_112, :string
attribute :field_113, :integer
attribute :field_113, :string
attribute :field_114, :integer
attribute :field_115
attribute :field_116, :integer
@ -142,6 +143,7 @@ class BulkUpload::Lettings::RowParser
validates :field_1, presence: { message: I18n.t("validations.not_answered", question: "letting type") },
inclusion: { in: (1..12).to_a, message: I18n.t("validations.invalid_option", question: "letting type") }
validates :field_4, presence: { if: proc { [2, 4, 6, 8, 10, 12].include?(field_1) } }
validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") }
validate :validate_data_types
validate :validate_nulls
@ -155,6 +157,19 @@ class BulkUpload::Lettings::RowParser
validate :validate_dont_know_disabled_needs_conjunction
validate :validate_no_and_dont_know_disabled_needs_conjunction
validate :validate_owning_org_permitted
validate :validate_owning_org_owns_stock
validate :validate_owning_org_exists
validate :validate_managing_org_related
validate :validate_managing_org_exists
validate :validate_scheme_related
validate :validate_scheme_exists
validate :validate_location_related
validate :validate_location_exists
def valid?
errors.clear
@ -173,15 +188,99 @@ class BulkUpload::Lettings::RowParser
end
def blank_row?
attribute_set.to_hash.reject { |k, _| %w[bulk_upload].include?(k) }.values.compact.empty?
attribute_set.to_hash.reject { |k, _| %w[bulk_upload block_log_creation].include?(k) }.values.compact.empty?
end
def log
@log ||= LettingsLog.new(attributes_for_log)
end
def block_log_creation!
self.block_log_creation = true
end
def block_log_creation?
block_log_creation
end
private
def validate_location_related
return if scheme.blank? || location.blank?
unless location.scheme == scheme
block_log_creation!
errors.add(:field_5, "Scheme code must relate to a location that is owned by owning organisation or managing organisation")
end
end
def location
return if scheme.nil?
@location ||= scheme.locations.find_by_id_on_mulitple_fields(field_5)
end
def validate_location_exists
if scheme && field_5.present? && location.nil?
errors.add(:field_5, "Location could be found with provided scheme code")
end
end
def validate_scheme_related
return unless field_4.present? && scheme.present?
owned_by_owning_org = owning_organisation && scheme.owning_organisation == owning_organisation
owned_by_managing_org = managing_organisation && scheme.owning_organisation == managing_organisation
unless owned_by_owning_org || owned_by_managing_org
block_log_creation!
errors.add(:field_4, "This management group code does not belong to your organisation, or any of your stock owners / managing agents")
end
end
def validate_scheme_exists
if field_4.present? && scheme.nil?
errors.add(:field_4, "The management group code is not correct")
end
end
def validate_managing_org_related
if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation)
block_log_creation!
errors.add(:field_113, "This managing organisation does not have a relationship with the owning organisation")
end
end
def validate_managing_org_exists
if managing_organisation.nil?
errors.delete(:field_113)
errors.add(:field_113, "The managing organisation code is incorrect")
end
end
def validate_owning_org_owns_stock
if owning_organisation && !owning_organisation.holds_own_stock?
block_log_creation!
errors.delete(:field_111)
errors.add(:field_111, "The owning organisation code provided is for an organisation that does not own stock")
end
end
def validate_owning_org_exists
if owning_organisation.nil?
errors.delete(:field_111)
errors.add(:field_111, "The owning organisation code is incorrect")
end
end
def validate_owning_org_permitted
if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation)
block_log_creation!
errors.delete(:field_111)
errors.add(:field_111, "You do not have permission to add logs for this owning organisation")
end
end
def validate_no_and_dont_know_disabled_needs_conjunction
if field_59 == 1 && field_60 == 1
errors.add(:field_59, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction"))
@ -447,6 +546,8 @@ private
def startdate
Date.new(field_98 + 2000, field_97, field_96) if field_98.present? && field_97.present? && field_96.present?
rescue Date::Error
Date.new
end
def renttype
@ -483,15 +584,19 @@ private
end
def owning_organisation
Organisation.find_by(old_visible_id: field_111)
Organisation.find_by_id_on_mulitple_fields(field_111)
end
def owning_organisation_id
owning_organisation&.id
end
def managing_organisation
Organisation.find_by_id_on_mulitple_fields(field_113)
end
def managing_organisation_id
Organisation.find_by(old_visible_id: field_113)&.id
managing_organisation&.id
end
def attributes_for_log
@ -506,6 +611,7 @@ private
attributes["managing_organisation_id"] = managing_organisation_id
attributes["renewal"] = renewal
attributes["scheme"] = scheme
attributes["location"] = location
attributes["created_by"] = bulk_upload.user
attributes["needstype"] = bulk_upload.needstype
attributes["rent_type"] = rent_type
@ -670,6 +776,8 @@ private
case rsnvac
when 15, 16, 17
1
else
0
end
end
@ -883,6 +991,6 @@ private
end
def scheme
@scheme ||= Scheme.find_by(old_visible_id: field_4)
@scheme ||= Scheme.find_by_id_on_mulitple_fields(field_4)
end
end

1
app/services/bulk_upload/lettings/validator.rb

@ -176,6 +176,7 @@ class BulkUpload::Lettings::Validator
def create_logs?
return false if any_setup_sections_incomplete?
return false if over_column_error_threshold?
return false if row_parsers.any?(&:block_log_creation?)
row_parsers.all? { |row_parser| row_parser.log.valid? }
end

45
app/views/content/privacy_notice.md

@ -1,46 +1,47 @@
## How are we using your information?
## How do we use your information?
If your household has entered a new social housing tenancy, social housing providers will share your personal information with the Department for Levelling Up, Housing & Communities (DLUHC) for research and statistical purposes.
If your household enters a new social housing tenancy or purchases a social housing property, social housing providers will share your personal information with the Department for Levelling Up, Housing & Communities (DLUHC) for research and statistical purposes only.
## How is this information provided?
## How do we get this information?
The information is provided via ‘<%= t('service_name') %>’, a service funded and managed by DLUHC. It collects information on the tenants or buyers, tenancy or sale, and the dwelling itself. Some of this information is personal and sensitive, so DLUHC is responsible for ensuring that all data is processed in line with data protection legislation.
The information is provided via ‘<%= t('service_name') %>’, a service funded and managed by DLUHC. It collects information on the tenants or residents, tenancy or sale, and the dwelling itself. Some of this data is personal and sensitive, so DLUHC is responsible for ensuring it’s processed in line with data protection legislation.
## Why are we sharing this information?
Information collected using this service is shared with other government departments and agencies. Data is shared with the Greater London Authority and the Regulator of Social Housing. Data providers can also access data for their organisations via the online service. Data is only shared for research and statistical purposes.
## Why do we share this information?
Information collected via CORE is shared with other government departments and agencies. It’s shared with the Greater London Authority and the Regulator of Social Housing. Data providers can also access data for their organisations via CORE. Data is only shared for research and statistical purposes.
## How does this affect you?
It will not affect your benefits, services or any treatments you receive. The information shared is anonymous and handled in accordance with the law. We are collecting and sharing your information to help us better understand the social housing market and inform social housing policy.
Information sharing will not affect your benefits, services or any treatments you receive. It’s anonymous and handled in accordance with the law. We collect and share your information to help us better understand the social housing market and inform social housing policy.
## If you want to know more
## To find out more…
Social housing lettings and sales data is collected on behalf of DLUHC for research and statistical purposes only. Data providers do not require the consent of tenants to provide the information, but tenants have the right to know how and for what purpose data is being collected, held and used.
Social housing lettings and sales data is collected on DLUHC’s behalf. Data providers do not require the tenant or buyer’s consent to provide this information, but tenants and buyers have the right to know how and for what purpose data is being collected, held and used.
The processing must have a lawful basis. In this case the processing is necessary for the performance of a task carried out in the public interest to meet a function of the Crown, a Minister of the Crown, or a government department.
Data processing must have a lawful basis. In this case it’s necessary for a task carried out in the public interest meeting a function of the Crown, a Minister of the Crown, or government department.
You have the right to object and you have the right to obtain confirmation that your data is being processed, and to access your personal data. You also have the right to have any incorrect personal data corrected.
You have the right to object, and obtain confirmation that your data is being processed, as well as access your personal data, and have any incorrect personal data corrected.
The information collected via this service relates to your tenancy, the dwelling you are living in or buying, and your household. Some of the information may have been provided by you as a tenant when signing the new tenancy or buying your property. Other information has been gathered from the housing management systems of social housing providers.
Information collected via CORE relates to your tenancy, the dwelling you are living in or buying, and your household. Some information may have been provided by you (as a tenant or buyer) when signing the new tenancy or buying your property. Other information has been gathered from the housing management systems of social housing providers.
Data collected will be held for as long as necessary for research and statistical purposes. When no longer needed, data will be deleted in a safe manner. We are aware that some of the data collected is particularly sensitive. For example:
Collected data will be held for as long as necessary for research and statistical purposes. When no longer needed, data will be deleted in a safe manner. We’re aware some collected data is particularly sensitive. For example:
* ethnic group
* if previous tenure is a hospital or prison or approved probation hostel support
* if previous tenure is a hospital, prison or approved probation hostel support
* if household left last settled home because discharged from prison, a long stay hospital or other institution
* if source of referral is probation or prison, youth offending team, community mental health team or health service
* if referral source is probation or prison, youth offending or community mental health team, or health service
All the information collected via this service is treated in accordance with data protection requirements and guidelines.
DLUHC publishes data annually, in aggregate form, as part of a report and complementary tables.
Data is published by DLUHC in aggregate form on an annual basis as part of a report and complementary tables.
* For annual lettings data, visit: [www.gov.uk/government/collections/rents-lettings-and-tenancies](www.gov.uk/government/collections/rents-lettings-and-tenancies)
You can visit <www.gov.uk/government/collections/rents-lettings-and-tenancies> to access the annual publications on lettings. Or visit <gov.uk/government/collections/social-housing-sales-including-right-to-buy-and-transfers> to view the publications on sales.
* For annual sales data, visit: [www.gov.uk/government/collections/social-housing-sales-including-right-to-buy-and-transfers](www.gov.uk/government/collections/social-housing-sales-including-right-to-buy-and-transfers)
The detail level data is anonymised and protected to minimise the risk of identification and held with the UK Data Archive for research purposes.
Detail-level data is anonymised and protected, minimising identification risk. It's held with the UK Data Archive.
## Making a complaint
## Complaints
If you are unhappy with any aspect of this privacy notice, or how your personal information is being processed, contact the Department Data Protection Officer at: <dataprotection@communities.gsi.gov.uk>
If you’re unhappy with any privacy notice aspect, or how we process your information, contact the Department Data Protection Officer: <dataprotection@communities.gsi.gov.uk>
If you are still not happy, you have the right to lodge a complaint with the Information Commissioner’s Office (ICO) at [ico.org.uk/concern](https://ico.org.uk/concern).
You also have the right to complain to the Information Commissioner’s Office (ICO): [www.ico.org.uk/concern](www.ico.org.uk/concern)

2
config/forms/2022_2023.json

@ -1,7 +1,7 @@
{
"form_type": "lettings",
"start_date": "2022-04-01T00:00:00.000+01:00",
"end_date": "2023-07-01T00:00:00.000+01:00",
"end_date": "2023-07-09T00:00:00.000+01:00",
"unresolved_log_redirect_page_id": "tenancy_start_date",
"sections": {
"tenancy_and_property": {

9
config/initializers/feature_toggle.rb

@ -1,9 +1,14 @@
class FeatureToggle
def self.startdate_two_week_validation_enabled?
# Disable collection window checks on preview apps to allow for testing of future forms
def self.saledate_collection_window_validation_enabled?
Rails.env.production? || Rails.env.test? || Rails.env.staging?
end
def self.saledate_collection_window_validation_enabled?
def self.startdate_collection_window_validation_enabled?
Rails.env.production? || Rails.env.test? || Rails.env.staging?
end
def self.startdate_two_week_validation_enabled?
Rails.env.production? || Rails.env.test? || Rails.env.staging?
end

19
config/locales/en.yml

@ -152,12 +152,17 @@ en:
intermediate_rent_product_name:
blank: "Enter name of other intermediate rent product"
saledate:
financial_year: "Date must be from 22/23 financial year, which is between 1st April 2022 and 31st March 2023"
current_financial_year:
Enter a date within the %{current_start_year_short}/%{current_end_year_short} financial year, which is between %{current_start_year_long} and %{current_end_year_long}
previous_and_current_financial_year:
"Enter a date within the %{previous_start_year_short}/%{previous_end_year_short} or %{previous_end_year_short}/%{current_end_year_short} financial years, which is between %{previous_start_year_long} and %{current_end_year_long}"
startdate:
later_than_14_days_after: "The tenancy start date must not be later than 14 days from today’s date"
before_scheme_end_date: "The tenancy start date must be before the end date for this supported housing scheme"
after_void_date: "Enter a tenancy start date that is after the void date"
after_major_repair_date: "Enter a tenancy start date that is after the major repair date"
year_not_two_digits: Tenancy start year must be 2 digits
location:
deactivated: "The location %{postcode} was deactivated on %{date} and was not available on the day you entered."
reactivating_soon: "The location %{postcode} is not available until %{date}. Select another location or edit the tenancy start date"
@ -224,8 +229,12 @@ en:
under_hard_min: "Net income cannot be less than £%{hard_min} per week given the tenant’s working situation"
freq_missing: "Select how often the household receives income"
earnings_missing: "Enter how much income the household has in total"
income1:
over_hard_max: "Income must be lower than £%{hard_max}"
income:
over_hard_max_for_london: "Income must not exceed £90,000 for properties within London local authorities"
over_hard_max_for_outside_london: "Income must not exceed £80,000 for properties outside London local authorities"
combined_over_hard_max_for_london: "Combined income must not exceed £90,000 for properties within London local authorities"
combined_over_hard_max_for_outside_london: "Combined income must not exceed £80,000 for properties outside London local authorities"
child_has_income: "Child's income must be £0"
negative_currency: "Enter an amount above 0"
rent:
less_than_shortfall: "Enter an amount that is more than the shortfall in basic rent"
@ -468,6 +477,8 @@ en:
message: "Net income is lower than expected based on the lead tenant’s working situation. Are you sure this is correct?"
in_soft_max_range:
message: "Net income is higher than expected based on the lead tenant’s working situation. Are you sure this is correct?"
income:
under_soft_min_for_economic_status: "You said income was %{income}, which is below this working situation's minimum (%{minimum})"
rent:
outside_range_title: "You told us the rent is %{brent}"
min_hint_text: "The minimum rent expected for this type of property in this local authority is £%{soft_min_for_period}."
@ -560,4 +571,4 @@ en:
one_argument: "This is based on the tenant’s work situation: %{ecstat1}"
title_text:
no_argument: "Some test text"
one_argument: "You said this: %{ecstat1}"
one_argument: "You said this: %{argument}"

5
db/migrate/20230105103134_add_income2_value_check.rb

@ -0,0 +1,5 @@
class AddIncome2ValueCheck < ActiveRecord::Migration[7.0]
def change
add_column :sales_logs, :income2_value_check, :integer
end
end

5
db/migrate/20230213140932_add_proplen_asked_to_sales_log.rb

@ -0,0 +1,5 @@
class AddProplenAskedToSalesLog < ActiveRecord::Migration[7.0]
def change
add_column :sales_logs, :proplen_asked, :integer
end
end

4
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: 2023_02_10_143120) do
ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -490,6 +490,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do
t.integer "prevten"
t.integer "mortgageused"
t.integer "wchair"
t.integer "income2_value_check"
t.integer "armedforcesspouse"
t.datetime "hodate", precision: nil
t.integer "hoday"
@ -528,6 +529,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do
t.integer "staircasesale"
t.integer "ethnic_group2"
t.integer "ethnicbuy2"
t.integer "proplen_asked"
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"
t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id"

5
spec/factories/location.rb

@ -10,6 +10,7 @@ FactoryBot.define do
startdate { Time.zone.local(2022, 4, 1) }
confirmed { true }
scheme
trait :export do
postcode { "SW1A 2AA" }
name { "Downing Street" }
@ -19,5 +20,9 @@ FactoryBot.define do
scheme { FactoryBot.create(:scheme, :export) }
old_visible_id { "111" }
end
trait :with_old_visible_id do
old_visible_id { rand(9_999_999).to_s }
end
end
end

4
spec/factories/organisation.rb

@ -17,6 +17,10 @@ FactoryBot.define do
trait :prp do
provider_type { "PRP" }
end
trait :does_not_own_stock do
holds_own_stock { false }
end
end
factory :organisation_rent_period do

4
spec/factories/scheme.rb

@ -21,5 +21,9 @@ FactoryBot.define do
primary_client_group { "G" }
secondary_client_group { "M" }
end
trait :with_old_visible_id do
old_visible_id { rand(9_999_999) }
end
end
end

53
spec/helpers/interruption_screen_helper_spec.rb

@ -13,6 +13,7 @@ RSpec.describe InterruptionScreenHelper do
earnings: 750,
incfreq: 1,
created_by: user,
sex1: "F",
)
end
@ -94,6 +95,54 @@ RSpec.describe InterruptionScreenHelper do
.to eq("")
end
end
context "when an argument is given not for a label" do
translation = "test.title_text.one_argument"
it "returns the correct text" do
informative_text_hash = {
"translation" => translation,
"arguments" => [
{
"key" => "earnings",
"i18n_template" => "argument",
},
],
}
expect(display_informative_text(informative_text_hash, lettings_log)).to eq(I18n.t(translation, argument: lettings_log.earnings))
end
end
context "when and argument is given with a key and arguments for the key" do
it "makes the correct method call" do
informative_text_hash = {
"arguments" => [
{
"key" => "retirement_age_for_person",
"arguments_for_key" => 1,
"i18n_template" => "argument",
},
],
}
allow(lettings_log).to receive(:retirement_age_for_person)
display_informative_text(informative_text_hash, lettings_log)
expect(lettings_log).to have_received(:retirement_age_for_person).with(1)
end
it "returns the correct text" do
translation = "test.title_text.one_argument"
informative_text_hash = {
"translation" => translation,
"arguments" => [
{
"key" => "retirement_age_for_person",
"arguments_for_key" => 1,
"i18n_template" => "argument",
},
],
}
expect(display_informative_text(informative_text_hash, lettings_log)).to eq(I18n.t(translation, argument: lettings_log.retirement_age_for_person(1)))
end
end
end
describe "display_title_text" do
@ -113,12 +162,12 @@ RSpec.describe InterruptionScreenHelper do
{
"key" => "ecstat1",
"label" => true,
"i18n_template" => "ecstat1",
"i18n_template" => "argument",
},
],
}
expect(display_title_text(title_text, lettings_log))
.to eq(I18n.t("test.title_text.one_argument", ecstat1: lettings_log.form.get_question("ecstat1", lettings_log).answer_label(lettings_log).downcase))
.to eq(I18n.t("test.title_text.one_argument", argument: lettings_log.form.get_question("ecstat1", lettings_log).answer_label(lettings_log).downcase))
end
end

147
spec/helpers/tasklist_helper_spec.rb

@ -1,100 +1,55 @@
require "rails_helper"
RSpec.describe TasklistHelper do
describe "with lettings" do
let(:empty_lettings_log) { FactoryBot.create(:lettings_log) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, needstype: 1) }
let(:fake_2021_2022_form) { Form.new("spec/fixtures/forms/2021_2022.json") }
let(:now) { Time.utc(2022, 6, 1) }
context "with 2021 2022 form" do
before do
allow(FormHandler.instance).to receive(:current_lettings_form).and_return(fake_2021_2022_form)
end
around do |example|
Timecop.freeze(now) do
Singleton.__init__(FormHandler)
example.run
end
Timecop.return
Singleton.__init__(FormHandler)
end
describe "get next incomplete section" do
it "returns the first subsection name if it is not completed" do
expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics")
end
describe "with lettings" do
let(:empty_lettings_log) { create(:lettings_log) }
let(:lettings_log) { build(:lettings_log, :in_progress, needstype: 1) }
it "returns the first subsection name if it is partially completed" do
lettings_log["tenancycode"] = 123
expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics")
end
describe "get next incomplete section" do
it "returns the first subsection name if it is not completed" do
expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics")
end
describe "get sections count" do
it "returns the total of sections if no status is given" do
expect(get_subsections_count(empty_lettings_log)).to eq(8)
end
it "returns 0 sections for completed sections if no sections are completed" do
expect(get_subsections_count(empty_lettings_log, :completed)).to eq(0)
end
it "returns the number of not started sections" do
expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(8)
end
it "returns the number of sections in progress" do
expect(get_subsections_count(lettings_log, :in_progress)).to eq(3)
end
it "returns 0 for invalid state" do
expect(get_subsections_count(lettings_log, :fake)).to eq(0)
end
it "returns the first subsection name if it is partially completed" do
lettings_log["tenancycode"] = 123
expect(get_next_incomplete_section(lettings_log).id).to eq("household_characteristics")
end
end
describe "get_next_page_or_check_answers" do
let(:subsection) { lettings_log.form.get_subsection("household_characteristics") }
let(:user) { FactoryBot.build(:user) }
it "returns the check answers page path if the section has been started already" do
expect(next_page_or_check_answers(subsection, lettings_log, user)).to match(/check-answers/)
end
it "returns the first question page path for the section if it has not been started yet" do
expect(next_page_or_check_answers(subsection, empty_lettings_log, user)).to match(/tenant-code-test/)
end
it "when first question being not routed to returns the next routed question link" do
empty_lettings_log.housingneeds_a = "No"
expect(next_page_or_check_answers(subsection, empty_lettings_log, user)).to match(/person-1-gender/)
end
describe "get sections count" do
it "returns the total of sections if no status is given" do
expect(get_subsections_count(empty_lettings_log)).to eq(1)
end
describe "subsection link" do
let(:subsection) { lettings_log.form.get_subsection("household_characteristics") }
let(:user) { FactoryBot.build(:user) }
context "with a subsection that's enabled" do
it "returns the subsection link url" do
expect(subsection_link(subsection, lettings_log, user)).to match(/household-characteristics/)
end
end
it "returns 0 sections for completed sections if no sections are completed" do
expect(get_subsections_count(empty_lettings_log, :completed)).to eq(0)
end
context "with a subsection that cannot be started yet" do
before do
allow(subsection).to receive(:status).with(lettings_log).and_return(:cannot_start_yet)
end
it "returns the number of not started sections" do
expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(1)
end
it "returns the label instead of a link" do
expect(subsection_link(subsection, lettings_log, user)).to match(subsection.label)
end
end
it "returns the number of sections in progress" do
expect(get_subsections_count(lettings_log, :in_progress)).to eq(2)
end
end
end
describe "#review_log_text" do
around do |example|
Timecop.freeze(now) do
Singleton.__init__(FormHandler)
example.run
it "returns 0 for invalid state" do
expect(get_subsections_count(lettings_log, :fake)).to eq(0)
end
Singleton.__init__(FormHandler)
end
context "with lettings log" do
describe "review_log_text" do
context "when collection_period_open? == true" do
context "with 2023 deadline" do
let(:now) { Time.utc(2022, 6, 1) }
@ -113,7 +68,7 @@ RSpec.describe TasklistHelper do
it "returns relevant text" do
expect(review_log_text(lettings_log)).to eq(
"You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(lettings_log)} until 1 July 2024.".html_safe,
"You can #{govuk_link_to 'review and make changes to this log', review_lettings_log_path(lettings_log)} until 9 July 2024.".html_safe,
)
end
end
@ -129,6 +84,30 @@ RSpec.describe TasklistHelper do
end
end
describe "subsection link" do
let(:lettings_log) { create(:lettings_log, :completed) }
let(:subsection) { lettings_log.form.get_subsection("household_characteristics") }
let(:user) { build(:user) }
context "with a subsection that's enabled" do
it "returns the subsection link url" do
expect(subsection_link(subsection, lettings_log, user)).to match(/household-characteristics/)
end
end
context "with a subsection that cannot be started yet" do
before do
allow(subsection).to receive(:status).with(lettings_log).and_return(:cannot_start_yet)
end
it "returns the label instead of a link" do
expect(subsection_link(subsection, lettings_log, user)).to match(subsection.label)
end
end
end
end
describe "#review_log_text" do
context "with sales log" do
context "when collection_period_open? == true" do
let(:now) { Time.utc(2022, 6, 1) }
@ -136,17 +115,19 @@ RSpec.describe TasklistHelper do
it "returns relevant text" do
expect(review_log_text(sales_log)).to eq(
"You can #{govuk_link_to 'review and make changes to this log', review_sales_log_path(id: sales_log, sales_log: true)} until 1 July 2023.".html_safe,
"You can #{govuk_link_to 'review and make changes to this log', review_sales_log_path(id: sales_log, sales_log: true)} until 7 July 2023.".html_safe,
)
end
end
context "when collection_period_open? == false" do
let(:now) { Time.utc(2023, 7, 8) }
let(:sales_log) { create(:sales_log, :completed, saledate: Time.utc(2023, 2, 8)) }
let(:now) { Time.utc(2022, 6, 1) }
let!(:sales_log) { create(:sales_log, :completed) }
it "returns relevant text" do
expect(review_log_text(sales_log)).to eq("This log is from the 2022/2023 collection window, which is now closed.")
Timecop.freeze(now + 1.year) do
expect(review_log_text(sales_log)).to eq("This log is from the 2021/2022 collection window, which is now closed.")
end
end
end
end

6
spec/models/form/lettings/pages/location_spec.rb

@ -6,6 +6,12 @@ RSpec.describe Form::Lettings::Pages::Location, type: :model do
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form) }
before do
allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1))
allow(subsection).to receive(:form).and_return(form)
end
it "has correct subsection" do
expect(page.subsection).to eq(subsection)

18
spec/models/form/lettings/questions/location_id_spec.rb

@ -6,6 +6,14 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:subsection) { instance_double(Form::Subsection) }
let(:form) { instance_double(Form) }
before do
allow(form).to receive(:start_date).and_return(Time.zone.local(2022, 4, 1))
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(form)
end
it "has correct page" do
expect(question.page).to eq(page)
@ -103,4 +111,14 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do
end
end
end
context "with collection year on or after 2023" do
before do
allow(form).to receive(:start_date).and_return(Time.zone.local(2023, 4, 1))
end
it "has the correct header" do
expect(question.header).to eq("Which location is this letting for?")
end
end
end

20
spec/models/form/sales/pages/living_before_purchase_spec.rb

@ -11,8 +11,24 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[proplen])
describe "questions" do
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
context "when 2022" do
let(:start_date) { Time.utc(2022, 2, 8) }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[proplen])
end
end
context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) }
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[proplen_asked proplen])
end
end
end
it "has the correct id" do

35
spec/models/form/sales/pages/purchase_price_spec.rb

@ -1,35 +0,0 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PurchasePrice, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { "purchase_price" }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[value])
end
it "has the correct id" do
expect(page.id).to eq("purchase_price")
end
it "has the correct header" do
expect(page.header).to be_nil
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has correct depends_on" do
expect(page.depends_on).to eq([
{ "ownershipsch" => 2, "rent_to_buy_full_ownership?" => false },
])
end
end

2
spec/models/form/sales/questions/buyer1_income_value_check_spec.rb

@ -16,7 +16,7 @@ RSpec.describe Form::Sales::Questions::Buyer1IncomeValueCheck, type: :model do
end
it "has the correct header" do
expect(question.header).to eq("Are you sure this income is correct?")
expect(question.header).to eq("Are you sure this is correct?")
end
it "has the correct check_answer_label" do

31
spec/models/form/sales/questions/living_before_purchase_spec.rb

@ -12,19 +12,19 @@ RSpec.describe Form::Sales::Questions::LivingBeforePurchase, type: :model do
end
it "has the correct id" do
expect(question.id).to eq("proplen")
expect(question.id).to eq("proplen_asked")
end
it "has the correct header" do
expect(question.header).to eq("How long did the buyer(s) live in the property before purchase?")
expect(question.header).to eq("Did the buyer live in the property before purchasing it?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Number of years living in the property before purchase")
expect(question.check_answer_label).to eq("Buyer lived in the property before purchasing")
end
it "has the correct type" do
expect(question.type).to eq("numeric")
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
@ -32,26 +32,11 @@ RSpec.describe Form::Sales::Questions::LivingBeforePurchase, type: :model do
end
it "has the correct hint" do
expect(question.hint_text).to eq("You should round this up to the nearest year. If the buyers haven't been living in the property, enter '0'")
expect(question.hint_text).to be_nil
end
it "has correct width" do
expect(question.width).to eq(5)
end
it "has correct step" do
expect(question.step).to eq(1)
end
it "has correct suffix" do
expect(question.suffix).to eq(" years")
end
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(80)
it "has the correct answer_options" do
expect(question.answer_options).to eq("0" => { "value" => "Yes" },
"1" => { "value" => "No" })
end
end

114
spec/models/form/sales/questions/living_before_purchase_years_spec.rb

@ -0,0 +1,114 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::LivingBeforePurchaseYears, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:page) { instance_double(Form::Page, subsection:) }
context "when 2022" do
let(:start_date) { Time.utc(2022, 2, 8) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("proplen")
end
it "has the correct header" do
expect(question.header).to eq("How long did the buyer(s) live in the property before purchase?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Number of years living in the property before purchase")
end
it "has the correct type" do
expect(question.type).to eq("numeric")
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct hint" do
expect(question.hint_text).to eq("You should round this up to the nearest year. If the buyers haven't been living in the property, enter '0'")
end
it "has correct width" do
expect(question.width).to eq(5)
end
it "has correct step" do
expect(question.step).to eq(1)
end
it "has correct suffix" do
expect(question.suffix).to eq(" years")
end
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(80)
end
end
context "when 2023" do
let(:start_date) { Time.utc(2023, 2, 8) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("proplen")
end
it "has the correct header" do
expect(question.header).to eq("How long did they live there?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Number of years living in the property before purchase")
end
it "has the correct type" do
expect(question.type).to eq("numeric")
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct hint" do
expect(question.hint_text).to eq("You should round up to the nearest year")
end
it "has correct width" do
expect(question.width).to eq(5)
end
it "has correct step" do
expect(question.step).to eq(1)
end
it "has correct suffix" do
expect(question.suffix).to eq(" years")
end
it "has correct min" do
expect(question.min).to eq(0)
end
it "has correct max" do
expect(question.max).to eq(80)
end
end
end

9
spec/models/form/sales/questions/previous_postcode_known_spec.rb

@ -47,4 +47,13 @@ RSpec.describe Form::Sales::Questions::PreviousPostcodeKnown, type: :model do
it "has the correct hint" do
expect(question.hint_text).to eq("This is also known as the household’s 'last settled home'")
end
it "has the correct hidden_in_check_answers" do
expect(question.hidden_in_check_answers).to eq({
"depends_on" => [
{ "ppcodenk" => 0 },
{ "ppcodenk" => 1 },
],
})
end
end

1
spec/models/form/sales/subsections/discounted_ownership_scheme_spec.rb

@ -19,7 +19,6 @@ RSpec.describe Form::Sales::Subsections::DiscountedOwnershipScheme, type: :model
extra_borrowing_price_value_check
about_price_not_rtb
grant_value_check
purchase_price_discounted_ownership
purchase_price_outright_ownership
discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount
mortgage_used_discounted_ownership

6
spec/models/form/sales/subsections/household_characteristics_spec.rb

@ -46,7 +46,8 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
buyer_2_gender_identity
gender_2_buyer_retirement_value_check
buyer_2_working_situation
working_situation_2_buyer_retirement_value_check
working_situation_2_retirement_value_check_joint_purchase
working_situation_buyer_2_income_value_check
buyer_2_live_in_property
number_of_others_in_property
person_2_known
@ -126,7 +127,8 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
buyer_2_ethnic_background_mixed
buyer_2_ethnic_background_white
buyer_2_working_situation
working_situation_2_buyer_retirement_value_check
working_situation_2_retirement_value_check_joint_purchase
working_situation_buyer_2_income_value_check
buyer_2_live_in_property
number_of_others_in_property
person_2_known

2
spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb

@ -27,6 +27,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
buyer_1_mortgage_value_check
buyer_2_income
buyer_2_income_mortgage_value_check
buyer_2_income_value_check
buyer_2_mortgage
buyer_2_mortgage_value_check
housing_benefits
@ -52,6 +53,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
buyer_1_mortgage_value_check
buyer_2_income
buyer_2_income_mortgage_value_check
buyer_2_income_value_check
buyer_2_mortgage
buyer_2_mortgage_value_check
housing_benefits

4
spec/models/form_handler_spec.rb

@ -54,14 +54,14 @@ RSpec.describe FormHandler do
it "is able to load a current sales form" do
form = form_handler.get_form("current_sales")
expect(form).to be_a(Form)
expect(form.pages.count).to eq(179)
expect(form.pages.count).to eq(180)
expect(form.name).to eq("2022_2023_sales")
end
it "is able to load a previous sales form" do
form = form_handler.get_form("previous_sales")
expect(form).to be_a(Form)
expect(form.pages.count).to eq(179)
expect(form.pages.count).to eq(180)
expect(form.name).to eq("2021_2022_sales")
end
end

2
spec/models/form_spec.rb

@ -223,7 +223,7 @@ RSpec.describe Form, type: :model do
expect(form.questions.count).to eq(16)
expect(form.questions.first.id).to eq("owning_organisation_id")
expect(form.start_date).to eq(Time.zone.parse("2022-04-01"))
expect(form.end_date).to eq(Time.zone.parse("2023-07-01"))
expect(form.end_date).to eq(Time.zone.parse("2023-07-07"))
expect(form.unresolved_log_redirect_page_id).to eq(nil)
end

23
spec/models/sales_log_spec.rb

@ -266,6 +266,7 @@ RSpec.describe SalesLog, type: :model do
relat4: "X",
relat5: "X",
relat6: "P",
income2: 0,
ecstat2: 9,
ecstat3: 7,
age1: 47,
@ -370,4 +371,26 @@ RSpec.describe SalesLog, type: :model do
expect(completed_sales_log.expected_shared_ownership_deposit_value).to eq(500)
end
end
describe "#field_formatted_as_currency" do
let(:completed_sales_log) { FactoryBot.create(:sales_log, :completed) }
it "returns small numbers correctly formatted as currency" do
completed_sales_log.update!(savings: 4)
expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£4.00")
end
it "returns quite large numbers correctly formatted as currency" do
completed_sales_log.update!(savings: 40_000)
expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£40,000.00")
end
it "returns very large numbers correctly formatted as currency" do
completed_sales_log.update!(savings: 400_000_000)
expect(completed_sales_log.field_formatted_as_currency("savings")).to eq("£400,000,000.00")
end
end
end

222
spec/models/validations/sales/financial_validations_spec.rb

@ -5,75 +5,110 @@ RSpec.describe Validations::Sales::FinancialValidations do
let(:validator_class) { Class.new { include Validations::Sales::FinancialValidations } }
describe "income validations" do
let(:record) { FactoryBot.create(:sales_log, ownershipsch: 1, la: "E08000035") }
context "with shared ownership" do
context "and non london borough" do
(0..8).each do |ecstat|
it "adds an error when buyer 1 income is over hard max for ecstat #{ecstat}" do
record.income1 = 85_000
record.ecstat1 = ecstat
financial_validator.validate_income1(record)
expect(record.errors["income1"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000))
expect(record.errors["ecstat1"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000))
expect(record.errors["ownershipsch"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000))
expect(record.errors["la"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000))
expect(record.errors["postcode_full"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 80_000))
end
end
it "validates that the income is within the expected range for the tenant’s employment status" do
record.income1 = 75_000
record.ecstat1 = 1
financial_validator.validate_income1(record)
expect(record.errors["income1"]).to be_empty
expect(record.errors["ecstat1"]).to be_empty
expect(record.errors["ownershipsch"]).to be_empty
expect(record.errors["la"]).to be_empty
expect(record.errors["postcode_full"]).to be_empty
end
end
context "and a london borough" do
before do
record.update!(la: "E09000030")
record.reload
end
(0..8).each do |ecstat|
it "adds an error when buyer 1 income is over hard max for ecstat #{ecstat}" do
record.income1 = 95_000
record.ecstat1 = ecstat
financial_validator.validate_income1(record)
expect(record.errors["income1"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000))
expect(record.errors["ecstat1"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000))
expect(record.errors["ownershipsch"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000))
expect(record.errors["la"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000))
expect(record.errors["postcode_full"])
.to include(match I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000))
end
end
it "validates that the income is within the expected range for the tenant’s employment status" do
record.income1 = 85_000
record.ecstat1 = 1
financial_validator.validate_income1(record)
expect(record.errors["income1"]).to be_empty
expect(record.errors["ecstat1"]).to be_empty
expect(record.errors["ownershipsch"]).to be_empty
expect(record.errors["la"]).to be_empty
expect(record.errors["postcode_full"]).to be_empty
end
describe "income validations for shared ownership" do
let(:record) { FactoryBot.create(:sales_log, ownershipsch: 1) }
context "when buying in a non london borough" do
before do
record.update!(la: "E08000035")
record.reload
end
it "adds errors if buyer 1 has income over 80,000" do
record.income1 = 85_000
financial_validator.validate_income1(record)
expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
end
it "adds errors if buyer 2 has income over 80,000" do
record.income2 = 85_000
financial_validator.validate_income2(record)
expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_outside_london"))
end
it "does not add errors if buyer 1 has income below 80_000" do
record.income1 = 75_000
financial_validator.validate_income1(record)
expect(record.errors).to be_empty
end
it "does not add errors if buyer 2 has income below 80_000" do
record.income2 = 75_000
financial_validator.validate_income2(record)
expect(record.errors).to be_empty
end
it "adds errors when combined income is over 80_000" do
record.income1 = 45_000
record.income2 = 40_000
financial_validator.validate_combined_income(record)
expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london"))
expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_outside_london"))
end
it "does not add errors when combined income is under 80_000" do
record.income1 = 35_000
record.income2 = 40_000
financial_validator.validate_combined_income(record)
expect(record.errors).to be_empty
end
end
context "when buying in a london borough" do
before do
record.update!(la: "E09000030")
record.reload
end
it "adds errors if buyer 1 has income over 90,000" do
record.income1 = 95_000
financial_validator.validate_income1(record)
expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
end
it "adds errors if buyer 2 has income over 90,000" do
record.income2 = 95_000
financial_validator.validate_income2(record)
expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["ownershipsch"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["la"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
expect(record.errors["postcode_full"]).to include(match I18n.t("validations.financial.income.over_hard_max_for_london"))
end
it "does not add errors if buyer 1 has income below 90_000" do
record.income1 = 75_000
financial_validator.validate_income1(record)
expect(record.errors).to be_empty
end
it "does not add errors if buyer 2 has income below 90_000" do
record.income2 = 75_000
financial_validator.validate_income2(record)
expect(record.errors).to be_empty
end
it "adds errors when combined income is over 90_000" do
record.income1 = 55_000
record.income2 = 40_000
financial_validator.validate_combined_income(record)
expect(record.errors["income1"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_london"))
expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.combined_over_hard_max_for_london"))
end
it "does not add errors when combined income is under 90_000" do
record.income1 = 35_000
record.income2 = 40_000
financial_validator.validate_combined_income(record)
expect(record.errors).to be_empty
end
end
end
@ -96,7 +131,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
it "does not add an error if the cash discount is in the expected range" do
record.cashdis = 10_000
financial_validator.validate_cash_discount(record)
expect(record.errors["cashdis"]).to be_empty
expect(record.errors).to be_empty
end
end
@ -107,16 +142,14 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.stairbought = 20
record.stairowned = 40
financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record)
expect(record.errors["stairbought"]).to be_empty
expect(record.errors["stairowned"]).to be_empty
expect(record.errors).to be_empty
end
it "does not add an error if the percentage bought is equal to the percentage owned" do
record.stairbought = 30
record.stairowned = 30
financial_validator.validate_percentage_bought_not_greater_than_percentage_owned(record)
expect(record.errors["stairbought"]).to be_empty
expect(record.errors["stairowned"]).to be_empty
expect(record.errors).to be_empty
end
it "adds an error to stairowned and not stairbought if the percentage bought is more than the percentage owned" do
@ -135,8 +168,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.type = 2
record.stairowned = 80
financial_validator.validate_percentage_owned_not_too_much_if_older_person(record)
expect(record.errors["stairowned"]).to be_empty
expect(record.errors["type"]).to be_empty
expect(record.errors).to be_empty
end
end
@ -145,8 +177,7 @@ RSpec.describe Validations::Sales::FinancialValidations do
record.type = 24
record.stairowned = 50
financial_validator.validate_percentage_owned_not_too_much_if_older_person(record)
expect(record.errors["stairowned"]).to be_empty
expect(record.errors["type"]).to be_empty
expect(record.errors).to be_empty
end
it "adds an error when percentage owned after staircasing transaction exceeds 75%" do
@ -158,4 +189,39 @@ RSpec.describe Validations::Sales::FinancialValidations do
end
end
end
describe "#validate_child_income" do
let(:record) { FactoryBot.create(:sales_log) }
context "when buyer 2 is not a child" do
before do
record.update!(ecstat2: rand(0..8))
record.reload
end
it "does not add an error if buyer 2 has an income" do
record.ecstat2 = rand(0..8)
record.income2 = 40_000
financial_validator.validate_child_income(record)
expect(record.errors).to be_empty
end
end
context "when buyer 2 is a child" do
it "does not add an error if buyer 2 has no income" do
record.ecstat2 = 9
record.income2 = 0
financial_validator.validate_child_income(record)
expect(record.errors).to be_empty
end
it "adds errors if buyer 2 has an income" do
record.ecstat2 = 9
record.income2 = 40_000
financial_validator.validate_child_income(record)
expect(record.errors["ecstat2"]).to include(match I18n.t("validations.financial.income.child_has_income"))
expect(record.errors["income2"]).to include(match I18n.t("validations.financial.income.child_has_income"))
end
end
end
end

97
spec/models/validations/sales/setup_validations_spec.rb

@ -6,43 +6,96 @@ RSpec.describe Validations::Sales::SetupValidations do
let(:validator_class) { Class.new { include Validations::Sales::SetupValidations } }
describe "#validate_saledate" do
context "when saledate is blank" do
let(:record) { FactoryBot.build(:sales_log, saledate: nil) }
context "with sales_in_crossover_period == false" do
context "when saledate is blank" do
let(:record) { build(:sales_log, saledate: nil) }
it "does not add an error" do
setup_validator.validate_saledate(record)
it "does not add an error" do
setup_validator.validate_saledate(record)
expect(record.errors).to be_empty
expect(record.errors).to be_empty
end
end
end
context "when saledate is in the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) }
context "when saledate is in the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) }
it "does not add an error" do
setup_validator.validate_saledate(record)
it "does not add an error" do
setup_validator.validate_saledate(record)
expect(record.errors).to be_empty
expect(record.errors).to be_empty
end
end
context "when saledate is before the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2020, 1, 1)) }
it "adds error" do
setup_validator.validate_saledate(record)
expect(record.errors[:saledate]).to include("Enter a date within the 22/23 financial year, which is between 1st April 2022 and 31st March 2023")
end
end
end
context "when saledate is before the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2022, 1, 1)) }
context "when saledate is after the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 4, 1)) }
it "adds error" do
setup_validator.validate_saledate(record)
it "adds error" do
setup_validator.validate_saledate(record)
expect(record.errors[:saledate]).to include(I18n.t("validations.setup.saledate.financial_year"))
expect(record.errors[:saledate]).to include("Enter a date within the 22/23 financial year, which is between 1st April 2022 and 31st March 2023")
end
end
end
context "when saledate is after the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 4, 1)) }
context "with sales_in_crossover_period == true" do
around do |example|
Timecop.freeze(Time.zone.local(2024, 5, 1)) do
Singleton.__init__(FormHandler)
example.run
end
Timecop.return
Singleton.__init__(FormHandler)
end
context "when saledate is blank" do
let(:record) { build(:sales_log, saledate: nil) }
it "does not add an error" do
setup_validator.validate_saledate(record)
expect(record.errors).to be_empty
end
end
context "when saledate is in the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2024, 1, 1)) }
it "does not add an error" do
setup_validator.validate_saledate(record)
expect(record.errors).to be_empty
end
end
context "when saledate is before the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2020, 5, 1)) }
it "adds error" do
setup_validator.validate_saledate(record)
expect(record.errors[:saledate]).to include("Enter a date within the 23/24 or 24/25 financial years, which is between 1st April 2023 and 31st March 2025")
end
end
context "when saledate is after the 22/23 financial year" do
let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 4, 1)) }
it "adds error" do
setup_validator.validate_saledate(record)
it "adds error" do
setup_validator.validate_saledate(record)
expect(record.errors[:saledate]).to include(I18n.t("validations.setup.saledate.financial_year"))
expect(record.errors[:saledate]).to include("Enter a date within the 23/24 or 24/25 financial years, which is between 1st April 2023 and 31st March 2025")
end
end
end
end

3
spec/rails_helper.rb

@ -6,6 +6,7 @@ require File.expand_path("../config/environment", __dir__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require "rspec/rails"
require "capybara/rspec"
require "capybara-screenshot/rspec"
require "selenium-webdriver"
require "view_component/test_helpers"
@ -13,7 +14,7 @@ Capybara.register_driver :headless do |app|
options = Selenium::WebDriver::Firefox::Options.new
options.add_argument("--headless")
Capybara::Selenium::Driver.new(app, browser: :firefox, capabilities: options)
Capybara::Selenium::Driver.new(app, browser: :firefox, options:)
end
Capybara.javascript_driver = :headless

226
spec/services/bulk_upload/lettings/row_parser_spec.rb

@ -8,8 +8,12 @@ RSpec.describe BulkUpload::Lettings::RowParser do
let(:attributes) { { bulk_upload: } }
let(:bulk_upload) { create(:bulk_upload, :lettings, user:) }
let(:user) { create(:user, organisation: owning_org) }
let(:owning_org) { create(:organisation, :with_old_visible_id) }
let(:managing_org) { create(:organisation, :with_old_visible_id) }
let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) }
let(:location) { create(:location, :with_old_visible_id, scheme:) }
let(:setup_section_params) do
{
bulk_upload:,
@ -23,6 +27,10 @@ RSpec.describe BulkUpload::Lettings::RowParser do
}
end
before do
create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org)
end
around do |example|
FormHandler.instance.use_real_forms!
@ -79,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
{
bulk_upload:,
field_1: "1",
field_4: "1",
field_4: scheme.old_visible_id,
field_7: "123",
field_96: now.day.to_s,
field_97: now.month.to_s,
@ -178,6 +186,12 @@ RSpec.describe BulkUpload::Lettings::RowParser do
field_80: "1234.56",
field_87: "1",
field_88: "234.56",
field_106: "15",
field_99: "0",
field_89: now.day.to_s,
field_90: now.month.to_s,
field_91: now.strftime("%g"),
}
end
@ -290,10 +304,90 @@ RSpec.describe BulkUpload::Lettings::RowParser do
context "when matching scheme cannot be found" do
let(:attributes) { { bulk_upload:, field_1: "1", field_4: "123" } }
xit "returns an error" do
it "returns an error" do
expect(parser.errors[:field_4]).to be_present
end
end
context "when scheme belongs to someone else" do
let(:other_scheme) { create(:scheme, :with_old_visible_id) }
let(:attributes) { { bulk_upload:, field_1: "1", field_4: other_scheme.old_visible_id, field_111: owning_org.old_visible_id } }
it "returns an error" do
expect(parser.errors[:field_4]).to include("This management group code does not belong to your organisation, or any of your stock owners / managing agents")
end
end
context "when scheme belongs to owning org" do
let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) }
let(:attributes) { { bulk_upload:, field_1: "1", field_4: scheme.old_visible_id, field_111: owning_org.old_visible_id } }
it "does not return an error" do
expect(parser.errors[:field_4]).to be_blank
end
end
context "when scheme belongs to managing org" do
let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: managing_org) }
let(:attributes) { { bulk_upload:, field_1: "1", field_4: scheme.old_visible_id, field_113: managing_org.old_visible_id } }
it "does not return an error" do
expect(parser.errors[:field_4]).to be_blank
end
end
end
describe "#field_5" do
context "when location does not exist" do
let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) }
let(:attributes) do
{
bulk_upload:,
field_1: "1",
field_4: scheme.old_visible_id,
field_5: "dontexist",
field_111: owning_org.old_visible_id,
}
end
it "returns an error" do
expect(parser.errors[:field_5]).to be_present
end
end
context "when location exists" do
let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) }
let(:attributes) do
{
bulk_upload:,
field_1: "1",
field_4: scheme.old_visible_id,
field_5: location.old_visible_id,
field_111: owning_org.old_visible_id,
}
end
it "does not return an error" do
expect(parser.errors[:field_5]).to be_blank
end
end
context "when location exists but not related" do
let(:location) { create(:scheme, :with_old_visible_id) }
let(:attributes) do
{
bulk_upload:,
field_1: "1",
field_4: scheme.old_visible_id,
field_5: location.old_visible_id,
field_111: owning_org.old_visible_id,
}
end
it "returns an error" do
expect(parser.errors[:field_5]).to be_present
end
end
end
describe "#field_7" do
@ -437,6 +531,24 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end
end
context "when field 98 is 4 digits instead of 2" do
let(:attributes) { { bulk_upload:, field_98: "2022" } }
it "returns an error" do
parser.valid?
expect(parser.errors[:field_98]).to include("Tenancy start year must be 2 digits")
end
end
context "when invalid date given" do
let(:attributes) { { bulk_upload:, field_1: "1", field_96: "a", field_97: "12", field_98: "2022" } }
it "does not raise an error" do
expect { parser.valid? }.not_to raise_error
end
end
context "when inside of collection year" do
let(:attributes) { { bulk_upload:, field_96: "1", field_97: "10", field_98: "22" } }
@ -472,6 +584,68 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end
end
describe "#field_111" do # owning org
context "when cannot find owning org" do
let(:attributes) { { bulk_upload:, field_111: "donotexist" } }
it "is not permitted" do
expect(parser.errors[:field_111]).to eql(["The owning organisation code is incorrect"])
end
end
context "when org is not stock owning" do
let(:owning_org) { create(:organisation, :with_old_visible_id, :does_not_own_stock) }
let(:attributes) { { bulk_upload:, field_111: owning_org.old_visible_id } }
it "is not permitted" do
expect(parser.errors[:field_111]).to eql(["The owning organisation code provided is for an organisation that does not own stock"])
end
it "blocks log creation" do
expect(parser).to be_block_log_creation
end
end
context "when not affiliated with owning org" do
let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) }
let(:attributes) { { bulk_upload:, field_111: unaffiliated_org.old_visible_id } }
it "is not permitted" do
expect(parser.errors[:field_111]).to eql(["You do not have permission to add logs for this owning organisation"])
end
it "blocks log creation" do
expect(parser).to be_block_log_creation
end
end
end
describe "#field_113" do # managing org
context "when cannot find managing org" do
let(:attributes) { { bulk_upload:, field_113: "donotexist" } }
it "is not permitted" do
expect(parser.errors[:field_113]).to eql(["The managing organisation code is incorrect"])
end
end
context "when not affiliated with managing org" do
let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) }
let(:attributes) { { bulk_upload:, field_111: owning_org.old_visible_id, field_113: unaffiliated_org.old_visible_id } }
it "is not permitted" do
expect(parser.errors[:field_113]).to eql(["This managing organisation does not have a relationship with the owning organisation"])
end
it "blocks log creation" do
expect(parser).to be_block_log_creation
end
end
end
describe "#field_134" do
context "when an unpermitted value" do
let(:attributes) { { bulk_upload:, field_134: 3 } }
@ -506,6 +680,46 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end
describe "#log" do
describe "#location" do
context "when lookup is via new core id" do
let(:attributes) { { bulk_upload:, field_4: scheme.old_visible_id, field_5: location.id, field_111: owning_org } }
it "assigns the correct location" do
expect(parser.log.location).to eql(location)
end
end
end
describe "#scheme" do
context "when lookup is via id prefixed with S" do
let(:attributes) { { bulk_upload:, field_4: "S#{scheme.id}", field_111: owning_org } }
it "assigns the correct scheme" do
expect(parser.log.scheme).to eql(scheme)
end
end
end
describe "#owning_organisation" do
context "when lookup is via id prefixed with ORG" do
let(:attributes) { { bulk_upload:, field_111: "ORG#{owning_org.id}" } }
it "assigns the correct org" do
expect(parser.log.owning_organisation).to eql(owning_org)
end
end
end
describe "#managing_organisation" do
context "when lookup is via id prefixed with ORG" do
let(:attributes) { { bulk_upload:, field_113: "ORG#{managing_org.id}" } }
it "assigns the correct org" do
expect(parser.log.managing_organisation).to eql(managing_org)
end
end
end
describe "#cbl" do
context "when field_75 is yes ie 1" do
let(:attributes) { { bulk_upload:, field_75: 1 } }
@ -946,6 +1160,14 @@ RSpec.describe BulkUpload::Lettings::RowParser do
expect(parser.log.first_time_property_let_as_social_housing).to eq(1)
end
end
context "when field_106 is not 15, 16, or 17" do
let(:attributes) { { bulk_upload:, field_106: "1" } }
it "sets to 0" do
expect(parser.log.first_time_property_let_as_social_housing).to eq(0)
end
end
end
describe "#housingneeds" do

20
spec/services/bulk_upload/lettings/validator_spec.rb

@ -87,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
end
end
describe "#should_create_logs?" do
describe "#create_logs?" do
context "when all logs are valid" do
let(:target_path) { file_fixture("2022_23_lettings_bulk_upload.csv") }
@ -111,9 +111,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
expect(validator).not_to be_create_logs
end
end
end
describe "#create_logs?" do
context "when a log is not valid?" do
let(:log_1) { build(:lettings_log, :completed, created_by: user) }
let(:log_2) { build(:lettings_log, :completed, created_by: user) }
@ -146,6 +144,22 @@ RSpec.describe BulkUpload::Lettings::Validator do
end
end
context "when a single log wants to block log creation" do
let(:unaffiliated_org) { create(:organisation) }
let(:log_1) { build(:lettings_log, :completed, renttype: 1, created_by: user, owning_organisation: unaffiliated_org) }
before do
file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.close
end
it "will not create logs" do
validator.call
expect(validator).not_to be_create_logs
end
end
context "when a log has incomplete setup secion" do
let(:log) { build(:lettings_log, :in_progress, created_by: user, startdate: Time.zone.local(2022, 5, 1)) }

Loading…
Cancel
Save