Skip to content

feat(spp_programs): deduplication — configure by method, recompute on each run, bulk return to draft - #445

Open
emjay0921 wants to merge 9 commits into
19.0from
fix/745-deduplication-resolution
Open

feat(spp_programs): deduplication — configure by method, recompute on each run, bulk return to draft#445
emjay0921 wants to merge 9 commits into
19.0from
fix/745-deduplication-resolution

Conversation

@emjay0921

Copy link
Copy Markdown
Contributor

Covers four tickets, all QA-passed: OP#1171, OP#796, OP#1170 and their parent OP#745.

Why is this change needed?

Adding a deduplication method meant editing the wrapper's manager_ref_id — a Reference field, which asks the user to pick a model and then find or create a record of it. That exposes internal plumbing, and picking an existing record belonging to another program silently mis-wires the manager: it keeps running against the program it was created for while this program's form claims it is configured (OP#1171).

Two more faults sat behind it:

  • Deduplicate only ever added flags. A membership marked duplicated stayed that way after the clash behind it was fixed, because a membership already in that state was never re-evaluated out of it (OP#796).
  • Nothing brought those memberships back. back_to_draft() existed only as a button on the membership form, so clearing duplicates meant opening records one at a time (OP#1170).

How was the change implemented?

  • Duplicate Detection is a card beside Compliance and Payment, with an +Add dialog that asks two plain questions — which method, and what to call it. The name is suggested from the method; a name you typed is never overwritten. Each configured method gets its own row and cog, because a program may legitimately check by shared members and by phone.
  • Selecting ID document also asks which ID types to compare. The check is id_type_id in supported_id_document_type_ids, so a method created with none set matched nothing and reported no duplicates at all — the card said Configured and Deduplicate ran clean. Required in the dialog and on the method's own form, and refused server-side.
  • No "Add a line". The field is a Many2many, and for those the list renderer decides that row from link, not create — so create="0" on the list was never read, and the row it left opened a picker listing every program's methods. Both keys are denied; unlink is untouched, so the ✕ still removes a method.
  • A removed method can be added again. Removing a row unlinks it without deleting it, and the duplicate check counted the leftover — so the method the card no longer showed still blocked its own re-adding. The check now reads what the program actually has, and adding first sweeps leftovers that no program links.
  • Deduplicate recomputes rather than accumulates: flags are cleared before the managers run, so whoever still clashes is flagged again and whoever no longer does is left in draft.
  • Bulk return to draft — a row button on the membership list (which is what the program's Duplicates smart button opens) and a Back to Draft server action bound to that list, matching the shape spp_programs already uses for Reset to Draft on entitlements.

New unit tests

spp_programs/tests/test_deduplication_setup_wizard.py (21 tests) covers the dialog end to end: each method can be added, the same one cannot be added twice, a removed one can be added back, the sweep spares a method another program links, a dangling Reference does not crash the Add button, the ID method is refused without types and the chosen types reach the manager. Plus arch assertions that no card offers an add row and that Edit appears only when exactly one method is configured. test_deduplication.py and test_program_membership.py cover the recompute and the bulk return.

Unit tests executed by the author

Full spp_programs suite after merging 19.0 in: 713 tests, 0 failed, 0 errors.

How to test manually

  1. Open a program → ConfigurationDuplicate Detection. With nothing configured you get an empty state and +Add; there is no "Add a line" anywhere.
  2. +Add → choose ID document: the dialog asks for ID types, and refuses to add without at least one. Add it, then confirm the row and its cog.
  3. Remove the method with the row's , then add the same one again — accepted.
  4. Give two registrants the same National ID, enrol both, run Deduplicate — flagged. Fix one ID and run again — the flag clears.
  5. From the program's Duplicates button, select several memberships and use Back to Draft.

Related links

Reviewer notes

  • spp_programs bumped to 19.0.2.3.0 (minor: adds the setup-wizard model and its ACL rows) with changelog entries, per the in-PR convention.
  • Worth knowing about the QA provenance: OP#745's pass reads "Done retesting after fixes done to similar branch. QA passed." — so it was verified against the sibling work rather than in isolation.
  • The link-vs-create distinction is the one thing here that arch-level tests cannot prove, since it lives in the web client. QA checked it in the browser; the tests assert the attributes that drive it.

…raft

Deduplication and eligibility checks put a membership into "duplicated" or
"not eligible" and nothing took it out again. back_to_draft() existed, but
only as a button on the membership form, so resolving a duplicate meant
opening each record one at a time (OP#1170).

Surfaces it where validators actually work:

- a row button on the membership list, which is what a program's Duplicates
  smart button opens;
- a "Back to Draft" server action bound to that list, so a whole selection
  can be cleared at once - the shape spp_programs already uses for "Reset to
  Draft" on entitlements, including filtering the selection and complaining
  only when nothing in it qualifies;
- a row button on the Participation list of both individual and group
  registrants, where the inline list gets no action menu of its own.

Validator-level in every place, matching the existing form button rather
than the officer-level Pause and Exit buttons beside it. Clearing a
duplicate flag from a list should not be easier than doing it on the record.

back_to_draft() is also guarded now. It wrote "draft" over any state at all,
which was fine while one button behind a state modifier was the only caller
and is not fine with four. A mixed selection is refused whole rather than
half-applied.
Deduplication only ever added the flag. A membership marked "duplicated"
stayed that way even once the clash behind it was fixed, because a
membership already in that state was never re-evaluated out of it. Correct
the ID or the phone number, press Deduplicate again, and nothing happened -
which is what left validators with almost no way back to draft (OP#796).

Clear the program's flags before the managers run, so the run recomputes
rather than accumulates: whoever still clashes is flagged again a moment
later, and whoever no longer does is left in draft. Both entry points go
through it, the program's button and the membership's. It is one
transaction, so a run that fails part-way leaves the flags as they were.

The notification was wrong once a run could also un-flag: it derived "new"
by subtracting counts, which goes negative as soon as something is
resolved. It now compares the recordsets and reports new, still flagged and
no longer duplicate.

Two things found and deliberately left alone: last_deduplication is
declared on the membership and written nowhere, and the duplicate record's
duplicate/not_duplicate state is set by nothing, read by nothing and
reachable from no button.
The button acts on the membership state, so it reads as following from
Status rather than crowding the registrant at the start of the row. The
registrant-side participation lists already placed it there.

Refs OP#1170
Duplicate Detection was the last section of the program configuration
page still rendered as a bare group under Optional Features, with the
wrapper's manager_ref_id edited inline. That is a Reference field: it
asks the user to pick a model and then find or create a record of it,
which exposes the wrapper/concrete split and lets a manager belonging
to another program be wired in by mistake.

It is now a card beside Compliance, Payment and the rest, and adding a
method goes through a dialog asking for the method and a name — the
same shape Compliance and Payment got in #952/#953, with the extra
step deduplication needs because it has three methods where those have
one.

The card body lists every configured method with its own cog rather
than a summary line: a program may legitimately check by shared
members and by phone. The header Edit is therefore offered only when
there is exactly one method — a single button cannot open two, and
opening them in a dialog list is worse still, because a dialog list
cannot drill into a form and the rows look clickable while doing
nothing.

Refs OP#1171
… removed methods

"Add a line" survived on the card. deduplication_manager_ids is a Many2many,
and for those the list renderer reads `"link" in activeActions ? link : create`,
so the list's create="0" and the create domain were both dead letters. The row
that survived opened the link picker: every deduplication manager in the
database, other programs' included, ready to be mis-wired into this one. The
domain now covers 'link'. 'unlink' is left alone, so the row's x still removes
a method.

Removing a method that way drops the relation but keeps the wrapper, whose
program_id still names the program. The duplicate check searched on that, so a
method the card no longer showed still refused to be added back. It now asks
the program's own field, and adding first sweeps wrappers that no program
links.
The Add dialog asked for a method and a name, which is not enough for the ID
document method. Its check is `id_type_id in supported_id_document_type_ids`,
so a manager created with that list empty matches nothing and reports no
duplicates at all — the card says Configured, Deduplicate runs, and nothing is
ever flagged. QA asked for the list to appear on selecting ID document
(OP#1171 finding 5).

The dialog now shows the ID document types when that method is selected, with
the same widget and options as the manager's own form, and requires at least
one. action_create_manager refuses an empty list as well, because the view's
required only binds the client and a programmatic caller would otherwise
create the silent no-op.

The manager's own form — reached from the card's cog — now requires the field
too. Without that, the same empty manager could still be saved from there.
Existing records keep what they have until someone edits them.

Six tests in the wizard's suite created the ID method without types, which is
the state now refused; they set them the way a user would, and one test
deliberately omits them to assert the refusal.
19.0.2.3.0 — a minor, since it adds the deduplication setup wizard model and
its access rows alongside the behaviour fixes. Covers OP#1171, OP#796 and
OP#1170; the bump is required for the new model, the ACL rows and the view
changes to load at all on an existing database.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.25287% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.39%. Comparing base (f66e8a0) to head (a3a06cb).

Files with missing lines Patch % Lines
spp_programs/models/program_manager_ui.py 64.28% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #445      +/-   ##
==========================================
+ Coverage   75.01%   75.39%   +0.38%     
==========================================
  Files         524      542      +18     
  Lines       35301    35879     +578     
==========================================
+ Hits        26480    27051     +571     
- Misses       8821     8828       +7     
Flag Coverage Δ
spp_api_v2_cycles 71.03% <ø> (ø)
spp_api_v2_entitlements 70.23% <ø> (ø)
spp_api_v2_programs 92.22% <ø> (ø)
spp_api_v2_simulation 71.19% <ø> (ø)
spp_base_common 91.07% <ø> (ø)
spp_case_entitlements 100.00% <ø> (ø)
spp_case_programs 100.00% <ø> (ø)
spp_cr_type_assign_program 92.07% <ø> (ø)
spp_dci_compliance 93.01% <ø> (ø)
spp_dci_demo 94.28% <ø> (ø)
spp_dci_server_social 89.57% <ø> (ø)
spp_farmer_registry_demo 63.43% <ø> (ø)
spp_gis_report_programs 100.00% <ø> (?)
spp_grm_demo 80.13% <ø> (?)
spp_grm_programs 92.13% <ø> (?)
spp_hazard_programs 98.55% <ø> (?)
spp_programs 66.71% <94.25%> (+1.18%) ⬆️
spp_registry 87.22% <ø> (+0.07%) ⬆️
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_programs/models/program_membership.py 75.36% <100.00%> (+8.03%) ⬆️
spp_programs/models/programs.py 88.91% <100.00%> (+1.18%) ⬆️
spp_programs/wizard/__init__.py 100.00% <100.00%> (ø)
spp_programs/wizard/deduplication_setup_wizard.py 100.00% <100.00%> (ø)
spp_programs/models/program_manager_ui.py 43.13% <64.28%> (+5.27%) ⬆️

... and 21 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.

@emjay0921
emjay0921 marked this pull request as ready for review August 20, 2026 10:36
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.

1 participant