Browse Source

Merge branch 'main' into CLDC-2171-fix-typo-in-q79

pull/2263/head
Robert Sullivan 2 years ago committed by GitHub
parent
commit
c835d21451
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 333
      Gemfile.lock
  2. 2
      app/models/form/lettings/questions/person_working_situation.rb
  3. 2
      app/models/form/sales/questions/age1.rb
  4. 2
      app/models/form/sales/questions/buyer1_age_known.rb
  5. 2
      app/models/form/sales/questions/buyer1_working_situation.rb
  6. 2
      app/models/form/sales/questions/buyer2_working_situation.rb
  7. 9
      app/models/form/sales/questions/number_joint_buyers.rb
  8. 2
      app/models/form/sales/questions/person_working_situation.rb
  9. 2
      app/models/form/sales/questions/property_number_of_bedrooms.rb
  10. 1
      app/models/log.rb
  11. 8
      app/models/validations/sales/soft_validations.rb
  12. 21
      app/services/bulk_upload/lettings/year2023/row_parser.rb
  13. 20
      app/services/bulk_upload/lettings/year2024/row_parser.rb
  14. 1
      config/locales/en.yml
  15. 2
      spec/models/form/sales/questions/age1_spec.rb
  16. 2
      spec/models/form/sales/questions/buyer1_age_known_spec.rb
  17. 2
      spec/models/form/sales/questions/buyer1_working_situation_spec.rb
  18. 2
      spec/models/form/sales/questions/buyer2_working_situation_spec.rb
  19. 16
      spec/models/form/sales/questions/number_joint_buyers_spec.rb
  20. 2
      spec/models/form/sales/questions/person_working_situation_spec.rb
  21. 20
      spec/models/lettings_log_spec.rb
  22. 50
      spec/models/log_spec.rb
  23. 23
      spec/models/validations/sales/soft_validations_spec.rb
  24. 14
      spec/requests/duplicate_logs_controller_spec.rb
  25. 105
      spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb
  26. 59
      spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb

333
Gemfile.lock

@ -1,134 +1,137 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.7.2)
actionpack (= 7.0.7.2)
activesupport (= 7.0.7.2)
actioncable (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.7.2)
actionpack (= 7.0.7.2)
activejob (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
actionmailbox (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.7.2)
actionpack (= 7.0.7.2)
actionview (= 7.0.7.2)
activejob (= 7.0.7.2)
activesupport (= 7.0.7.2)
actionmailer (7.0.8.1)
actionpack (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activesupport (= 7.0.8.1)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.7.2)
actionview (= 7.0.7.2)
activesupport (= 7.0.7.2)
actionpack (7.0.8.1)
actionview (= 7.0.8.1)
activesupport (= 7.0.8.1)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.7.2)
actionpack (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
actiontext (7.0.8.1)
actionpack (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.7.2)
activesupport (= 7.0.7.2)
actionview (7.0.8.1)
activesupport (= 7.0.8.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.7.2)
activesupport (= 7.0.7.2)
activejob (7.0.8.1)
activesupport (= 7.0.8.1)
globalid (>= 0.3.6)
activemodel (7.0.7.2)
activesupport (= 7.0.7.2)
activerecord (7.0.7.2)
activemodel (= 7.0.7.2)
activesupport (= 7.0.7.2)
activestorage (7.0.7.2)
actionpack (= 7.0.7.2)
activejob (= 7.0.7.2)
activerecord (= 7.0.7.2)
activesupport (= 7.0.7.2)
activemodel (7.0.8.1)
activesupport (= 7.0.8.1)
activerecord (7.0.8.1)
activemodel (= 7.0.8.1)
activesupport (= 7.0.8.1)
activestorage (7.0.8.1)
actionpack (= 7.0.8.1)
activejob (= 7.0.8.1)
activerecord (= 7.0.8.1)
activesupport (= 7.0.8.1)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.7.2)
activesupport (7.0.8.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.1)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
auto_strip_attributes (2.6.0)
activerecord (>= 4.0)
aws-eventstream (1.2.0)
aws-partitions (1.714.0)
aws-sdk-core (3.170.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-eventstream (1.3.0)
aws-partitions (1.894.0)
aws-sdk-core (3.191.3)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.62.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (1.77.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.119.1)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-s3 (1.143.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.18)
better_html (2.0.1)
base64 (0.2.0)
bcrypt (3.1.20)
better_html (2.0.2)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.6)
bindex (0.8.1)
bootsnap (1.16.0)
bootsnap (1.18.3)
msgpack (~> 1.2)
builder (3.2.4)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
capybara (3.38.0)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
capybara-lockstep (1.3.0)
activesupport (>= 3.2)
capybara (>= 2.0)
capybara-lockstep (2.2.0)
activesupport (>= 4.2)
capybara (>= 3.0)
ruby2_keywords
selenium-webdriver (>= 3)
selenium-webdriver (>= 4.0)
capybara-screenshot (1.0.26)
capybara (>= 1.0, < 4)
launchy
childprocess (4.1.0)
childprocess (5.0.0)
coderay (1.1.3)
concurrent-ruby (1.2.2)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crack (0.4.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
date (3.3.3)
devise (4.8.1)
date (3.3.4)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -140,14 +143,14 @@ GEM
rails (>= 3.1.1)
randexp
rotp (>= 4.0.0)
diff-lcs (1.5.0)
diff-lcs (1.5.1)
docile (1.4.0)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
dotenv (3.0.2)
dotenv-rails (3.0.2)
dotenv (= 3.0.2)
railties (>= 6.1)
encryptor (3.0.0)
erb_lint (0.3.1)
erb_lint (0.5.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
@ -157,47 +160,48 @@ GEM
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
excon (0.99.0)
factory_bot (6.2.1)
excon (0.109.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.1.1)
faker (3.2.3)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
fugit (1.8.1)
ffi (1.16.3)
fugit (1.10.0)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
globalid (1.1.0)
activesupport (>= 5.0)
govuk-components (5.1.0)
globalid (1.2.1)
activesupport (>= 6.1)
govuk-components (5.2.1)
html-attributes-utils (~> 1.0.0, >= 1.0.0)
pagy (~> 6.0)
view_component (>= 3.9, < 3.11)
govuk_design_system_formbuilder (5.0.0)
govuk_design_system_formbuilder (5.2.0)
actionview (>= 6.1)
activemodel (>= 6.1)
activesupport (>= 6.1)
html-attributes-utils (~> 1)
govuk_markdown (2.0.0)
govuk_markdown (2.0.1)
activesupport
redcarpet
hashdiff (1.0.1)
hashdiff (1.1.0)
html-attributes-utils (1.0.2)
activesupport (>= 6.1.4.4)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
iniparse (1.5.0)
jmespath (1.6.2)
jsbundling-rails (1.1.1)
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
json-schema (3.0.0)
json-schema (4.1.1)
addressable (>= 2.8)
jwt (2.7.0)
jwt (2.8.0)
base64
launchy (2.5.2)
addressable (~> 2.8)
listen (3.8.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.22.0)
@ -212,16 +216,16 @@ GEM
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.5)
minitest (5.20.0)
msgpack (1.6.0)
net-imap (0.3.7)
minitest (5.22.2)
msgpack (1.7.2)
net-imap (0.4.10)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.1)
net-protocol (0.2.2)
timeout
net-smtp (0.3.3)
net-smtp (0.4.0.1)
net-protocol
nio4r (2.7.0)
nokogiri (1.16.2-arm64-darwin)
@ -230,30 +234,31 @@ GEM
racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux)
racc (~> 1.4)
notifications-ruby-client (5.4.0)
notifications-ruby-client (6.0.0)
jwt (>= 1.5, < 3)
orm_adapter (0.5.0)
overcommit (0.60.0)
childprocess (>= 0.6.3, < 5)
overcommit (0.63.0)
childprocess (>= 0.6.3, < 6)
iniparse (~> 1.4)
rexml (~> 3.2)
pagy (6.5.0)
paper_trail (14.0.0)
activerecord (>= 6.0)
paper_trail (15.1.0)
activerecord (>= 6.1)
request_store (~> 1.4)
paper_trail-globalid (0.2.0)
globalid
paper_trail (>= 3.0.0)
parallel (1.22.1)
parallel_tests (4.2.0)
parallel (1.24.0)
parallel_tests (4.5.1)
parallel
parser (3.2.1.0)
parser (3.3.0.5)
ast (~> 2.4.1)
pg (1.4.5)
racc
pg (1.5.5)
possessive (1.0.1)
postcodes_io (0.4.0)
excon (~> 0.39)
propshaft (0.6.4)
propshaft (0.8.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
@ -264,34 +269,34 @@ GEM
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
public_suffix (5.0.1)
public_suffix (5.0.4)
puma (5.6.8)
nio4r (~> 2.0)
pundit (2.3.0)
pundit (2.3.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack (2.2.8.1)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.7.2)
actioncable (= 7.0.7.2)
actionmailbox (= 7.0.7.2)
actionmailer (= 7.0.7.2)
actionpack (= 7.0.7.2)
actiontext (= 7.0.7.2)
actionview (= 7.0.7.2)
activejob (= 7.0.7.2)
activemodel (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
rails (7.0.8.1)
actioncable (= 7.0.8.1)
actionmailbox (= 7.0.8.1)
actionmailer (= 7.0.8.1)
actionpack (= 7.0.8.1)
actiontext (= 7.0.8.1)
actionview (= 7.0.8.1)
activejob (= 7.0.8.1)
activemodel (= 7.0.8.1)
activerecord (= 7.0.8.1)
activestorage (= 7.0.8.1)
activesupport (= 7.0.8.1)
bundler (>= 1.15.0)
railties (= 7.0.7.2)
railties (= 7.0.8.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@ -299,51 +304,51 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.7.2)
actionpack (= 7.0.7.2)
activesupport (= 7.0.7.2)
railties (7.0.8.1)
actionpack (= 7.0.8.1)
activesupport (= 7.0.8.1)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rake (13.1.0)
randexp (0.1.7)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.17.0)
redis-client (0.20.0)
connection_pool
regexp_parser (2.7.0)
request_store (1.5.1)
regexp_parser (2.9.0)
request_store (1.6.0)
rack (>= 1.4)
responders (3.1.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.2.5)
roo (2.10.0)
rexml (3.2.6)
roo (2.10.1)
nokogiri (~> 1)
rubyzip (>= 1.3.0, < 3.0.0)
rotp (6.2.2)
rspec-core (3.12.1)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
rotp (6.3.0)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.3)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
rspec-support (~> 3.13.0)
rspec-rails (6.1.1)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.11)
rspec-expectations (~> 3.11)
rspec-mocks (~> 3.11)
rspec-support (~> 3.11)
rspec-support (3.12.0)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.13.1)
rubocop (1.25.0)
parallel (~> 1.10)
parser (>= 3.1.0.0)
@ -361,7 +366,7 @@ GEM
rubocop-rails (= 2.13.2)
rubocop-rake (= 0.6.0)
rubocop-rspec (= 2.7.0)
rubocop-performance (1.16.0)
rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.13.2)
@ -372,26 +377,28 @@ GEM
rubocop (~> 1.0)
rubocop-rspec (2.7.0)
rubocop (~> 1.19)
ruby-progressbar (1.11.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
selenium-webdriver (4.8.1)
selenium-webdriver (4.18.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sentry-rails (5.8.0)
sentry-rails (5.16.1)
railties (>= 5.0)
sentry-ruby (~> 5.8.0)
sentry-ruby (5.8.0)
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (7.1.3)
sidekiq (7.2.2)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
sidekiq-cron (1.9.1)
redis-client (>= 0.19.0)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
sidekiq (>= 4.2.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@ -399,39 +406,39 @@ GEM
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
stimulus-rails (1.2.1)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
thor (1.2.2)
timecop (0.9.6)
timeout (0.4.0)
thor (1.3.0)
timecop (0.9.8)
timeout (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uk_postcode (2.1.8)
unicode-display_width (2.4.2)
unread (0.13.0)
unicode-display_width (2.5.0)
unread (0.13.1)
activerecord (>= 6.1)
view_component (3.9.0)
view_component (3.10.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.18.1)
webmock (3.23.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.9)
websocket (1.2.10)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.11)
zeitwerk (2.6.13)
PLATFORMS
arm64-darwin-21

2
app/models/form/lettings/questions/person_working_situation.rb

@ -12,8 +12,8 @@ class Form::Lettings::Questions::PersonWorkingSituation < ::Form::Question
end
ANSWER_OPTIONS = {
"2" => { "value" => "Part-time – Less than 30 hours" },
"1" => { "value" => "Full-time – 30 hours or more" },
"2" => { "value" => "Part-time – Less than 30 hours" },
"7" => { "value" => "Full-time student" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::Age1 < ::Form::Question
def initialize(id, hsh, page)
super
@id = "age1"
@check_answer_label = "Lead buyer’s age"
@check_answer_label = "Buyer 1’s age"
@header = "Age"
@type = "numeric"
@width = 2

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

@ -2,7 +2,7 @@ class Form::Sales::Questions::Buyer1AgeKnown < ::Form::Question
def initialize(id, hsh, page)
super
@id = "age1_known"
@check_answer_label = "Lead buyer’s age"
@check_answer_label = "Buyer 1’s age"
@header = "Do you know buyer 1’s age?"
@type = "radio"
@answer_options = ANSWER_OPTIONS

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

@ -18,8 +18,8 @@ class Form::Sales::Questions::Buyer1WorkingSituation < ::Form::Question
end
ANSWER_OPTIONS = {
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

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

@ -17,8 +17,8 @@ class Form::Sales::Questions::Buyer2WorkingSituation < ::Form::Question
end
ANSWER_OPTIONS = {
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

9
app/models/form/sales/questions/number_joint_buyers.rb

@ -4,7 +4,6 @@ class Form::Sales::Questions::NumberJointBuyers < ::Form::Question
@id = "jointmore"
@check_answer_label = "More than 2 joint buyers"
@header = "Are there more than 2 joint buyers of this property?"
@hint_text = "You should still try to answer all questions even if the buyer wasn't interviewed in person"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@question_number = 10
@ -15,4 +14,12 @@ class Form::Sales::Questions::NumberJointBuyers < ::Form::Question
"2" => { "value" => "No" },
"3" => { "value" => "Don’t know" },
}.freeze
def hint_text
if form.start_year_after_2024?
nil
else
"You should still try to answer all questions even if the buyer wasn't interviewed in person"
end
end
end

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

@ -16,8 +16,8 @@ class Form::Sales::Questions::PersonWorkingSituation < ::Form::Question
end
ANSWER_OPTIONS = {
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

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

@ -6,7 +6,7 @@ class Form::Sales::Questions::PropertyNumberOfBedrooms < ::Form::Question
@header = "How many bedrooms does the property have?"
@hint_text = "A bedsit has 1 bedroom"
@type = "numeric"
@width = 10
@width = 2
@min = 1
@max = 9
@step = 1

1
app/models/log.rb

@ -179,6 +179,7 @@ class Log < ApplicationRecord
def blank_compound_invalid_non_setup_fields!
self.ppcodenk = nil if errors.attribute_names.include? :ppostcode_full
self.previous_la_known = nil if errors.attribute_names.include? :prevloc
if errors.of_kind?(:uprn, :uprn_error)
self.uprn_known = nil

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

@ -89,9 +89,11 @@ module Validations::Sales::SoftValidations
return unless cashdis || !is_type_discount?
return unless deposit && value && equity
cash_discount = cashdis || 0
mortgage_value = mortgage || 0
mortgage_value + deposit + cash_discount != value * equity / 100
!within_tolerance?(mortgage_deposit_and_discount_total, value * equity / 100, 1)
end
def within_tolerance?(expected, actual, tolerance)
(expected - actual).abs <= tolerance
end
def mortgage_plus_deposit_less_than_discounted_value?

21
app/services/bulk_upload/lettings/year2023/row_parser.rb

@ -391,6 +391,7 @@ class BulkUpload::Lettings::Year2023::RowParser
validate :validate_correct_intermediate_rent_type, on: :after_log, if: proc { renttype == :intermediate }
validate :validate_correct_affordable_rent_type, on: :after_log, if: proc { renttype == :affordable }
validate :validate_all_charges_given, on: :after_log, if: proc { is_carehome.zero? }
def self.question_for_field(field)
QUESTIONS[field]
@ -854,6 +855,20 @@ private
end
end
def validate_all_charges_given
return if supported_housing? && field_125 == 1
{ field_128: "basic rent",
field_129: "service charge",
field_130: "personal service charge",
field_131: "support charge",
field_132: "total charge" }.each do |field, charge|
if public_send(field.to_sym).blank?
errors.add(field, I18n.t("validations.financial.charges.missing_charges", question: charge))
end
end
end
def setup_question?(question)
log.form.setup_sections[0].subsections[0].questions.include?(question)
end
@ -1192,7 +1207,7 @@ private
attributes["supcharg"] = field_131
attributes["tcharge"] = field_132
attributes["chcharge"] = field_127
attributes["is_carehome"] = field_127.present? ? 1 : 0
attributes["is_carehome"] = is_carehome
attributes["household_charge"] = supported_housing? ? field_125 : nil
attributes["hbrentshortfall"] = field_133
attributes["tshortfall_known"] = tshortfall_known
@ -1574,4 +1589,8 @@ private
0
end
end
def is_carehome
field_127.present? ? 1 : 0
end
end

20
app/services/bulk_upload/lettings/year2024/row_parser.rb

@ -382,6 +382,7 @@ class BulkUpload::Lettings::Year2024::RowParser
validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? }
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_all_charges_given, on: :after_log, if: proc { is_carehome.zero? }
def self.question_for_field(field)
QUESTIONS[field]
@ -810,6 +811,19 @@ private
end
end
def validate_all_charges_given
return if supported_housing? && field_125 == 1
{ field_125: "basic rent",
field_126: "service charge",
field_127: "personal service charge",
field_128: "support charge" }.each do |field, charge|
if public_send(field.to_sym).blank?
errors.add(field, I18n.t("validations.financial.charges.missing_charges", question: charge))
end
end
end
def setup_question?(question)
log.form.setup_sections[0].subsections[0].questions.include?(question)
end
@ -1152,7 +1166,7 @@ private
attributes["pscharge"] = field_127
attributes["supcharg"] = field_128
attributes["chcharge"] = field_124
attributes["is_carehome"] = field_124.present? ? 1 : 0
attributes["is_carehome"] = is_carehome
attributes["household_charge"] = supported_housing? ? field_122 : nil
attributes["hbrentshortfall"] = field_129
attributes["tshortfall_known"] = tshortfall_known
@ -1479,4 +1493,8 @@ private
12
end
def is_carehome
field_124.present? ? 1 : 0
end
end

1
config/locales/en.yml

@ -418,6 +418,7 @@ en:
above_hard_max: "Rent is higher than the absolute maximum expected for a property of this type based on this period"
charges:
complete_1_of_3: "Answer either the ‘household rent and charges’ question or ‘is this accommodation a care home‘, or select ‘no’ for ‘does the household pay rent or charges for the accommodation?’"
missing_charges: "Please enter the %{question}. If there is no %{question}, please enter '0'."
tcharge:
under_10: "Enter a total charge that is at least £10.00 per week"
rent_period:

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

@ -20,7 +20,7 @@ RSpec.describe Form::Sales::Questions::Age1, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Lead buyer’s age")
expect(question.check_answer_label).to eq("Buyer 1’s age")
end
it "has the correct type" do

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

@ -20,7 +20,7 @@ RSpec.describe Form::Sales::Questions::Buyer1AgeKnown, type: :model do
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Lead buyer’s age")
expect(question.check_answer_label).to eq("Buyer 1’s age")
end
it "has the correct type" do

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

@ -33,8 +33,8 @@ RSpec.describe Form::Sales::Questions::Buyer1WorkingSituation, type: :model do
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

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

@ -37,8 +37,8 @@ RSpec.describe Form::Sales::Questions::Buyer2WorkingSituation, type: :model do
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

16
spec/models/form/sales/questions/number_joint_buyers_spec.rb

@ -6,6 +6,12 @@ RSpec.describe Form::Sales::Questions::NumberJointBuyers, type: :model do
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:subsection) { instance_double(Form::Subsection) }
before do
allow(page).to receive(:subsection).and_return(subsection)
allow(subsection).to receive(:form).and_return(instance_double(Form, start_year_after_2024?: false))
end
it "has correct page" do
expect(question.page).to eq(page)
@ -42,4 +48,14 @@ RSpec.describe Form::Sales::Questions::NumberJointBuyers, type: :model do
"3" => { "value" => "Don’t know" },
})
end
context "with 2024 form" do
before do
allow(subsection).to receive(:form).and_return(instance_double(Form, start_year_after_2024?: true))
end
it "has no hint_text" do
expect(question.hint_text).to be_nil
end
end
end

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

@ -22,8 +22,8 @@ RSpec.describe Form::Sales::Questions::PersonWorkingSituation, type: :model do
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"2" => { "value" => "Part-time - Less than 30 hours" },
"1" => { "value" => "Full-time - 30 hours or more" },
"2" => { "value" => "Part-time - Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"6" => { "value" => "Not seeking work" },

20
spec/models/lettings_log_spec.rb

@ -3382,26 +3382,6 @@ RSpec.describe LettingsLog do
end
end
describe "#blank_invalid_non_setup_fields!" do
context "when a setup field is invalid" do
subject(:model) { described_class.new(needstype: 404) }
it "does not blank it" do
model.valid?
expect { model.blank_invalid_non_setup_fields! }.not_to change(model, :needstype)
end
end
context "when a non setup field is invalid" do
subject(:model) { build(:lettings_log, :completed, offered: 234) }
it "blanks it" do
model.valid?
expect { model.blank_invalid_non_setup_fields! }.to change(model, :offered)
end
end
end
describe "#beds_for_la_rent_range" do
context "when beds nil" do
let(:lettings_log) { build(:lettings_log, beds: nil) }

50
spec/models/log_spec.rb

@ -27,4 +27,54 @@ RSpec.describe Log, type: :model do
expect(in_progress_lettings_log.calculate_status).to eq "in_progress"
end
end
describe "#blank_invalid_non_setup_fields!" do
context "when a setup field is invalid for a lettings log" do
subject(:model) { build(:lettings_log, needstype: 404) }
it "does not blank it" do
model.valid?
expect { model.blank_invalid_non_setup_fields! }.not_to change(model, :needstype)
end
end
context "when a setup field is invalid for a sales log" do
subject(:model) { build(:sales_log, companybuy: 404) }
it "does not blank it" do
model.valid?
expect { model.blank_invalid_non_setup_fields! }.not_to change(model, :companybuy)
end
end
context "when a non setup field is invalid for a lettings log" do
subject(:model) { build(:lettings_log, :completed, offered: 234) }
it "blanks it" do
model.valid?
model.blank_invalid_non_setup_fields!
expect(model.offered).to be_nil
end
end
context "when a non setup field is invalid for a sales log" do
subject(:model) { build(:sales_log, :completed, age1: 10) }
it "blanks it" do
model.valid?
model.blank_invalid_non_setup_fields!
expect(model.age1).to be_nil
end
end
context "when prevloc is invalid for a lettings log" do
subject(:model) { build(:lettings_log, :completed, previous_la_known: 1, prevloc: nil) }
it "blanks previous_la_known" do
model.valid?
model.blank_invalid_non_setup_fields!
expect(model.previous_la_known).to be_nil
end
end
end
end

23
spec/models/validations/sales/soft_validations_spec.rb

@ -415,6 +415,17 @@ RSpec.describe Validations::Sales::SoftValidations do
.not_to be_shared_ownership_deposit_invalid
end
it "returns false if MORTGAGE + DEPOSIT + CASHDIS are within 1£ of VALUE * EQUITY/100" do
record.mortgage = 500
record.deposit = 500
record.cashdis = 500
record.value = 3001
record.equity = 50
expect(record)
.not_to be_shared_ownership_deposit_invalid
end
it "returns false if mortgage is used and no mortgage is given" do
record.mortgage = nil
record.deposit = 1000
@ -473,6 +484,18 @@ RSpec.describe Validations::Sales::SoftValidations do
.to be_shared_ownership_deposit_invalid
end
it "returns false if no cashdis not routed to and MORTGAGE + DEPOSIT are within 1£ of VALUE * EQUITY/100" do
record.mortgage = 500
record.deposit = 500
record.type = 2
record.cashdis = nil
record.value = 1999
record.equity = 50
expect(record)
.not_to be_shared_ownership_deposit_invalid
end
it "returns false if no value is given" do
record.mortgage = 1000
record.deposit = 1000

14
spec/requests/duplicate_logs_controller_spec.rb

@ -169,7 +169,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 3)
expect(page).to have_content("Q2 - Purchaser code", count: 3)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 3)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 3)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 3)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 3)
expect(page).to have_content("Q15 - Postcode", count: 3)
@ -187,7 +187,7 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("Q1 - Sale completion date", count: 3)
expect(page).to have_content("Q2 - Purchaser code", count: 3)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 3)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 3)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 3)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 3)
expect(page).to have_content("Postcode (from UPRN)", count: 3)
@ -215,7 +215,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 1)
expect(page).to have_content("Q2 - Purchaser code", count: 1)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 1)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 1)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 1)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 1)
expect(page).to have_content("Q15 - Postcode", count: 1)
@ -241,7 +241,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 1)
expect(page).to have_content("Q2 - Purchaser code", count: 1)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 1)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 1)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 1)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 1)
expect(page).to have_content("Q15 - Postcode", count: 1)
@ -379,7 +379,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 3)
expect(page).to have_content("Q2 - Purchaser code", count: 3)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 3)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 3)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 3)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 3)
expect(page).to have_content("Q15 - Postcode", count: 3)
@ -407,7 +407,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 1)
expect(page).to have_content("Q2 - Purchaser code", count: 1)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 1)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 1)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 1)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 1)
expect(page).to have_content("Q15 - Postcode", count: 1)
@ -433,7 +433,7 @@ RSpec.describe DuplicateLogsController, type: :request do
it "displays check your answers for each log with correct questions" do
expect(page).to have_content("Q1 - Sale completion date", count: 1)
expect(page).to have_content("Q2 - Purchaser code", count: 1)
expect(page).to have_content("Q20 - Lead buyer’s age", count: 1)
expect(page).to have_content("Q20 - Buyer 1’s age", count: 1)
expect(page).to have_content("Q21 - Buyer 1’s gender identity", count: 1)
expect(page).to have_content("Q25 - Buyer 1's working situation", count: 1)
expect(page).to have_content("Q15 - Postcode", count: 1)

105
spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb

@ -2294,19 +2294,76 @@ RSpec.describe BulkUpload::Lettings::Year2023::RowParser do
end
describe "#chcharge" do
let(:attributes) { { bulk_upload:, field_127: "123.45" } }
let(:attributes) { { bulk_upload:, field_127: "123.45", field_131: "123.45", field_130: "123.45", field_129: "123.45", field_128: "123.45" } }
it "sets value given" do
expect(parser.log.chcharge).to eq(123.45)
end
it "sets is care home to yes" do
expect(parser.log.is_carehome).to eq(1)
end
it "clears any other given charges" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
end
describe "#tcharge" do
let(:attributes) { { bulk_upload:, field_132: "123.45" } }
let(:attributes) { { bulk_upload:, field_132: "123.45", field_127: "123.45", field_128: "123.45", field_129: "123.45", field_130: "123.45", field_131: "123.45" } }
it "sets value given" do
expect(parser.log.tcharge).to eq(123.45)
end
context "when other charges are not given" do
context "and it is carehome" do
let(:attributes) { { bulk_upload:, field_132: "123.45", field_127: "123.45", field_128: nil, field_129: nil, field_130: nil, field_131: nil } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "does not add errors to missing charges" do
parser.valid?
expect(parser.errors[:field_128]).to be_empty
expect(parser.errors[:field_129]).to be_empty
expect(parser.errors[:field_130]).to be_empty
expect(parser.errors[:field_131]).to be_empty
end
end
context "and it is not carehome" do
let(:attributes) { { bulk_upload:, field_132: "123.45", field_127: nil, field_128: nil, field_129: nil, field_130: nil, field_131: nil } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "adds an error to all missing charges" do
parser.valid?
expect(parser.errors[:field_128]).to eql(["Please enter the basic rent. If there is no basic rent, please enter '0'."])
expect(parser.errors[:field_129]).to eql(["Please enter the service charge. If there is no service charge, please enter '0'."])
expect(parser.errors[:field_130]).to eql(["Please enter the personal service charge. If there is no personal service charge, please enter '0'."])
expect(parser.errors[:field_131]).to eql(["Please enter the support charge. If there is no support charge, please enter '0'."])
end
end
end
end
describe "#supcharg" do
@ -2315,6 +2372,50 @@ RSpec.describe BulkUpload::Lettings::Year2023::RowParser do
it "sets value given" do
expect(parser.log.supcharg).to eq(123.45)
end
context "when other charges are not given" do
context "and it is carehome" do
let(:attributes) { { bulk_upload:, field_132: nil, field_127: "123.45", field_128: nil, field_129: nil, field_130: nil, field_131: "123.45" } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "does not add errors to missing charges" do
parser.valid?
expect(parser.errors[:field_128]).to be_empty
expect(parser.errors[:field_129]).to be_empty
expect(parser.errors[:field_130]).to be_empty
expect(parser.errors[:field_131]).to be_empty
end
end
context "and it is not carehome" do
let(:attributes) { { bulk_upload:, field_132: "123.45", field_127: nil, field_128: nil, field_129: nil, field_130: nil, field_131: nil } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "adds an error to all missing charges" do
parser.valid?
expect(parser.errors[:field_128]).to eql(["Please enter the basic rent. If there is no basic rent, please enter '0'."])
expect(parser.errors[:field_129]).to eql(["Please enter the service charge. If there is no service charge, please enter '0'."])
expect(parser.errors[:field_130]).to eql(["Please enter the personal service charge. If there is no personal service charge, please enter '0'."])
expect(parser.errors[:field_131]).to eql(["Please enter the support charge. If there is no support charge, please enter '0'."])
end
end
end
end
describe "#pscharge" do

59
spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb

@ -2159,11 +2159,24 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
end
describe "#chcharge" do
let(:attributes) { { bulk_upload:, field_124: "123.45" } }
let(:attributes) { { bulk_upload:, field_124: "123.45", field_125: "123.45", field_126: "123.45", field_127: "123.45", field_128: "123.45" } }
it "sets value given" do
expect(parser.log.chcharge).to eq(123.45)
end
it "sets is care home to yes" do
expect(parser.log.is_carehome).to eq(1)
end
it "clears any other given charges" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
end
describe "#supcharg" do
@ -2172,6 +2185,50 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do
it "sets value given" do
expect(parser.log.supcharg).to eq(123.45)
end
context "when other charges are not given" do
context "and it is carehome" do
let(:attributes) { { bulk_upload:, field_128: "123.45", field_124: "123.45", field_125: nil, field_126: nil, field_127: nil } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "does not add errors to missing charges" do
parser.valid?
expect(parser.errors[:field_125]).to be_empty
expect(parser.errors[:field_126]).to be_empty
expect(parser.errors[:field_127]).to be_empty
expect(parser.errors[:field_128]).to be_empty
end
end
context "and it is not carehome" do
let(:attributes) { { bulk_upload:, field_128: "123.45", field_124: nil, field_125: nil, field_126: nil, field_127: nil } }
it "does not set charges values" do
parser.log.save!
expect(parser.log.tcharge).to be_nil
expect(parser.log.brent).to be_nil
expect(parser.log.supcharg).to be_nil
expect(parser.log.pscharge).to be_nil
expect(parser.log.scharge).to be_nil
end
it "adds an error to all missing charges" do
parser.valid?
expect(parser.errors[:field_125]).to eql(["Please enter the basic rent. If there is no basic rent, please enter '0'."])
expect(parser.errors[:field_126]).to eql(["Please enter the service charge. If there is no service charge, please enter '0'."])
expect(parser.errors[:field_127]).to eql(["Please enter the personal service charge. If there is no personal service charge, please enter '0'."])
expect(parser.errors[:field_128]).to be_empty
end
end
end
end
describe "#pscharge" do

Loading…
Cancel
Save