Skip to content

feat: CEL ↔ DCI bridge (spp_cel_dci_bridge) + OpenG2P preset (spp_dci_openg2p)#199

Draft
gonzalesedwin1123 wants to merge 68 commits into
19.0from
feat/spp-cel-dci-bridge
Draft

feat: CEL ↔ DCI bridge (spp_cel_dci_bridge) + OpenG2P preset (spp_dci_openg2p)#199
gonzalesedwin1123 wants to merge 68 commits into
19.0from
feat/spp-cel-dci-bridge

Conversation

@gonzalesedwin1123
Copy link
Copy Markdown
Member

Summary

Enables CEL eligibility rules of the form has_disability == true to fetch
values from external DCI registries (OpenG2P or any compliant DR), cache
them in spp.data.value, and resolve via the existing CEL metric SQL fast
path during program enrollment. See ADR-023 (in .claude-shared/docs/architecture/decisions/)
for the full design.

Two new modules:

  • spp_cel_dci_bridge — registry-type-agnostic infrastructure. Overrides
    spp.data.cache.manager._compute_variable_values to route DCI-backed
    external CEL variables through a dispatcher that picks the right DCI
    service (DR / CRVS / IBR) by registry_type. Schema extensions on
    spp.data.provider (dci_data_source_id) and spp.cel.variable
    (dci_attribute_path, external_failure_policy). New audit model
    spp.dci.fetch.audit records one row per subject per fetch.

  • spp_dci_openg2p — permanent OpenG2P vendor preset. Config-only in
    v1 (no Python). Ships three data/ records: DCI data source, CEL data
    provider, and an in-place override of spp_studio.var_has_disability
    that repoints the semantic has_disability accessor at the OpenG2P
    provider. CEL accessor name stays vendor-neutral (ADR-023 §1a).

One upstream fix bundled in for unblock: spp_cel_domain.cel_executor
handles boolean RHS in the metric SQL fast path (previously True/False
went through the numeric branch because bool is a subclass of int,
producing broken SQL numeric = boolean).

Architecture (demo flow)

CEL: has_disability == true
  → resolver → metric('has_disability', me) == true
  → translator → executor SQL fast path
  → id IN (SELECT subject_id FROM spp_data_value WHERE value::boolean = true)

Cache population (triggered by cycle pre-fetch, already wired in spp_programs):
  → cache_mgr.precompute_cached_variables(subject_ids)
  → _compute_variable_values  (overridden here)
  → spp.cel.dci.dispatcher.fetch_values_for_variable
  → DRService.get_disability_status(partner)  ←  mocked DCIClient in tests
  → write spp.data.value rows + spp.dci.fetch.audit rows

Test plan

  • ./scripts/test_single_module.sh spp_cel_dci_bridge — 47/47 passing
  • ./scripts/test_single_module.sh spp_dci_openg2p — 4/4 passing
  • pre-commit run --files — clean (ruff/format/semgrep/bandit/pylint)
  • Manual smoke test against a real OpenG2P endpoint (awaiting credentials)
  • Demo dry-run before SPDCI event

Notable design choices

Decision Where it's locked in
Variable-as-metric (has_disability == true), not function call ADR-023 §1
CEL accessors are semantic, not vendor-named ADR-023 §1a
Override _compute_variable_values is the single integration point ADR-023 §2
Sync per-subject in v1; batching deferred to v2 Plan §6.3
Dedicated spp.dci.fetch.audit model, not spp.audit.log reuse Plan §6.4
Identity mapping fallback: warn + null + no raise; fail policy opts in Plan §6.5
Two modules: bridge + vendor preset (not _demo, not _client_) Plan §6.7
Override spp_studio.var_has_disability in place rather than creating a parallel variable (single source-of-truth for the semantic accessor) Step 11

Out of scope (tracked in ADR-023 §"Future Work")

  • Async DCI flows (callback-driven cache updates)
  • Multi-subject batching in DCI search_request envelopes
  • Rate limiting / circuit breakers
  • Cross-registry composition with fallback ordering
  • Consent gating
  • Real OpenG2P fixtures (using mocked DCIClient per existing test pattern)

Files

  • 12 commits, ~1900 lines added across both modules
  • See git log 19.0..HEAD --oneline for the staged commit sequence

Empty module skeleton for the CEL <-> DCI external-fetch bridge described
in ADR-023. Installs cleanly; subsequent commits add schema extensions,
dispatcher, registry-type handlers, cache-manager override, and tests.
Add dci_data_source_id + is_dci_backed on spp.data.provider, and
dci_attribute_path + external_failure_policy on spp.cel.variable.
Constraint ensures DCI-backed externals declare an attribute path.
Inherit form/list views to expose the new fields.

Validates ADR-023 schema additions (additive only, no removals).
spp.cel.dci.dispatcher routes fetch_values_for_variable() by the DCI
data source's registry_type to per-type handlers (DR, CRVS, IBR, SR, FR).
Handlers return {} in this step; subsequent commits implement DR (step 4),
CRVS and IBR (steps 9-10).

Tests verify routing logic, graceful empty returns for missing/inactive
setup, UserError on unknown registry types, and the nested-attribute
path extraction helper used by handlers.
_handler_dr instantiates DRService per subject, extracts the configured
dci_attribute_path from the response payload, and returns {subject_id: value}.
Subjects without DR records, without resolvable identifiers, or that error
during the fetch are omitted (not raised) so a single bad subject can't
fail the batch. Failure policy (step 6) decides what null means.

Tests cover happy path, false values, nested attribute extraction, empty
responses, missing identifiers, multi-subject batches, and per-subject
error tolerance. Uses MagicMock to patch DCIClient — matching the
established mocking pattern in spp_dci_client_dr/tests.

spp_dci_client_dr is declared as a hard dependency for v1; the runtime
ImportError guard remains so future deployments without DR can refactor.
Inherit spp.data.cache.manager and override _compute_variable_values:
when source_type='external' and provider.is_dci_backed, call the
dispatcher; otherwise super(). This fills the documented blind spot in
spp_cel_domain/models/data_evaluator.py:108-116 ("Values must be pushed
via API or scoring run") for the DCI case.

The cycle pre-fetch path (cycle_manager_base._precompute_cycle_cached_variables)
flows through this override unchanged — the rest of the eligibility
plumbing requires no edits.

Tests verify routing, super-fallthrough for non-DCI externals,
non-interference with source_type='field', the end-to-end precompute path
writes to spp.data.value, and dispatcher errors yield {} not raise.
Three policies as defined in ADR-023 §8:
- null (default): swallow errors; missing entries leave CEL evaluating against null
- last_known: surface most recent non-null spp.data.value row, regardless of expiry
- fail: propagate exception as UserError; eligibility check aborts

last_known queries spp.data.value sorted by recorded_at desc, takes the
first non-null payload per subject, and logs a warning per fallback so
operators see what's degraded. Per-subject errors inside the handler
loop continue to fall under whichever policy applies; only the wholesale
dispatcher exception triggers the fail re-raise in v1.

Tests cover all three policies for both wholesale and partial failures,
plus the partial-success case where some subjects get live values and
others get last-known fallbacks.
New lightweight model spp.dci.fetch.audit records one row per subject
per fetch attempt: provider, data source, registry type, variable,
subject, outcome (ok/not_found/error), error message, elapsed ms, user.

Decided in ADR-023 §6.4 to ship a dedicated model rather than reuse
spp.audit.log, which is CRUD-shaped and would require synthetic audit
rules to record non-CRUD events.

DR handler now wraps each subject fetch with start/stop timing and
calls _record_audit with the appropriate result. Audit writes go
through sudo so background workers can record regardless of user
context; audit failures are caught so they can't poison a fetch.

Read access granted to all internal users; write to spp admin only.
`isinstance(True, int)` is True in Python — bool is a subclass of int.
The previous _metric_cmp_supported and _metric_inselect_sql checked int|float
first, routing boolean rhs through the numeric SQL path. That generates:
    AND (CASE WHEN jsonb_typeof(...) = 'object' THEN (...)::numeric ... END) = true
which postgres rejects with "operator does not exist: numeric = boolean".

Fix: check isinstance(rhs, bool) BEFORE the int|float branch in both methods,
and emit ::boolean casts plus a boolean rhs comparison. Affects any cached
variable with value_type=boolean queried via `var == true/false` in CEL —
including the new DCI-backed has_disability variable used by spp_cel_dci_bridge.
Exercises the full chain: precompute_cached_variables() -> cache manager
override -> dispatcher -> DR handler -> mocked DCIClient -> spp.data.value
rows -> CEL service.compile_expression() -> SQL fast path against
spp_data_value -> domain that filters to the right partners.

The cache manager override now fills missing subjects with explicit None
after policy is applied. This keeps the cache complete across the queried
cohort, which is what the executor needs to use the metric SQL fast path
(have == base) instead of falling back to Python evaluation that requires
spp.indicator. CEL semantics line up: a subject with value=null fails
`has_disability == true` filtering, which is the right answer when the
external registry returned no data.

Tests cover the happy path (all subjects have DR records) and the
partial-results case (only some subjects matched). Several earlier
failure-policy tests had to be updated for the new contract: "missing
subject" now appears in the result as None rather than being absent.
…ormalizer

CRVS and IBR handlers follow the DR pattern: loop subjects, call the
service, extract via dci_attribute_path, record audit. Each tolerates
its respective DCI client module being uninstalled via try/ImportError.

CRVS's verify_birth(id_type, id_value) needs the partner's identifier
resolved first; added _first_identifier helper that reads from reg_ids.
IBR's check_duplication(partner) takes a partner directly; IBRService
also has a different constructor signature ((data_source, env) instead
of (env, data_source_code=...)) — handled in the handler.

The three DCI services use inconsistent registry_type strings:
  DR  -> "DR"
  CRVS -> "ns:org:RegistryType:Civil"
  IBR  -> "ibr"
Dispatcher now normalizes via _REGISTRY_TYPE_ALIASES before key lookup
so a deployment's source value (URI or short code, any of the legacy
shapes) routes to the right handler. Upstream cleanup of the field is
tracked separately.
Three data/ records ship the OpenG2P wiring:
  - spp.dci.data.source 'openg2p_dr' (DR registry, base_url placeholder,
    auth_type='none' — admins configure OAuth2 after install so no
    secrets land in source control)
  - spp.data.provider 'openg2p_dr' linked to the source
  - In-place override of spp_studio.var_has_disability: switches the
    semantic 'has_disability' CEL accessor from source_type='field'
    (local res.partner.is_person_with_disability) to source_type='external'
    routed through the OpenG2P DCI provider, cache_strategy=ttl, ttl=300s
    for demo visibility, failure_policy=null

Installing the preset declaratively states "OpenG2P is the authority
for disability status in this deployment." Existing CEL rules that
reference `has_disability == true` continue to work — they now evaluate
against the cached DCI value instead of the local field.

The CEL accessor stays semantic (vendor-neutral per ADR-023 §1a). The
OpenG2P-ness lives only in the data-source and provider records.
Repointing at a different DCI Disability Registry is a configuration
change on the data source, never a CEL change.

Smoke tests confirm the three records exist, are correctly linked,
and the accessor names contain no vendor strings.
- DESCRIPTION/USAGE/CONFIGURE markdown fragments for both modules
- Auto-generated README.rst + pyproject.toml + static/description (OCA hook)
- ruff-format applied to all changed Python
- Add # nosemgrep justification on the audit sudo() call (background
  workers without spp admin rights must still write audit rows)
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 81.34851% with 213 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.89%. Comparing base (30f6364) to head (0a5f269).
⚠️ Report is 109 commits behind head on 19.0.

Files with missing lines Patch % Lines
...dci_server_disability/routers/disability_router.py 18.86% 86 Missing ⚠️
spp_dci_openspp_dr/services/openspp_dr_service.py 63.51% 27 Missing ⚠️
spp_cel_dci_bridge/models/dci_dispatcher.py 89.86% 15 Missing ⚠️
spp_dci_openg2p/wizards/sr_import_wizard.py 94.05% 12 Missing ⚠️
...ci_server_disability/models/fastapi_endpoint_dr.py 36.84% 12 Missing ⚠️
spp_dci_openg2p/models/dci_dispatcher.py 79.48% 8 Missing ⚠️
spp_dci_openg2p/services/openg2p_social_service.py 84.61% 8 Missing ⚠️
spp_dci_openspp_dr/models/dci_dispatcher.py 79.48% 8 Missing ⚠️
...r_disability/services/disability_search_service.py 89.87% 8 Missing ⚠️
spp_cel_dci_bridge/models/eligibility_manager.py 80.55% 7 Missing ⚠️
... and 9 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #199      +/-   ##
==========================================
+ Coverage   71.68%   71.89%   +0.21%     
==========================================
  Files         942      980      +38     
  Lines       55470    56722    +1252     
==========================================
+ Hits        39763    40780    +1017     
- Misses      15707    15942     +235     
Flag Coverage Δ
spp_analytics 93.13% <ø> (ø)
spp_api_v2 80.33% <ø> (ø)
spp_api_v2_change_request 66.85% <ø> (ø)
spp_api_v2_cycles 71.12% <ø> (ø)
spp_api_v2_data 64.41% <ø> (ø)
spp_api_v2_entitlements 70.19% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products 66.27% <ø> (ø)
spp_api_v2_service_points 70.94% <ø> (ø)
spp_api_v2_simulation 71.12% <ø> (ø)
spp_api_v2_vocabulary 57.26% <ø> (ø)
spp_approval 50.29% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_cel_dci_bridge 90.41% <90.41%> (?)
spp_cel_domain 61.09% <57.14%> (-0.06%) ⬇️
spp_dci_openg2p 92.20% <92.20%> (?)
spp_dci_openspp_dr 75.67% <75.67%> (?)
spp_dci_server_disability 63.92% <63.92%> (?)
spp_programs 64.84% <ø> (+0.25%) ⬆️
spp_security 66.66% <ø> (ø)

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

Files with missing lines Coverage Δ
spp_cel_dci_bridge/__init__.py 100.00% <100.00%> (ø)
spp_cel_dci_bridge/exceptions.py 100.00% <100.00%> (ø)
spp_cel_dci_bridge/models/__init__.py 100.00% <100.00%> (ø)
spp_cel_dci_bridge/models/cel_variable.py 100.00% <100.00%> (ø)
spp_cel_dci_bridge/models/data_provider.py 100.00% <100.00%> (ø)
spp_cel_dci_bridge/models/dci_data_source.py 100.00% <100.00%> (ø)
spp_dci_openg2p/__init__.py 100.00% <100.00%> (ø)
spp_dci_openg2p/models/__init__.py 100.00% <100.00%> (ø)
spp_dci_openg2p/models/dci_data_source.py 100.00% <100.00%> (ø)
spp_dci_openg2p/services/__init__.py 100.00% <100.00%> (ø)
... and 29 more

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

Copy link
Copy Markdown

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

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 the spp_cel_dci_bridge module, which integrates OpenSPP's CEL expression engine with external DCI registries (DR, CRVS, IBR). It allows CEL variables to fetch and cache external values for use in SQL-based eligibility filters. The PR also includes an OpenG2P preset module and updates the CEL executor to support boolean comparisons in SQL. Review feedback highlights performance concerns regarding historical cache retrieval and audit log creation, suggesting the use of DISTINCT ON and batched database writes to optimize these operations.

Comment thread spp_cel_dci_bridge/models/data_cache_manager.py Outdated
Comment thread spp_cel_dci_bridge/models/dci_dispatcher.py
_record_audit() escalated to sudo() to write the audit row, which made
the user_id field's default lambda resolve self.env.user against the
sudoed env — recording every fetch as user_root. This defeated the
audit's compliance purpose (we recorded WHAT but lost WHO triggered it).

Capture self.env.uid into acting_user_id BEFORE sudo() and pass it
explicitly. Audit rows now record the operator who triggered enrollment.

Test reworked to drive the dispatcher via .with_user(officer) where
officer is a non-admin internal user, then assert the row records that
officer rather than user_root.
…urationError

Previously, configuration errors silently degraded into "no one is
eligible":
- spp_dci_client_dr not installed -> ImportError branch returned {}
- _handler_sr / _handler_fr stubs returned {}
- Unknown registry_type raised UserError that the cache manager could
  swallow under null policy

_compute_dci_values would then fill every subject with None, the cache
looked fresh, and the eligibility CEL filter silently excluded everyone.
This is the worst kind of compliance bug — silently wrong, indistinguishable
from "no one is disabled."

Introduce DCIConfigurationError (subclass of UserError so existing
catch-blocks still work). Raise it from:
- The three ImportError branches (_handler_dr/_handler_crvs/_handler_ibr)
- The two v1 stub handlers (_handler_sr, _handler_fr)
- The unknown-registry-type dispatch failure

_compute_dci_values now distinguishes configuration errors from runtime
errors: configuration errors propagate unconditionally regardless of
external_failure_policy. Runtime errors (transient network failures,
registry returns 500) continue to follow the policy.

Tests:
- Dispatcher raises DCIConfigurationError for SR/FR/unknown
- Cache manager lets DCIConfigurationError propagate under all three
  failure policies (null/last_known/fail)
spp_studio.var_has_disability lacks noupdate=1 in its declaring module,
so a future `-u spp_studio` (run as part of any unrelated upgrade) will
silently reset the variable back to source_type='field', breaking the
demo deployment with no error. The preset's own noupdate=1 only protects
against re-applying THIS module's data file, not upstream resets.

post_init_hook re-asserts the DCI binding (source_type='external',
external_provider_id, dci_attribute_path, ttl, failure_policy) after
every install/upgrade. Odoo upgrade ordering guarantees this fires
after spp_studio's data files have loaded, so any silent reset gets
undone here. Idempotent: when the binding is already correct, the hook
short-circuits without writing.

Tests:
- Simulated spp_studio reset then ran hook: binding restored
- Idempotency check: hook on clean state is a no-op
action_dci_fetch_audit was previously unreachable from any menu. Two
entries because two operator personas need the log:
- DCI > Activity Logs > DCI Fetch Audit (DCI ops persona)
- CEL Domain > Data Management > DCI Fetch Audit (CEL ops persona)

Both gated to spp_admin since audit data is sensitive.
…n provider form

Previously inserted dci_data_source_id inside the parent "Connection"
group on the data provider form, right next to base_url and auth_type.
This was confusing: when DCI routing is active, those parent fields are
runtime-ignored (the linked DCI Data Source has its own URL and auth),
but the form let operators edit them as if they mattered.

New layout:
- New "DCI Integration" notebook page (first, before "Authentication")
- Hosts dci_data_source_id with no_create/no_quick_create options
- Info alert appears when is_dci_backed, explaining that the legacy
  Base URL/Auth fields are ignored at runtime
- Legacy base_url and auth_type become readonly when DCI-backed
Previously, dci_attribute_path and external_failure_policy showed for
any external-source variable, including non-DCI providers (REST APIs,
scoring services) where they have no meaning.

Add external_provider_is_dci_backed as a related field on spp.cel.variable
so the view can gate visibility through the provider's flag. Tighten
both fields' invisible= conditions to require it.

Also: mark dci_attribute_path required= when the provider is DCI-backed.
Previously the constraint at _check_dci_attribute_path() raised
ValidationError only on save; now the form renders the red asterisk and
Odoo's client-side check catches it before save.
The audit list displayed subject_id as a bare integer with no resolution
to the partner. Compliance reviewers looking at "subject_id 4271" had no
way to trace it back to a person.

Add subject_ref as a computed Reference field that resolves
(subject_model, subject_id) to a partner record. Not stored — falsy when
the partner has been deleted since the fetch — but the immutable
subject_id snapshot is preserved as the historical truth.

The list shows subject_ref by default; subject_id is hidden in the list
(toggle-able) and remains the canonical search field for historical
investigations.

Tests:
- subject_ref resolves to the current partner record
- subject_ref is False when subject_id points to a missing partner, but
  the integer subject_id is preserved
_augment_with_last_known previously Python-filtered a search() result
to pick the latest non-null row per subject. That works at demo scale
but is O(history × cohort) — a deployment with daily TTL refresh over
6 months × 1k subjects fetches ~180k rows just to surface the latest
1k. Reported by gemini-code-assist on the PR.

Replace with a single SQL query using DISTINCT ON (subject_id)
ORDER BY subject_id, recorded_at DESC, id DESC. JSON null is filtered
at the SQL layer so historical {"value": null} rows aren't surfaced
as "last known."

Behavior is unchanged for the existing tests (single row per subject,
no rows, null rows). Added a new test that exercises multiple history
rows per subject to lock in the "most recent wins" semantic that the
prior Python loop also produced.

Defer Gemini's other suggestion (batch the per-subject audit create()
calls) to a separate follow-up — the durability tradeoff vs. throughput
deserves its own design discussion.
…om CI

Three issues caught by CI that the local --files run had missed because
it didn't exercise --all-files semantics:

1. oca-checks-odoo-module: spp_dci_openg2p/security/ir.model.access.csv
   was empty (preset module defines no custom models, no ACL needed).
   Removed the file and the now-empty security/ directory rather than
   adding it to the manifest's data list — there's nothing to ACL.

2. ruff E501: dci_fetch_audit.py:50 subject_ref help= string was 189
   chars on one line. Split into a parenthesised multi-line string.

3. ruff-format: tests/test_failure_policy.py and models/data_cache_manager.py
   carried minor formatting drift that --files didn't catch. Reformatted.

All 56 spp_cel_dci_bridge tests and 6 spp_dci_openg2p tests still pass.
…/safety paths

Codecov flagged 41 missing lines on the PR. Most of the gap is in
parallel-structure code paths that DR has tests for but CRVS and IBR
don't. Added focused tests for:

CRVS handler:
- Per-subject exception swallow + audit error row
- Empty registry response -> not_found audit
- Successful response with missing dci_attribute_path -> not_found

IBR handler (same shape, patches check_duplication directly since the
inner search_by_id is swallowed by the IBR service itself):
- Per-subject exception
- Missing attribute path

Dispatcher:
- _record_audit's outer try/except: if the audit write itself fails,
  the fetch must still complete and return values

post_init_hook safety branches (spp_dci_openg2p):
- spp_studio.var_has_disability missing -> log warning, no raise
- openg2p_dr_provider missing -> log error, no raise

CEL executor boolean SQL:
- `has_disability != true` exercises the boolean `!=` operator in the
  metric SQL fast path (symmetric with `==` but a separate emission
  branch in _metric_inselect_sql)

Test count: 56 -> 62 on bridge, 6 -> 8 on preset. Total 70 tests.

The remaining uncovered lines are the two __manifest__.py files (Odoo
manifests are evaluated as dicts at module discovery, not imported as
Python — coverage cannot reach them) plus a handful of branches in
edge-case I/O paths that aren't worth dedicated tests.
…olicy

pylint_odoo W8113 (attribute-string-redundant). I missed this one
during the earlier sweep (caught it only on external_provider_is_dci_backed).
Auto-label from the field name is functionally equivalent.
@gonzalesedwin1123 gonzalesedwin1123 marked this pull request as draft May 14, 2026 01:00
Three tests imported `patch` inside the method body even though it was
already imported at the top of the file (or trivially could be):

- test_audit_logging.py:135 - test_audit_write_failure_does_not_break_fetch
- test_crvs_ibr_handlers.py:174 - test_ibr_handler_swallows_per_subject_error
- test_install.py:95,107 - the two new safety-branch tests

Promote `patch` to a top-level import in test_install.py and remove
the inner reimports. Behaviour identical; pylint_odoo W0404 cleared.
Live test against partner-registry.play.openg2p.org revealed two protocol
quirks vs. upstream spp_dci_client:

1. idtype-value query shape: OpenG2P expects nested
   {type: "idtype-value", value: {id_type, id_value}} but upstream emits
   {type: <id_type>, value: <id_value>}. Server rejects upstream form
   with rjct.search_criteria.invalid.

2. Required reg_record_type on search_criteria, omitted by upstream's
   SearchCriteria Pydantic model.

This adapter (ADR-023 §6 Option C path) absorbs both:

- spp.dci.data.source gets a `vendor` Selection field; preset's data
  source ships with vendor='openg2p'.
- OpenG2PDCIClient subclasses DCIClient and overrides _parse_query
  (nested shape) + _build_search_envelope (inject reg_record_type,
  re-sign).
- OpenG2PFRService mirrors DRService's surface but queries the FR
  registry (reg_type=Social, reg_record_type=Farmer) and unwraps
  data.reg_records[] to extract the first record.
- Bridge dispatcher inherits and overrides _handler_dr: when source
  has vendor='openg2p', route to OpenG2PFRService instead of DRService.

FR-as-DR pretense for the demo: presence of any farmer record for
a partner -> has_disability=True. CEL surface stays `has_disability == true`.
The demo audience sees a real DCI round-trip; behind the scenes we're
querying the Farmer Registry because OpenG2P hasn't published their
Disability Registry yet.

Migration plan (in readme/CONFIGURE.md) when real DR arrives: clear the
`vendor` field on the data source. Bridge falls back to upstream
DRService. No code or CEL rule changes.

Tests: 25 in preset (was 8), 62 in bridge unchanged. Covers query
shape regression, reg_record_type injection, response unwrap, vendor
routing, fallback when vendor is cleared.
OpenG2PFRService.IDENTIFIER_PRIORITY = ("UIN", "DRN", "NATIONAL_ID", "NID")
expects matching vocabulary codes on the urn:openspp:vocab:id-type
vocabulary so the registrant form's Identity tab can pick them as
ID Type. spp_vocabulary ships only lowercase generic codes (national_id,
passport, ...), so without this seed an operator could not pick UIN
when adding a reg_id to a test partner — the dispatcher would fall back
through the priority list and find nothing.

Adds spp_dci_openg2p/data/openg2p_id_types.xml seeding `UIN` (uppercase
to match SPDCI wire convention). Vocabulary is referenced via
spp_vocabulary.vocab_id_type — adds spp_vocabulary to module deps.

Test updates: two test classes used to create a fresh `UIN` code in
setUpClass; that now collides with the preset's seed. Switched both to
env.ref('spp_dci_openg2p.id_type_uin'). New tests assert the seed exists
and matches the service's IDENTIFIER_PRIORITY first entry.

If OpenG2P later returns records under DRN / NATIONAL_ID / NID, follow
the same pattern in openg2p_id_types.xml.
Comment thread spp_dci_server_disability/services/disability_search_service.py Fixed
Comment thread spp_dci_server_disability/services/disability_search_service.py Fixed
… var

Verified against partner-nsr.play.openg2p.org on 2026-05-15: OpenG2P's
SR reg_record exposes neither is_poor nor has_dependent_under_school_age
as top-level fields. The closest poverty signal is `income_level`
(string: "low" / "medium" / "high"); no signal for under-school-age
dependents exists at all.

Changes:
- var_is_poor: value_type boolean -> string; dci_attribute_path
  is_poor -> income_level. CEL rules now read `is_poor == "low"` rather
  than `== true`. Variable name kept semantic.
- var_has_dependent_under_school_age: parked state=inactive,
  active=False. Record stays registered as a deferred-feature
  placeholder so revival is a config-only change once OpenG2P exposes
  the data (or once we wire a secondary household-search call).

Implementation:
- post_init_hook's _PRESET_VARIABLES table extended to carry per-variable
  value_type and state. Hook now re-asserts both the active is_poor
  binding AND the inactive placeholder on every -i/-u, preventing UI
  edits from silently re-activating the deferred variable.
- _EXPECTED_BINDING_FIELDS gains value_type so type drift is caught.
- Tests updated: test_var_is_poor_bound_to_dci_provider asserts the new
  string/income_level pair; test_var_has_dependent_under_school_age_
  parked_inactive replaces the prior active-state assertion;
  test_post_init_hook_parks_deferred_variable_inactive locks the
  hook's drag-back-to-inactive behaviour.
- Dispatcher routing test now mocks {"income_level": "low", ...} and
  asserts the dispatcher surfaces the raw string to CEL.

Docs: CONFIGURE.md gains a "Deferred features" table documenting why
has_dependent_under_school_age is inactive and the path to revive it.
…tation

Audits ADR-023, ADR-024, the federated demo plan, and every module's
readme fragments against the as-shipped code. Corrects stale facts and
adds operational guidance discovered during end-to-end demo wiring on
2026-05-15.

Key corrections:

- ADR statuses: Proposed -> Accepted on both ADR-023 (bridge shipped)
  and ADR-024 (federated demo wired end-to-end). ADR-024 gains a
  resolution block for the original Open Items: is_poor binds to
  income_level (string compare); has_dependent_under_school_age is
  permanently deferred (no field on OpenG2P's per-individual record).

- Federated demo plan: prepended a "Post-shipment deltas" section
  capturing every place the body drifted from implementation —
  OpenG2P host (partner-nsr vs partner-registry), DR endpoint path
  (/dci_api/v1 prefix), DR-side dev-mode flags, real DR field names,
  UIN seed ownership, standalone DR docker-compose project.

- spp_dci_server_disability DESCRIPTION/CONFIGURE: replaced the
  fabricated wire-format JSON sample and Disability-fields table.
  Actual fields read are has_disability (Boolean from assessment
  chain), disability_severity_code, disability_review_category,
  disability_next_review — NOT is_person_with_disability /
  disability_certified / disability_percentage. Added the sudo()
  rationale and the dev-mode flag table.

- spp_dci_openspp_dr CONFIGURE: documented both required dev-mode
  flags (dci.allow_unsigned_requests + dci.bypass_bearer_auth) and
  the UIN-seed dependency on spp_dci_openg2p (no longer ships its
  own UIN).

- spp_dci_openg2p CONFIGURE: called out the partner-nsr host
  override that operators must apply manually (noupdate=1 means
  the XML default cannot be rewritten on upgrade). Added vendor
  field selection guidance.

- spp_cel_dci_bridge DESCRIPTION/USAGE: documented the hoisted
  vendor field, the dci_data_source_views.xml, and the eager
  pre-warm behaviour with the inactive-variable opt-out.

- spp_cel_dci_bridge/readme/USAGE.md: cel code-fence retitled to
  plain text (Pygments has no cel lexer; was blocking README.rst
  regeneration). Operator-facing example uses CEL && operator.

Regenerated README.rst and static/description/index.html for every
preset whose readme fragment changed (oca-gen-addon-readme).
Matches the whool-based packaging stub every other spp_* module ships.
Auto-generated; included to keep the new modules consistent with the
rest of the repo's Python-packaging convention.
Creates 4 demo personas (Maria Widow, Kim Lee, Priya Rivera, Noah Rivera)
on both sides of the federated topology. UINs match real OpenG2P SR
seeds (IND-NSR-0001/0002/0003/0007) so the SP-side is_poor lookup
returns a live income_level. DR side gets approved disability
assessments for the two flagged personas.

NOT A MODULE. Production module installs create zero registrants —
this is an out-of-band seed script run ONLY before a demo. Designed to
be deleted along with its partner records after the demo.

Usage:
  docker compose exec openspp-dev odoo shell -d openspp --no-http \
    < scripts/demo/setup_federated_demo.py
  docker compose -f docker-compose.dr.yml exec openspp-dr \
    odoo shell -d openspp_dr --no-http \
    < scripts/demo/setup_federated_demo.py

Idempotent on rerun. Cleanup recipe included in the script docstring.
Was creating 4 partners; OpenG2P has 15 seeded records (IND-NSR-0001..
IND-NSR-0015). Wiring every UIN gives the demo a complete eligibility
matrix:

  - 4 partners ENROLLED (poor + disabled across both registries)
  - 3 partners fail has_disability (poor on SR, no DR assessment)
  - 4 partners fail is_poor (disabled on DR, non-low income on SR)
  - 4 partners fail both

Names mirror OpenG2P's actual seed values (Alex Rivera, Morgan Cole,
Taylor Brooks, etc.) so the federation story stays honest — an SP
audit row tagged "Alex Rivera" matches what OpenG2P returns on probe.

Cleanup recipe updated to iterate the full 15-UIN range.
Two operational tweaks for tonight's dry-run:

1. RENAME instead of skip when a UIN reg_id already exists on the
   partner. Edwin's existing IND-NSR-0001 partner is already attached
   to programs (memberships, change requests) so unlink would orphan
   that state. Writing name/given_name/family_name in-place rebrands
   the existing record while preserving its FK relationships.

2. SP-side only: add every demo partner as a draft membership of
   spp.program record id=DEMO_PROGRAM_ID (default 1). This lets the
   operator demo Enroll Eligible directly — no need to walk through
   the change-request flow to register members first (a colleague
   demonstrates that on a separate instance). Memberships start in
   state='draft'; eligibility evaluation flips them based on the
   CEL rule.

DEMO_PROGRAM_ID is a named constant at the top of the script so
operators with a non-1 program id can override before running.
Resets the 15 demo memberships on program id=1 back to state='draft'
and wipes the DCI value cache for the demo partners. Lets the operator
re-run Enroll Eligible multiple times during a presentation without
manually resetting each membership through the UI.

WIPE_DCI_CACHE constant at the top toggles whether the script also
flushes the cache (default True). With cache wiped, the next eligibility
click fires fresh DCI queries to OpenG2P SR and OpenSPP-DR — useful
when the demo audience should see the live round-trip in the SP log.

Same scripts/demo/ pattern as setup_spdci_demo.py — out-of-band, not
a module, not shipped to production.
…ions

The CEL executor's top-level compile_and_preview took only the FIRST
override_domain from metrics_info, breaking compound expressions of the
form `metric('a', me) == X and metric('b', me) == Y`. Each MetricCompare's
SQL fast path successfully built its subquery clause and pushed it to
metrics_info, but only the first was used in the final domain — silently
dropping the AND'd second clause.

Symptom in tonight's SPDCI dry-run: rule
`has_disability == true && is_poor == "low"` evaluated as just
`has_disability == true`, so 8 partners enrolled (all with DR
assessments) instead of the expected 4 (intersection with low-income).

Fix: collect ALL override_domains from metrics_info and AND them on
final_domain. Single-metric case is unchanged (one clause is identical
to "take the first").

Known limitation: an OR of metric clauses still mis-composes — the per-
clause SQL subqueries cannot be UNION'd through Odoo domain syntax, so
ids materialization in _exec_metric would be needed for correctness.
Not addressed here; OR-of-metrics is uncommon in eligibility rules and
not in scope for the federated demo.

573 spp_cel_domain tests + 67 spp_cel_dci_bridge tests still pass.
Single-page reference covering:

- The 15-persona demo matrix with each registrant's UIN, OpenG2P
  income_level, OpenSPP-DR has_disability, and expected eligibility
  verdict — the four ENROLLED rows are highlighted.
- ASCII topology diagram showing SP, DR, and OpenG2P SR with the DCI
  search-sync flows between them.
- Glossary of every acronym used in the demo (SPDCI, DCI, search-sync,
  SR, DR, CRVS, IBR, FR, CEL, CEL accessor, metric() call,
  spp.dci.data.source / data.provider / dci.dispatcher / data.value /
  dci.fetch.audit, vendor adapter, MOSIP, eSignet, OpenG2P).
- Cross-references to ADRs and demo scripts.

Located alongside the seed/reset scripts in scripts/demo/ so the
operator's presentation kit is in one place.
Sweep of all touched files against the project's pre-commit suite:

- ruff-format + prettier formatting (consistent line-length and
  multi-line wrapping across services, tests, demo scripts, view XMLs).
- C8107 translation-required: wrap user-facing UserError /
  ValidationError messages in self.env._() so they participate in
  Odoo's translation pipeline. Applied in OpenG2PSocialService and
  OpenSPPDRService.
- Updated the stale top-of-file docstring on
  spp_dci_server_disability/services/disability_search_service.py to
  describe the actual fields read (has_disability boolean from the
  approved assessment + severity/review-category/next-review) instead
  of the deleted is_person_with_disability/disability_certified/
  disability_percentage names.
- Repositioned the sudo() nosemgrep markers in
  disability_search_service._find_partner_by_identifier so the
  odoo-sudo-on-sensitive-models rule also recognizes them. Added an
  inline authorization-context comment block explaining why sudo()
  is the right call here (upstream signature/bearer middleware is the
  real auth boundary; the service surface is read-only).

All 705 tests across the five touched modules still pass
(spp_cel_domain 573, spp_cel_dci_bridge 67, spp_dci_openg2p 34,
spp_dci_openspp_dr 20, spp_dci_server_disability 11). Pre-commit
overall returns exit 0.
Comment thread spp_dci_server_disability/services/disability_search_service.py Fixed
Comment thread spp_dci_server_disability/services/disability_search_service.py Fixed
The previous nosemgrep marker landed on the closing-paren line after
ruff-format wrapped the return statement across three lines, so
semgrep's per-line suppression didn't apply to line 236 where the
actual self.env["res.partner"].sudo() call lives. CI semgrep flagged
both odoo-sudo-on-sensitive-models (critical) and
odoo-sudo-without-context (warning) on this line.

Fix: extract the sudoed env reference to a named local on the same
line as its nosemgrep marker; the .browse() call moves to its own
unmarked statement (no sudo() on it).

Functionally identical; this is purely about the marker placement.
Three classes of fixes:

1. scripts/lint/check_naming.py: anchor the deprecated g2p_/g2p. import
   rule to a path-segment boundary so it doesn't false-positive against
   `openg2p_*` (the OpenG2P platform's distinct namespace). Previous
   substring check ("g2p_" in module_name) flagged
   `.openg2p_dci_client` as deprecated even though it's the local
   module in spp_dci_openg2p.

2. scripts/demo/{setup,reset}_spdci_demo.py: add file-scope linter
   directives. These are interactive Odoo-shell scripts — env is
   injected at runtime (so ruff's F821 is wrong), print() is the
   right output channel for an operator at a REPL (so pylint_odoo's
   W8116 is wrong), and the summary loop in setup intentionally
   unpacks the full row (B007). Directives:
     ruff: noqa: F821, B007
     pylint: disable=print-used

CI was failing on:
  - Semgrep OSS (already fixed in 166155e — nosemgrep marker
    placement on the actual sudo() line)
  - pre-commit's full-repo run flagging this branch's check_naming.py
    false positive and the demo scripts' linter mismatches

Local `pre-commit run --all-files` now returns exit 0. All 705 tests
across the five touched modules still pass.
Captures every IND-NSR-0001..IND-NSR-0015 record as returned by
partner-nsr.play.openg2p.org on 2026-05-15. Columns include:

  - identity:   uin, given_name, surname, sex, birth_date
  - SR data:    marital_status, employment_status, occupation,
                income_level, education_level
  - disability: openg2p_is_disabled, openg2p_self_id_disability
                (informational — the demo's has_disability comes from
                the OpenSPP-DR, not OpenG2P)
  - household:  household_id, relationship_to_head,
                citizenship_category, displacement_status
  - demo:       demo_dr_has_disability (will the script approve a DR
                assessment for this UIN?), demo_expected_verdict
                (eligibility outcome under
                has_disability == true && is_poor == "low")

Useful for the SPDCI presentation — drop into a spreadsheet or
slide table to show the source-of-truth data and the expected
demo outcomes.
Keeps uin, given_name, surname, sex, birth_date, marital_status,
employment_status, occupation — the presentation-ready subset. The
SR-specific (income_level, education_level), disability, household,
and demo-annotation columns are documented elsewhere in
SPDCI_DEMO_BRIEFING.md and don't need to clutter the audience-facing
CSV.
End-to-end technical narrative for explaining how
`has_disability == true && is_poor == "low"` triggers two federated
DCI calls and composes the eligibility decision.

Eleven steps, code-path references at each layer:

  1. Enroll Eligible button -> eligibility manager
  2. Pre-warm fans out every active DCI-backed CEL variable
  3. Dispatcher routes by registry_type + vendor adapter
  4. Vendor service builds and POSTs a DCI envelope
  5. Remote registry answers (OpenSPP-DR or OpenG2P SR)
  6. Service unwraps reg_records[0], dispatcher extracts via
     dci_attribute_path, audit row written
  7. Cache write to spp.data.value
  8. CEL parser -> translator -> plan (AND[MetricCompare, MetricCompare])
  9. Executor builds per-clause SQL subqueries
 10. PostgreSQL composes the final WHERE
 11. Memberships flip to enrolled/not_eligible

Plus ASCII overview diagram, wire-format JSON envelope snippets for
both DR and SR sides, a "why this matters for SPDCI" framing block,
and the demo verification commands.
…trant ingest

Replaces the manual setup_spdci_demo.py script with a UI flow under
Registry → Import from External Registry (DCI). The wizard fires
DCI search-sync requests against the configured OpenG2P SR data
source, previews matched records, and imports the selected ones as
res.partner + spp.registry.id rows on the SP — optionally enrolling
them into a target program in one step.

Discovery semantics: SPDCI search-sync is lookup-only (no "list all
registrants" operation), so the wizard offers two practical modes:

  - Range sweep: contiguous identifier range (e.g., IND-NSR-0001..
    IND-NSR-0015). Works against the OpenG2P playground.
  - Identifier list: paste/type identifiers one per line. Matches the
    production-shaped workflow where the SR operator hands over a
    partner list out of band.

Scope: captures only the bare minimum partner fields (given_name,
surname, sex, birth_date) plus a UIN reg_id. Eligibility rules
continue to read income_level etc. on demand via the CEL ↔ DCI
bridge — this wizard is NOT a full SR replica.

Implementation:
  - spp.dci.sr.import.wizard (TransientModel) with three-state form
    (configure → preview → done)
  - spp.dci.sr.import.wizard.line for preview rows; carries
    already_exists + existing_partner_id so the operator can see
    which UINs are already on the SP
  - View renders all states on one form, gated by `invisible="state
    != 'X'"` per pane
  - Menuitem under spp_registry.spp_main_menu_root, sequence=90

Tests:
  - 10 new tests in test_sr_import_wizard.py covering identifier
    collection (range padding, list dedup/comments, empty-input
    validation), preview (matched/not_found/error/already_exists),
    import (selected only, skip-existing, auto-enroll), and
    back-to-configure state reset.
  - Module total: 44 tests passing.

Module manifest gains spp_registry as a dependency for the menu
parent xmlid; security/ir.model.access.csv added for the two
TransientModel records.
… to "Social Registry"

Operators see neutral terminology in the SR-import wizard and the
DCI configuration screens; vendor identity stays in technical slugs
(xml ids, vendor='openg2p' dispatcher key, code='openg2p_dr', Python
class names) so the routing layer is unaffected.

Touched UI strings only: data source / data provider names, CEL
variable labels and descriptions, UIN id-type definition, wizard
form title, and the Source Registry field's help tooltip.
…zard

The wizard's data_source_id domain filtered on the short alias
'SR', which only exists in the dispatcher's URI-to-alias routing
map — not as a stored field value. spp.dci.data.source.registry_type
is a Selection over URIs from spp_dci.schemas.constants.RegistryType,
so the filter never matched and the Source Registry dropdown rendered
empty.

Switch the domain, the _default_data_source search, and the seeded
data XML to 'ns:org:RegistryType:Social' so the dropdown picks up
the Social Registry source on fresh installs and the operator sees
the configured source pre-filled.
Captures the leaf modules to install on each side (SP:
spp_dci_openspp_dr + spp_dci_openg2p; DR: spp_dci_server_disability),
the dependency tree they pull in, the full down/wipe/re-init flow,
post-install wiring (DR base_url, demo seeding, optional auth bypass),
and a sanity-check table — so the federated demo can be rebuilt from
scratch without rediscovering the steps.
The next demo run wipes only the SP; the DR keeps its database, its
8 approved disability assessments, and its DCI-server bypass flags
from prior runs. Rewrite the reset section to stop/wipe only the SP
container + filestore, leave docker-compose.dr.yml alone, drop the
DR-side reseed step from the post-install wiring, and note explicitly
that the DR config carries over.
So an operator running both the SP and the DR side-by-side during the
SPDCI demo can tell the two OpenSPP UIs apart at a glance — SP keeps
the default OpenSPP blue from spp_base_common, the DR shows Material
green-800 in the top navbar and green-900 in the apps-sidebar hover
and active states. The SCSS overrides ship with the DR-only module, so
no styling bleeds onto SP installs.
Presenter-oriented runbook that ties each click and screen in the live
SPDCI federated-eligibility demo to its talking points and expected
audience reaction. Includes a pre-demo state table, ten scene
breakdowns (frame → empty SP → SR import → CEL rule → enroll → audit
drill-down → live SR verify → failure-mode recap → architecture →
roadmap), a 5-minute lightning cut, pre-canned Q&A anchors (including
the marital_status filtering question we probed today), and a
pre-flight checklist so the host can verify environment health before
the audience walks in. Complements the existing briefing sheet
(narrative + personas + glossary), CEL-to-DCI internals walkthrough,
and modules/reset reference.
…theme

The first pass missed three places where the OpenSPP blue still leaked
through on the DR's backend:

  - .mk_apps_sidebar_panel (sidebar base background — muk_web_appsbar
    paints this from $mk-appbar-background, which spp_base_common
    overrides to brand blue).
  - .o_main_navbar .o_menu_sections .o_nav_entry /
    .o_main_navbar .o_menu_sections .dropdown-toggle (per-section
    menu-entry backgrounds — theme_openspp_muk uses descendant
    selectors that the existing direct-child override didn't match).
  - .btn-primary / .btn-primary:hover (action buttons).

After the fix, the DR backend is green throughout the chrome operators
look at during the demo while the SP stays on default OpenSPP blue.
Leftover from commit 50c9b59 which moved the data source's
registry_type to the URI form 'ns:org:RegistryType:Social'. Two
tests still asserted the short alias 'SR' and started failing once
the DR-side install pulled the dispatcher routing test set in. Update
both to assert the canonical URI.
… install

Operators running a fresh DR deploy used to hit a 401 on the SP's
first lookup because the bearer-token middleware enforces auth by
default, and the parameter keys are easy to mistype in the
System Parameters UI. Add a post_init_hook that creates
`dci.bypass_bearer_auth=true` and `dci.allow_unsigned_requests=true`
on initial install only, with a loud WARNING in the boot log so the
demo-mode bypass is obvious.

Existing values are respected on upgrade — operators who later flip
either flag to 'false' for production keep their choice.
…aft flow

Replaces the SR-import wizard's mirror-to-DR path with a DCI-native
write operation, and lets the SR's self-reported disability claim
surface to the DR's assessor backlog as draft assessments without
shortcutting the clinical workflow.

  --- DCI register-individual endpoint (DR side) ---

  spp_dci_server_disability:
    - schemas.py: RegisterIndividualItem / RegisterRequest / RegisterResponse(Item).
      Custom action name 'register-individual' since the standard DCI MessageAction
      enum has no generic 'create-individual' operation; envelope shape mirrors
      the search side (transaction_id + per-item reference_id + per-item status).
    - services/disability_register_service.py: idempotent upsert by UIN reg_id.
      New UIN -> create partner + reg_id ('created'). Existing UIN, refresh=False
      -> 'skipped' (partner untouched). Existing UIN, refresh=True -> partner
      identity rewritten ('updated'). Failures surface as per-item rjct rows
      rather than rolling back the whole transaction.
    - routers/disability_router.py: POST /sync/register endpoint, same signature +
      bearer middleware as /sync/search, response envelope follows the same
      'succ' / 'rjct' / 'part' overall-status convention.
    - Manifest: depends on spp_disability_registry (the register service now
      creates spp.disability.assessment records).

  --- SR self-report -> draft DR assessment ---

  spp_dci_server_disability:
    - schemas: RegisterIndividualItem.is_disabled bool, RegisterResponseItem
      .draft_assessment_created bool.
    - service: when is_disabled=true AND the registrant has no prior assessment
      (any state), create a DRAFT spp.disability.assessment with WG fields LEFT
      BLANK and post an SR-provenance chatter message. The clinical separation
      holds — has_disability stays false until an assessor conducts the
      Washington Group interview and approves. Idempotent: re-runs don't pile
      on additional drafts behind the assessor's back.

  --- DCI client + wizard refactor (SP side) ---

  spp_dci_openspp_dr:
    - openspp_dr_service.py: new register_individuals() method using
      DCIClient._build_envelope('register-individual', ...) and ._make_request().
      Coerces Odoo's False-for-empty-Char convention to Pydantic-friendly None
      on the wire.

  spp_dci_openg2p:
    - wizards/sr_import_wizard.py: drop the four XML-RPC fields (URL / db /
      user / password), add a single dr_data_source_id Many2one to the same DR
      DCI source the bridge already uses for has_disability lookups. mirror_to_dr
      collects items across the SP loop and fires ONE batched DCI envelope at
      the end. Forwards sr_is_disabled from the SR payload onto the wire.
      Surfaces DR per-item counts and draft-assessment count in the done summary.
    - views/sr_import_wizard_views.xml: replace XML-RPC fields with the single
      DCI source Many2one; new "SR claim" column in the preview grid.
    - Manifest: depends on spp_dci_openspp_dr (the wizard's mirror path now
      calls OpenSPPDRService).

  --- Tests ---

  spp_dci_server_disability: 7 new tests covering upsert semantics (new /
  exist / refresh on-off) AND draft-creation semantics (is_disabled true/false
  combined with prior-assessment yes/no, including the existing-partner-but-no-
  prior-assessment edge case).

  spp_dci_openg2p: mirror tests rewritten to mock OpenSPPDRService.register_
  individuals instead of xmlrpc.client.ServerProxy. New test covers the
  refresh-flag plumbing through the wire.

Why a custom action vs. SPDCI standard: DCI has no generic 'create-individual'
operation today (ENROLLMENT is program-enrollment, not registrant onboarding).
The action name and envelope shape stay close enough to the search side that
the same middleware, signing, audit, and dispatcher infrastructure works
unchanged. Same trust boundary as the read side — no XML-RPC admin
credentials on the SP, no second auth surface to manage.
set either parameter to 'false' (or anything else), upgrading the module
leaves their choice alone. Only first installs get the demo defaults.
"""
Param = env["ir.config_parameter"].sudo()
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