Skip to content

spp_change_request_v2: group-scope conflict candidates drop members with a future-dated exit (not m.ended_date is not "ended") #493

Description

@kneckinator

Filed at origin/19.0 (23f43963, the merge of #477). From the expert review of #477 — did not block that PR.

Summary

ConflictDetectionMixin._get_group_member_ids (spp_change_request_v2/models/conflict_mixin.py:265) treats a membership as ended whenever ended_date is set at all:

registrant.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual.id")   # L282
for membership in registrant.individual_membership_ids.filtered(lambda m: not m.ended_date):   # L288
group.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual.id")        # L292

spp.group.membership itself defines "ended" as ended_date <= now — both _compute_is_ended and _compute_status (spp_registry/models/group_membership.py:124-131 and the status compute below it). The two definitions disagree on exactly one case: a future-dated ended_date, i.e. a scheduled exit.

That state is not hypothetical — this module produces it. spp.cr.apply.exit_registrant writes the user-supplied detail.exit_date straight into ended_date (spp_change_request_v2/strategies/exit_registrant.py:32-44 for an individual, 54-66 for a group), and nothing constrains that date to the past. "This member leaves the household on 2026-12-31" is ordinary, supported input.

Impact

Between apply and the exit date arriving, the member is still a live member of the household by every other reading — status == "active", group rosters, API search — but is invisible to group-scope conflict detection. A group-scope rule configured action = "block" silently stops firing for them: a fail-open in a control whose entire purpose is to block. With action = "warn" the warning is simply lost.

Suggested fix

Evaluate the window instead of the presence of the date, in all three filters:

now = fields.Datetime.now()
...filtered(lambda m: not (m.ended_date and m.ended_date <= now))

Do not substitute m.is_ended or m.status. Both are store=True computes that depend only on ended_date and compare it against now() at write time, so a future-dated exit stays is_ended = False forever once the date passes — nothing recomputes it. That staleness is tracked in #417 (with #421 and #420 covering the related start_date and active gaps in the same model). Until #417 is fixed, an explicit datetime comparison is the correct predicate here.

Tests

  • A membership with a future ended_date must still resolve as a conflict candidate — the regression this issue is about.
  • test_group_scope_ended_membership_excluded (tests/test_conflict_detection_extended.py:571) uses an individual registrant, so only the nested filter at L292 ever runs in an excluding state; the group-registrant filter at L282 is never exercised with an ended membership. Ending a membership inside test_group_scope_group_registrant (L555) and asserting absence closes that gap in one line.

While in this method (cosmetic, from the same review)

  • The hasattr() guards at L280/L287 are dead — spp_registry is a hard dependency, so those fields always exist. Tracked separately in chore(spp_change_request_v2): drop dead hasattr() guards on hard-dependency fields (one of them is always False) #496.
  • list(set(member_ids)) (L295) → sorted(set(...)), so the generated search domain is reproducible.
  • The docstring advertises the method as an override point but doesn't document the branch asymmetry: an individual expands to their groups plus those groups' members, while a group expands only to itself plus its own members — not to those members' other groups. One sentence saves an overrider the read.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions