You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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-datedended_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(lambdam: not (m.ended_dateandm.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 futureended_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)
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.
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 wheneverended_dateis set at all:spp.group.membershipitself defines "ended" asended_date <= now— both_compute_is_endedand_compute_status(spp_registry/models/group_membership.py:124-131and thestatuscompute below it). The two definitions disagree on exactly one case: a future-datedended_date, i.e. a scheduled exit.That state is not hypothetical — this module produces it.
spp.cr.apply.exit_registrantwrites the user-supplieddetail.exit_datestraight intoended_date(spp_change_request_v2/strategies/exit_registrant.py:32-44for an individual,54-66for 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 configuredaction = "block"silently stops firing for them: a fail-open in a control whose entire purpose is to block. Withaction = "warn"the warning is simply lost.Suggested fix
Evaluate the window instead of the presence of the date, in all three filters:
Do not substitute
m.is_endedorm.status. Both arestore=Truecomputes that depend only onended_dateand compare it againstnow()at write time, so a future-dated exit staysis_ended = Falseforever once the date passes — nothing recomputes it. That staleness is tracked in #417 (with #421 and #420 covering the relatedstart_dateandactivegaps in the same model). Until #417 is fixed, an explicit datetime comparison is the correct predicate here.Tests
ended_datemust 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 insidetest_group_scope_group_registrant(L555) and asserting absence closes that gap in one line.While in this method (cosmetic, from the same review)
hasattr()guards at L280/L287 are dead —spp_registryis a hard dependency, so those fields always exist. Tracked separately in chore(spp_change_request_v2): drop deadhasattr()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.