Plan: Medicaid SSA Orchestrator Wiring (Issue #384)

On this page

Status

Step Description Status

0

New canopy-verification SOLQ surface. Create services/canopy-verification/src/noop_solq.rs mirroring the noop.rs (IEVS) and noop_save.rs (SAVE) templates with deterministic SSN-suffix-keyed SolqRecord test data (SSI status, monthly SSI amount, OASDI/widow benefit category, disability onset date, COLA-loss flag). Add services/canopy-verification/src/api/ssa.rs modelled on api/save.rs: internal_routes(state) exposing POST /internal/v1/ssa/solq behind the existing x-service-api-key header check, returning a SolqHttpResponse { application_id, person_id, #[serde(flatten)] record }. Register the module in services/canopy-verification/src/api/mod.rs. Wire the adapter in services/canopy-verification/src/main.rs next to the IEVS + SAVE blocks (gated on feature = "noop-adapters"), passing the existing internal_api_key clone into a SsaState { adapter, api_key }.

Done (2026-05-11)

1

Orchestrator-side SOLQ client. Extend services/canopy-eligibility/src/orchestrator.rs with a fetch_ssa_solq(client, verification_base_url, person_id, service_token) helper modelled on the existing fetch_household_context shape — same ServiceTokenSource::current().await acquisition, same with_service_identity(&svc_jwt) builder extension (ADR-019), same 5-10s reqwest timeout, same graceful Err(_) → None degrade pattern. Add a verification_base_url: &'a str field to DetermineConfig and thread it through main.rs from the existing per-program-service URL config layer. Persons-fetch lives directly in orchestrator.rs today; the SOLQ fetch follows the same convention rather than introducing a new clients/ directory — keeps the codepath uniform and avoids a half-done refactor.

Done (2026-05-11)

2

Pre-dispatch SOLQ enrichment + ApplicationContext wiring. After fetch_household_context returns, branch on `request.programs.iter().any(

p

p.eq_ignore_ascii_case("medicaid"))` AND needs_solq(&member_contexts) (heuristic: any member with age >= 65, `disability_status == "disabled"

"disabled_veteran"`, or institutional flag forwarded from canopy-applications). For each qualifying member fire fetch_ssa_solq and collect results into a HashMap<Uuid, Option<SolqRecord>> (one entry per member; failed fetches store None so call sites don’t need a separate "asked but didn’t get an answer" sentinel). Extend services/canopy-eligibility/src/orchestrator.rs::ApplicationContext with pub ssa_solq: Option<HashMap<Uuid, SolqRecord>>. Mirror that field on services/canopy-medicaid/src/determine.rs::ApplicationContext. This is the dispatch payload — the orchestrator → program-service request body — not the response envelope; per ADR-002 and #387 the response envelope (SignableDetermination.program_extension) remains the program service’s outbound channel and is not touched by this step.

Done (2026-05-11)

3

NonMagiInput plumbing inside canopy-medicaid. Non-MAGI evaluation runs through canopy-rules via JDM rulesets (medicaid-non-magi.json), not through inline Rust evaluators — services/canopy-medicaid/src/determine.rs builds a NonMagiInput and calls rules.evaluate_non_magi(input, thresholds, bearer_token) (post-#424 5-arg RulesClient::evaluate(rule_set_name, context_type, context_id, input, token) underneath). The five ABD FBR SSA-linked booleans (lost_ssi_due_to_cola, is_disabled_adult_child, is_disabled_widow, is_widow_60_64, lost_ssi_as_disabled_child) and the Phase E flags (hospice_election, length_of_stay_days, etc. — already on NonMagiInput) currently fall back to Option::unwrap_or(false / 0) at determine.rs:270-281. This step replaces those defaults with values derived from the per-applicant ssa_solq map carried on the inbound ApplicationContext: derive_abd_flags_from_solq(&ctx.ssa_solq, applicant_id) returns the five booleans (e.g., lost_ssi_due_to_cola = solq.lost_ssi_due_to_cola_flag, is_disabled_adult_child = solq.benefit_category == "DAC", etc.). When ssa_solq is None or no entry exists for the applicant, derivation returns false across the board — matches today’s behaviour, no regression for SNAP/MAGI requests that never call SOLQ. The derived booleans flow into the existing NonMagiInput fields; the JDM ruleset is unchanged.

Done (2026-05-11)

4 (a)

Tests, docs, plumbing wiring. 4 unit tests in services/canopy-eligibility/src/orchestrator.rs’s `mod tests: (i) MAGI-only request → no SOLQ fetch (assert verification HTTP mock never called), (ii) Medicaid + age 65 member → SOLQ fetched + record present in dispatch payload, (iii) SOLQ fetch timeout → ssa_solq entry is None, dispatch still occurs (degrade), (iv) multi-member household with mixed ages → only qualifying members fetched. 3 unit tests in canopy-medicaid covering the derive_abd_flags_from_solq helper (DAC mapping, widow 60-64 mapping, empty-map fallback). 1 integration test through devstack at services/canopy-eligibility/tests/medicaid_ssa_solq_test.rs exercising the Pickle happy path against the NoopSolqAdapter. CHANGELOG === Added. Per-service Antora pages (canopy-eligibility.adoc, canopy-verification.adoc, canopy-medicaid.adoc) updated with the new pre-dispatch step + internal endpoint + SOLQ-fed COAs. Roadmap Tier 3 row for Medicaid Phases D-E flips from deferred (SSA orchestrator wiring) to operational against the Noop adapter.

Done (2026-05-11)

4 (b)

Real SSA SOLQ/BINDEX cutover. Replace NoopSolqAdapter with a transport-backed implementation (mTLS to SSA’s SOLQ/BINDEX gateway, response parsing, retry/backoff, hash-chained audit emission per ADR-014). Distinct from step 0 because real SOLQ access requires (1) a signed Computer Matching Agreement (CMA) per ADR-004 between GADHS and SSA; (2) production SSA endpoint credentials; (3) Pub 1075-equivalent audit + access controls validated by SSA. Until the CMA lands, deliverable (a) is the shippable surface and runs against the Noop in dev/UAT.

Blocked (CMA execution — tracker: #384)

Issue: #384
Branch: feat/medicaid-ssa-orchestrator-wiring
Labels: type::feature, priority::medium, service::eligibility, service::medicaid, service::verification, program::medicaid, compliance::cma, workflow::ready

Deliverable (a) landed on 2026-05-11; the only open row is step 4(b), which stays Blocked (CMA execution) per the plan footer. Plan archived once (a) ships.

Context

The archived medicaid-coa-phase-d-abd-fbr-ssa plan (predecessor) added the five Pickle/DAC/DW/Widow 60-64/Former SSI Disabled Child boolean flags to ApplicationContext and NonMagiInput and wired the corresponding match arms in eligible_fn / denial_reason_fn. It explicitly deferred the orchestrator-side data flow: the boolean flags are accepted by canopy-medicaid but the orchestrator never populates them, so every applicant evaluating against any of those COAs sees the default false and is denied for "no_ssi_loss_due_to_cola" / "not_disabled_adult_child" / etc. — even when they would qualify.

This plan completes the data flow by:

  1. introducing a SOLQ surface in canopy-verification (no SOLQ adapter exists today — only IEVS and SAVE),

  2. wiring `canopy-eligibility’s orchestrator to call that surface pre-dispatch for Medicaid requests,

  3. plumbing the response into the existing NonMagiInput fields that canopy-rules already reads via the JDM medicaid-non-magi ruleset.

The Phase E waiver/institutional flags (hospice_election, length_of_stay_days, is_child_disabled_at_home, in_foster_care, etc.) sit on NonMagiInput next to the Phase D flags and consume the same ssa_solq plumbing where the underlying signal is SSA-sourced (e.g., disability onset date for TEFRA confirmation). Non-SSA flags (hospice election filed via state workflow, length-of-stay from facility intake) remain populated from other inbound dispatch fields.

Today’s reality (verified against the tree on 2026-05-11)

  • services/canopy-verification/src/api/ contains only ievs.rs and save.rs. No ssa.rs. No SOLQ endpoint.

  • services/canopy-verification/src/ contains noop.rs (IEVS) and noop_save.rs (SAVE). No noop_solq.rs. The closest existing structs are SsaSdxRecord and SsaBendexRecord on ievs.rs:45,51, but those are scoped to the SNAP IEVS path under 7 USC §2025(e) per ADR-004 — they cannot be reused as a Medicaid-side data source without violating the legally-scoped data tenancy boundary.

  • services/canopy-eligibility/src/ has no clients/ directory. The orchestrator dispatches via ProgramServiceRegistry + reqwest directly (orchestrator.rs:400-468) and fetches household context inline at fetch_household_context (orchestrator.rs:71-218). The SOLQ fetcher follows the same inline convention.

  • services/canopy-medicaid/src/determine.rs:46 defines ApplicationContext (the inbound dispatch payload). Lines 116-130 hold the Phase D Option<bool> fields that read as None today.

  • services/canopy-medicaid/src/rules_client.rs:115-145 defines NonMagiInput. The five Phase D booleans (lost_ssi_due_to_cola, is_disabled_adult_child, is_disabled_widow, is_widow_60_64, lost_ssi_as_disabled_child) are at lines 131-135. evaluate_non_magi(input, thresholds, token) calls into RulesClient::evaluate(rule_set_name, context_type, context_id, input, token) — the 5-arg signature post-#424.

  • services/canopy-eligibility/src/orchestrator.rs:272-288 defines the orchestrator-side ApplicationContext (the dispatch payload sent to each program service). This is the struct that grows the new ssa_solq field.

  • crates/canopy-signing/src/envelope.rs:66+ defines SignableDetermination, whose program_extension: Option<serde_json::Value> slot (line 114) is the response envelope — what each program service emits back to the orchestrator (ADR-002, #387). SSA data must NOT enter that slot; it flows in the opposite direction.

Code references

  • services/canopy-eligibility/src/orchestrator.rs — dispatch path (fetch_household_context, ApplicationContext, determine).

  • services/canopy-verification/src/noop.rs + noop_save.rs — adapter templates for the new noop_solq.rs.

  • services/canopy-verification/src/api/save.rs — endpoint template for the new api/ssa.rs.

  • services/canopy-verification/src/main.rs:40-51 — adapter wiring site for the new SsaState.

  • services/canopy-medicaid/src/determine.rs — non-MAGI dispatch + NonMagiInput assembly.

  • services/canopy-medicaid/src/rules_client.rsNonMagiInput, evaluate_non_magi.

  • Archived: medicaid-coa-phase-d-abd-fbr-ssa.adoc — predecessor; established the boolean fields this plan now populates.

  • ADR-002 — orchestrator → program-service envelope contract.

  • ADR-004 — IEVS data is SNAP-only; SOLQ is Medicaid-scoped under the CMA.

  • ADR-019 — service-class token forwarding pattern used by the new SOLQ fetch.

Scope

In scope:

  • New noop_solq.rs + api/ssa.rs in canopy-verification with a deterministic SOLQ surface for dev/UAT, modelled on the IEVS/SAVE pattern.

  • SolqRecord struct (in canopy-verification, exported for cross-service deserialisation in canopy-eligibility + canopy-medicaid).

  • Pre-dispatch SOLQ enrichment in the orchestrator with per-request caching keyed by person_id.

  • ApplicationContext.ssa_solq field on both the orchestrator-side and medicaid-side context structs.

  • derive_abd_flags_from_solq helper in canopy-medicaid::determine that maps SOLQ records onto the existing NonMagiInput booleans.

  • Graceful degrade on SOLQ fetch failure (Option<SolqRecord>::None flows through; existing false defaults in the JDM ruleset preserve current behaviour).

  • Unit + integration tests + Antora doc updates + CHANGELOG.

Out of scope:

  • Real SSA SOLQ/BINDEX transport implementation (deliverable b — blocked on CMA).

  • SOLQ result caching beyond per-request scope. A persistent SOLQ cache (24h freshness window per Pub 1075 §5.5.1) is a follow-on once the real adapter lands and we have audit-emission guarantees.

  • CMA audit-log entries beyond what canopy-verification + canopy-security already emit for the existing IEVS / SAVE internal endpoints.

  • Reusing the SNAP-scoped SsaSdxRecord / SsaBendexRecord from ievs.rs — ADR-004 prohibits cross-program reuse without a separate legal authorisation; the SOLQ surface is a distinct API path with its own audit envelope.

Dependencies

  • Archived medicaid-coa-phase-d-abd-fbr-ssa (predecessor; not reopened — it shipped the boolean fields this plan now populates).

  • No blocking dependency on medicaid-jdm-completion.adoc (#386); the non-MAGI JDM ruleset already reads the five SSA-linked booleans.

Design

As-built deviation (2026-05-11): SolqRequest and SolqRecord landed in crates/canopy-reference/src/types.rs, not in services/canopy-verification/src/solq.rs as originally sketched. The orchestrator (canopy-eligibility) and the consumer (canopy-medicaid) both deserialise the same wire shape; the cleanest way to share types across three crates without a thin canopy-verification-types shim — or violating ADR-001 by depending on another service’s lib surface — is the existing universally-available types crate. canopy-reference picked up a rust_decimal dep (previously chrono-only). The SolqAdapter trait stayed in services/canopy-verification/src/solq.rs (no cross-service consumer; only noop_solq.rs implements it); the file now re-exports the types from canopy-reference. Also as-built: derive_abd_flags_from_solq returns a new AbdSsaFlags struct rather than mutating five let bindings inline — the override-channel idiom (ctx.x.unwrap_or(solq_flags.x)) lands verbatim per the original sketch.

Step 0 sketch — SOLQ adapter surface

services/canopy-verification/src/solq.rs (new — types module, sibling to ievs.rs / save.rs):

//! SSA SOLQ/BINDEX adapter. Per ADR-004, SOLQ access is Medicaid-scoped
//! under the Computer Matching Agreement; raw responses never leave
//! canopy-medicaid's database. Distinct from the IEVS SSA SDX/BENDEX path
//! in `ievs.rs`, which is SNAP-only under 7 USC §2025(e).

use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolqRequest {
    pub ssn: String,
    pub first_name: String,
    pub last_name: String,
    pub date_of_birth: NaiveDate,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolqRecord {
    /// SSI active at lookup time (SSI Medicaid COA gate).
    pub ssi_active: bool,
    pub monthly_ssi_amount: Option<Decimal>,
    /// Lost SSI due to a Social Security COLA increase (Pickle, PAMMS 2120).
    pub lost_ssi_due_to_cola_flag: bool,
    /// SSA benefit category: "DAC" | "WIDOW" | "DISABLED_WIDOW" | "OASDI" | ...
    pub benefit_category: Option<String>,
    pub monthly_benefit_amount: Option<Decimal>,
    pub disability_onset_date: Option<NaiveDate>,
    /// Former SSI-disabled child redetermination outcome (Zebley / age-18).
    pub lost_ssi_as_disabled_child_flag: bool,
}

pub trait SolqAdapter: Send + Sync {
    fn query_solq(
        &self,
        req: &SolqRequest,
    ) -> impl std::future::Future<Output = anyhow::Result<Option<SolqRecord>>> + Send;
}

services/canopy-verification/src/noop_solq.rs (new — mirrors noop.rs):

//! NoopSolqAdapter — deterministic test data keyed by SSN suffix.
//! Suffix bands chosen to exercise each Phase D / Phase E COA branch.

#[cfg(feature = "noop-adapters")]
pub struct NoopSolqAdapter;

#[cfg(feature = "noop-adapters")]
impl SolqAdapter for NoopSolqAdapter {
    async fn query_solq(&self, req: &SolqRequest) -> anyhow::Result<Option<SolqRecord>> {
        // 00-19: no SSA record (returns None)
        // 20-29: active SSI
        // 30-39: Pickle — lost SSI due to COLA
        // 40-49: DAC — disabled adult child
        // 50-59: Disabled Widow 50-64
        // 60-69: Widow 60-64 (non-disabled)
        // 70-79: Former SSI disabled child (Zebley / age-18)
        // 80-99: OASDI benefits, no SSI loss
        // (implementation follows noop.rs's match-on-suffix shape)
    }
}

services/canopy-verification/src/api/ssa.rs (new — mirrors api/save.rs):

//! Internal SOLQ verification endpoint.
//! Called by canopy-eligibility pre-dispatch for Medicaid requests.
//! Authentication: X-Service-Api-Key header.

pub struct SsaState<A: SolqAdapter> {
    pub adapter: A,
    pub api_key: String,
}

pub fn internal_routes<A: SolqAdapter + 'static>(state: Arc<SsaState<A>>) -> Router {
    Router::new()
        .route("/internal/v1/ssa/solq", post(handle_solq::<A>))
        .with_state(state)
}

Register in services/canopy-verification/src/api/mod.rs (add pub mod ssa;) and wire in services/canopy-verification/src/main.rs next to the existing IEVS / SAVE blocks:

#[cfg(feature = "noop-adapters")]
let ssa_state = Arc::new(api::ssa::SsaState {
    adapter: noop_solq::NoopSolqAdapter,
    api_key: internal_api_key.clone(),
});

// ...

#[cfg(feature = "noop-adapters")]
{
    router = router
        .merge(api::ievs::internal_routes(ievs_state))
        .merge(api::save::internal_routes(save_state))
        .merge(api::ssa::internal_routes(ssa_state));
}

Step 1-2 sketch — Orchestrator-side fetch + dispatch enrichment

// services/canopy-eligibility/src/orchestrator.rs

async fn fetch_ssa_solq(
    client: &reqwest::Client,
    verification_base_url: &str,
    person: &MemberContext,
    service_token: &ServiceTokenSource,
) -> Option<SolqRecord> {
    if !needs_solq_for(person) {
        return None;
    }
    let svc_jwt = service_token.current().await.ok()?;
    let url = format!("{verification_base_url}/internal/v1/ssa/solq");
    let resp = client
        .post(&url)
        .with_service_identity(&svc_jwt)
        .header("x-service-api-key", /* injected from secrets */)
        .timeout(std::time::Duration::from_secs(5))
        .json(&SolqHttpRequest { /* ssn, name, dob from person */ })
        .send()
        .await
        .ok()?;
    if !resp.status().is_success() {
        tracing::warn!(
            person_id = %person.person_id,
            status = %resp.status(),
            "SOLQ fetch failed; degrading to None"
        );
        return None;
    }
    resp.json::<SolqRecord>().await.ok()
}

fn needs_solq_for(member: &MemberContext) -> bool {
    member.age.map(|a| a >= 65).unwrap_or(false)
        || matches!(
            member.disability_status.as_deref(),
            Some("disabled" | "disabled_veteran")
        )
}

// Inside `determine` between fetch_household_context and the dispatch loop:
let ssa_solq: Option<HashMap<Uuid, SolqRecord>> = if request
    .programs
    .iter()
    .any(|p| p.eq_ignore_ascii_case("medicaid"))
{
    let mut map = HashMap::new();
    for m in &member_contexts {
        if let (Some(pid_str), Some(rec)) = (
            Some(&m.person_id),
            fetch_ssa_solq(cfg.client, cfg.verification_base_url, m, cfg.service_token).await,
        ) && let Ok(pid) = Uuid::parse_str(pid_str) {
            map.insert(pid, rec);
        }
    }
    if map.is_empty() { None } else { Some(map) }
} else {
    None
};

The dispatch payload (ApplicationContext) grows:

#[derive(Debug, Clone, Serialize)]
pub struct ApplicationContext {
    // ... existing fields ...
    pub jurisdiction: String,
    /// SSA SOLQ records keyed by `person_id`. Populated by the orchestrator
    /// pre-dispatch for Medicaid requests against the canopy-verification
    /// SOLQ surface. `None` when the request never asked for Medicaid or
    /// when no member qualified for the SOLQ gate.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ssa_solq: Option<HashMap<Uuid, SolqRecord>>,
}

The medicaid-side ApplicationContext in services/canopy-medicaid/src/determine.rs:46 gains the same field (and the SolqRecord type — either re-exported via a thin shared canopy-verification-types crate or duplicated as a #[serde]-compatible mirror struct; the rewrite picks one in the implementation MR).

Step 3 sketch — NonMagiInput derivation

// services/canopy-medicaid/src/determine.rs

fn derive_abd_flags_from_solq(
    map: &Option<HashMap<Uuid, SolqRecord>>,
    applicant: Uuid,
) -> AbdSsaFlags {
    let Some(record) = map.as_ref().and_then(|m| m.get(&applicant)) else {
        return AbdSsaFlags::default(); // all false
    };
    AbdSsaFlags {
        lost_ssi_due_to_cola: record.lost_ssi_due_to_cola_flag,
        is_disabled_adult_child: record
            .benefit_category
            .as_deref()
            .is_some_and(|c| c == "DAC"),
        is_disabled_widow: record
            .benefit_category
            .as_deref()
            .is_some_and(|c| c == "DISABLED_WIDOW"),
        is_widow_60_64: record
            .benefit_category
            .as_deref()
            .is_some_and(|c| c == "WIDOW"),
        lost_ssi_as_disabled_child: record.lost_ssi_as_disabled_child_flag,
    }
}

// Replace today's:
//   let lost_ssi_due_to_cola = ctx.lost_ssi_due_to_cola.unwrap_or(false);
//   let is_disabled_adult_child = ctx.is_disabled_adult_child.unwrap_or(false);
//   ...
// with:
let solq_flags = derive_abd_flags_from_solq(&ctx.ssa_solq, applicant_id);
let lost_ssi_due_to_cola =
    ctx.lost_ssi_due_to_cola.unwrap_or(solq_flags.lost_ssi_due_to_cola);
let is_disabled_adult_child =
    ctx.is_disabled_adult_child.unwrap_or(solq_flags.is_disabled_adult_child);
// ... etc. The pre-existing `Option<bool>` ApplicationContext fields stay
// as an override channel (test fixtures, manual worker overrides) and win
// when present; SOLQ derivation is the implicit default.

The derived values feed into the existing NonMagiInput (rules_client.rs:115-145); the medicaid-non-magi.json JDM ruleset is unchanged.

Envelope direction note

This plan touches the dispatch payload only — the orchestrator → program-service request body shaped by services/canopy-eligibility/src/orchestrator.rs::ApplicationContext and services/canopy-medicaid/src/determine.rs::ApplicationContext. The response envelope (canopy_signing::SignableDetermination with its program_extension: Option<serde_json::Value> slot, used by canopy-medicaid to ship assigned_coa / assigned_coa_track / denial_reason back to the orchestrator per #387 and ADR-002) is not modified — that slot flows in the opposite direction and carries program-specific output, not orchestrator-sourced input.

Files Touched

File Change

services/canopy-verification/src/solq.rs (new)

SolqRequest, SolqRecord, SolqAdapter trait.

services/canopy-verification/src/noop_solq.rs (new)

NoopSolqAdapter with SSN-suffix-keyed deterministic test data.

services/canopy-verification/src/api/ssa.rs (new)

POST /internal/v1/ssa/solq endpoint behind x-service-api-key.

services/canopy-verification/src/api/mod.rs

Register pub mod ssa;.

services/canopy-verification/src/main.rs

Wire SsaState next to IevsState + SaveState; merge ssa::internal_routes.

services/canopy-eligibility/src/orchestrator.rs

Add verification_base_url to DetermineConfig. Add fetch_ssa_solq + needs_solq_for. Extend ApplicationContext with ssa_solq. Pre-dispatch enrichment branch for Medicaid requests.

services/canopy-eligibility/src/main.rs

Thread the verification base URL from config into DetermineConfig.

services/canopy-eligibility/src/config.rs

Surface the verification base URL (already present as a per-service URL; add to the orchestrator config struct if it is not yet there).

services/canopy-medicaid/src/determine.rs

Extend ApplicationContext with ssa_solq: Option<HashMap<Uuid, SolqRecord>>. Add derive_abd_flags_from_solq helper. Replace the five unwrap_or(false) lines for Phase D flags with unwrap_or(solq_flags.*).

services/canopy-eligibility/tests/medicaid_ssa_solq_test.rs (new)

1 devstack integration test: Pickle happy path against NoopSolqAdapter.

docs/modules/ROOT/openapi/canopy-eligibility.json, canopy-verification.json

Regenerated via cargo xtask api-docs.

docs/modules/ROOT/pages/services/canopy-eligibility.adoc, canopy-verification.adoc, canopy-medicaid.adoc

Document the new pre-dispatch step, internal SOLQ endpoint, and SOLQ-fed COAs.

docs/modules/ROOT/pages/roadmap.adoc

Flip Medicaid Phases D-E to operational against the Noop adapter; add a Blocked row for deliverable (b).

CHANGELOG.adoc

=== Added entry under == Unreleased.

Verification

  1. cargo nextest run -p canopy-eligibility -p canopy-medicaid -p canopy-verification --lib — unit tests pass (including the new derive_abd_flags_from_solq cases and the orchestrator SOLQ fetcher cases).

  2. cargo xtask api-docs — OpenAPI snapshots regenerate clean for canopy-eligibility + canopy-verification.

  3. cargo xtask dev start && cargo nextest run -p canopy-eligibility --test medicaid_ssa_solq_test --run-ignored only — devstack integration test green against the NoopSolqAdapter.

  4. cargo xtask docs plan-lint — Status vocabulary clean; the Blocked row carries the #384 tracker reference.

  5. Manual smoke: dispatch a Medicaid determination for a 67-year-old applicant with an SSN suffix that maps to "Pickle" in NoopSolqAdapter. Confirm: (i) orchestrator log shows the SOLQ fetch, (ii) canopy-medicaid receives ssa_solq populated for the applicant, (iii) the CMD cascade records pickle_eligible: true and EE15 assigns Pickle.

  6. cargo xtask validate — full battery green (fmt + clippy + nextest + docker build).

Documentation Updates

  • CHANGELOG.adoc — entry under == Unreleased / === Added.

  • docs/modules/ROOT/pages/services/canopy-eligibility.adoc — note the SSA pre-dispatch step + verification_base_url config.

  • docs/modules/ROOT/pages/services/canopy-verification.adoc — document the new internal SOLQ endpoint + NoopSolqAdapter SSN-suffix table.

  • docs/modules/ROOT/pages/services/canopy-medicaid.adoc — Phases D-E now data-flow-complete against the Noop adapter; Blocked on real-SSA CMA.

  • docs/modules/ROOT/pages/roadmap.adoc — Tier 3 Medicaid row flip; new Tier 5/6 Blocked row tracking the CMA cutover.

  • Plan archive: this plan moves to plans/archive/ once deliverable (a) is merged and step 4 (b) is the only remaining open row. The Blocked-on-CMA row keeps the tracker reference (#384) so the deferred work stays discoverable per ADR-013.

Edit this page · default