Browse Source

Merge branch 'main' into CLDC-4114-update-no-dsa-signing-warning

CLDC-4114-update-no-dsa-signing-warning
Samuel Young 2 weeks ago committed by GitHub
parent
commit
3b6f06a4fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .github/workflows/production_pipeline.yml
  2. 23
      .github/workflows/review_app_hint.yml
  3. 30
      .github/workflows/review_deploy.yml
  4. 3
      .github/workflows/review_deploy_image_only.yml
  5. 3
      .github/workflows/review_teardown_pipeline.yml
  6. 45
      Gemfile.lock
  7. 23
      app/models/derived_variables/lettings_log_variables.rb
  8. 10
      app/models/form.rb
  9. 5
      app/models/form/lettings/pages/person_lead_partner.rb
  10. 6
      app/models/form/lettings/questions/person_partner.rb
  11. 6
      app/models/form/lettings/subsections/household_characteristics.rb
  12. 4
      app/models/form/question.rb
  13. 10
      app/models/lettings_log.rb
  14. 21
      app/models/sales_log.rb
  15. 6
      app/services/bulk_upload/sales/year2025/row_parser.rb
  16. 6
      app/services/bulk_upload/sales/year2026/row_parser.rb
  17. 2
      app/services/feature_toggle.rb
  18. 1
      spec/factories/sales_log.rb
  19. 72
      spec/features/form/page_routing_spec.rb
  20. 10
      spec/features/sales_log_spec.rb
  21. 6
      spec/fixtures/files/lettings_log_csv_export_codes_26.csv
  22. 6
      spec/fixtures/files/lettings_log_csv_export_labels_26.csv
  23. 6
      spec/models/form/lettings/pages/person_lead_partner_spec.rb
  24. 86
      spec/models/form/lettings/questions/person_partner_spec.rb
  25. 7
      spec/models/form/lettings/subsections/household_characteristics_spec.rb
  26. 37
      spec/models/form_spec.rb
  27. 319
      spec/models/lettings_log_derived_fields_spec.rb
  28. 18
      spec/models/lettings_log_spec.rb
  29. 18
      spec/requests/duplicate_logs_controller_spec.rb
  30. 2
      spec/services/bulk_upload/sales/validator_spec.rb
  31. 4
      spec/services/bulk_upload/sales/year2025/row_parser_spec.rb
  32. 4
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

1
.github/workflows/production_pipeline.yml

@ -3,7 +3,6 @@ name: Production CI/CD Pipeline
on: on:
release: release:
types: [released] types: [released]
workflow_dispatch:
defaults: defaults:
run: run:

23
.github/workflows/review_app_hint.yml

@ -0,0 +1,23 @@
name: "Review App: PR Hint Comment"
on:
pull_request:
types: [opened]
jobs:
hint:
name: Add review app hint
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: 'To deploy a review app for this PR, add the `review-app` label.',
});

30
.github/workflows/review_pipeline.yml → .github/workflows/review_deploy.yml

@ -1,4 +1,4 @@
name: Review app pipeline name: "Review App: Deploy"
concurrency: concurrency:
group: review-${{ github.event.pull_request.number }} group: review-${{ github.event.pull_request.number }}
@ -6,9 +6,9 @@ concurrency:
on: on:
pull_request: pull_request:
types: types:
- opened
- synchronize - synchronize
- reopened - reopened
- labeled
workflow_dispatch: workflow_dispatch:
defaults: defaults:
@ -18,6 +18,7 @@ defaults:
jobs: jobs:
infra: infra:
name: Deploy review app infrastructure name: Deploy review app infrastructure
if: contains(github.event.pull_request.labels.*.name, 'review-app')
uses: communitiesuk/submit-social-housing-lettings-and-sales-data-infrastructure/.github/workflows/create_review_app_infra.yml@main uses: communitiesuk/submit-social-housing-lettings-and-sales-data-infrastructure/.github/workflows/create_review_app_infra.yml@main
with: with:
key: ${{ github.event.pull_request.number }} key: ${{ github.event.pull_request.number }}
@ -27,6 +28,7 @@ jobs:
code: code:
name: Deploy review app code name: Deploy review app code
if: contains(github.event.pull_request.labels.*.name, 'review-app')
needs: [infra] needs: [infra]
uses: ./.github/workflows/aws_deploy.yml uses: ./.github/workflows/aws_deploy.yml
with: with:
@ -40,6 +42,7 @@ jobs:
comment: comment:
name: Add link to PR name: Add link to PR
if: contains(github.event.pull_request.labels.*.name, 'review-app')
needs: [code] needs: [code]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -48,10 +51,21 @@ jobs:
steps: steps:
- name: Comment on PR with URL - name: Comment on PR with URL
uses: unsplash/comment-on-pr@v1.3.0 uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
msg: "Created review app at https://review.submit-social-housing-data.communities.gov.uk/${{ github.event.pull_request.number }}. Note that the review app will be automatically deprovisioned after 30 days and will need the review app pipeline running again." script: |
check_for_duplicate_msg: true const prNumber = context.issue.number;
duplicate_msg_pattern: Created review app at* const msg = `Created review app at https://review.submit-social-housing-data.communities.gov.uk/${prNumber}. Note that the review app will be automatically deprovisioned after 30 days and will need the review app pipeline running again.`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
if (!comments.find(c => c.body.startsWith('Created review app at'))) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: msg,
});
}

3
.github/workflows/manual_review_code_pipeline.yml → .github/workflows/review_deploy_image_only.yml

@ -1,4 +1,5 @@
name: Manual review app code pipeline # Pushes the Docker image to a review app, for use when the ECS image has expired.
name: "Review App: Deploy (ECS Image only)"
concurrency: concurrency:
group: review-${{ inputs.review_app_key }} group: review-${{ inputs.review_app_key }}

3
.github/workflows/review_teardown_pipeline.yml

@ -1,4 +1,4 @@
name: Review app teardown pipeline name: "Review App: Teardown"
concurrency: concurrency:
group: review-${{ github.event.pull_request.number }} group: review-${{ github.event.pull_request.number }}
@ -7,7 +7,6 @@ on:
pull_request: pull_request:
types: types:
- closed - closed
workflow_dispatch:
env: env:
app_repo_role: arn:aws:iam::815624722760:role/core-application-repo app_repo_role: arn:aws:iam::815624722760:role/core-application-repo

45
Gemfile.lock

@ -112,7 +112,7 @@ GEM
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
base64 (0.3.0) base64 (0.3.0)
bcrypt (3.1.20) bcrypt (3.1.21)
benchmark (0.5.0) benchmark (0.5.0)
better_html (2.2.0) better_html (2.2.0)
actionview (>= 7.0) actionview (>= 7.0)
@ -160,13 +160,13 @@ GEM
cssbundling-rails (1.4.0) cssbundling-rails (1.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
csv (3.3.2) csv (3.3.2)
date (3.4.1) date (3.5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
devise (4.9.3) devise (5.0.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 7.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise_two_factor_authentication (3.0.0) devise_two_factor_authentication (3.0.0)
@ -184,6 +184,7 @@ GEM
drb (2.2.3) drb (2.2.3)
dumb_delegator (1.0.0) dumb_delegator (1.0.0)
encryptor (3.0.0) encryptor (3.0.0)
erb (6.0.2)
erb_lint (0.9.0) erb_lint (0.9.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
@ -236,9 +237,10 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ice_nine (0.11.2) ice_nine (0.11.2)
iniparse (1.5.0) iniparse (1.5.0)
io-console (0.8.0) io-console (0.8.2)
irb (1.15.1) irb (1.17.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
@ -269,7 +271,7 @@ GEM
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)
logger (1.7.0) logger (1.7.0)
loofah (2.25.0) loofah (2.25.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@ -329,7 +331,7 @@ GEM
racc racc
pg (1.5.5) pg (1.5.5)
possessive (1.0.1) possessive (1.0.1)
pp (0.6.2) pp (0.6.3)
prettyprint prettyprint
prettyprint (0.2.0) prettyprint (0.2.0)
prism (1.9.0) prism (1.9.0)
@ -344,7 +346,7 @@ GEM
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)
psych (5.2.3) psych (5.3.1)
date date
stringio stringio
public_suffix (5.0.4) public_suffix (5.0.4)
@ -364,7 +366,7 @@ GEM
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.2.0) rack-test (2.2.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.3.1)
rack (>= 3) rack (>= 3)
rails (7.2.2.2) rails (7.2.2.2)
actioncable (= 7.2.2.2) actioncable (= 7.2.2.2)
@ -384,8 +386,8 @@ GEM
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.7.0)
loofah (~> 2.21) loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails_admin (3.3.0) rails_admin (3.3.0)
activemodel-serializers-xml (>= 1.0) activemodel-serializers-xml (>= 1.0)
@ -403,25 +405,27 @@ GEM
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.3.1)
randexp (0.1.7) randexp (0.1.7)
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)
rdoc (6.12.0) rdoc (7.2.0)
erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort
redcarpet (3.6.0) redcarpet (3.6.0)
redis (4.8.1) redis (4.8.1)
redis-client (0.22.1) redis-client (0.22.1)
connection_pool connection_pool
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.0) reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.7.0) request_store (1.7.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.2.0)
actionpack (>= 5.2) actionpack (>= 7.0)
railties (>= 5.2) railties (>= 7.0)
rexml (3.4.4) rexml (3.4.4)
roo (2.10.1) roo (2.10.1)
nokogiri (~> 1) nokogiri (~> 1)
@ -519,11 +523,12 @@ GEM
smart_properties (1.17.0) smart_properties (1.17.0)
stimulus-rails (1.3.3) stimulus-rails (1.3.3)
railties (>= 6.0.0) railties (>= 6.0.0)
stringio (3.1.5) stringio (3.2.0)
thor (1.4.0) thor (1.4.0)
thread_safe (0.3.6) thread_safe (0.3.6)
timecop (0.9.8) timecop (0.9.8)
timeout (0.4.3) timeout (0.4.3)
tsort (0.2.0)
turbo-rails (2.0.13) turbo-rails (2.0.13)
actionpack (>= 7.1.0) actionpack (>= 7.1.0)
railties (>= 7.1.0) railties (>= 7.1.0)
@ -563,7 +568,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.18) zeitwerk (2.7.5)
PLATFORMS PLATFORMS
arm64-darwin arm64-darwin

23
app/models/derived_variables/lettings_log_variables.rb

@ -75,6 +75,10 @@ module DerivedVariables::LettingsLogVariables
self.beds = nil self.beds = nil
end end
if form.start_year_2026_or_later?
infer_at_most_one_relationship!
end
clear_child_constraints_for_age_changes! clear_child_constraints_for_age_changes!
child_under_16_constraints! child_under_16_constraints!
@ -325,6 +329,25 @@ private
end end
end end
def infer_at_most_one_relationship!
new_partner_numbers = partner_numbers.select { |i| public_send("relat#{i}_changed?") }
return unless new_partner_numbers.any?
infer_only_partner!(new_partner_numbers.first)
end
def infer_only_partner!(partner_number)
return unless hhmemb
(2..hhmemb).each do |i|
next if i == partner_number
if ["P", nil].include?(public_send("relat#{i}"))
self["relat#{i}"] = "X"
end
end
end
def household_type def household_type
return unless totelder && totadult && totchild return unless totelder && totadult && totchild

10
app/models/form.rb

@ -108,7 +108,8 @@ class Form
return :check_answers if next_page.nil? return :check_answers if next_page.nil?
return next_page.id if next_page.routed_to?(log, current_user) && return next_page.id if next_page.routed_to?(log, current_user) &&
(!ignore_answered || next_page.has_unanswered_questions?(log)) (!ignore_answered || next_page.has_unanswered_questions?(log)) &&
next_page.questions.any? { |question| !question.skip_question_in_form_flow?(log) }
next_page_id(next_page, log, current_user, ignore_answered:) next_page_id(next_page, log, current_user, ignore_answered:)
end end
@ -323,7 +324,12 @@ class Form
if value.is_a?(Hash) && value.key?("operator") if value.is_a?(Hash) && value.key?("operator")
operator = value["operator"] operator = value["operator"]
operand = value["operand"] operand = value["operand"]
log[question]&.send(operator, operand)
if operator == "!=" # This branch is needed as `nil` does not behave as expected with the default logic (`nil&.send("!=", nil)` => `nil` i.e., `false`).
log[question] != operand
else
log[question]&.send(operator, operand)
end
else else
parts = question.split(".") parts = question.split(".")
log_value = send_chain(parts, log) log_value = send_chain(parts, log)

5
app/models/form/lettings/pages/person_lead_partner.rb

@ -19,7 +19,10 @@ class Form::Lettings::Pages::PersonLeadPartner < ::Form::Page
"operand" => 16, "operand" => 16,
}, },
}, },
{ "details_known_#{@person_index}" => 0, "age#{@person_index}" => nil }, {
"details_known_#{@person_index}" => 0,
"age#{@person_index}" => nil,
},
] ]
else else
[{ "details_known_#{@person_index}" => 0 }] [{ "details_known_#{@person_index}" => 0 }]

6
app/models/form/lettings/questions/person_partner.rb

@ -35,6 +35,10 @@ class Form::Lettings::Questions::PersonPartner < ::Form::Question
end end
def derived?(log) def derived?(log)
form.start_year_2026_or_later? && log.is_partner_inferred?(@person_index) form.start_year_2026_or_later? && log.is_person_under_16?(@person_index)
end
def skip_question_in_form_flow?(log)
form.start_year_2026_or_later? && log.is_any_person_partner?
end end
end end

6
app/models/form/lettings/subsections/household_characteristics.rb

@ -52,15 +52,15 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index:), Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index:),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?), (Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
relationship_question(person_index:), relationship_question(person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("relationship_#{person_index}_partner_under_16_value_check", nil, self, person_index:) if form.start_year_2024_or_later? && !form.start_year_2026_or_later?), (Form::Lettings::Pages::PartnerUnder16ValueCheck.new("relationship_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::MultiplePartnersValueCheck.new("relationship_#{person_index}_multiple_partners_value_check", nil, self, person_index:) if form.start_year_2024_or_later?), (Form::Lettings::Pages::MultiplePartnersValueCheck.new("relationship_#{person_index}_multiple_partners_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::FemalesInSoftAgeRangeInPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_person_age_#{person_index}_check", nil, self, person_index:) if form.start_year_2026_or_later?), (Form::Lettings::Pages::NoHouseholdMemberLikelyToBePregnantCheck.new("no_household_member_likely_to_be_pregnant_person_age_#{person_index}_check", nil, self, person_index:) if form.start_year_2026_or_later?),
Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("age_#{person_index}_under_retirement_value_check", nil, self, person_index:), Form::Lettings::Pages::PersonUnderRetirementValueCheck.new("age_#{person_index}_under_retirement_value_check", nil, self, person_index:),
Form::Lettings::Pages::PersonOverRetirementValueCheck.new("age_#{person_index}_over_retirement_value_check", nil, self, person_index:), Form::Lettings::Pages::PersonOverRetirementValueCheck.new("age_#{person_index}_over_retirement_value_check", nil, self, person_index:),
(Form::Lettings::Pages::PartnerUnder16ValueCheck.new("age_#{person_index}_partner_under_16_value_check", nil, self, person_index:) if form.start_year_2024_or_later? && !form.start_year_2026_or_later?), (Form::Lettings::Pages::PartnerUnder16ValueCheck.new("age_#{person_index}_partner_under_16_value_check", nil, self, person_index:) unless form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonSexRegisteredAtBirth.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?), (Form::Lettings::Pages::PersonSexRegisteredAtBirth.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderSameAsSex.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?), (Form::Lettings::Pages::PersonGenderSameAsSex.new(nil, nil, self, person_index:) if form.start_year_2026_or_later?),
(Form::Lettings::Pages::PersonGenderIdentity.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?), (Form::Lettings::Pages::PersonGenderIdentity.new(nil, nil, self, person_index:) unless form.start_year_2026_or_later?),

4
app/models/form/question.rb

@ -304,6 +304,10 @@ class Form::Question
nil nil
end end
def skip_question_in_form_flow?(_log)
false
end
private private
def selected_answer_option_is_derived?(log) def selected_answer_option_is_derived?(log)

10
app/models/lettings_log.rb

@ -445,10 +445,14 @@ class LettingsLog < Log
unittype_gn_changed? && unittype_gn_was == 2 unittype_gn_changed? && unittype_gn_was == 2
end end
def is_partner_inferred?(person_index) def is_person_under_16?(person_index)
public_send("age#{person_index}") && public_send("age#{person_index}") < 16 public_send("age#{person_index}") && public_send("age#{person_index}") < 16
end end
def is_any_person_partner?
!partner_numbers.empty?
end
def age_changed_from_below_16(person_index) def age_changed_from_below_16(person_index)
public_send("age#{person_index}_was") && public_send("age#{person_index}_was") < 16 public_send("age#{person_index}_was") && public_send("age#{person_index}_was") < 16
end end
@ -943,6 +947,10 @@ private
end end
end end
def partner_numbers
(2..8).select { |i| public_send("relat#{i}") == "P" }
end
def age_refused? def age_refused?
[age1_known, age2_known, age3_known, age4_known, age5_known, age6_known, age7_known, age8_known].any?(1) [age1_known, age2_known, age3_known, age4_known, age5_known, age6_known, age7_known, age8_known].any?(1)
end end

21
app/models/sales_log.rb

@ -18,6 +18,7 @@ class SalesLog < Log
include Validations::Sales::SoftValidations include Validations::Sales::SoftValidations
include Validations::SoftValidations include Validations::SoftValidations
include MoneyFormattingHelper include MoneyFormattingHelper
include CollectionTimeHelper
self.inheritance_column = :_type_disabled self.inheritance_column = :_type_disabled
@ -37,6 +38,8 @@ class SalesLog < Log
belongs_to :managing_organisation, class_name: "Organisation", optional: true belongs_to :managing_organisation, class_name: "Organisation", optional: true
scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) } scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) }
scope :filter_by_year_or_later, ->(year) { where("sales_logs.saledate >= ?", Time.zone.local(year.to_i, 4, 1)) }
scope :filter_by_year_or_earlier, ->(year) { where("sales_logs.saledate < ?", Time.zone.local(year.to_i + 1, 4, 1)) }
scope :filter_by_years_or_nil, lambda { |years, _user = nil| scope :filter_by_years_or_nil, lambda { |years, _user = nil|
first_year = years.shift first_year = years.shift
query = filter_by_year(first_year) query = filter_by_year(first_year)
@ -69,12 +72,14 @@ class SalesLog < Log
} }
scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) } scope :age1_answered, -> { where.not(age1: nil).or(where(age1_known: [1, 2])) }
scope :ecstat1_answered, -> { where.not(ecstat1: nil).or(where("saledate >= ?", Time.zone.local(2025, 4, 1))) } scope :ecstat1_answered, -> { where.not(ecstat1: nil).or(where("saledate >= ?", Time.zone.local(2025, 4, 1))) }
scope :sex1_answered, -> { where.not(sex1: nil).filter_by_year_or_earlier(2025).or(where.not(sexrab1: nil).filter_by_year_or_later(2026)) }
scope :address_answered, -> { where.not(postcode_full: nil).where.not(address_line1: nil).or(where.not(uprn: nil)) }
scope :duplicate_logs, lambda { |log| scope :duplicate_logs, lambda { |log|
visible.where(log.slice(*DUPLICATE_LOG_ATTRIBUTES)) visible.where(log.slice(*DUPLICATE_LOG_ATTRIBUTES))
.where.not(id: log.id) .where.not(id: log.id)
.where.not(saledate: nil) .where.not(saledate: nil)
.where.not(sex1: nil) .sex1_answered
.where.not(postcode_full: nil) .address_answered
.ecstat1_answered .ecstat1_answered
.age1_answered .age1_answered
} }
@ -84,8 +89,8 @@ class SalesLog < Log
scope = visible scope = visible
.group(*DUPLICATE_LOG_ATTRIBUTES) .group(*DUPLICATE_LOG_ATTRIBUTES)
.where.not(saledate: nil) .where.not(saledate: nil)
.where.not(sex1: nil) .sex1_answered
.where.not(postcode_full: nil) .address_answered
.age1_answered .age1_answered
.ecstat1_answered .ecstat1_answered
.having("COUNT(*) > 1") .having("COUNT(*) > 1")
@ -98,7 +103,7 @@ class SalesLog < Log
} }
OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze OPTIONAL_FIELDS = %w[purchid othtype buyers_organisations].freeze
DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 ecstat1 postcode_full].freeze DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id purchid saledate age1_known age1 sex1 sexrab1 ecstat1 postcode_full uprn address_line1].freeze
def lettings? def lettings?
false false
@ -538,10 +543,12 @@ class SalesLog < Log
["owning_organisation_id", ["owning_organisation_id",
"saledate", "saledate",
"purchid", "purchid",
"address_line1",
"postcode_full",
"uprn",
"age1", "age1",
"sex1",
"ecstat1", "ecstat1",
uprn.blank? ? "postcode_full" : "uprn"].compact form.start_year_2026_or_later? ? "sexrab1" : "sex1"].compact
end end
def soctenant_is_inferred? def soctenant_is_inferred?

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

@ -533,6 +533,8 @@ class BulkUpload::Sales::Year2025::RowParser
"field_2", # saledate "field_2", # saledate
"field_3", # saledate "field_3", # saledate
"field_7", # purchaser_code "field_7", # purchaser_code
"field_16", # uprn
"field_17", # address_line1
"field_21", # postcode "field_21", # postcode
"field_22", # postcode "field_22", # postcode
"field_28", # age1 "field_28", # age1
@ -1287,6 +1289,8 @@ private
ecstat1 ecstat1
owning_organisation owning_organisation
postcode_full postcode_full
uprn
address_line1
purchid purchid
] ]
end end
@ -1457,6 +1461,8 @@ private
errors.add(:field_1, error_message) # Sale completion date errors.add(:field_1, error_message) # Sale completion date
errors.add(:field_2, error_message) # Sale completion date errors.add(:field_2, error_message) # Sale completion date
errors.add(:field_3, error_message) # Sale completion date errors.add(:field_3, error_message) # Sale completion date
errors.add(:field_16, error_message) # UPRN
errors.add(:field_17, error_message) # Address line 1
errors.add(:field_21, error_message) # Postcode errors.add(:field_21, error_message) # Postcode
errors.add(:field_22, error_message) # Postcode errors.add(:field_22, error_message) # Postcode
errors.add(:field_28, error_message) # Buyer 1 age errors.add(:field_28, error_message) # Buyer 1 age

6
app/services/bulk_upload/sales/year2026/row_parser.rb

@ -582,6 +582,8 @@ class BulkUpload::Sales::Year2026::RowParser
"field_2", # saledate "field_2", # saledate
"field_3", # saledate "field_3", # saledate
"field_7", # purchaser_code "field_7", # purchaser_code
"field_16", # uprn
"field_17", # address_line1
"field_21", # postcode "field_21", # postcode
"field_22", # postcode "field_22", # postcode
"field_29", # age1 "field_29", # age1
@ -1386,6 +1388,8 @@ private
ecstat1 ecstat1
owning_organisation owning_organisation
postcode_full postcode_full
uprn
address_line1
purchid purchid
] ]
end end
@ -1556,6 +1560,8 @@ private
errors.add(:field_1, error_message) # Sale completion date errors.add(:field_1, error_message) # Sale completion date
errors.add(:field_2, error_message) # Sale completion date errors.add(:field_2, error_message) # Sale completion date
errors.add(:field_3, error_message) # Sale completion date errors.add(:field_3, error_message) # Sale completion date
errors.add(:field_16, error_message) # UPRN
errors.add(:field_17, error_message) # Address line 1
errors.add(:field_21, error_message) # Postcode errors.add(:field_21, error_message) # Postcode
errors.add(:field_22, error_message) # Postcode errors.add(:field_22, error_message) # Postcode
errors.add(:field_29, error_message) # Buyer 1 age errors.add(:field_29, error_message) # Buyer 1 age

2
app/services/feature_toggle.rb

@ -4,7 +4,7 @@ class FeatureToggle
end end
def self.bulk_upload_duplicate_log_check_enabled? def self.bulk_upload_duplicate_log_check_enabled?
!Rails.env.staging? true
end end
def self.upload_enabled? def self.upload_enabled?

1
spec/factories/sales_log.rb

@ -67,6 +67,7 @@ FactoryBot.define do
sexrab1 { "F" } sexrab1 { "F" }
sex1 { "F" } sex1 { "F" }
ecstat1 { 1 } ecstat1 { 1 }
address_line1 { "same address line 1" }
postcode_full { "A1 1AA" } postcode_full { "A1 1AA" }
noint { 2 } noint { 2 }
uprn_known { 0 } uprn_known { 0 }

72
spec/features/form/page_routing_spec.rb

@ -270,6 +270,78 @@ RSpec.describe "Form Page Routing" do
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(true) expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(true)
end end
describe "using the 'not equal to' operator" do
it "returns true if both values are non-nil and different" do
depends_on = [
{
"relat2" => {
"operator" => "!=",
"operand" => "P",
},
},
]
lettings_log.relat2 = "X"
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(true)
end
it "returns false if both values are non-nil and the same" do
depends_on = [
{
"relat2" => {
"operator" => "!=",
"operand" => "P",
},
},
]
lettings_log.relat2 = "P"
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(false)
end
it "returns true if the being tested is nil and the operand is non-nil" do
depends_on = [
{
"relat2" => {
"operator" => "!=",
"operand" => "P",
},
},
]
lettings_log.relat2 = nil
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(true)
end
it "returns true if the being tested is non-nil and the operand is nil" do
depends_on = [
{
"relat2" => {
"operator" => "!=",
"operand" => nil,
},
},
]
lettings_log.relat2 = "P"
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(true)
end
it "returns false if both values are nil" do
depends_on = [
{
"relat2" => {
"operator" => "!=",
"operand" => nil,
},
},
]
lettings_log.relat2 = nil
expect(lettings_log.form.depends_on_met(depends_on, lettings_log)).to be(false)
end
end
end end
end end

10
spec/features/sales_log_spec.rb

@ -352,16 +352,6 @@ RSpec.describe "Sales Log Features" do
end end
context "when a log becomes a duplicate" do context "when a log becomes a duplicate" do
before do
Timecop.freeze(Time.zone.local(2024, 3, 3))
Singleton.__init__(FormHandler)
end
after do
Timecop.return
Singleton.__init__(FormHandler)
end
context "and updating duplicate log" do context "and updating duplicate log" do
let(:user) { create(:user, :data_coordinator) } let(:user) { create(:user, :data_coordinator) }
let(:sales_log) { create(:sales_log, :duplicate, assigned_to: user) } let(:sales_log) { create(:sales_log, :duplicate, assigned_to: user) }

6
spec/fixtures/files/lettings_log_csv_export_codes_26.csv vendored

File diff suppressed because one or more lines are too long

6
spec/fixtures/files/lettings_log_csv_export_labels_26.csv vendored

File diff suppressed because one or more lines are too long

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

@ -28,8 +28,6 @@ RSpec.describe Form::Lettings::Pages::PersonLeadPartner, type: :model do
end end
context "with start year < 2026", metadata: { year: 25 } do context "with start year < 2026", metadata: { year: 25 } do
let(:person_question_count) { 4 }
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[{ "details_known_2" => 0 }], [{ "details_known_2" => 0 }],
@ -39,7 +37,6 @@ RSpec.describe Form::Lettings::Pages::PersonLeadPartner, type: :model do
context "with start year >= 2026", metadata: { year: 26 } do context "with start year >= 2026", metadata: { year: 26 } do
let(:start_year_2026_or_later?) { true } let(:start_year_2026_or_later?) { true }
let(:person_question_count) { 5 }
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
@ -70,8 +67,6 @@ RSpec.describe Form::Lettings::Pages::PersonLeadPartner, type: :model do
end end
context "with start year < 2026", metadata: { year: 25 } do context "with start year < 2026", metadata: { year: 25 } do
let(:person_question_count) { 4 }
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[{ "details_known_3" => 0 }], [{ "details_known_3" => 0 }],
@ -81,7 +76,6 @@ RSpec.describe Form::Lettings::Pages::PersonLeadPartner, type: :model do
context "with start year >= 2026", metadata: { year: 26 } do context "with start year >= 2026", metadata: { year: 26 } do
let(:start_year_2026_or_later?) { true } let(:start_year_2026_or_later?) { true }
let(:person_question_count) { 5 }
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(

86
spec/models/form/lettings/questions/person_partner_spec.rb

@ -24,6 +24,8 @@ RSpec.describe Form::Lettings::Questions::PersonPartner, type: :model do
) )
end end
let(:person_index) { 2 } let(:person_index) { 2 }
let(:is_any_person_partner?) { false }
let(:log) { instance_double(LettingsLog, is_any_person_partner?: is_any_person_partner?) }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -47,6 +49,48 @@ RSpec.describe Form::Lettings::Questions::PersonPartner, type: :model do
expect(question.hidden_in_check_answers).to be_nil expect(question.hidden_in_check_answers).to be_nil
end end
describe "#skip_page_in_form_flow?" do
context "with start year < 2026", metadata: { year: 25 } do
let(:year) { 2025 }
context "when no other person is the partner of the lead tenant" do
let(:is_any_person_partner?) { false }
it "returns false" do
expect(question.skip_question_in_form_flow?(log)).to be false
end
end
context "when another person is the partner of the lead tenant" do
let(:is_any_person_partner?) { true }
it "returns false" do
expect(question.skip_question_in_form_flow?(log)).to be false
end
end
end
context "with start year >= 2026", metadata: { year: 26 } do
let(:year) { 2026 }
context "when no other person is the partner of the lead tenant" do
let(:is_any_person_partner?) { false }
it "returns false" do
expect(question.skip_question_in_form_flow?(log)).to be false
end
end
context "when another person is the partner of the lead tenant" do
let(:is_any_person_partner?) { true }
it "returns true" do
expect(question.skip_question_in_form_flow?(log)).to be true
end
end
end
end
context "with person 2" do context "with person 2" do
it "has the correct id" do it "has the correct id" do
expect(question.id).to eq("relat2") expect(question.id).to eq("relat2")
@ -56,39 +100,49 @@ RSpec.describe Form::Lettings::Questions::PersonPartner, type: :model do
expect(question.check_answers_card_number).to eq(2) expect(question.check_answers_card_number).to eq(2)
end end
context "with person 2 age < 16" do context "and in 2025", metadata: { year: 25 } do
let(:log) { build(:lettings_log, age2: 10) } let(:year) { 2025 }
let(:person_question_count) { 4 }
context "and in 2025", metadata: { year: 25 } do it "has the correct question number" do
let(:year) { 2025 } expect(question.question_number).to eq(37)
end
context "with person 2 age < 16" do
let(:log) { build(:lettings_log, age2: 10) }
it "is not marked as derived" do it "is not marked as derived" do
expect(question.derived?(log)).to be false expect(question.derived?(log)).to be false
end end
end end
context "and in 2026", metadata: { year: 26 } do context "with person 2 age >= 16" do
let(:year) { 2026 } let(:log) { build(:lettings_log, age2: 20) }
it "is marked as derived" do it "is not marked as derived" do
expect(question.derived?(log)).to be true expect(question.derived?(log)).to be false
end end
end end
end end
context "with person 2 age >= 16" do context "and in 2026", metadata: { year: 26 } do
let(:log) { build(:lettings_log, age2: 20) } let(:year) { 2026 }
let(:person_question_count) { 5 }
context "and in 2025", metadata: { year: 25 } do it "has the correct question number" do
let(:year) { 2025 } expect(question.question_number).to eq(38)
end
it "is not marked as derived" do context "with person 2 age < 16" do
expect(question.derived?(log)).to be false let(:log) { build(:lettings_log, age2: 10) }
it "is marked as derived" do
expect(question.derived?(log)).to be true
end end
end end
context "and in 2026", metadata: { year: 26 } do context "with person 2 age >= 16" do
let(:year) { 2026 } let(:log) { build(:lettings_log, age2: 20) }
it "is not marked as derived" do it "is not marked as derived" do
expect(question.derived?(log)).to be false expect(question.derived?(log)).to be false

7
spec/models/form/lettings/subsections/household_characteristics_spec.rb

@ -357,7 +357,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_2_known person_2_known
person_2_age person_2_age
person_2_lead_partner person_2_lead_partner
relationship_2_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_2_check no_household_member_likely_to_be_pregnant_person_age_2_check
age_2_under_retirement_value_check age_2_under_retirement_value_check
age_2_over_retirement_value_check age_2_over_retirement_value_check
@ -372,7 +371,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_3_known person_3_known
person_3_age person_3_age
person_3_lead_partner person_3_lead_partner
relationship_3_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_3_check no_household_member_likely_to_be_pregnant_person_age_3_check
age_3_under_retirement_value_check age_3_under_retirement_value_check
age_3_over_retirement_value_check age_3_over_retirement_value_check
@ -387,7 +385,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_4_known person_4_known
person_4_age person_4_age
person_4_lead_partner person_4_lead_partner
relationship_4_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_4_check no_household_member_likely_to_be_pregnant_person_age_4_check
age_4_under_retirement_value_check age_4_under_retirement_value_check
age_4_over_retirement_value_check age_4_over_retirement_value_check
@ -402,7 +399,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_5_known person_5_known
person_5_age person_5_age
person_5_lead_partner person_5_lead_partner
relationship_5_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_5_check no_household_member_likely_to_be_pregnant_person_age_5_check
age_5_under_retirement_value_check age_5_under_retirement_value_check
age_5_over_retirement_value_check age_5_over_retirement_value_check
@ -417,7 +413,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_6_known person_6_known
person_6_age person_6_age
person_6_lead_partner person_6_lead_partner
relationship_6_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_6_check no_household_member_likely_to_be_pregnant_person_age_6_check
age_6_under_retirement_value_check age_6_under_retirement_value_check
age_6_over_retirement_value_check age_6_over_retirement_value_check
@ -432,7 +427,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_7_known person_7_known
person_7_age person_7_age
person_7_lead_partner person_7_lead_partner
relationship_7_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_7_check no_household_member_likely_to_be_pregnant_person_age_7_check
age_7_under_retirement_value_check age_7_under_retirement_value_check
age_7_over_retirement_value_check age_7_over_retirement_value_check
@ -447,7 +441,6 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_8_known person_8_known
person_8_age person_8_age
person_8_lead_partner person_8_lead_partner
relationship_8_multiple_partners_value_check
no_household_member_likely_to_be_pregnant_person_age_8_check no_household_member_likely_to_be_pregnant_person_age_8_check
age_8_under_retirement_value_check age_8_under_retirement_value_check
age_8_over_retirement_value_check age_8_over_retirement_value_check

37
spec/models/form_spec.rb

@ -19,10 +19,45 @@ RSpec.describe Form, type: :model do
let(:previous_page_id) { form.get_page("person_1_age") } let(:previous_page_id) { form.get_page("person_1_age") }
let(:value_check_previous_page) { form.get_page("net_income_value_check") } let(:value_check_previous_page) { form.get_page("net_income_value_check") }
it "returns the next page given the previous" do it "returns the next page, given the previous" do
expect(form.next_page_id(previous_page_id, lettings_log, user)).to eq("person_1_gender") expect(form.next_page_id(previous_page_id, lettings_log, user)).to eq("person_1_gender")
end end
context "when the next page has more than one question" do
let(:previous_page_id) { form.get_page("tenancy_start_date") }
let(:next_page) { form.get_page("rent_type") }
context "when every question on the next page returns `true` from its `skip_question_in_form_flow?` method" do
before do
allow(next_page.questions.first).to receive(:skip_question_in_form_flow?)
.with(lettings_log)
.and_return(true)
allow(next_page.questions.second).to receive(:skip_question_in_form_flow?)
.with(lettings_log)
.and_return(true)
end
it "returns the page after next, given the previous" do
expect(form.next_page_id(previous_page_id, lettings_log, user)).to eq("tenant_code")
end
end
context "when at least question on the next page returns `false` from its `skip_question_in_form_flow?` method" do
before do
allow(next_page.questions.first).to receive(:skip_question_in_form_flow?)
.with(lettings_log)
.and_return(true)
allow(next_page.questions.second).to receive(:skip_question_in_form_flow?)
.with(lettings_log)
.and_return(false)
end
it "returns the next page, given the previous" do
expect(form.next_page_id(previous_page_id, lettings_log, user)).to eq("rent_type")
end
end
end
context "when the current page is a value check page" do context "when the current page is a value check page" do
before do before do
lettings_log.hhmemb = 1 lettings_log.hhmemb = 1

319
spec/models/lettings_log_derived_fields_spec.rb

@ -1588,4 +1588,323 @@ RSpec.describe LettingsLog, type: :model do
end end
end end
end end
describe "#infer_at_most_one_relationship!" do
context "when 2025", metadata: { year: 25 } do
before do
Timecop.freeze(collection_start_date_for_year(2025))
Singleton.__init__(FormHandler)
end
after do
Timecop.return
Singleton.__init__(FormHandler)
end
context "when there are no existing relationships" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat2: nil, relat3: "X", relat4: "X", relat5: "R") }
context "when a new relationship is added" do
before do
log.relat4 = "P"
end
it "does not infer no to any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
end
context "when there is an existing relationship" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat2: "X", relat3: "P", relat4: "R", relat5: "X") }
context "when a new relationship is added" do
before do
log.relat2 = "P"
end
it "does not infer no to any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
context "when the relationship is removed" do
before do
log.relat3 = "X"
end
it "does not reset any answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
end
context "when more than one relationship is set" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat2: "X", relat3: "X", relat4: "R", relat5: "X") }
before do
log.relat2 = "P"
log.relat3 = "P"
end
it "does not infer no to any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
end
context "when 2026", metadata: { year: 26 } do
before do
Timecop.freeze(collection_start_date_for_year(2026))
Singleton.__init__(FormHandler)
end
after do
Timecop.return
Singleton.__init__(FormHandler)
end
context "when there are no existing relationships" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat3: "X", relat4: "X", relat5: "R") }
context "when a new relationship is added" do
before do
log.relat4 = "P"
end
it "infers no to unanswered questions" do
expect { log.set_derived_fields! }
.to change(log, :relat2).to("X")
.and change(log, :relat6).to("X")
end
it "does not change relationship answers for people not in the household" do
expect { log.set_derived_fields! }
.to not_change(log, :relat7)
.and not_change(log, :relat8)
end
it "does not change relationship answers of no or prefer not to say" do
expect { log.set_derived_fields! }
.to not_change(log, :relat3)
.and not_change(log, :relat5)
end
it "does not change the relationship answer for the newly added relationship" do
expect { log.set_derived_fields! }.to not_change(log, :relat4)
end
end
context "when a 'no' is changed to 'prefers not to say'" do
before do
log.relat4 = "R"
end
it "does not change any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
context "when a 'prefers not to say' is changed to 'no'" do
before do
log.relat5 = "X"
end
it "does not change any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
end
context "when there is an existing relationship" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat2: "X", relat3: "P", relat4: "R", relat5: "X") }
context "and a new relationship is added to an earlier person than the existing one" do
before do
log.relat6 = nil # This is necessary because `log.set_derived_fields!` runs when the log is created from the factory, which sets `relat6` to "X".
log.relat2 = "P"
end
it "infers no to the existing relationship" do
expect { log.set_derived_fields! }.to change(log, :relat3).to "X"
end
it "infers no to unanswered questions" do
expect { log.set_derived_fields! }.to change(log, :relat6).to "X"
end
it "does not change relationship answers for people not in the household" do
expect { log.set_derived_fields! }
.to not_change(log, :relat7)
.and not_change(log, :relat8)
end
it "does not change relationship answers of no or prefer not to say" do
expect { log.set_derived_fields! }
.to not_change(log, :relat5)
.and not_change(log, :relat4)
end
it "does not change the relationship answer for the newly added relationship" do
expect { log.set_derived_fields! }.to not_change(log, :relat2)
end
end
context "and a new relationship is added to an later person than the existing one" do
before do
log.relat6 = nil # This is necessary because `log.set_derived_fields!` runs when the log is created from the factory, which sets `relat6` to "X".
log.relat5 = "P"
end
it "infers no to the existing relationship" do
expect { log.set_derived_fields! }.to change(log, :relat3).to "X"
end
it "infers no to unanswered questions" do
expect { log.set_derived_fields! }.to change(log, :relat6).to "X"
end
it "does not change relationship answers for people not in the household" do
expect { log.set_derived_fields! }
.to not_change(log, :relat7)
.and not_change(log, :relat8)
end
it "does not change relationship answers of no or prefer not to say" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat4)
end
it "does not change the relationship answer for the newly added relationship" do
expect { log.set_derived_fields! }.to not_change(log, :relat5)
end
end
context "when a 'no' is changed to 'prefers not to say'" do
before do
log.relat4 = "R"
end
it "does not change any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
context "when a 'prefers not to say' is changed to 'no'" do
before do
log.relat5 = "X"
end
it "does not change any relationship answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
context "and the relationship is removed" do
before do
log.relat3 = "X"
end
it "does not reset any answers" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and not_change(log, :relat3)
.and not_change(log, :relat4)
.and not_change(log, :relat5)
.and not_change(log, :relat6)
.and not_change(log, :relat7)
.and not_change(log, :relat8)
end
end
end
context "when more than one relationship is set" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 6, details_known_2: 0, details_known_3: 0, details_known_4: 0, details_known_5: 0, details_known_6: 0, relat2: "X", relat3: "X", relat4: "R", relat5: "X") }
before do
log.relat2 = "P"
log.relat3 = "P"
end
it "keeps the lower numbered relationship and infers the higher numbered one to false" do
expect { log.set_derived_fields! }
.to not_change(log, :relat2)
.and change(log, :relat3).to("X")
end
it "infers no to unanswered questions" do
expect { log.set_derived_fields! }.to change(log, :relat6).to "X"
end
it "does not change relationship answers for people not in the household" do
expect { log.set_derived_fields! }
.to not_change(log, :relat7)
.and not_change(log, :relat8)
end
it "does not change relationship answers of no or prefer not to say" do
expect { log.set_derived_fields! }
.to not_change(log, :relat5)
.and not_change(log, :relat4)
end
end
end
end
end end

18
spec/models/lettings_log_spec.rb

@ -2349,5 +2349,23 @@ RSpec.describe LettingsLog do
end end
end end
end end
describe "#is_any_person_partner?" do
context "when no person is the partner of the lead tenant" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 3, details_known_2: 0, details_known_3: 0, relat2: "X", relat3: "R") }
it "returns false" do
expect(log.is_any_person_partner?).to be false
end
end
context "when a person is the partner of the lead tenant" do
let(:log) { create(:lettings_log, :in_progress, hhmemb: 3, details_known_2: 0, details_known_3: 0, relat2: "X", relat3: "P") }
it "returns true" do
expect(log.is_any_person_partner?).to be true
end
end
end
end end
# rubocop:enable RSpec/MessageChain # rubocop:enable RSpec/MessageChain

18
spec/requests/duplicate_logs_controller_spec.rb

@ -170,7 +170,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 3) expect(page).to have_content("- Buyer 1’s gender identity", count: 3)
expect(page).to have_content("- Buyer 1’s working situation", count: 3) expect(page).to have_content("- Buyer 1’s working situation", count: 3)
expect(page).to have_content("- Postcode", count: 3) expect(page).to have_content("- Postcode", count: 3)
expect(page).to have_link("Change", count: 21) expect(page).to have_content("- Address line 1", count: 3)
expect(page).to have_link("Change", count: 24)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&organisation_id=#{sales_log.owning_organisation_id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
@ -216,7 +217,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 7) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 8)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -242,7 +244,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 7) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 8)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -377,7 +380,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 3) expect(page).to have_content("- Buyer 1’s gender identity", count: 3)
expect(page).to have_content("- Buyer 1’s working situation", count: 3) expect(page).to have_content("- Buyer 1’s working situation", count: 3)
expect(page).to have_content("- Postcode", count: 3) expect(page).to have_content("- Postcode", count: 3)
expect(page).to have_link("Change", count: 18) expect(page).to have_content("- Address line 1", count: 3)
expect(page).to have_link("Change", count: 21)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?first_remaining_duplicate_id=#{duplicate_logs[0].id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[0].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs") expect(page).to have_link("Change", href: "/sales-logs/#{duplicate_logs[1].id}/purchaser-code?first_remaining_duplicate_id=#{sales_log.id}&original_log_id=#{sales_log.id}&referrer=duplicate_logs")
@ -405,7 +409,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 6) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 7)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end
@ -431,7 +436,8 @@ RSpec.describe DuplicateLogsController, type: :request do
expect(page).to have_content("- Buyer 1’s gender identity", count: 1) expect(page).to have_content("- Buyer 1’s gender identity", count: 1)
expect(page).to have_content("- Buyer 1’s working situation", count: 1) expect(page).to have_content("- Buyer 1’s working situation", count: 1)
expect(page).to have_content("- Postcode", count: 1) expect(page).to have_content("- Postcode", count: 1)
expect(page).to have_link("Change", count: 6) expect(page).to have_content("- Address line 1", count: 1)
expect(page).to have_link("Change", count: 7)
expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen") expect(page).to have_link("Change", href: "/sales-logs/#{sales_log.id}/purchaser-code?original_log_id=#{sales_log.id}&referrer=interruption_screen")
end end

2
spec/services/bulk_upload/sales/validator_spec.rb

@ -225,7 +225,7 @@ RSpec.describe BulkUpload::Sales::Validator do
end end
it "creates errors" do it "creates errors" do
expect { validator.call }.to change(BulkUploadError.where(category: :setup, error: "This is a duplicate of a log in your file."), :count).by(20) expect { validator.call }.to change(BulkUploadError.where(category: :setup, error: "This is a duplicate of a log in your file."), :count).by(24)
end end
end end
end end

4
spec/services/bulk_upload/sales/year2025/row_parser_spec.rb

@ -784,6 +784,8 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age
@ -814,6 +816,8 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_28, # Buyer 1 age :field_28, # Buyer 1 age

4
spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

@ -790,6 +790,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_29, # Buyer 1 age :field_29, # Buyer 1 age
@ -820,6 +822,8 @@ RSpec.describe BulkUpload::Sales::Year2026::RowParser do
:field_1, # Sale completion date :field_1, # Sale completion date
:field_2, # Sale completion date :field_2, # Sale completion date
:field_3, # Sale completion date :field_3, # Sale completion date
:field_16, # UPRN
:field_17, # Address line 1
:field_21, # Postcode :field_21, # Postcode
:field_22, # Postcode :field_22, # Postcode
:field_29, # Buyer 1 age :field_29, # Buyer 1 age

Loading…
Cancel
Save