Skip to content

feat: geofence-based geographic targeting for programs (re-land from #76)#273

Open
gonzalesedwin1123 wants to merge 10 commits into
19.0from
reland/gis-geofence
Open

feat: geofence-based geographic targeting for programs (re-land from #76)#273
gonzalesedwin1123 wants to merge 10 commits into
19.0from
reland/gis-geofence

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Re-lands the scope described in reverted PR #76 (revert: #271), as part of splitting that PR into description-consistent pieces. This description incorporates the relevant content of #76 so reviewers do not need to open it; everything listed below is contained in THIS PR's diff.

Summary

  • Add spp_program_geofence module for geofence-based program targeting and eligibility management.
  • Extend spp_gis spatial operators to support MultiPolygon and GeometryCollection.
  • Make MapTiler API key optional in GIS UI, with OSM fallback when key is not configured (placeholder key YOUR_MAPTILER_API_KEY_HERE treated as unconfigured).
  • Stabilize GIS edit/map widgets (re-render lifecycle, draw interaction behavior).

Functional detail (from #76, verified against this diff)

spp_program_geofence (new module)

  • Program-level geofence_ids and geofence_count.
  • Geofence eligibility manager (spp.program.membership.manager.geofence) with:
    • Tier 1: coordinate intersection.
    • Tier 2: area intersection fallback (optional, with area type filter).
  • Program configuration UI integration and program creation wizard support.
  • Geofence management views/action/menu, gated by explicit read ACLs for Programs Viewer/Validator roles (menu groups restriction avoids exposing items to users lacking read access).

spp_gis

  • Operator support for complex geometry types via ST_GeomFromGeoJSON; for MultiPolygon/GeometryCollection, (geojson, distance) applies ST_Buffer(...) correctly (EPSG:3857 transform parity when SRID is 4326), with regression tests for the distance-buffer path on both complex types.
  • OSM style fallback in renderer/edit widgets when MapTiler key is missing; placeholder-key handling in the controller.
  • Geofence GeoJSON output includes the record uuid as feature id; new spp.gis.geofence.tag model replaces vocabulary-based geofence tags (+ACLs).

Added on top of #76 (not in the original)

  • Upgrade migration (spp_gis 19.0.2.1.0): geofence tag_ids changes co-model from spp.vocabulary to spp.gis.geofence.tag over the same rel table. Release v19.0.2.0.0 (Biliran) ships the old schema, so a pre-migration parks legacy links (the FK swap would otherwise fail) and a post-migration recreates them as tag records. Fully literal, value-parameterized SQL (clears Semgrep/pylint-odoo SQL findings; GHAS alerts #1837–#1842 fixed). Regression tests: remap, dedup across geofences, valid-link survival, fresh-DB no-op.
  • gemini-code-assist review findings, all applied: queued import guards against archived/deleted programs (+regression test) and builds membership values from ids; tier-2 domain drops the NOT-IN anti-pattern (the tier1 | tier2 union already dedups); enrolled-beneficiary exclusion via recordset subtraction; user-facing strings wrapped in _(); the map edit widget re-creates the MapLibre map only when the geometry value actually changed (prevents zoom/pan loss and WebGL context churn — a quick manual map-edit smoke test during review is recommended).
  • Coverage tests (codecov patch 96.9%): eligibility tier-2/preview/queued-import/cycle-verification paths, action_open_geofences, and the MapTiler key controller (configured/placeholder/absent key).
  • Docs: spp_gis DESCRIPTION now describes geofences, complex-geometry queries, and the OSM fallback; spp_program_geofence gains an initial HISTORY fragment; version bump + HISTORY entry for spp_gis; READMEs rendered by CI's pinned generator.

Not in this PR (other pieces of #76, re-landed separately)

spp_hazard CAP severity (#274), spp_cel_domain (#275), spp_api_v2 (#276), PHL demo data (#277), infra/tooling (#278, merged), spp_metric_service (#279), spp_gis_report (#280), spp_api_v2_gis (#281), spp_mis_demo_v2 (#282).

Verification

  • ./spp t spp_gis: 96 passed, 0 failed (includes 4 migration tests + 4 MapTiler controller tests)
  • ./spp t spp_program_geofence: 41 passed, 0 failed (includes archived-program regression test)
  • codecov: patch 96.9% (pass), project pass; remaining uncovered lines are module-level ImportError fallbacks
  • All 20 gemini-code-assist threads resolved with fixing-commit references

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces geofence-based geographic targeting for OpenSPP programs by adding the new spp_program_geofence module and extending spp_gis to support complex geometry types like MultiPolygon and GeometryCollection. It also replaces vocabulary-based geofence tags with a dedicated spp.gis.geofence.tag model, complete with migration scripts, and implements an OpenStreetMap fallback when no MapTiler API key is configured. The review feedback focuses on improving performance and code quality in the new Odoo models and JS widgets. Key recommendations include avoiding map re-creation on every patch in the GIS edit widget, optimizing recordset operations (using subtraction instead of lambda filtering and avoiding heavy NOT IN clauses in search domains), checking for active records in asynchronous tasks, and wrapping user-facing strings in _() for internationalization.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spp_program_geofence/models/eligibility_manager.py
Comment thread spp_program_geofence/models/eligibility_manager.py
Comment thread spp_program_geofence/models/eligibility_manager.py Outdated
Comment thread spp_program_geofence/models/eligibility_manager.py Outdated
Comment thread spp_program_geofence/models/eligibility_manager.py
Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.92308% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.79%. Comparing base (bf61488) to head (5cb9df5).
⚠️ Report is 1 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_program_geofence/models/eligibility_manager.py 96.29% 5 Missing ⚠️
spp_program_geofence/__manifest__.py 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #273      +/-   ##
==========================================
- Coverage   74.86%   71.79%   -3.08%     
==========================================
  Files        1093      282     -811     
  Lines       63718    16590   -47128     
==========================================
- Hits        47701    11910   -35791     
+ Misses      16017     4680   -11337     
Flag Coverage Δ
endpoint_route_handler ?
fastapi ?
spp_aggregation ?
spp_alerts ?
spp_analytics ?
spp_api_v2 ?
spp_api_v2_change_request 66.41% <ø> (ø)
spp_api_v2_cycles 70.65% <ø> (ø)
spp_api_v2_data ?
spp_api_v2_entitlements 69.96% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products ?
spp_api_v2_programs 92.03% <ø> (ø)
spp_api_v2_service_points ?
spp_api_v2_simulation ?
spp_api_v2_vocabulary ?
spp_approval ?
spp_area ?
spp_area_hdx 81.43% <ø> (ø)
spp_attachment_av_scan ?
spp_audit ?
spp_audit_programs 0.00% <ø> (ø)
spp_banking ?
spp_base_common 90.26% <ø> (ø)
spp_base_setting ?
spp_case_base ?
spp_case_cel ?
spp_case_demo ?
spp_case_entitlements 97.61% <ø> (ø)
spp_case_graduation ?
spp_case_programs 97.14% <ø> (ø)
spp_case_registry ?
spp_case_session ?
spp_cel_domain ?
spp_cel_event ?
spp_cel_registry_search ?
spp_cel_vocabulary ?
spp_change_request_v2 75.46% <ø> (ø)
spp_claim_169 ?
spp_cr_type_assign_program 91.17% <ø> (ø)
spp_cr_types_advanced 0.00% <ø> (ø)
spp_cr_types_base 0.00% <ø> (ø)
spp_dci ?
spp_dci_client ?
spp_dci_client_dr ?
spp_dci_client_ibr ?
spp_dci_client_sr ?
spp_dci_compliance ?
spp_dci_demo 69.23% <ø> (ø)
spp_dci_indicators ?
spp_dci_server ?
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_disability_registry ?
spp_drims ?
spp_drims_sl ?
spp_drims_sl_demo ?
spp_encryption ?
spp_farmer_registry ?
spp_farmer_registry_cr ?
spp_farmer_registry_demo ?
spp_farmer_registry_vocabularies ?
spp_gis 75.85% <100.00%> (+1.75%) ⬆️
spp_gis_indicators ?
spp_gis_report ?
spp_graduation ?
spp_grm ?
spp_grm_case_link ?
spp_grm_demo ?
spp_hazard ?
spp_hazard_programs ?
spp_hxl_area ?
spp_import_match ?
spp_indicator ?
spp_irrigation ?
spp_land_record ?
spp_metric ?
spp_metric_service ?
spp_metrics_core ?
spp_metrics_services ?
spp_mis_demo_v2 ?
spp_oauth ?
spp_program_geofence 96.47% <96.47%> (+16.23%) ⬆️
spp_programs 65.27% <ø> (ø)
spp_registrant_gis ?
spp_registry 86.83% <ø> (ø)
spp_registry_group_hierarchy ?
spp_scoring ?
spp_scoring_programs ?
spp_security 66.66% <ø> (ø)
spp_service_points ?
spp_simulation ?
spp_starter_disability_registry ?
spp_starter_farmer_registry ?
spp_starter_social_registry ?
spp_starter_sp_mis ?
spp_statistic ?
spp_storage_backend ?
spp_studio ?
spp_studio_change_requests ?

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

Files with missing lines Coverage Δ
spp_gis/__manifest__.py 0.00% <ø> (ø)
spp_gis/controllers/main.py 100.00% <100.00%> (+37.50%) ⬆️
spp_gis/models/geofence.py 88.54% <100.00%> (+0.90%) ⬆️
spp_gis/operators.py 81.25% <100.00%> (+7.40%) ⬆️
spp_program_geofence/__init__.py 100.00% <100.00%> (ø)
spp_program_geofence/models/__init__.py 100.00% <100.00%> (ø)
spp_program_geofence/models/program.py 100.00% <100.00%> (+16.66%) ⬆️
spp_program_geofence/wizard/__init__.py 100.00% <100.00%> (ø)
...p_program_geofence/wizard/create_program_wizard.py 100.00% <100.00%> (ø)
spp_program_geofence/__manifest__.py 0.00% <0.00%> (ø)
... and 1 more

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

Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/post-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
Comment thread spp_gis/migrations/19.0.2.1.0/pre-migration.py Fixed
…tion (from #76)

Re-lands the PR #76 description scope: spp_gis complex-geometry operators
(MultiPolygon/GeometryCollection + distance buffering), OSM fallback when no
MapTiler key is configured, renderer/edit-widget lifecycle fixes, geofence
uuid in GeoJSON output, the spp.gis.geofence.tag model, and the new
spp_program_geofence module (geofence eligibility manager, program UI,
creation wizard).

Adds what the original PR lacked: a migration pair remapping existing
vocabulary-based geofence tag links (release v19.0.2.0.0 schema) onto
spp.gis.geofence.tag records — the rel table is reused, so legacy rows are
parked pre-upgrade and restored post-upgrade. Includes regression tests.
…generate spp_gis README

Semgrep flagged f-string SQL in the tag migration scripts; identifiers were
constants but composed SQL via psycopg2.sql removes the pattern entirely.
README.rst regenerated from the updated HISTORY fragment (in-scope module
only).
Replaces psycopg2.sql identifier composition with prebuilt literal queries
(the aux table name and the valid-link filter are compile-time constants),
clearing the odoo-sql-injection-string-format code-scanning alerts. Values
remain parameterized; behavior unchanged (spp_gis suite 92/0).
Every module carries a readme/HISTORY.md; the module was created in #76
without one. README.rst History section will be aligned to CI's renderer
output in a follow-up commit.
…r-facing strings in _()

- README.rst History section applied verbatim from CI's pre-commit diff.
- Address gemini-code-assist review: translatable message_post/lock-reason and
  preview notification strings (i18n).
…back

DESCRIPTION.md predates the geofence move (#74) and did not mention the
capabilities this PR adds. README will be aligned to CI's renderer output in
a follow-up commit.
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Disposition of gemini-code-assist findings:

Applied (commit 9d9afdb): i18n _() wraps for message_post/lock-reason and preview notification strings.

Deferred as follow-ups (pre-existing #76 code; this PR re-lands reviewed behavior and we're keeping the diff scoped):

  • eligibility_manager.py:228 active-record check in the queued import — behavior change needing product decision on deactivated-program semantics.
  • eligibility_manager.py:157 NOT-IN removal and :189 recordset subtraction — valid perf/idiom improvements; tier1/tier2 counts are surfaced separately so the dedup semantics deserve their own reviewed change.
  • field_gis_edit_map.esm.js:62 map re-creation on patch — real concern; needs manual UI verification of zoom/pan/draw state across edits, best done as a dedicated GIS-widget PR.

… ladder

The module's fragments use #/## headings; ### produced an RST title-level
skip that crashed oca-gen-addon-readme.
…fence

READMEs regenerated by CI's pinned generator from the updated DESCRIPTION and
new HISTORY fragments; applied verbatim from its pre-commit diff.
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Review & merge order for the PR #76 re-land stack

This PR is part of a 10-PR stack re-landing reverted #76 (revert: #271) in description-consistent pieces. Reviews within a batch can run in parallel; order only matters at merge time. All PR descriptions are self-contained.

Batch 1 — review now, merge in any order (independent, CI green)

PR Scope Review attention points
#273 (this) spp_gis operators/OSM fallback/widgets + new spp_program_geofence Geofence-tag migration (pre/post pair over the reused rel table)
#274 spp_hazard severity→CAP vocabulary + drims/sl_demo/hazard_programs Severity migration mapping: 1→minor, 2→moderate, 3→severe, 4→severe, 5→extreme (deliberate product decision); docs-only commit aligning five base READMEs with CI's renderer
#275 spp_cel_domain: SQL CASE compiler, smart-op lookup, cache tests 27 added edge-path tests (patch coverage 99.3%)
#276 spp_api_v2: OpenAPI polymorphic bodies, OAuth2 auth scheme, bundles Auth middleware change is security-relevant
#277 PHL geodata + spp_demo_phl_luzon + prep script Mostly demo data; batched loader refactor

Batch 2 — merge strictly after prerequisites

PR Merge only after Mechanics
#279 metric-service #275 Auto-retargets to 19.0 on parent merge; wait for its CI to re-run green before merging
#280 gis-report #279 Same retarget-then-CI pattern
#281 api-v2-gis (draft) #273 + #274 + #276 + #279 + #280 (all) Will be rebased onto 19.0 and marked ready-for-review once all five land
#282 mis-demo-v2 (draft) #281 Marked ready once #281 lands

Cautions

Verification summary per PR is in each description; the deferred gemini-code-assist suggestions are documented as comments on the affected PRs.

…and MapTiler-key paths (codecov patch target)
- eligibility_manager: guard the queued import against archived/deleted
  programs (with regression test); drop the NOT-IN anti-pattern from the
  tier-2 domain (tier1 | tier2 already dedups); recordset subtraction for
  already-enrolled exclusion; iterate ids when building membership values.
- gis_edit_map widget: only re-create the MapLibre map when the geometry
  value actually changed (prevents zoom/pan loss and WebGL context churn on
  unrelated OWL patches).
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Update: the four previously-deferred gemini-code-assist findings are now applied as well (per maintainer request):

  • Queued import guards against archived/deleted programs (+regression test).
  • Tier-2 domain drops the NOT-IN anti-pattern (tier1 | tier2 already dedups).
  • Enrolled-beneficiary exclusion via recordset subtraction; membership values built from ids.
  • gis_edit_map re-creates the MapLibre map only when the geometry value actually changed (no more zoom/pan loss or WebGL churn on unrelated OWL patches). Recommend a quick manual map-edit smoke test during review.

Suites: spp_program_geofence 41/0, spp_gis 96/0. All gemini findings on this PR are now addressed.

@kneckinator

Copy link
Copy Markdown
Contributor

Code Review — PR #273: geofence-based geographic targeting for programs

Reviewed the full gh pr diff (39 files, +2825/-253) plus surrounding code on base branch 19.0. Overall this is a well-structured, well-tested re-land. The new module is self-contained, the spp_gis changes are backward-compatible for in-repo consumers, and the migration pair is careful. Findings below, severity-ranked. No blocking correctness defect was confirmed.

Overview

  • New module spp_program_geofence: program-level geofence_ids/geofence_count, a pluggable eligibility manager (spp.program.membership.manager.geofence) with Tier 1 (coordinate intersection) and Tier 2 (area-intersection fallback, optional + area-type filter), preview action, program form + creation-wizard integration, ACLs and menu gating.
  • spp_gis: spatial operators gain MultiPolygon/GeometryCollection support via ST_GeomFromGeoJSON with an ST_Buffer distance path; MapTiler key made optional with an OSM raster fallback (shared osm_fallback_style.esm.js); geofence tags move from spp.vocabulary to a new spp.gis.geofence.tag model over the same rel table, with a pre/post migration pair; to_geojson now emits the record uuid as the feature id; gis renderer/edit-map widget lifecycle refactors.

Downstream impact (in-repo) — no breakage found

Two in-repo modules _inherit "spp.gis.geofence":

  • spp_hazard/models/geofence.py — only extends geofence_type selection and adds incident_id. Does not touch tag_ids. Unaffected.
  • spp_api_v2_gis/models/geofence.py — extends geofence_type and overrides _get_geojson_properties. Does not touch tag_ids.
    • spp_api_v2_gis/services/export_service.py:134 calls geofence.to_geojson(). Adding a GeoJSON-standard id key is additive; spp_api_v2_gis/tests/test_geofence_model.py:123 only asserts on props, so the new id does not break it. Verify manually that the OGC/export consumers tolerate a feature-level id (they should — it is standard GeoJSON).
  • tag_ids model swap is transparent to consumers: _get_geojson_properties does self.tag_ids.mapped("name"), and both the old spp.vocabulary and the new spp.gis.geofence.tag expose name, so it keeps working.
  • spp_gis drops spp_vocabulary from depends. spp_api_v2_gis declares spp_vocabulary in its own manifest, so it is unaffected. Grep confirms no remaining spp.vocabulary reference inside spp_gis after this PR (only the migration reads the legacy table by name).
  • ACL group refs (spp_programs.group_programs_viewer, group_programs_validator, group_registry_read) and the coordinates/geo_polygon fields used in domains all exist in-repo.

Correctness

  • Tier 2 domain (eligibility_manager.py:1984-1998): dropping the id not in tier1.ids anti-pattern is fine because tier1 | tier2 recordset union dedups. test_hybrid_no_duplicates covers it. Good.
  • _get_combined_geometry filters if gf.geometry; a shapely empty polygon is falsy in shapely 2.x, so POLYGON EMPTY rows collapse to Nonetest_combined_geometry_no_shapes exercises exactly this. Correct.
  • Complex-geometry distance path (operators.py:349-356): when srid == 4326 it transforms both operands to 3857 before buffering so the distance is in metres, matching the existing coordinate path. Parameters are bound via SQL("... %s ...", ...) — no interpolation, no injection. Good.
  • _import_registrants (eligibility_manager.py:2062-2080) guards against archived/deleted programs (not program.exists() or not program.active) — covered by test_import_skips_archived_program. Good defensive fix.
  • Migration pair: on a real upgrade the new spp_gis_geofence_tag table does not exist yet at pre-migration time, so the non-SKIP branch runs; the SKIP_VALID branch (rerun/tests) is covered by test_03_valid_new_links_survive. Both PARK/DELETE queries are literal + value-parameterized.

Things to verify manually (not confirmed defects)

  • _import_registrants diverges from the base manager. The base (spp_programs/.../eligibility_manager.py:175-188) uses bulk_create_memberships(vals_list, skip_duplicates=True) and invalidates the recordset cache; the geofence override uses a plain list create() and relies solely on the upfront new_beneficiaries -= get_beneficiaries().mapped("partner_id") subtraction for dedup. For OpenSPP's large registries this is the relevant path: (a) plain create() is slower than bulk_create_memberships on big async chunks (10k each), and (b) it loses the skip_duplicates safety net against races/concurrent enrolments. Correctness is fine for the tested single-run case; consider reusing bulk_create_memberships for parity and performance. Worth a maintainer decision.
  • Tier 2 ("area_id", "in", matching_areas.ids) is bounded by the number of intersecting areas. With no fallback_area_type_id on a national-scale program this list can be large; acceptable for now but flag for the large-deployment case. The spatial gis_intersects on spp.area.geo_polygon is the real cost — ensure a GiST index exists on that column (out of scope for this PR, but relevant to the feature's performance).
  • Async group import guard (eligibility_manager.py:6-9): wrapping from odoo.addons.job_worker.delay import group in try/except sets group=None; if job_worker were absent and the >1000 async path ran, group(*jobs) would TypeError. In practice spp_programs hard-imports it, so it is always present — harmless, but the guard gives a false sense of optionality.
  • Migration reads spp_vocabulary by table name in the PARK query without a _table_exists guard on that specific table (only the rel table is guarded). Realistic upgrade paths keep spp_vocabulary present during the transaction, so risk is low; a defensive check would remove the residual edge case.

Tests

Strong coverage: operator MultiPolygon/GeometryCollection + buffer + shapely-object paths; MapTiler controller (configured/placeholder/absent); migration remap/dedup/valid-survival/fresh-DB no-op; eligibility tiers, target-type, disabled exclusion, multi-geofence union, enroll/import/dedup, async threshold, cycle verification, preview success/error, archived-program skip; officer-role read/create; wizard geofence-vs-default manager selection.

Minor test smells (non-blocking): test_import_registrants_async_uses_identity_key and ..._on_done_uses_statistics_refresh wrap the call in bare try/except Exception: pass, which can mask real failures while asserting only on captured mock kwargs. README.rst has no trailing newline.

Security

  • New spp.gis.geofence.tag model has proper ACLs (admin RW, registry manager/read); program viewers/validators get read-only on geofence + tag. Menu is groups-gated so it is not shown to users lacking read access. No sudo misuse in the new model/wizard code; the only sudo (controller ir.config_parameter) is pre-existing and nosemgrep-annotated. No SQL injection — all dynamic SQL uses bound parameters.

Conventions

  • Manifest version bump (spp_gis 19.0.2.0.0 → 19.0.2.1.0) plus matching HISTORY.md / DESCRIPTION.md / rendered README.rst / static/description/index.html; new module ships its own HISTORY/DESCRIPTION and pyproject.toml. User-facing strings wrapped in _(). Style matches surrounding Odoo/OpenSPP conventions.

Verdict

Clean, well-tested PR with no confirmed defects and no in-repo downstream breakage. The one substantive item worth a maintainer decision is the _import_registrants divergence from the base bulk_create_memberships path (performance + duplicate-safety on large imports); everything else is minor or verify-manually.

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.

3 participants