Skip to content

fix(registry): let a removed ID type be used again - #437

Open
emjay0921 wants to merge 2 commits into
19.0from
fix/1136-reinstate-id-after-cr-removal
Open

fix(registry): let a removed ID type be used again#437
emjay0921 wants to merge 2 commits into
19.0from
fix/1136-reinstate-id-after-cr-removal

Conversation

@emjay0921

Copy link
Copy Markdown
Contributor

Why is this change needed?

Removing an ID through a change request keeps the record and marks it Invalid rather than deleting it. Two separate checks counted that dead row, so once an ID had been removed the registrant was left with an Invalid ID and no way to add a valid one of the same type — which defeats the point of the Remove action (OP#1136).

Both checks had to change; fixing either alone still leaves the user stuck.

How was the change implemented?

  • UNIQUE(partner_id, id_type_id) on spp.registry.id becomes a partial unique index over rows WHERE status IS DISTINCT FROM 'invalid'. A table constraint cannot express the condition. IS DISTINCT FROM rather than != is what keeps a NULL status blocking: those are IDs added straight through the registry, which are live records, not removed ones.
  • The change request add path searched for any ID of the type regardless of status and refused on the strength of it. It now looks for a live one, so the Remove-then-Add sequence in the ticket completes.
  • Uniqueness is asserted before the write rather than through @api.constrains. Constraints run on flush, by which point the INSERT has already hit the index and the user sees a raw psycopg UniqueViolation instead of a sentence naming the ID type. The index stays as the race-safe guarantee.

Note for upgrades: init() drops the old constraint explicitly rather than relying on the ORM noticing it is no longer declared, so an existing database needs spp_registry upgraded, not merely restarted.

New unit tests

  • spp_registry/tests/test_reg_id.py — a live ID still reserves its type, an ID with no status still reserves its type, successive removals leave several Invalid rows behind without blocking, and flipping an Invalid row back to valid while a live one exists is refused.
  • spp_change_request_v2/tests/test_update_id_strategy.py — the add path accepts a type whose only existing row is Invalid, and still refuses one with a live row.

Unit tests executed by the author

Run per module, the way CI's matrix does, on this branch after merging 19.0 in:

  • spp_registry250 tests, 0 failed, 0 errors
  • spp_change_request_v2331 tests, 0 failed, 0 errors

How to test manually

  1. Open a registrant and add an ID — say National ID with some number.
  2. Raise a change request to remove that ID and complete it. The ID stays on the record marked Invalid.
  3. Raise a change request to add a National ID again: it is accepted, where before it was refused because the Invalid row still counted.
  4. Try to add a second National ID while a valid one exists: still refused, with a message naming the type.
  5. Repeat remove-then-add twice more — the extra Invalid rows do not block.

Related links

Removing an ID through a change request keeps the record and marks it
Invalid rather than deleting it. Two separate checks counted that dead row,
so once an ID had been removed the registrant was left with an Invalid ID
and no way to add a valid one of the same type — which defeats the point of
the Remove action.

Both had to change; fixing either alone still leaves the user stuck.

UNIQUE(partner_id, id_type_id) on spp.registry.id becomes a partial unique
index over rows WHERE status IS DISTINCT FROM 'invalid'. A table constraint
cannot express the condition. IS DISTINCT FROM rather than != is what keeps
a NULL status blocking: those are IDs added straight through the registry,
which are live records, not removed ones.

The change request add path searched for any ID of the type regardless of
status and refused on the strength of it. It now looks for a live one, so
the Remove-then-Add sequence the ticket describes completes.

Uniqueness is asserted before the write rather than through @api.constrains.
Constraints run on flush, by which point the INSERT has already hit the
index and the user sees a raw psycopg UniqueViolation instead of a sentence
naming the ID type. The index stays as the race-safe guarantee.

Covers the spec exactly: a live ID still reserves its type, an ID with no
status still reserves its type, successive removals leave several Invalid
rows behind without blocking, and flipping an Invalid row back to valid
while a live one exists is refused.

Note for upgrades: init() drops the old constraint explicitly rather than
relying on the ORM noticing it is no longer declared, so an existing
database needs spp_registry upgraded, not merely restarted.

OP#1136
@emjay0921
emjay0921 marked this pull request as ready for review August 18, 2026 08:01
Comment on lines +102 to +108
self.env.cr.execute(
f"""
CREATE UNIQUE INDEX IF NOT EXISTS {self._UNIQUE_ACTIVE_INDEX}
ON spp_registry_id (partner_id, id_type_id)
WHERE status IS DISTINCT FROM 'invalid'
"""
)
]
if exclude_id:
domain.append(("id", "!=", exclude_id))
clash = self.sudo().search(domain, limit=1)
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.69%. Comparing base (0820667) to head (7754ea1).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #437      +/-   ##
==========================================
+ Coverage   72.24%   72.69%   +0.45%     
==========================================
  Files         419      560     +141     
  Lines       29813    38397    +8584     
==========================================
+ Hits        21539    27914    +6375     
- Misses       8274    10483    +2209     
Flag Coverage Δ
spp_analytics 93.25% <ø> (ø)
spp_api_v2 79.99% <ø> (?)
spp_api_v2_change_request 66.53% <ø> (ø)
spp_api_v2_cycles 71.03% <ø> (ø)
spp_api_v2_data 77.77% <ø> (ø)
spp_api_v2_entitlements 70.23% <ø> (ø)
spp_api_v2_gis 71.57% <ø> (ø)
spp_api_v2_products 65.86% <ø> (?)
spp_api_v2_programs 92.22% <ø> (ø)
spp_api_v2_service_points 71.03% <ø> (ø)
spp_api_v2_simulation 71.19% <ø> (?)
spp_api_v2_vocabulary 57.75% <ø> (?)
spp_approval 50.34% <ø> (?)
spp_area 80.16% <ø> (?)
spp_area_hdx 81.60% <ø> (?)
spp_base_common 91.07% <ø> (ø)
spp_change_request_v2 77.83% <ø> (ø)
spp_programs 65.27% <ø> (ø)
spp_registry 87.57% <100.00%> (+0.42%) ⬆️
spp_security 69.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_change_request_v2/strategies/update_id.py 89.87% <ø> (ø)
spp_registry/models/reg_id.py 96.42% <100.00%> (+0.92%) ⬆️

... and 142 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants