# Manual Testing Plan — ACCESS Core Database ## Overview This plan covers end-to-end manual testing of the ACCESS Core integration pipeline, from Healthie sandbox intake forms through webhook ingestion into our PostgreSQL database, and CMS API operations against the sandbox endpoints. **Testing tiers:** 1. **Local fixture replay** — no external dependencies, validates normalization logic 2. **Live Healthie sandbox** — real webhooks, GraphQL fetches, full pipeline 3. **CMS API sandbox** — eligibility, alignment, reporting, unalignment operations --- ## Prerequisites ### 1. Healthie Sandbox Account You need a Healthie sandbox organization with: - API key (GraphQL + webhooks enabled) - Webhook signing secret (`whsec_`) - An intake form created in the sandbox (note its `custom_module_form.id`) If you don't have these yet, request them from your Healthie contact. The Group Plan with API add-on is sufficient — FHIR is not needed for this testing phase. ### 2. ngrok (or alternative tunnel) Healthie webhooks need a publicly reachable HTTPS endpoint. Install and configure ngrok: ```bash # Install (macOS example, adjust for your platform) brew install ngrok/ngrok/ngrok # Start tunnel to local Django app on port 8000 ngrok http 8000 ``` Note the ngrok forward URL — it will look like `https://abc123.ngrok-free.app`. You'll need this for webhook configuration in Healthie. **Alternative:** Cloudflare Tunnel, localhost.run, or a deployed staging server. ### 3. Environment Configuration Create `.env.local` (loaded automatically alongside `.env`) and update: ```bash # Healthie Sandbox HEALTHIE_API_BASE_URL=https://api.gethealthie.com/graphql HEALTHIE_API_KEY= HEALTHIE_WEBHOOK_SECRET= HEALTHIE_INTAKE_FORM_ID= # Webhook verification (disable for initial testing, re-enable later) HEALTHIE_WEBHOOK_VERIFY_SIGNATURE=false # CMS API — Mock Server (for local development / sandbox testing) CMS_API_BASE_URL=http://localhost:3001 CMS_API_CLIENT_ID=mock-id CMS_API_CLIENT_SECRET=mock-secret ``` **Important:** `HEALTHIE_INTAKE_FORM_ID` must match the ID of your intake form in the sandbox. This is the value Healthie returns as `custom_module_form.id` when you query or create a form. Without this, all form submissions will be routed to daily check-in normalization instead of intake processing. --- ## Phase 1 — Local Fixture Replay (No External Dependencies) **Goal:** Validate that webhook handlers parse payloads correctly and normalize data into the Core DB models without calling Healthie's live API. ### Step 1.1: Start Docker Services ```bash docker compose up -d db # Wait for database to be ready sleep 5 ``` ### Step 1.2: Run Migrations ```bash docker compose exec django python manage.py migrate --noinput ``` ### Step 1.3: Seed Synthetic Data ```bash docker compose exec django python manage.py seed_synthetic_data --clear-first --replay-webhooks --no-input ``` **Expected output:** - Models with `is_synthetic` flag get cleared (IntakeRecord, Beneficiary, ClinicalObservation, LabResult, ImagingStudy, StructuralEvidence, PromResponse) - Fixture files load from `sample_data/fixtures/` - Webhook fixtures replay from `sample_data/fixtures/webhook_fixtures/` **Verify:** Check that the command completes without errors and reports fixture counts. ### Step 1.4: Verify Database State ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord, RawIngestionRecord from access_core.identity.models import Beneficiary print(f'IntakeRecords: {IntakeRecord.objects.count()}') print(f'RawIngestionRecords: {RawIngestionRecord.objects.count()}') print(f'Beneficiaries: {Beneficiary.objects.count()}') " ``` **Expected:** Non-zero counts if fixtures seeded data successfully. ### Step 1.5: Replay Individual Fixtures Test specific event types in isolation: ```bash # Replay intake form fixture docker compose exec django python manage.py replay_webhook_fixture 001 --dry-run # Actually run it docker compose exec django python manage.py replay_webhook_fixture 001 ``` Check the Django admin for results: - Navigate to `http://localhost:8000/admin/` (credentials from seed data or create superuser) - Verify `RawIngestionRecord` was created with status `"normalized"` - Verify corresponding `IntakeRecord`, sub-models, and `Beneficiary` records exist ### Step 1.6: Replay All Fixtures ```bash docker compose exec django python manage.py replay_webhook_fixture --dry-run # Then without --dry-run to actually process them docker compose exec django python manage.py replay_webhook_fixture ``` **Verify:** Each fixture processes without errors. Check the output for `"Result:"` lines showing the handler outcome (`"normalized"`, `"created"`, etc.). --- ## Phase 2 — Live Healthie Sandbox Webhooks (End-to-End) **Goal:** Fill out an intake form in Healthie sandbox, receive the webhook at our Django app, fetch full data via GraphQL, and normalize into Core DB models. ### Step 2.1: Start Services with Public Tunnel ```bash # Terminal 1: Docker services docker compose up -d db django # Terminal 2: ngrok tunnel ngrok http 8000 ``` Note the ngrok URL (e.g., `https://abc123.ngrok-free.app`). ### Step 2.2: Configure Healthie Webhook Endpoint In your Healthie sandbox organization settings, configure a webhook endpoint pointing to: ``` /api/intake/webhooks/healthie/ ``` For example: `https://abc123.ngrok-free.app/api/intake/webhooks/healthie/` **Events to subscribe to:** - `form_answer_group.created` (primary — intake forms + daily check-ins) - `patient.created`, `patient.updated` (profile changes) - `metric_entry.created`, `metric_entry.updated` (BP, weight, A1C, etc.) - `entry.created`, `entry.updated` (symptoms, journal entries) - `care_plan.created`, `care_plan.updated`, `care_plan.activated`, `care_plan.deactivated`, `care_plan.deleted` - `document.created`, `document.updated` ### Step 2.3: Verify Health Check ```bash curl https://abc123.ngrok-free.app/health/ # Expected: {"status": "ok", "service": "access-core-db"} ``` ### Step 2.4: Fill Out the Intake Form in Healthie 1. Log into your Healthie sandbox organization as a provider or patient (depending on how forms are configured) 2. Navigate to the intake form you created 3. Fill out all fields, including DOB, sex, primary concerns, medical conditions, medications, allergies, lifestyle data 4. Submit the form ### Step 2.5: Monitor Webhook Reception **Watch Django logs in real time:** ```bash docker compose logs -f django ``` **Expected log output after submitting the form:** 1. `Webhook processed: form_answer_group.created resource= -> normalized` 2. GraphQL query to fetch full FormAnswerGroup data 3. Intake normalization messages ### Step 2.6: Verify Database State After webhook processing completes, check the database: ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord, RawIngestionRecord from access_core.identity.models import Beneficiary # Check raw ingestion records records = RawIngestionRecord.objects.filter(source_system='healthie').order_by('-ingested_at')[:5] for r in records: print(f'RawIngestionRecord #{r.id}: status={r.processing_status}, event={r.raw_payload.get(\"event_type\", \"unknown\")}') # Check intake records intakes = IntakeRecord.objects.filter(is_synthetic=True).order_by('-created_at')[:3] for i in intakes: print(f'IntakeRecord: {i.first_name} {i.last_name}, status={i.status}, dob={i.dob}') # Check beneficiaries (if promotion occurred) bens = Beneficiary.objects.filter(is_synthetic=True).order_by('-created_at')[:3] for b in bens: print(f'Beneficiary: {b.first_name} {b.last_name}') " ``` **Expected:** - At least one `RawIngestionRecord` with status `"normalized"` and event_type `"form_answer_group.created"` - At least one `IntakeRecord` with the patient's name, DOB, sex matching what you entered in Healthie - Sub-models populated: `IntakeAssessment`, `IntakeLifestyle`, etc. ### Step 2.7: Verify Sub-Model Data Check specific sub-model fields match your form submission: ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord intake = IntakeRecord.objects.filter(is_synthetic=True).order_by('-created_at').first() if intake: print(f'Intake: {intake.first_name} {intake.last_name}') # Check assessment sub-model assessments = intake.assessments.all() if assessments: a = assessments.first() print(f' Primary concerns: {a.primary_concerns[:50] if a.primary_concerns else \"N/A\"}...') print(f' Medical conditions: {a.medical_conditions[:50] if a.medical_conditions else \"N/A\"}...') # Check lifestyle sub-model lifestyles = intake.lifestyle.all() if lifestyles: l = lifestyles.first() print(f' Weight: {l.weight_lbs}, BMI: {l.bmi}') " ``` ### Step 2.8: Enable Signature Verification (After Initial Success) Once Phase 2.4–2.7 works with `HEALTHIE_WEBHOOK_VERIFY_SIGNATURE=false`, re-enable signature verification: 1. Set in `.env`: `HEALTHIE_WEBHOOK_VERIFY_SIGNATURE=true` 2. Restart Django container: `docker compose restart django` 3. Submit another form and verify the webhook is accepted (no `"Invalid signature"` errors) If you get signature errors, check: - The signing key matches exactly what Healthie provided (without the `whsec_` prefix in your `.env`) - The Content-Digest and Signature headers are present in incoming requests ### Step 2.9: Test Non-Intake Form Submission Submit a daily check-in or other form type (not matching `HEALTHIE_INTAKE_FORM_ID`). Verify it routes to `_normalize_other_form()` instead of intake processing: ```bash docker compose exec django python manage.py shell -c " from access_core.monitoring.models import DailyCheckinSubmission, RawDailyCheckinSubmission print(f'DailyCheckinSubmissions: {DailyCheckinSubmission.objects.count()}') print(f'RawDailyCheckinSubmissions: {RawDailyCheckinSubmission.objects.count()}') " ``` ### Step 2.10: Test Patient Profile Update Update a patient's name, phone number, or other profile data in Healthie sandbox. Verify the `patient.updated` webhook creates/updates records: ```bash docker compose exec django python manage.py shell -c " from access_core.identity.models import ExternalSystemIdentifier ids = ExternalSystemIdentifier.objects.filter(source_system='healthie')[:3] for e in ids: print(f'ExternalID: {e.source_record_id} -> beneficiary={e.beneficiary}') " ``` --- ## Phase 3 — CMS API Sandbox Testing **Goal:** Validate all four (plus polling) CMS ACCESS APIs work against the local `access-fhir-apis` mock server. The mock implements spec-accurate business logic from the published IG: ICD-10 validation, LOINC measure requirements, control group randomization, timing windows, and OAP target evaluation. ### Prerequisites for Phase 3 #### Option A: Docker Compose (Recommended) The mock server runs as a service in `docker-compose.yml`. Start it alongside Django: ```bash # Start all services including CMS mock + the polling worker docker compose up -d db django cms-mock cms-worker # Verify mock is running curl http://localhost:3001/fhir/metadata | head -5 # Expected: {"resourceType": "CapabilityStatement", ...} ``` The `cms-worker` service runs `manage.py cms_polling_worker`, which drains due `CMSApiSubmissionStatus` rows on a schedule (`CMS_POLL_LOOP_SECONDS`, default 2s in compose). All CMS operations are submit-only — the commands return as soon as the POST is accepted, and the worker polls `$submission-status` and applies outcomes. If you don't start it (or run Option B/C), drive polling manually after each step: ```bash docker compose exec django python manage.py poll_cms_submissions --force ``` #### Option B: Run Locally (Node.js required) Clone the upstream mock server at the same pinned commit used by `access_fhir_sandbox/Dockerfile`: ```bash git clone https://github.com/hadijaveed/access-fhir-apis.git /tmp/access-fhir-apis cd /tmp/access-fhir-apis && git checkout 4e897bb npm install npm run start:mock # → http://localhost:3001 ``` #### Option C: Interactive API Explorer The mock server serves a browser-based explorer at `http://localhost:3001/`. Use it to: - Walk through the complete patient lifecycle (screen → enroll → report → exit) - Edit FHIR payloads and see how the system responds - Switch between tracks (eCKM, CKM, MSK, BH) with correct ICD-10 codes and LOINC measures - View 24 pre-built scenarios covering every result code #### Configuration for Production Mock Server The mock server uses non-versioned FHIR paths (`/fhir/Patient/$op`, not `/fhir/R4/Patient/$op`) and takes the submission id as a query param. Ensure `.env.local` has: ```bash CMS_API_BASE_URL=http://localhost:3001 CMS_FHIR_VERSION_PATH= # empty = no version segment in URL # Mock form of $submission-status (query param instead of path segment): CMS_API_SUBMISSION_STATUS_PATH=/fhir/Patient/$submission-status?submissionID={submission_id} # No client_id/client_secret needed — auth is skipped when empty ``` For real BCDA/production endpoints (when credentials are provisioned): ```bash CMS_API_BASE_URL=https:// CMS_API_CLIENT_ID= CMS_API_CLIENT_SECRET= CMS_FHIR_VERSION_PATH=R4 # production uses versioned paths ``` ### Mock Server Configuration Override defaults via environment variables: | Variable | Default | Purpose | |----------|---------|---------| | `MOCK_PORT` | `3001` | Port to listen on | | `LOCK_IN_DAYS` | `90` | Days before patient can switch participants | | `BASELINE_WINDOW_DAYS` | `60` | Days to submit baseline after alignment | | `ASYNC_DELAY_MS` | `500` | Simulated processing delay (ms) | Debug endpoints: - `GET /fhir/$mock-state` — view current alignments, submissions, reports - `POST /fhir/$mock-reset` — clear all in-memory state ### Step 3.1: Eligibility Check — Dry Run ```bash docker compose exec django python manage.py check_cms_eligibility --dry-run --limit 5 ``` **Expected output:** Lists the `EligibilityRequest`s (patient, track, ICD-10 codes) that would be checked without making API calls. Reports filter description and count. ### Step 3.2: Eligibility Check — Live (Single Intake) ```bash # Create the reviewer's eligibility request (track + ICD-10 diagnosis) first — # Healthie intake data carries neither, so a reviewer assigns them manually. docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord from access_core.monitoring.models import ClinicalTrack, EligibilityRequest, EligibilityRequestCondition intake = IntakeRecord.objects.filter(is_synthetic=True).first() track = ClinicalTrack.objects.get(track_name='CKM') req = EligibilityRequest.objects.create(intake_record=intake, clinical_track=track) EligibilityRequestCondition.objects.create(request=req, icd10_code='N18', icd10_description='Chronic kidney disease') print(f'intake={intake.id} request={req.id}') " # Then run the eligibility check for that specific intake (all of its requests) docker compose exec django python manage.py check_cms_eligibility --intake-id ``` **Admin alternative:** create the request at `/admin/monitoring/eligibilityrequest/add/` — the ICD-10 code field has typeahead backed by a local CDC ICD-10-CM snapshot (type 2+ characters; selecting a suggestion autofills the official description). If search fails the field falls back to manual entry. **Expected:** - FHIR Parameters resource built with patient DOB, gender, name + MBI identifier + the request's ICD-10 condition(s) - `$check-eligibility` operation submitted to `POST /fhir/Patient/$check-eligibility` - HTTP 202 Accepted → command prints SUBMITTED and returns immediately (no blocking poll) - The polling worker then checks `GET /fhir/Patient/$submission-status?submissionID=...` until complete - Worker applies the result: stored in `EligibilityCheck` model; on success IntakeRecord transitions to `pending_consent` **Mock business logic:** The mock validates ICD-10 codes against track-specific prefixes, checks for exclusion conditions (ESRD/dialysis), and applies SHA-256 hash-based control group assignment (~20% of patients). See the [Interactive Explorer](http://localhost:3001/) to test specific scenarios. ### Step 3.3: Verify Eligibility Check Results ```bash docker compose exec django python manage.py shell -c " from access_core.monitoring.models import EligibilityCheck checks = EligibilityCheck.objects.all().order_by('-checked_at')[:5] for c in checks: print(f'EligibilityCheck #{c.id}: status={c.check_status}, track={c.requested_track.track_name}') print(f' CMS response: {str(c.cms_response)[:200]}...') " ``` ### Step 3.4: Alignment — Dry Run ```bash docker compose exec django python manage.py run_cms_alignment --track CKM --dry-run --limit 5 ``` **Expected:** Shows which eligible intakes would be aligned without making API calls. ### Step 3.5: Alignment — Live (Single Intake) ```bash # Run alignment for a specific intake record (uses --intake-id, not --beneficiary-id) docker compose exec django python manage.py run_cms_alignment --intake-id --track CKM ``` **Expected:** - US Core Patient resource built from IntakeRecord data with MBI identifier - `$align` operation submitted to `POST /fhir/Patient/$align`; command prints SUBMITTED and returns immediately (no blocking poll) - The polling worker completes the submission; on success it promotes a `Beneficiary` and creates a `CarePeriod` (atomic transaction), IntakeRecord → `active_access_beneficiary` - Result stored in `Alignment` model **Mock business logic:** Validates ICD-10 codes match track prefixes. Enforces 90-day lock-in period (patient can't switch participants within 90 days of alignment). Check mock state with `GET http://localhost:3001/fhir/$mock-state`. ### Step 3.6: Verify Alignment Results ```bash docker compose exec django python manage.py shell -c " from access_core.monitoring.models import Alignment, CarePeriod alignments = Alignment.objects.all().order_by('-aligned_at')[:5] for a in alignments: print(f'Alignment #{a.id}: status={a.status}, beneficiary={a.beneficiary}') care_periods = CarePeriod.objects.filter(care_period_status='active').order_by('-start_date')[:3] for cp in care_periods: print(f'CarePeriod: {cp.beneficiary} ({cp.start_date} to {cp.end_date}), status={cp.care_period_status}') " ``` ### Step 3.7: Data Reporting — Dry Run Create some synthetic OAPMeasureObservation records first, then test reporting: ```bash docker compose exec django python manage.py run_cms_data_reporting --dry-run --limit 5 ``` **Expected:** Shows which observations would be reported without making API calls. Lists measure name, observation ID, and beneficiary for each unreported approved observation. ### Step 3.8: Data Reporting — Live ```bash docker compose exec django python manage.py run_cms_data_reporting --limit 10 ``` **Expected:** - US Core Observation resources built with LOINC codes and UCUM units - Batch `$report-data` submission to `POST /fhir/$report-data`; each observation gets a pending marker (`cms_submission_response.status = pending_cms_processing`) and the command returns immediately (no blocking poll) - The polling worker applies the batch outcome: accepted observations are updated to `submitted_to_cms`, rejected ones stay approved with the rejection recorded - Summary shows submitted/failed counts at submit time, with per-observation details **Mock business logic:** Determines report type from timing (baseline within 60 days, quarterly every 70-110 days, end-of-period after 365 days). Validates required LOINC measures per track and report type. At end-of-period, evaluates OAP targets by comparing current vs baseline values. ### Step 3.9: Unalignment — Live (Single Beneficiary) ```bash docker compose exec django python manage.py run_cms_unalignment --beneficiary-id --reason "geographic-relocated" ``` **Expected:** `$unalign` operation submitted to `POST /fhir/Patient/$unalign`; command prints SUBMITTED and returns immediately. The polling worker then completes the submission and updates the CarePeriod to `unaligned`. Valid reason codes: `geographic-relocated`, `loss-of-contact`, `no-longer-clinically-eligible`, `patient-initiated`. ### Step 3.10: Verify CMS Audit Logs ```bash docker compose exec django python manage.py shell -c " from access_core.cms_integration.models import CMSApiRequest, CMSApiResponse requests = CMSApiRequest.objects.all().order_by('-created_at')[:5] for r in requests: print(f'CMSApiRequest: operation={r.operation}, status_code={r.status_code}') responses = CMSApiResponse.objects.all().order_by('-created_at')[:5] for resp in responses: print(f'CMSApiResponse: request={resp.request_id}, severity={resp.severity}') " ``` --- ## Phase 4 — Signal Wiring Verification **Goal:** Verify that Django signal handlers automatically trigger CMS service calls when model states change. ### Step 4.1: Test IntakeRecord Status Transition (Alignment Trigger) When an IntakeRecord transitions from `pending_consent` to `pending_alignment`, the pre_save signal should auto-trigger CMS alignment: ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord # Find an intake with eligibility check completed intake = IntakeRecord.objects.filter( is_synthetic=True, status='pending_consent' ).first() if intake: print(f'Transitioning {intake.first_name} {intake.last_name}') intake.status = 'pending_alignment' intake.save(update_fields=['status']) print('Saved. Check logs for alignment trigger.') else: print('No pending_consent intakes found. Create one first via eligibility check.') " ``` **Check Django logs:** Look for alignment service invocation messages. ### Step 4.2: Test CarePeriod Status Transition (Unalignment Trigger) When a CarePeriod transitions from `active` to `ended`, `withdrawn`, or `unaligned`: ```bash docker compose exec django python manage.py shell -c " from access_core.monitoring.models import CarePeriod cp = CarePeriod.objects.filter(care_period_status='active').first() if cp: print(f'Transitioning CarePeriod {cp.id}') cp.care_period_status = 'ended' cp.save(update_fields=['care_period_status']) else: print('No active care periods found.') " ``` ### Step 4.3: Test OAPMeasureObservation Status Transition (Reporting Trigger) When an observation transitions from `draft` to `approved`: ```bash docker compose exec django python manage.py shell -c " from access_core.clinical.models import OAPMeasureObservation obs = OAPMeasureObservation.objects.filter(status='draft').first() if obs: print(f'Approving observation {obs.id}') obs.status = 'approved' obs.save(update_fields=['status']) else: print('No draft observations found.') " ``` --- ## Phase 5 — Error Path Testing ### Step 5.1: Missing Required Patient Data Submit an intake form without DOB (if Healthie allows it). Verify the eligibility check handles missing data gracefully: **Expected:** Eligibility check should skip or fail with a clear error message, not crash. ### Step 5.2: Eligibility Request Without ICD-10 Conditions `$check-eligibility` accepts zero conditions — a reviewer may submit a request with no diagnoses yet: ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import IntakeRecord from access_core.monitoring.models import ClinicalTrack, EligibilityRequest intake = IntakeRecord.objects.filter(is_synthetic=True, status='pending_cms_eligibility').first() track = ClinicalTrack.objects.get(track_name='CKM') req = EligibilityRequest.objects.create(intake_record=intake, clinical_track=track) print(req.id) " docker compose exec django python manage.py check_cms_eligibility --request-id ``` **Expected:** No `condition` parameter in the submitted payload (defaults are never invented); the mock decides eligibility from patient data alone. Track validity is enforced by the `EligibilityRequest.clinical_track` FK, not a CLI flag. ### Step 5.3: CMS API Authentication Failure Set an invalid client secret and run a command: ```bash docker compose exec django python manage.py check_cms_eligibility --intake-id ``` **Expected:** `CmsApiError` logged with authentication failure details, not a raw HTTP exception. EligibilityCheck stored with status `"error"`. ### Step 5.4: Webhook Handler Failure (No Registered Handler) Send a webhook event type we don't handle (e.g., `unknown.event`): ```bash curl -X POST http://localhost:8000/api/intake/webhooks/healthie/ \ -H "Content-Type: application/json" \ -d '{"resource_id": "99999", "resource_id_type": "UnknownType", "event_type": "unknown.event", "changed_fields": []}' ``` **Expected:** Returns HTTP 200, stores a `RawIngestionRecord` with status `"received"`, logs warning about unhandled event type. --- ## Phase 6 — Database State Audit After completing all phases, run a comprehensive audit: ```bash docker compose exec django python manage.py shell -c " from access_core.intake.models import * from access_core.identity.models import * from access_core.clinical.models import * from access_core.monitoring.models import * from access_core.cms_integration.models import * from access_core.governance.models import * models = [ RawIngestionRecord, IntakeRecord, Beneficiary, CarePeriod, EligibilityCheck, Alignment, Unalignment, ClinicalObservation, OAPMeasureObservation, CMSApiRequest, CMSApiResponse, DailyCheckinSubmission, MonitoringPlan, SafetySignalEvent, ] print('=== Database State Audit ===') for model in models: count = model.objects.count() print(f'{model.__name__:30s}: {count}') " ``` --- ## Troubleshooting ### Webhook not reaching Django app - Verify ngrok tunnel is running and URL hasn't changed (ngrok free tier rotates URLs) - Check `docker compose logs django` for incoming requests - Test manually: `curl -X POST http://localhost:8000/api/intake/webhooks/healthie/ -H "Content-Type: application/json" -d '{"resource_id":"1","resource_id_type":"FormAnswerGroup","event_type":"form_answer_group.created"}'` ### GraphQL fetch fails (webhook received but no data) - Check `HEALTHIE_API_KEY` is set correctly in `.env` and passed to the Django container - Verify API key has read permissions for FormAnswerGroup resources - Check logs: look for `"GraphQL error"` messages with details from Healthie's response ### Form routed to wrong handler (check-in instead of intake) - Verify `HEALTHIE_INTAKE_FORM_ID` matches your sandbox form's `custom_module_form.id` - The routing logic at consumers.py line 250 compares: `str(form_template_id) == str(intake_form_id)` ### Signature verification fails after enabling it - Ensure the webhook secret in `.env` is the raw secret value (without `whsec_` prefix) - Check that Healthie's signing key format matches what our verifier expects (`sig-fix-v1` draft) ### CMS API OAuth failures - For mock server (`localhost:3001`): auth is disabled — if you see auth errors, check `CMS_API_CLIENT_ID` in `.env.local` (should be empty or unset) - For production/BCDA: verify client ID and secret are correct, token endpoint is `/auth/token` with Basic Auth header - Check `CMSApiRequest` records in admin for error details ### Mock server not responding - Ensure Docker service is running: `docker compose ps cms-mock` - Start if stopped: `docker compose up -d cms-mock` - Verify port is free: `lsof -i :3001` or `ss -tlnp | grep 3001` - Check mock server logs: `docker compose logs -f cms-mock` ### Mock returns unexpected eligibility result The mock enforces spec-accurate business logic. Common causes: - **Control group assignment:** SHA-256 hash of MBI+track puts ~20% in control arm → `not-eligible-control-group`. Try a different test patient or reset state with `POST http://localhost:3001/fhir/$mock-reset` - **ICD-10 mismatch:** Code must match track-specific prefixes (eCKM: I10/E11/etc., CKM: N18/I10, MSK: M17/M54, BH: F32/F41). Check `ig/ValueSet-*` files for valid codes - **Already aligned:** Patient was previously aligned in the mock. Reset state or use a new test patient ### Management command argument errors (`unrecognized arguments`) - Django converts hyphens to underscores in option keys internally. Commands use `--kebab-case` on the CLI but access via `options["snake_case"]` in code. - Command-specific options: - `run_cms_alignment`: `--intake-id`, not `--beneficiary-id` (targets IntakeRecord, not Beneficiary) - `run_cms_unalignment`: `--beneficiary-id` (targets Beneficiary directly) - `run_cms_data_reporting`: `--dry-run`, `--limit`, `--beneficiary-id`, `--care-period-id` all supported --- ## Test Data Cleanup To reset between test runs without losing schema/migrations: ```bash # Clear all synthetic data and replay fixtures fresh docker compose exec django python manage.py seed_synthetic_data --clear-first --replay-webhooks --no-input ``` Or start completely clean (drops and recreates the database): ```bash docker compose down -v # Removes volumes including pgdata docker compose up -d db # Wait for DB, then restart Django to trigger migrations + seed data docker compose restart django ```