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. 61
      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. 113
      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. 16
      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. 176
      spec/models/validations/sales/financial_validations_spec.rb
  60. 65
      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 # GOV UK frontend components
gem "govuk-components" gem "govuk-components"
# GOV UK component form builder DSL # 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 # Convert Markdown into GOV.UK frontend-styled HTML
gem "govuk_markdown" gem "govuk_markdown"
# GOV UK Notify # GOV UK Notify
@ -91,6 +91,7 @@ end
group :test do group :test do
gem "capybara", require: false gem "capybara", require: false
gem "capybara-lockstep" gem "capybara-lockstep"
gem "capybara-screenshot"
gem "faker" gem "faker"
gem "rspec-rails", require: false gem "rspec-rails", require: false
gem "selenium-webdriver", require: false gem "selenium-webdriver", require: false

276
Gemfile.lock

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

16
app/helpers/collection_time_helper.rb

@ -23,4 +23,20 @@ module CollectionTimeHelper
def current_collection_end_date def current_collection_end_date
Time.zone.local(current_collection_start_year + 1, 3, 31) Time.zone.local(current_collection_start_year + 1, 3, 31)
end 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 end

33
app/helpers/interruption_screen_helper.rb

@ -1,17 +1,10 @@
module InterruptionScreenHelper module InterruptionScreenHelper
def display_informative_text(informative_text, lettings_log) def display_informative_text(informative_text, log)
return "" unless informative_text["arguments"] return "" unless informative_text["arguments"]
translation_params = {} translation_params = {}
informative_text["arguments"].each do |argument| informative_text["arguments"].each do |argument|
value = if argument["label"] value = get_value_from_argument(log, argument)
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
translation_params[argument["i18n_template"].to_sym] = value translation_params[argument["i18n_template"].to_sym] = value
end end
@ -24,21 +17,27 @@ module InterruptionScreenHelper
end end
end end
def display_title_text(title_text, lettings_log) def display_title_text(title_text, log)
return "" if title_text.nil? return "" if title_text.nil?
translation_params = {} translation_params = {}
arguments = title_text["arguments"] || {} arguments = title_text["arguments"] || {}
arguments.each do |argument| arguments.each do |argument|
value = if argument["label"] value = get_value_from_argument(log, argument)
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
translation_params[argument["i18n_template"].to_sym] = value translation_params[argument["i18n_template"].to_sym] = value
end end
I18n.t(title_text["translation"], **translation_params).to_s I18n.t(title_text["translation"], **translation_params).to_s
end 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 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? } log.form.subsections.count { |subsection| subsection.status(log) == status && subsection.applicable_questions(log).count.positive? }
end 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) def next_question_page(subsection, log, current_user)
if subsection.pages.first.routed_to?(log, current_user) if subsection.pages.first.routed_to?(log, current_user)
subsection.pages.first.id 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." "This log is from the #{log.form.start_date.year}/#{log.form.start_date.year + 1} collection window, which is now closed."
end end
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 end

6
app/models/form.rb

@ -6,7 +6,11 @@ class Form
def initialize(form_path, start_year = "", sections_in_form = [], type = "lettings") def initialize(form_path, start_year = "", sections_in_form = [], type = "lettings")
if sales_or_start_year_after_2022?(type, start_year) if sales_or_start_year_after_2022?(type, start_year)
@start_date = Time.zone.local(start_year, 4, 1) @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)] @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) } @form_sections = sections_in_form.map { |sec| sec.new(nil, nil, self) }
@type = type @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) def initialize(_id, hsh, page)
super("location_id", hsh, page) super("location_id", hsh, page)
@check_answer_label = "Location" @check_answer_label = "Location"
@header = "Which location is this log for?" @header = header_text
@type = "radio" @type = "radio"
@answer_options = answer_options @answer_options = answer_options
@inferred_answers = { @inferred_answers = {
@ -47,4 +47,12 @@ private
def selected_answer_option_is_derived?(_lettings_log) def selected_answer_option_is_derived?(_lettings_log)
false false
end 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 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", "translation" => "soft_validations.purchase_price.hint_text",
"arguments" => [ "arguments" => [
{ {
"key" => "purchase_price_soft_min_or_soft_max", "key" => "field_formatted_as_currency",
"label" => false, "arguments_for_key" => "purchase_price_soft_min_or_soft_max",
"i18n_template" => "soft_min_or_soft_max", "i18n_template" => "soft_min_or_soft_max",
"currency" => true,
}, },
{ {
"key" => "purchase_price_min_or_max_text", "key" => "purchase_price_min_or_max_text",
"label" => false,
"i18n_template" => "min_or_max", "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, "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 = {} @informative_text = {}
end 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 class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page
def questions def questions
@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
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 super
@id = "income1_value_check" @id = "income1_value_check"
@check_answer_label = "Income confirmation" @check_answer_label = "Income confirmation"
@header = "Are you sure this income is correct?" @header = "Are you sure this is correct?"
@type = "interruption_screen" @type = "interruption_screen"
@answer_options = { @answer_options = {
"0" => { "value" => "Yes" }, "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" @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." @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 @min = 0
@max = 999_999
@step = 1 @step = 1
@width = 5 @width = 5
@prefix = "£" @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 class Form::Sales::Questions::LivingBeforePurchase < ::Form::Question
def initialize(id, hsh, page) def initialize(id, hsh, page)
super super
@id = "proplen" @id = "proplen_asked"
@check_answer_label = "Number of years living in the property before purchase" @check_answer_label = "Buyer lived in the property before purchasing"
@header = "How long did the buyer(s) live in the property before purchase?" @header = "Did the buyer live in the property before purchasing it?"
@hint_text = "You should round this up to the nearest year. If the buyers haven't been living in the property, enter '0'" @hint_text = nil
@type = "numeric" @type = "radio"
@min = 0 @answer_options = ANSWER_OPTIONS
@max = 80 @conditional_for = {
@step = 1 "proplen" => [0],
@width = 5 }
@suffix = " years" @hidden_in_check_answers = {
"depends_on" => [
{
"proplen_asked" => 0,
},
],
}
end end
ANSWER_OPTIONS = {
"0" => { "value" => "Yes" },
"1" => { "value" => "No" },
}.freeze
end 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], "ppostcode_full" => [0],
} }
@hint_text = "This is also known as the household’s 'last settled home'" @hint_text = "This is also known as the household’s 'last settled home'"
@hidden_in_check_answers = {
"depends_on" => [
{
"ppcodenk" => 0,
},
{
"ppcodenk" => 1,
},
],
}
end end
ANSWER_OPTIONS = { 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::ExtraBorrowingValueCheck.new("extra_borrowing_price_value_check", nil, self),
Form::Sales::Pages::AboutPriceNotRtb.new(nil, nil, self), Form::Sales::Pages::AboutPriceNotRtb.new(nil, nil, self),
Form::Sales::Pages::GrantValueCheck.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::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::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), 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), Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2),
ethnic_pages_for_buyer_2, ethnic_pages_for_buyer_2,
Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self), 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::Buyer2LiveInProperty.new(nil, nil, self),
Form::Sales::Pages::NumberOfOthersInProperty.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), 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::MortgageValueCheck.new("buyer_1_mortgage_value_check", nil, self, 1),
Form::Sales::Pages::Buyer2Income.new(nil, nil, self), 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::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::Buyer2Mortgage.new(nil, nil, self),
Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2), Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2),
Form::Sales::Pages::HousingBenefits.new(nil, nil, self), Form::Sales::Pages::HousingBenefits.new(nil, nil, self),

8
app/models/form_handler.rb

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

6
app/models/location.rb

@ -366,6 +366,12 @@ class Location < ApplicationRecord
enum type_of_unit: TYPE_OF_UNIT 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) def postcode=(postcode)
if postcode if postcode
super UKPostcode.parse(postcode).to_s super UKPostcode.parse(postcode).to_s

4
app/models/log.rb

@ -147,4 +147,8 @@ private
self[is_inferred_key] = false self[is_inferred_key] = false
self[postcode_key] = nil self[postcode_key] = nil
end end
def format_as_currency(num_string)
ActionController::Base.helpers.number_to_currency(num_string, unit: "£")
end
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_agent_relationships, foreign_key: :parent_organisation_id, class_name: "OrganisationRelationship"
has_many :managing_agents, through: :managing_agent_relationships, source: :child_organisation 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_name, ->(name) { where("name ILIKE ?", "%#{name}%") }
scope :search_by, ->(param) { search_by_name(param) } scope :search_by, ->(param) { search_by_name(param) }
has_paper_trail has_paper_trail
auto_strip_attributes :name auto_strip_attributes :name
@ -35,6 +48,20 @@ class Organisation < ApplicationRecord
validates :name, presence: { message: I18n.t("validations.organisation.name_missing") } validates :name, presence: { message: I18n.t("validations.organisation.name_missing") }
validates :provider_type, presence: { message: I18n.t("validations.organisation.provider_type_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 def lettings_logs
LettingsLog.filter_by_organisation(self) LettingsLog.filter_by_organisation(self)
end end

18
app/models/sales_log.rb

@ -124,6 +124,10 @@ class SalesLog < Log
la && LONDON_BOROUGHS.include?(la) la && LONDON_BOROUGHS.include?(la)
end end
def property_not_in_london?
!london_property?
end
def income1_used_for_mortgage? def income1_used_for_mortgage?
inc1mort == 1 inc1mort == 1
end end
@ -256,4 +260,18 @@ class SalesLog < Log
def purchase_price_soft_max def purchase_price_soft_max
LaSaleRange.find_by(start_year: collection_start_year, la:, bedrooms: beds).soft_max LaSaleRange.find_by(start_year: collection_start_year, la:, bedrooms: beds).soft_max
end 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 end

10
app/models/scheme.rb

@ -110,6 +110,16 @@ class Scheme < ApplicationRecord
enum arrangement_type: ARRANGEMENT_TYPE, _suffix: true 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 def id_to_display
"S#{id}" "S#{id}"
end end

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

@ -3,21 +3,37 @@ module Validations::Sales::FinancialValidations
# or 'validate_' to run on submit as well # or 'validate_' to run on submit as well
def validate_income1(record) def validate_income1(record)
if record.ecstat1 && record.income1 && record.la && record.ownershipsch == 1 return unless record.income1 && record.la && record.shared_ownership_scheme?
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 relevant_fields = %i[income1 ownershipsch la postcode_full]
record.errors.add :ecstat1, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 if record.london_property? && 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 relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_london") }
record.errors.add :la, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 elsif record.property_not_in_london? && record.income1 > 80_000
record.errors.add :postcode_full, I18n.t("validations.financial.income1.over_hard_max", hard_max: 90_000) if record.income1 > 90_000 relevant_fields.each { |field| record.errors.add field, I18n.t("validations.financial.income.over_hard_max_for_outside_london") }
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 end
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 end
def validate_cash_discount(record) def validate_cash_discount(record)
@ -36,6 +52,15 @@ module Validations::Sales::FinancialValidations
end end
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) def validate_percentage_owned_not_too_much_if_older_person(record)
return unless record.old_persons_shared_ownership? && record.stairowned 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") record.errors.add :type, I18n.t("validations.financial.staircasing.older_person_percentage_owned_maximum_75")
end end
end end
private
def is_relationship_child?(relationship)
relationship == "C"
end
def is_economic_status_child?(economic_status)
economic_status == 9
end
end end

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

@ -1,11 +1,45 @@
module Validations::Sales::SetupValidations module Validations::Sales::SetupValidations
include Validations::SharedValidations include Validations::SharedValidations
include CollectionTimeHelper
def validate_saledate(record) def validate_saledate(record)
return unless record.saledate && date_valid?("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? unless record.saledate.between?(active_collection_start_date, current_collection_end_date) || !FeatureToggle.saledate_collection_window_validation_enabled?
record.errors.add :saledate, I18n.t("validations.setup.saledate.financial_year") 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 end
end end

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

@ -1,5 +1,5 @@
module Validations::Sales::SoftValidations module Validations::Sales::SoftValidations
ALLOWED_INCOME_RANGES = { ALLOWED_INCOME_RANGES_SALES = {
1 => OpenStruct.new(soft_min: 5000), 1 => OpenStruct.new(soft_min: 5000),
2 => OpenStruct.new(soft_min: 1500), 2 => OpenStruct.new(soft_min: 1500),
3 => OpenStruct.new(soft_min: 1000), 3 => OpenStruct.new(soft_min: 1000),
@ -8,9 +8,15 @@ module Validations::Sales::SoftValidations
}.freeze }.freeze
def income1_under_soft_min? 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 end
def staircase_bought_above_fifty? 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 include ActiveModel::Attributes
attribute :bulk_upload attribute :bulk_upload
attribute :block_log_creation, :boolean, default: -> { false }
attribute :field_1, :integer attribute :field_1, :integer
attribute :field_2 attribute :field_2
attribute :field_3 attribute :field_3
attribute :field_4, :integer attribute :field_4, :string
attribute :field_5, :integer attribute :field_5, :integer
attribute :field_6 attribute :field_6
attribute :field_7, :string attribute :field_7, :string
@ -114,9 +115,9 @@ class BulkUpload::Lettings::RowParser
attribute :field_108, :string attribute :field_108, :string
attribute :field_109, :string attribute :field_109, :string
attribute :field_110 attribute :field_110
attribute :field_111, :integer attribute :field_111, :string
attribute :field_112, :string attribute :field_112, :string
attribute :field_113, :integer attribute :field_113, :string
attribute :field_114, :integer attribute :field_114, :integer
attribute :field_115 attribute :field_115
attribute :field_116, :integer 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") }, 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") } 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_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_data_types
validate :validate_nulls validate :validate_nulls
@ -155,6 +157,19 @@ class BulkUpload::Lettings::RowParser
validate :validate_dont_know_disabled_needs_conjunction validate :validate_dont_know_disabled_needs_conjunction
validate :validate_no_and_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? def valid?
errors.clear errors.clear
@ -173,15 +188,99 @@ class BulkUpload::Lettings::RowParser
end end
def blank_row? 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 end
def log def log
@log ||= LettingsLog.new(attributes_for_log) @log ||= LettingsLog.new(attributes_for_log)
end end
def block_log_creation!
self.block_log_creation = true
end
def block_log_creation?
block_log_creation
end
private 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 def validate_no_and_dont_know_disabled_needs_conjunction
if field_59 == 1 && field_60 == 1 if field_59 == 1 && field_60 == 1
errors.add(:field_59, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) errors.add(:field_59, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction"))
@ -447,6 +546,8 @@ private
def startdate def startdate
Date.new(field_98 + 2000, field_97, field_96) if field_98.present? && field_97.present? && field_96.present? 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 end
def renttype def renttype
@ -483,15 +584,19 @@ private
end end
def owning_organisation def owning_organisation
Organisation.find_by(old_visible_id: field_111) Organisation.find_by_id_on_mulitple_fields(field_111)
end end
def owning_organisation_id def owning_organisation_id
owning_organisation&.id owning_organisation&.id
end end
def managing_organisation
Organisation.find_by_id_on_mulitple_fields(field_113)
end
def managing_organisation_id def managing_organisation_id
Organisation.find_by(old_visible_id: field_113)&.id managing_organisation&.id
end end
def attributes_for_log def attributes_for_log
@ -506,6 +611,7 @@ private
attributes["managing_organisation_id"] = managing_organisation_id attributes["managing_organisation_id"] = managing_organisation_id
attributes["renewal"] = renewal attributes["renewal"] = renewal
attributes["scheme"] = scheme attributes["scheme"] = scheme
attributes["location"] = location
attributes["created_by"] = bulk_upload.user attributes["created_by"] = bulk_upload.user
attributes["needstype"] = bulk_upload.needstype attributes["needstype"] = bulk_upload.needstype
attributes["rent_type"] = rent_type attributes["rent_type"] = rent_type
@ -670,6 +776,8 @@ private
case rsnvac case rsnvac
when 15, 16, 17 when 15, 16, 17
1 1
else
0
end end
end end
@ -883,6 +991,6 @@ private
end end
def scheme def scheme
@scheme ||= Scheme.find_by(old_visible_id: field_4) @scheme ||= Scheme.find_by_id_on_mulitple_fields(field_4)
end end
end end

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

@ -176,6 +176,7 @@ class BulkUpload::Lettings::Validator
def create_logs? def create_logs?
return false if any_setup_sections_incomplete? return false if any_setup_sections_incomplete?
return false if over_column_error_threshold? 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? } row_parsers.all? { |row_parser| row_parser.log.valid? }
end 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? ## 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 * 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 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", "form_type": "lettings",
"start_date": "2022-04-01T00:00:00.000+01:00", "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", "unresolved_log_redirect_page_id": "tenancy_start_date",
"sections": { "sections": {
"tenancy_and_property": { "tenancy_and_property": {

9
config/initializers/feature_toggle.rb

@ -1,9 +1,14 @@
class FeatureToggle 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? Rails.env.production? || Rails.env.test? || Rails.env.staging?
end 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? Rails.env.production? || Rails.env.test? || Rails.env.staging?
end end

19
config/locales/en.yml

@ -152,12 +152,17 @@ en:
intermediate_rent_product_name: intermediate_rent_product_name:
blank: "Enter name of other intermediate rent product" blank: "Enter name of other intermediate rent product"
saledate: 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: startdate:
later_than_14_days_after: "The tenancy start date must not be later than 14 days from today’s date" 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" 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_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" 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: location:
deactivated: "The location %{postcode} was deactivated on %{date} and was not available on the day you entered." 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" 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" 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" freq_missing: "Select how often the household receives income"
earnings_missing: "Enter how much income the household has in total" earnings_missing: "Enter how much income the household has in total"
income1: income:
over_hard_max: "Income must be lower than £%{hard_max}" 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" negative_currency: "Enter an amount above 0"
rent: rent:
less_than_shortfall: "Enter an amount that is more than the shortfall in basic 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?" 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: 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?" 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: rent:
outside_range_title: "You told us the rent is %{brent}" 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}." 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}" one_argument: "This is based on the tenant’s work situation: %{ecstat1}"
title_text: title_text:
no_argument: "Some test 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -490,6 +490,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do
t.integer "prevten" t.integer "prevten"
t.integer "mortgageused" t.integer "mortgageused"
t.integer "wchair" t.integer "wchair"
t.integer "income2_value_check"
t.integer "armedforcesspouse" t.integer "armedforcesspouse"
t.datetime "hodate", precision: nil t.datetime "hodate", precision: nil
t.integer "hoday" t.integer "hoday"
@ -528,6 +529,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_10_143120) do
t.integer "staircasesale" t.integer "staircasesale"
t.integer "ethnic_group2" t.integer "ethnic_group2"
t.integer "ethnicbuy2" t.integer "ethnicbuy2"
t.integer "proplen_asked"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" 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" 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) } startdate { Time.zone.local(2022, 4, 1) }
confirmed { true } confirmed { true }
scheme scheme
trait :export do trait :export do
postcode { "SW1A 2AA" } postcode { "SW1A 2AA" }
name { "Downing Street" } name { "Downing Street" }
@ -19,5 +20,9 @@ FactoryBot.define do
scheme { FactoryBot.create(:scheme, :export) } scheme { FactoryBot.create(:scheme, :export) }
old_visible_id { "111" } old_visible_id { "111" }
end end
trait :with_old_visible_id do
old_visible_id { rand(9_999_999).to_s }
end
end end
end end

4
spec/factories/organisation.rb

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

4
spec/factories/scheme.rb

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

53
spec/helpers/interruption_screen_helper_spec.rb

@ -13,6 +13,7 @@ RSpec.describe InterruptionScreenHelper do
earnings: 750, earnings: 750,
incfreq: 1, incfreq: 1,
created_by: user, created_by: user,
sex1: "F",
) )
end end
@ -94,6 +95,54 @@ RSpec.describe InterruptionScreenHelper do
.to eq("") .to eq("")
end end
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 end
describe "display_title_text" do describe "display_title_text" do
@ -113,12 +162,12 @@ RSpec.describe InterruptionScreenHelper do
{ {
"key" => "ecstat1", "key" => "ecstat1",
"label" => true, "label" => true,
"i18n_template" => "ecstat1", "i18n_template" => "argument",
}, },
], ],
} }
expect(display_title_text(title_text, lettings_log)) 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
end end

113
spec/helpers/tasklist_helper_spec.rb

@ -1,15 +1,20 @@
require "rails_helper" require "rails_helper"
RSpec.describe TasklistHelper do RSpec.describe TasklistHelper do
describe "with lettings" do let(:now) { Time.utc(2022, 6, 1) }
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") }
context "with 2021 2022 form" do around do |example|
before do Timecop.freeze(now) do
allow(FormHandler.instance).to receive(:current_lettings_form).and_return(fake_2021_2022_form) Singleton.__init__(FormHandler)
example.run
end end
Timecop.return
Singleton.__init__(FormHandler)
end
describe "with lettings" do
let(:empty_lettings_log) { create(:lettings_log) }
let(:lettings_log) { build(:lettings_log, :in_progress, needstype: 1) }
describe "get next incomplete section" do describe "get next incomplete section" do
it "returns the first subsection name if it is not completed" do it "returns the first subsection name if it is not completed" do
@ -24,7 +29,7 @@ RSpec.describe TasklistHelper do
describe "get sections count" do describe "get sections count" do
it "returns the total of sections if no status is given" do it "returns the total of sections if no status is given" do
expect(get_subsections_count(empty_lettings_log)).to eq(8) expect(get_subsections_count(empty_lettings_log)).to eq(1)
end end
it "returns 0 sections for completed sections if no sections are completed" do it "returns 0 sections for completed sections if no sections are completed" do
@ -32,11 +37,11 @@ RSpec.describe TasklistHelper do
end end
it "returns the number of not started sections" do it "returns the number of not started sections" do
expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(8) expect(get_subsections_count(empty_lettings_log, :not_started)).to eq(1)
end end
it "returns the number of sections in progress" do it "returns the number of sections in progress" do
expect(get_subsections_count(lettings_log, :in_progress)).to eq(3) expect(get_subsections_count(lettings_log, :in_progress)).to eq(2)
end end
it "returns 0 for invalid state" do it "returns 0 for invalid state" do
@ -44,57 +49,7 @@ RSpec.describe TasklistHelper do
end end
end end
describe "get_next_page_or_check_answers" do describe "review_log_text" 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
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
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
end
describe "#review_log_text" do
around do |example|
Timecop.freeze(now) do
Singleton.__init__(FormHandler)
example.run
end
Singleton.__init__(FormHandler)
end
context "with lettings log" do
context "when collection_period_open? == true" do context "when collection_period_open? == true" do
context "with 2023 deadline" do context "with 2023 deadline" do
let(:now) { Time.utc(2022, 6, 1) } let(:now) { Time.utc(2022, 6, 1) }
@ -113,7 +68,7 @@ RSpec.describe TasklistHelper do
it "returns relevant text" do it "returns relevant text" do
expect(review_log_text(lettings_log)).to eq( 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
end end
@ -129,6 +84,30 @@ RSpec.describe TasklistHelper do
end end
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 "with sales log" do
context "when collection_period_open? == true" do context "when collection_period_open? == true" do
let(:now) { Time.utc(2022, 6, 1) } let(:now) { Time.utc(2022, 6, 1) }
@ -136,17 +115,19 @@ RSpec.describe TasklistHelper do
it "returns relevant text" do it "returns relevant text" do
expect(review_log_text(sales_log)).to eq( 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
end end
context "when collection_period_open? == false" do context "when collection_period_open? == false" do
let(:now) { Time.utc(2023, 7, 8) } let(:now) { Time.utc(2022, 6, 1) }
let(:sales_log) { create(:sales_log, :completed, saledate: Time.utc(2023, 2, 8)) } let!(:sales_log) { create(:sales_log, :completed) }
it "returns relevant text" do 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 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_id) { nil }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) } 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 it "has correct subsection" do
expect(page.subsection).to eq(subsection) 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_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) } 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 it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -103,4 +111,14 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do
end end
end 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 end

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

@ -11,9 +11,25 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
end end
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 it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[proplen]) expect(page.questions.map(&:id)).to eq(%w[proplen])
end 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 it "has the correct id" do
expect(page.id).to eq(nil) expect(page.id).to eq(nil)

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 end
it "has the correct header" do 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 end
it "has the correct check_answer_label" do 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 end
it "has the correct id" do it "has the correct id" do
expect(question.id).to eq("proplen") expect(question.id).to eq("proplen_asked")
end end
it "has the correct header" do 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 end
it "has the correct check_answer_label" do 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 end
it "has the correct type" do it "has the correct type" do
expect(question.type).to eq("numeric") expect(question.type).to eq("radio")
end end
it "is not marked as derived" do it "is not marked as derived" do
@ -32,26 +32,11 @@ RSpec.describe Form::Sales::Questions::LivingBeforePurchase, type: :model do
end end
it "has the correct hint" do 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 end
it "has correct width" do it "has the correct answer_options" do
expect(question.width).to eq(5) expect(question.answer_options).to eq("0" => { "value" => "Yes" },
end "1" => { "value" => "No" })
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 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 it "has the correct hint" do
expect(question.hint_text).to eq("This is also known as the household’s 'last settled home'") expect(question.hint_text).to eq("This is also known as the household’s 'last settled home'")
end 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 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 extra_borrowing_price_value_check
about_price_not_rtb about_price_not_rtb
grant_value_check grant_value_check
purchase_price_discounted_ownership
purchase_price_outright_ownership purchase_price_outright_ownership
discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount discounted_ownership_deposit_and_mortgage_value_check_after_value_and_discount
mortgage_used_discounted_ownership 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 buyer_2_gender_identity
gender_2_buyer_retirement_value_check gender_2_buyer_retirement_value_check
buyer_2_working_situation 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 buyer_2_live_in_property
number_of_others_in_property number_of_others_in_property
person_2_known person_2_known
@ -126,7 +127,8 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
buyer_2_ethnic_background_mixed buyer_2_ethnic_background_mixed
buyer_2_ethnic_background_white buyer_2_ethnic_background_white
buyer_2_working_situation 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 buyer_2_live_in_property
number_of_others_in_property number_of_others_in_property
person_2_known 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_1_mortgage_value_check
buyer_2_income buyer_2_income
buyer_2_income_mortgage_value_check buyer_2_income_mortgage_value_check
buyer_2_income_value_check
buyer_2_mortgage buyer_2_mortgage
buyer_2_mortgage_value_check buyer_2_mortgage_value_check
housing_benefits housing_benefits
@ -52,6 +53,7 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
buyer_1_mortgage_value_check buyer_1_mortgage_value_check
buyer_2_income buyer_2_income
buyer_2_income_mortgage_value_check buyer_2_income_mortgage_value_check
buyer_2_income_value_check
buyer_2_mortgage buyer_2_mortgage
buyer_2_mortgage_value_check buyer_2_mortgage_value_check
housing_benefits 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 it "is able to load a current sales form" do
form = form_handler.get_form("current_sales") form = form_handler.get_form("current_sales")
expect(form).to be_a(Form) 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") expect(form.name).to eq("2022_2023_sales")
end end
it "is able to load a previous sales form" do it "is able to load a previous sales form" do
form = form_handler.get_form("previous_sales") form = form_handler.get_form("previous_sales")
expect(form).to be_a(Form) 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") expect(form.name).to eq("2021_2022_sales")
end end
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.count).to eq(16)
expect(form.questions.first.id).to eq("owning_organisation_id") expect(form.questions.first.id).to eq("owning_organisation_id")
expect(form.start_date).to eq(Time.zone.parse("2022-04-01")) 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) expect(form.unresolved_log_redirect_page_id).to eq(nil)
end end

23
spec/models/sales_log_spec.rb

@ -266,6 +266,7 @@ RSpec.describe SalesLog, type: :model do
relat4: "X", relat4: "X",
relat5: "X", relat5: "X",
relat6: "P", relat6: "P",
income2: 0,
ecstat2: 9, ecstat2: 9,
ecstat3: 7, ecstat3: 7,
age1: 47, age1: 47,
@ -370,4 +371,26 @@ RSpec.describe SalesLog, type: :model do
expect(completed_sales_log.expected_shared_ownership_deposit_value).to eq(500) expect(completed_sales_log.expected_shared_ownership_deposit_value).to eq(500)
end end
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 end

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

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

@ -6,8 +6,9 @@ RSpec.describe Validations::Sales::SetupValidations do
let(:validator_class) { Class.new { include Validations::Sales::SetupValidations } } let(:validator_class) { Class.new { include Validations::Sales::SetupValidations } }
describe "#validate_saledate" do describe "#validate_saledate" do
context "with sales_in_crossover_period == false" do
context "when saledate is blank" do context "when saledate is blank" do
let(:record) { FactoryBot.build(:sales_log, saledate: nil) } let(:record) { build(:sales_log, saledate: nil) }
it "does not add an error" do it "does not add an error" do
setup_validator.validate_saledate(record) setup_validator.validate_saledate(record)
@ -17,7 +18,7 @@ RSpec.describe Validations::Sales::SetupValidations do
end end
context "when saledate is in the 22/23 financial year" do context "when saledate is in the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) } let(:record) { build(:sales_log, saledate: Time.zone.local(2023, 1, 1)) }
it "does not add an error" do it "does not add an error" do
setup_validator.validate_saledate(record) setup_validator.validate_saledate(record)
@ -27,22 +28,74 @@ RSpec.describe Validations::Sales::SetupValidations do
end end
context "when saledate is before the 22/23 financial year" do context "when saledate is before the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2022, 1, 1)) } let(:record) { build(:sales_log, saledate: Time.zone.local(2020, 1, 1)) }
it "adds error" do it "adds error" do
setup_validator.validate_saledate(record) 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 end
context "when saledate is after the 22/23 financial year" do context "when saledate is after the 22/23 financial year" do
let(:record) { FactoryBot.build(:sales_log, saledate: Time.zone.local(2023, 4, 1)) } let(:record) { build(:sales_log, saledate: Time.zone.local(2025, 4, 1)) }
it "adds error" do it "adds error" do
setup_validator.validate_saledate(record) 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 "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)
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 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? abort("The Rails environment is running in production mode!") if Rails.env.production?
require "rspec/rails" require "rspec/rails"
require "capybara/rspec" require "capybara/rspec"
require "capybara-screenshot/rspec"
require "selenium-webdriver" require "selenium-webdriver"
require "view_component/test_helpers" require "view_component/test_helpers"
@ -13,7 +14,7 @@ Capybara.register_driver :headless do |app|
options = Selenium::WebDriver::Firefox::Options.new options = Selenium::WebDriver::Firefox::Options.new
options.add_argument("--headless") options.add_argument("--headless")
Capybara::Selenium::Driver.new(app, browser: :firefox, capabilities: options) Capybara::Selenium::Driver.new(app, browser: :firefox, options:)
end end
Capybara.javascript_driver = :headless 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(:attributes) { { bulk_upload: } }
let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } let(:bulk_upload) { create(:bulk_upload, :lettings, user:) }
let(:user) { create(:user, organisation: owning_org) } let(:user) { create(:user, organisation: owning_org) }
let(:owning_org) { create(:organisation, :with_old_visible_id) } let(:owning_org) { create(:organisation, :with_old_visible_id) }
let(:managing_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 let(:setup_section_params) do
{ {
bulk_upload:, bulk_upload:,
@ -23,6 +27,10 @@ RSpec.describe BulkUpload::Lettings::RowParser do
} }
end end
before do
create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org)
end
around do |example| around do |example|
FormHandler.instance.use_real_forms! FormHandler.instance.use_real_forms!
@ -79,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
{ {
bulk_upload:, bulk_upload:,
field_1: "1", field_1: "1",
field_4: "1", field_4: scheme.old_visible_id,
field_7: "123", field_7: "123",
field_96: now.day.to_s, field_96: now.day.to_s,
field_97: now.month.to_s, field_97: now.month.to_s,
@ -178,6 +186,12 @@ RSpec.describe BulkUpload::Lettings::RowParser do
field_80: "1234.56", field_80: "1234.56",
field_87: "1", field_87: "1",
field_88: "234.56", 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 end
@ -290,10 +304,90 @@ RSpec.describe BulkUpload::Lettings::RowParser do
context "when matching scheme cannot be found" do context "when matching scheme cannot be found" do
let(:attributes) { { bulk_upload:, field_1: "1", field_4: "123" } } 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 expect(parser.errors[:field_4]).to be_present
end end
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 end
describe "#field_7" do describe "#field_7" do
@ -437,6 +531,24 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
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 context "when inside of collection year" do
let(:attributes) { { bulk_upload:, field_96: "1", field_97: "10", field_98: "22" } } 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
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 describe "#field_134" do
context "when an unpermitted value" do context "when an unpermitted value" do
let(:attributes) { { bulk_upload:, field_134: 3 } } let(:attributes) { { bulk_upload:, field_134: 3 } }
@ -506,6 +680,46 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
describe "#log" do 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 describe "#cbl" do
context "when field_75 is yes ie 1" do context "when field_75 is yes ie 1" do
let(:attributes) { { bulk_upload:, field_75: 1 } } 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) expect(parser.log.first_time_property_let_as_social_housing).to eq(1)
end end
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 end
describe "#housingneeds" do describe "#housingneeds" do

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

@ -87,7 +87,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
end end
end end
describe "#should_create_logs?" do describe "#create_logs?" do
context "when all logs are valid" do context "when all logs are valid" do
let(:target_path) { file_fixture("2022_23_lettings_bulk_upload.csv") } 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 expect(validator).not_to be_create_logs
end end
end end
end
describe "#create_logs?" do
context "when a log is not valid?" do context "when a log is not valid?" do
let(:log_1) { build(:lettings_log, :completed, created_by: user) } let(:log_1) { build(:lettings_log, :completed, created_by: user) }
let(:log_2) { 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
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 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)) } let(:log) { build(:lettings_log, :in_progress, created_by: user, startdate: Time.zone.local(2022, 5, 1)) }

Loading…
Cancel
Save