fix(user): restrict_with_error on case_assignments to prevent history loss#6947
Open
mvanhorn wants to merge 1 commit into
Open
fix(user): restrict_with_error on case_assignments to prevent history loss#6947mvanhorn wants to merge 1 commit into
mvanhorn wants to merge 1 commit into
Conversation
… loss The has_many :case_assignments association on User had `dependent: :destroy` with a `# TODO destroy is wrong` comment. Cascading destroy on a volunteer would hard-delete every case_assignment, losing the audit trail (active flag, dates, the volunteer↔case mapping). The supported retirement flow is Volunteer#deactivate, which marks the user and each case_assignment inactive while preserving the rows. `dependent: :restrict_with_error` makes that invariant explicit: anything that tries to hard-delete a volunteer with live assignments now fails fast and surfaces an error instead of silently losing history. Adds a shoulda-matchers assertion for the dependent option and two behavioral specs that verify the destroy is refused and the case_assignment rows are preserved. Resolves rubyforgood#6911
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What github issue is this PR for, if any?
Resolves #6911
What changed, and why?
The
has_many :case_assignmentsassociation onUsercarrieddependent: :destroywith a# TODO destroy is wrongcomment that has been on that line for a while. The cascade is wrong becausecase_assignmentsis the volunteer↔case join record and carries history (theactiveflag, timestamps, the relationship itself); hard-deleting it loses that history. The class also does not use Paranoia, so there is no soft-delete recovery path either.The supported retirement flow is
Volunteer#deactivate, which setsactive: falseon the user and updates every case_assignment withcase_assignments.update_all(active: false), preserving the rows. There is no destroy path called from the app, so the cascade only ever fires from an out-of-band action (Rails console, future destroy controller, a fixture cleanup). The right semantics is "destroy is forbidden while assignments exist; you must deactivate."Swapping
dependent: :destroyfordependent: :restrict_with_errormakes that invariant explicit. Anything that tries to hard-delete a volunteer with live assignments now fails fast: the destroy returns false, an error is added toerrors[:base], and the case_assignment rows stay intact. The TODO is resolved both in code and in the comment that now documents the intent and points to the deactivation entry point.Spec coverage on
User:have_many(:case_assignments)line so the dependent option is now part of the contract.describe "destroying a volunteer with case_assignments"block adds two behavioral specs: one asserts the destroy is refused (volunteer stays persisted, CaseAssignment count unchanged), the other asserts the assignment row still exists after the destroy attempt.I deliberately scoped the change to the
case_assignmentsassociation called out in the issue's acceptance criteria. The sibling associations the issue's "What to investigate" list mentions (case_contacts,followups,other_duties,learning_hours, etc.) deserve their own audit, but each has a slightly different cascade story (case_contactsandfollowupsalready have nodependent:option;learning_hoursis volunteer-owned historical data with similar concerns). Mixing them in here would inflate the diff and the review surface, so I left them for a follow-up.How is this tested? (please write rspec and jest tests!) 💖💪
Two new RSpec examples in
spec/models/user_spec.rb:CaseAssignment.exists?(id)is still true afterward.Plus the shoulda-matchers line is upgraded from
have_many(:case_assignments)tohave_many(:case_assignments).with_foreign_key(:volunteer_id).dependent(:restrict_with_error), so the dependent option is now part of the model contract.The existing
#deactivatespecs inspec/models/volunteer_spec.rbalready cover the supported flow and continue to pass; this PR doesn't changedeactivatebehavior.I could not run
bin/rails specin my workspace (system Ruby 2.6, project uses 4.0.3), so CI is the final word on the suite. The change itself is two lines inuser.rb(option swap + a comment explaining the invariant) and an additive spec block, so I'd be surprised by a regression.Screenshots please :)
Behavior-only change to a model association, no UI surface. Closest thing to a screenshot is the new spec output, which CI will render.
Feelings gif (optional)
AI disclosure: authored with Claude as a coding assistant; I reviewed the change before pushing.