Skip to content

$bulk-member-match improvements: fact-check, test datasets#5

Open
Seryiza wants to merge 8 commits into
mainfrom
bulk-member-match-docs-improvements
Open

$bulk-member-match improvements: fact-check, test datasets#5
Seryiza wants to merge 8 commits into
mainfrom
bulk-member-match-docs-improvements

Conversation

@Seryiza
Copy link
Copy Markdown
Member

@Seryiza Seryiza commented May 28, 2026

No description provided.

Seryiza added 2 commits May 28, 2026 04:55
….1 + interop

- Retarget all PDex links from build.fhir.org (CI 2.2.0) to hl7.org STU2.1 — IG version drift mismatched stated STU 2.1.0.
- Pin HRex links to STU1.1 (PDex 2.1.0 dependency); unpinned links would silently follow future versions.
- Mark opt-out Consent.category code (`pdex-consent-api-purpose|provider-access`) as Payerbox-specific and forward-compatible with PDex 2.2.0 — STU 2.1's `pdex-provider-consent` pins `v3-ActCode|IDSCL`.
- Use canonical exportType value `hl7.fhir.us.davinci-pdex#payertopayer` in cross-links (was shorthand `payertopayer`).
- Split 404 errors row by endpoint — cancel on a `cancelled` Task returns 202 and sweeps outputs, only status/output return 404.
- "Aidbox Console sessions" → "Aidbox Console UI sessions" for precision.
Bundle the 12 resources interop's own tests already use (Clients with
NPI, AccessPolicies, Organizations, Patients, Coverages, opt-out
Consent) as a single gzipped ndjson and document a one-shot Aidbox
Console $load against the public URL. Rewrite the Request/Response
examples on the P2P pillar and on the $bulk-member-match /
$Patient/$member-match reference pages so every payload references the
seed cohort — captured verbatim from a live run against the dataset.
@Seryiza Seryiza self-assigned this May 28, 2026
@Nesmeshnoy
Copy link
Copy Markdown
Contributor

/qa протеструй

@Nesmeshnoy
Copy link
Copy Markdown
Contributor

Verified end-to-end against a fresh payerbox-local (aidboxone:edge + interop:edge + minio + postgres) — seed loads, both $bulk-member-match and $provider-member-match produce the documented three buckets, error paths (400/403/404) match the PR's tables, and the doc Response examples are byte-identical to the implementation output. Three things worth fixing before merge:

1. $load doesn't activate the seed's AccessPolicies

The PR instructs to load the seed via POST /fhir/$load (with the GitHub raw URL). On aidboxone:edge, after that call test-payer-client and test-provider-client get HTTP 403 on every request — GET /fhir/Patient and both operations — until someone PUTs the policies with a content change.

Minimal repro:

# Step 1: clean state
# Step 2:
POST /fhir/$load  { "source": "<seed url>" }       → 200
GET /fhir/Patient  (as test-payer-client)          → 403
# Step 3: idempotent PUT of the same AccessPolicy
PUT /fhir/AccessPolicy/allow-test-payer-client …   → 200
GET /fhir/Patient                                  → 403   (still!)
# Step 4: PUT with any content change (e.g. add description: "x")
PUT /fhir/AccessPolicy/allow-test-payer-client …   → 200
GET /fhir/Patient                                  → 200

vs. the same seed loaded as a transaction Bundle:

POST /fhir   { "resourceType": "Bundle", "type": "transaction", "entry": [ … same resources, PUT … ] }   → 200
GET /fhir/Patient (as test-payer-client)           → 200
POST /fhir/Group/$bulk-member-match                → 202
POST /fhir/Group/$provider-member-match            → 202

Root cause looks Aidbox-side: AccessPolicy is evaluated against an in-memory cache that's refreshed by the normal write path (PUT / POST /fhir / transaction Bundle). $load writes resources directly to Postgres via a bulk-ingest path and doesn't trigger that refresh — the policy is in the database but not active. A subsequent PUT with a content change refreshes the cache and access starts working (an idempotent PUT doesn't, because Aidbox sees no version change and skips the refresh).

Not a problem with the seed shape — engine: allow is valid and works through the normal write path. It bites this PR because the documented "Load" snippet uses $load. Two options:

  • Switch the three load snippets (bulk-member-match.md, provider-member-match.md, payer-to-payer.md) from POST /fhir/$load to POST /fhir with Bundle{type:transaction} — reader still gets a one-shot copy-paste, but it goes through the path that activates AccessPolicies.
  • Or keep $load and add a one-liner explaining that the AccessPolicies need a follow-up PUT (with a content change) to activate after a bulk load.

2. Inconsistent IG-link policy across the three files

The PR moves bulk's davinci-pdex links from build.fhir.org to hl7.org/fhir/us/davinci-pdex/STU2.1/, but provider-member-match.md and payer-to-payer.md still link to build.fhir.org/ig/HL7/davinci-epdx/ for the same kind of profiles. Reader sees two different IG anchors in one PR.

File davinci-pdex link target
bulk-member-match.md hl7.org/.../STU2.1/...
provider-member-match.md build.fhir.org/ig/HL7/davinci-epdx/...
payer-to-payer.md build.fhir.org/ig/HL7/davinci-epdx/...

Notably, pdex-member-no-match-group and pdex-member-match-group exist in both STU 2.1.0 and CI build, but provider/p2p link them to CI build while bulk links them to STU 2.1.

Pick one and apply across all three.

3. provider-member-match.md is missing the forward-compat note that bulk-member-match.md has

bulk-member-match.md adds:

the opt-out category code below is Payerbox-specific, forward-compatible with the pdex-consent-api-purpose CodeSystem introduced in PDex 2.2.0 — STU 2.1's pdex-provider-consent profile pins Consent.category = v3-ActCode|IDSCL instead

provider-member-match.md references four URLs that don't exist in STU 2.1.0 IG (all 404 on hl7.org/fhir/us/davinci-pdex/STU2.1/, 200 on build.fhir.org/ig/HL7/davinci-epdx/):

  • provider-parameters-multi-member-match-bundle-out (top-level Parameters.meta.profile in the Response example)
  • pdex-treatment-relationship (MatchedMembers.meta.profile)
  • pdex-member-opt-out (ConsentConstrainedMembers.meta.profile)
  • CodeSystem/opt-out-scope (ConsentConstrainedMembers.characteristic.valueCodeableConcept)

No equivalent note in the provider doc. Reader hitting any of those in meta.profile and trying to reconcile with STU 2.1.0 will see four 404s with no explanation.

Suggest adding the same kind of note in provider's ## Polling and output (or wherever the Response body is introduced), listing the four out-of-package URLs as forward-compatible with PDex 2.2 / CI build.

@qa-assist-bot
Copy link
Copy Markdown

qa-assist-bot Bot commented May 28, 2026

QA Report: PR #5 — $bulk-member-match improvements: fact-check, test datasets

PR: HealthSamurai/payerbox-docs#5
Branch: bulk-member-match-docs-improvementsmain
Author: @Seryiza
Type: Documentation review (no running application — docs-only repo)

Summary

This PR updates three documentation pages (bulk-member-match.md, provider-member-match.md, payer-to-payer.md) with:

  1. Fact-checked linksbulk-member-match.md IG profile URLs migrated from CI build (build.fhir.org) to published versioned specs (hl7.org/.../STU2.1/ for PDex, .../STU1.1/ for HRex). All 9 updated links verified ✅
  2. Test dataset — new assets/seeds/p2p-member-match.ndjson.gz with reproducible seed data (3 patients, orgs, coverages, opt-out consent). All three pages reference the same dataset with $load instructions.
  3. Richer examples — generic placeholder data (Smith/John) replaced with seeded test patients (Johnson/Robert → match, Williams/Sarah → consent-constrained, Unknown/Nobody → no match). Three MemberBundles demonstrate all three output buckets.
  4. Content fixesexportType canonical format, 404 error table split (status vs cancel semantics), consent-check parenthetical on PDex 2.2.0 forward-compatibility, auth headers added to examples.

A binary seed file is included: p2p-member-match.ndjson.gz (confirmed present on the branch).

Documentation Analysis

Overall quality: Good. The fact-check of bulk-member-match.md is thorough — all 9 IG profile links point to stable published versions and resolve correctly. Examples are significantly improved with reproducible test data that clearly demonstrates the three output buckets.

Three issues found (minor-to-medium severity):

# Severity Issue
1 Medium provider-member-match.md intro text mismatch: the paragraph says "defined by the Da Vinci PDex IG (STU 2.1.0)" but links to the CI build which serves v2.2.0-dev. The provider-specific profiles (pdex-treatment-relationship, provider-parameters-multi-member-match-bundle-in/out, pdex-member-opt-out) confirmed absent from published STU2.1 (404). So the CI build links are technically correct, but the text claiming "STU 2.1.0" is inaccurate — should say "STU 2.2.0 (CI build)" or similar.
2 Low payer-to-payer.md bucket table links: the three-bucket profile table links to build.fhir.org for pdex-member-match-group and pdex-member-no-match-group, which do exist in STU2.1 and are already linked as hl7.org/.../STU2.1/ in bulk-member-match.md. Could be aligned for consistency.
3 Low payer-to-payer.md auth header: uses raw placeholder Authorization: Basic <test-payer-client:test-payer-secret> while bulk-member-match.md uses the actual Base64-encoded token dGVzdC1wYXllci1jbGllbnQ6dGVzdC1wYXllci1zZWNyZXQ= for the same credentials. Minor copy-paste friction for readers.

Not a bug — intentional design: provider-member-match.md uses build.fhir.org CI build links because the provider-access profiles are 2.2.0-only and not yet published. The HRex links in that file (http:// unversioned) were not part of this PR's fact-check scope.

Test Environment

Parameter Value
Type Documentation review (static analysis)
Repo HealthSamurai/payerbox-docs
Branch bulk-member-match-docs-improvements
Tools Link verification (WebFetch), cross-document diff analysis

Test Cases

Link Verification — STU2.1 versioned URLs (bulk-member-match.md)

ID Status Description Expected Result
TC-01 PASS PDex IG root hl7.org/.../STU2.1/ HTTP 200 200 OK — Da Vinci PDex v2.1.0
TC-02 PASS pdex-parameters-multi-member-match-bundle-in HTTP 200 200 OK
TC-03 PASS pdex-parameters-multi-member-match-bundle-out HTTP 200 200 OK
TC-04 PASS pdex-member-match-group HTTP 200 200 OK
TC-05 PASS pdex-member-no-match-group HTTP 200 200 OK
TC-06 PASS hrex-patient-demographics (STU1.1) HTTP 200 200 OK
TC-07 PASS hrex-coverage (STU1.1) HTTP 200 200 OK
TC-08 PASS hrex-consent (STU1.1) HTTP 200 200 OK
TC-09 PASS FHIR Bulk Data kick-off pattern HTTP 200 200 OK

Link Verification — CI build URLs (provider-member-match.md)

ID Status Description Expected Result
TC-10 PASS build.fhir.org PDex IG root HTTP 200 200 OK — v2.2.0-dev
TC-11 PASS provider-parameters-multi-member-match-bundle-in HTTP 200 200 OK
TC-12 PASS provider-parameters-multi-member-match-bundle-out HTTP 200 200 OK
TC-13 PASS pdex-treatment-relationship HTTP 200 200 OK
TC-14 PASS pdex-member-opt-out HTTP 200 200 OK

Provider Profiles — STU2.1 availability check

ID Status Description Expected Result
TC-15 PASS pdex-treatment-relationship NOT in STU2.1 HTTP 404 404 — CI build only
TC-16 PASS provider-parameters-multi-member-match-bundle-in NOT in STU2.1 HTTP 404 404 — CI build only
TC-17 PASS pdex-member-opt-out NOT in STU2.1 HTTP 404 404 — CI build only

Cross-document Consistency

ID Status Description Expected Result
TC-18 PASS Patient names/IDs consistent across 3 files Consistent Johnson/Robert=test-member-001, Williams/Sarah=test-member-002, Unknown/Nobody — identical in all files
TC-19 PASS Same dataset file referenced in all 3 pages p2p-member-match.ndjson.gz All three reference same file with same $load URL
TC-20 PASS Output ordering consistent: Matched → NonMatched → ConsentConstrained Consistent Both operation docs use same order
TC-21 PASS Seed file assets/seeds/p2p-member-match.ndjson.gz exists on branch File present Confirmed (binary, gzipped)
TC-22 FAIL payer-to-payer.md bucket table should use STU2.1 URLs (like bulk-member-match.md) hl7.org/.../STU2.1/ Uses build.fhir.org — inconsistent with the fact-checked bulk-member-match.md
TC-23 FAIL payer-to-payer.md auth header format Base64-encoded Raw placeholder <test-payer-client:test-payer-secret>

Content Accuracy

ID Status Description Expected Result
TC-24 FAIL provider-member-match.md intro says "STU 2.1.0" but links to CI build 2.2.0-dev Version label = link target Text says STU 2.1.0, link resolves to 2.2.0-dev. Provider profiles are 2.2.0-only.
TC-25 PASS exportType uses canonical format hl7.fhir.us.davinci-pdex#payertopayer Correct in both bulk-member-match.md and payer-to-payer.md
TC-26 PASS Williams: IRCP=other-payer-001 → ConsentConstrained Correct routing Consent recipient doesn't match caller → correctly routed
TC-27 PASS Johnson: IRCP=test-payer-001 → MatchedMembers Correct routing Consent recipient matches caller → correctly routed
TC-28 PASS Unknown: no demographic match → NonMatchedMembers Correct routing No matching patient → correctly routed

Conclusion

PASS with notes — 25/28 checks passed. All 3 failures are minor consistency issues in files adjacent to the main change, not blockers.

The core deliverable — fact-checking bulk-member-match.md links to versioned STU2.1 URLs and adding reproducible test datasets — is solid. All 9 migrated links resolve correctly, and the three-member example elegantly demonstrates all output buckets.

Recommended before merge (non-blocking):

  1. Fix provider-member-match.md intro to say "PDex IG (STU 2.2.0, CI build)" instead of "STU 2.1.0" — the provider-access profiles don't exist in published STU2.1.
  2. Update payer-to-payer.md bucket table links from build.fhir.org to hl7.org/.../STU2.1/ for pdex-member-match-group and pdex-member-no-match-group (these profiles exist in STU2.1).
  3. Encode the auth header in payer-to-payer.md to match bulk-member-match.md style: Authorization: Basic dGVzdC1wYXllci1jbGllbnQ6dGVzdC1wYXllci1zZWNyZXQ=.

cc @Aitem @Seryiza

Seryiza added 5 commits May 29, 2026 16:06
Both $bulk-member-match and $provider-member-match target the PDex CI
build (v2.2.0), per the smartbox-product member-match MVP spec. Swap
STU2.1 profile URLs to build.fhir.org/davinci-epdx, drop the now-stale
"(STU 2.1.0)" pin on $provider-member-match, and unversion HRex
canonical URLs (HRex versions independently of PDex).
…ch docs

The auth/error wording around NPI-bearing OAuth Clients overspecifies an
implementation detail that is being reworked. Trim the Auth sections to
a one-line cross-link, drop the no-NPI / ambiguous-NPI Response tabs and
errors-table rows on $bulk-member-match, restate cross-tenant guards as
"originating requester" instead of "matching NPI", and remove the
trailing NPI sentence from the Payer-to-Payer pillar. NPI values inside
example payloads and output Group identifiers stay.
`POST /fhir/$load` writes resources via Aidbox's bulk-ingest path and does
not refresh the in-memory AccessPolicy / Client cache, so callers loaded
through `$load` get 403 until a normal write triggers a refresh. Replace
the `$load` snippet on all three member-match pages with a transaction
Bundle inlined inside a collapsed <details> block, which goes through the
normal FHIR write path and activates policies immediately. Drop the
ndjson.gz seed asset — the snippet is fully self-contained.
… + provider

Both member-match docs are now pinned to PDex 2.2.0, so the STU 2.1
forward-compat framing in bulk's parenthetical is obsolete. Trim it to a
one-liner and add an equivalent mention near provider's opt-out search
for parity (per PR review comment).
Matches the bulk-member-match / provider-member-match example style — reader
copy-pastes a runnable header instead of a placeholder.
@Seryiza
Copy link
Copy Markdown
Member Author

Seryiza commented May 29, 2026

/qa Проверь документацию на ошибки и замечания, протестируй примеры aidbox запросов с тестовыми данными

@qa-assist-bot
Copy link
Copy Markdown

qa-assist-bot Bot commented May 29, 2026

QA Report: PR #5 — $bulk-member-match improvements: fact-check, test datasets

Summary

PR updates documentation for $bulk-member-match, $provider-member-match, and the Payer-to-Payer pillar page. Key changes:

  • PDex IG version reference updated from "STU 2.1.0" to "v2.2.0"
  • exportType value updated from payertopayer to hl7.fhir.us.davinci-pdex#payertopayer
  • Reproducible test dataset (transaction Bundle) added to all three pages
  • HRex profile URLs updated from -html format to canonical /StructureDefinition/ format
  • Auth section simplified — NPI-specific language removed, 403 and 409 error codes removed
  • Output Groups reordered: MatchedMembersNonMatchedMembersConsentConstrainedMembers
  • Consent in payer-to-payer example simplified: single IRCP actor, no meta.profile/performer/sourceReference

Documentation Analysis

Issues Found

# Severity File Issue Details
1 HIGH bulk-member-match.md 403 error code removed but API still returns it The PR removes the 403 Forbidden response tab and the 403 row from the Errors table. However, testing with an NPI-less OAuth client against aidboxone:edge + interop:edge confirms the API still returns 403 with "OAuth client must carry an NPI identifier". The error code should be kept in the docs until the API behavior is actually changed.
2 MEDIUM bulk-member-match.md 409 error code removed — behavior unclear The 409 Conflict (ambiguous NPI) was removed. Could not verify whether the API still produces it, as it requires a specific multi-Organization NPI collision setup. If the behavior is still present in edge, it should remain documented.
3 LOW payer-to-payer.md Statement "NPI-less callers rejected with 403" removed Lines "The requesting payer's NPI must be present on the OAuth Client resource — $bulk-member-match rejects NPI-less callers with 403" were deleted. Given finding #1, this sentence is still accurate and should be retained until the API behavior changes.
4 LOW payer-to-payer.md Consent example simplified — removed meta.profile The old Consent example included meta.profile, performer, sourceReference, and two actor entries (performer + IRCP). New version uses single IRCP actor only. If the API still validates against the HRex Consent profile and requires meta.profile, the example is incomplete.
5 INFO provider-member-match.md 403 removal is correct for $provider-member-match Testing confirmed that $provider-member-match accepts NPI-less clients (returns 202), so removing the 403 from this page is correct.

Positive Changes

# File What was improved
1 All three pages Reproducible test dataset with concrete resource IDs — great for developer onboarding
2 bulk-member-match.md Three MemberBundles now demonstrate all three output buckets (match/no-match/consent-constrained)
3 All three pages Authorization: Basic ... headers added to request examples — makes them copy-paste runnable
4 bulk-member-match.md Explanatory notes after the request example clarify which member lands in which bucket
5 All three pages HRex URLs updated to canonical /StructureDefinition/ format (both old and new resolve correctly)
6 All three pages PDex IG version correctly updated to v2.2.0 (verified at build.fhir.org)
7 All three pages exportType value correctly uses the canonical format hl7.fhir.us.davinci-pdex#payertopayer
8 bulk-member-match.md Base64 auth header dGVzdC1wYXllci1jbGllbnQ6dGVzdC1wYXllci1zZWNyZXQ= correctly decodes to test-payer-client:test-payer-secret
9 provider-member-match.md Base64 auth header dGVzdC1wcm92aWRlci1jbGllbnQ6dGVzdC1wcm92aWRlci1zZWNyZXQ= correctly decodes to test-provider-client:test-provider-secret

External Links Verification

All 13 external IG links verified:

  • PDex IG (build.fhir.org): 10/10 resolve correctly with full content (200 OK)
  • HRex profiles (hl7.org): 3/3 resolve via JS-based redirect (functional in browsers; canonical URLs return 301→XML representation)

Test Environment

  • Aidbox: healthsamurai/aidboxone:edge on port 8790
  • Interop: healthsamurai/interop:edge on port 8089
  • FHIR packages: hl7.fhir.us.davinci-pdex#2.1.0 + dependencies

Test Cases

Dataset Loading

ID Status Description Expected Result
TC-01 PASS Load test dataset transaction Bundle 200 OK All 12 resources (Clients, AccessPolicies, Organizations, Patients, Coverages, Consent) created successfully
TC-02 PASS Verify Patient/test-member-001 200, Johnson, Robert Correct
TC-03 PASS Verify Patient/test-member-002 200, Williams, Sarah Correct
TC-04 PASS Verify Organization/test-payer-001 with NPI 200, NPI 5555555555 Correct
TC-05 PASS Verify Coverage/test-coverage-001 200, subscriberId=SUB-001 Correct
TC-06 PASS Verify Consent opt-out for member-002 200, provider-access, deny Correct

$bulk-member-match Kick-off

ID Status Description Expected Result
TC-07 PASS Kick-off with 3 MemberBundles 202 Accepted + Content-Location header 202, task ID returned
TC-08 PASS Missing Prefer: respond-async header 400 Bad Request 400, OperationOutcome: "This operation requires Prefer: respond-async header"
TC-09 PASS Invalid input (empty Patient) 422 Unprocessable Entity 422, validation errors for missing CoverageToMatch and Consent
TC-10 FAIL NPI-less client kick-off Docs say: accepted (no 403 in error table) Actually returns 403: "OAuth client must carry an NPI identifier" — docs are incorrect

$bulk-member-match Async Processing

ID Status Description Expected Result
TC-11 FAIL Poll status → manifest → NDJSON download 200 OK with manifest 500 — interop:edge fails to write Binary output due to terminology validation bug (urn:ietf:bcp:13 not found). Infrastructure issue, not a documentation problem.

$provider-member-match Kick-off

ID Status Description Expected Result
TC-12 PASS Kick-off with test-provider-client 202 Accepted 202, task ID returned
TC-13 PASS NPI-less client kick-off Docs say: accepted (no 403) 202 Accepted — docs are correct for provider-member-match

Summary

Metric Value
Total tests 13
Passed 11
Failed 2
Result FAIL

Critical finding: $bulk-member-match still returns 403 Forbidden for NPI-less OAuth clients. The PR removes this from documentation, but the behavior is still active in interop:edge. Either:

  1. The docs should keep the 403 error code until a corresponding interop code change removes the NPI check, OR
  2. If this is an intentional docs-first change (documenting planned behavior), it should be noted as "planned" with a link to the code ticket.

Infrastructure note: End-to-end async processing (polling → manifest → NDJSON download) could not be tested due to a terminology validation regression in aidboxone:edge when writing Binary resources (Invalid terminology configuration). This blocks verification of the output Group structure and order. Previous QA run (interop#289 on aidboxone:2504) confirmed the output structure works correctly on stable builds.

Recommendation: Address finding #1 (403 removal) before merging. The rest of the PR is a significant improvement — reproducible test datasets, corrected IG version, canonical URLs, and clearer examples.


cc @Aitem @Seryiza

QA bot on PR #5 confirmed `interop:edge` still emits `403` for unresolvable
requesting-payer identity. Restore `403` and `409` rows in the Errors table
plus the payer-to-payer rejection sentence, but phrased generically (OAuth
client identity, not NPI) so docs survive the planned NPI -> UDAP
(organization_id) migration.
@Seryiza Seryiza requested a review from Nesmeshnoy May 29, 2026 12:16
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