Files
cyclone/docs/reference/co-medicaid.md
T
Tyler 62bb09f183 feat(sp11): tamper-evident hash-chained audit_log
- New audit_log table (migration 0009, user_version=9):
  * id, event_type, entity_type, entity_id, actor, payload_json,
    created_at, prev_hash, hash
  * Indexes on (entity_type, entity_id), event_type, created_at
- New cyclone.audit_log module:
  * append_event(session, AuditEvent) — appends one chained row
  * verify_chain(session) — walks the chain, returns first bad id
  * SHA-256 hash over canonical row form (unit-separator delimited)
  * Genesis prev_hash = 64 zeros (Bitcoin-style sentinel)
- New AuditLog ORM model
- New admin API endpoints:
  * GET /api/admin/audit-log (paginated, filterable)
  * GET /api/admin/audit-log/verify (returns ok/first_bad_id/reason)
- Hooked into existing endpoints to append events:
  * /api/parse-999 → 'claim.rejected' per matched claim
  * /api/parse-277ca → 'claim.payer_rejected' per matched claim
  * /api/clearhouse/submit → 'clearhouse.submitted' per claim
- HIPAA §164.316(b)(2) compliance note in docs

Tests: 688 -> 705 (9 audit + 8 audit-API). All 705 backend tests pass.
2026-06-20 23:45:43 -06:00

231 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Colorado Medicaid — payer specifics
Cyclone's CO Medicaid (`PayerConfig.co_medicaid()` and
`PayerConfig835.co_medicaid_835()`) factories encode the trading-partner
and code-set rules that the Colorado Department of Health Care Policy &
Financing (HCPF) requires.
## Trading Partner IDs (TPID)
| Role | 837P | 835 |
|---|---|---|
| Sender | `SKCO0` | (varies; usually the clearinghouse) |
| Receiver | `COHCPF` | (varies) |
These appear in the `NM1*PR` (payer) and `NM1*40` (receiver) segments of
the 837P file.
## dzinesco's TPID (clearinghouse identity)
| Field | Value |
|---|---|
| Clearinghouse name | `dzinesco` |
| dzinesco TPID | `11525703` |
| Submitter name (`NM1*41`) | `Dzinesco` |
| Submitter contact | Tyler Martinez <tyler@dzinesco.com> |
| SFTP host | `mft.gainwelltechnologies.com` |
| SFTP username | `colorado-fts\coxix_prod_11525703` |
| SFTP outbound dir | `/CO XIX/PROD/coxix_prod_11525703/FromHPE` |
| SFTP inbound dir | `/CO XIX/PROD/coxix_prod_11525703/ToHPE` |
## dzinesco's 3 billing-provider NPIs
All 3 NPIs are registered to the same Montrose corporate office. They
share tax ID `721587149` and taxonomy `251E00000X` (Home Health).
| NPI | Label | Legal name | Address |
|---|---|---|---|
| `1881068062` | Montrose | TOC, Inc. | 1100 East Main St, Suite A, Montrose, CO 814014063 |
| `1851446637` | Delta | TOC, Inc. | 1100 East Main St, Suite A, Montrose, CO 814014063 |
| `1467507269` | Salida | TOC, Inc. | 1100 East Main St, Suite A, Montrose, CO 814014063 |
Production data (June 2026) confirms all 3 NPIs submit through the same
Montrose address block. See SP9 spec
`docs/superpowers/specs/2026-06-20-cyclone-multi-payer-npi-sftp-design.md`.
## SFTP submission to Gainwell
dzinesco submits 837P files to Gainwell's MFT (Managed File Transfer)
at `mft.gainwelltechnologies.com`. The full SFTP path layout is
specified by the user (2026-06-20):
- **Outbound** (we send): `/CO XIX/PROD/coxix_prod_11525703/FromHPE`
- **Inbound** (HPE/Gainwell sends to us): `/CO XIX/PROD/coxix_prod_11525703/ToHPE`
### File naming
Per the HCPF X12 File Naming Standards Quick Guide
(<https://hcpf.colorado.gov/tp-x12-filenaming>):
- **Outbound** (we send): `TPID-TransactionType-yyyymmddhhmmssSSS-1of1.X12`
- Example: `11525703-837P-20260620132243505-1of1.x12`
- 17-digit millisecond precision, Mountain Time (NOT UTC)
- "1of1" is the only accepted sequence value
- **Inbound** (HPE sends): `TP<TPID>-<OrigTx>_M<Tracking>-<ts>-1of1_<FileType>.x12`
- Example: `TP11525703-837P_M019048402-20260520231513488-1of1_999.x12`
- `FileType` is one of: `999`, `TA1`, `270`, `271`, `276`, `277`, `277CA`, `278`, `820`, `834`, `835`, `ENCR`
- 277CA is distinguished by `ST*277CA` content (filename uses `277`)
### SP9 stub vs SP13 wire-up
The SP9 SFTP client is a **stub** that writes generated 837 files to
`./var/sftp/staging/{outbound_path}/{filename}` instead of opening a
real SFTP connection. The structural interface matches the future
`paramiko`-backed implementation, so SP13 is a one-file swap.
`clearhouse.sftp_block.stub` is `true` by default. Set to `false` (and
create the Keychain entry, see below) to enable real SFTP.
### Keychain setup (one-time, by operator)
```sh
security add-generic-password -s cyclone -a sftp.gainwell.password -w '<password>'
security find-generic-password -s cyclone -a sftp.gainwell.password -w
```
The `cyclone/secrets.py` module fetches the secret by name. When the
entry is missing or `keyring` is not installed, the SFTP stub falls
back to `<stub-secret>` so the local flow still works.
### Verification
```sh
# List providers (3 NPIs)
curl http://localhost:8000/api/config/providers
# List payers (1 = CO_TXIX)
curl http://localhost:8000/api/config/payers
# List CO_TXIX configs
curl http://localhost:8000/api/config/payers/CO_TXIX/configs
# Clearhouse identity
curl http://localhost:8000/api/clearhouse
# Submit 2 claims to the SFTP stub
curl -X POST http://localhost:8000/api/clearhouse/submit \
-H 'Content-Type: application/json' \
-d '{"claim_ids": ["CLM-1", "CLM-2"], "payer_id": "CO_TXIX"}'
# Files appear at ./var/sftp/staging/CO XIX/PROD/coxix_prod_11525703/FromHPE/
ls -la "./var/sftp/staging/CO XIX/PROD/coxix_prod_11525703/FromHPE/"
```
## Payer IDs
### 837P (claims)
- `NM1*PR N104 = "SKCO0"` (COHCPF)
### 835 (remittance)
- `BPR10 = "81-1725341"` — TXIX (Medicaid) account
- `BPR10 = "84-0644739"` — BHA (behavioral health) account
- `N1*PR N104 = "7912900843"` — payer ID
- `N1*PR N102` is one of `"CO_TXIX"` or `"CO_BHA"`
## Allowed codes
### 837P `CLM05-3` (frequency code)
| Code | Meaning |
|---|---|
| 1 | Original claim |
| 7 | Replacement claim |
| 8 | Void / cancel |
Cyclone raises `R030_frequency_allowed` if any other value appears.
### 835 `CLP02` (claim status code)
Allowed: `{1, 2, 3, 4, 19, 20, 21, 22, 23, 25}`. See the 835 reference
note for the meaning of each.
### Place of service (POS)
All CMS POS codes `01``99` are accepted. The canonical list lives in
`cyclone/parsers/payer.py` as `CMS_PLACE_OF_SERVICE_CODES` and is the
source of truth for validation and any UI dropdowns.
## Audit log (SP11)
Cyclone persists every state-changing event to a tamper-evident
hash-chained audit log. Each row carries a SHA-256 hash of
`(id, event_type, entity_type, entity_id, actor, payload_json,
created_at, prev_hash)`, where `prev_hash` is the previous row's
hash. Modifying any row's payload invalidates every subsequent row's
hash.
To list events:
```bash
curl 'http://localhost:8000/api/admin/audit-log?entity_type=claim&entity_id=C-123'
```
To verify the chain (run nightly or on demand):
```bash
curl http://localhost:8000/api/admin/audit-log/verify
# → {"ok": true, "checked": 1234}
# → {"ok": false, "checked": 1180, "first_bad_id": 1181, "reason": "hash mismatch ..."}
```
Events written today:
- `claim.rejected` (999 ACK AK5 R/E)
- `claim.payer_rejected` (277CA STC A4/A6/A7)
- `clearhouse.submitted` (SFTP submit)
Compliance: HIPAA §164.316(b)(2) requires 6-year retention. The
schema doesn't enforce retention — that's a separate vacuum job.
## 277CA Claim Acknowledgment (SP10)
After Gainwell accepts our 837P file (999 AK5=A) and adjudicates the
claims, they send back a 277CA per X12 005010X214. The 277CA carries
one `STC` segment per claim with a category code:
| STC code | Meaning | Cyclone lane |
|---|---|---|
| A1, A2, A3 | Acknowledged / accepted | (logged, no action) |
| A4, A6, A7 | Rejected by payer | **Inbox Payer-Rejected** |
| A8, A9 | Pended | (logged for follow-up) |
| P1P5 | Paid | (835 follow-up expected) |
The Payer-Rejected lane is distinct from the 999 envelope "rejected"
lane: a claim can be syntactically valid (999 A) but semantically
denied (277CA STC A6).
To upload a 277CA:
```bash
curl -X POST http://localhost:8000/api/parse-277ca \
-F "file=@TP11525703-837P_M019048402-...-1of1_277.x12"
```
The response includes `matched_claim_ids` (which Cyclone claims were
stamped payer-rejected) and `orphan_status_codes` (status entries we
couldn't tie to a Cyclone claim — usually because the PCN in REF*1K
doesn't match anything we sent).
The Inbox at `/api/inbox/lanes` returns the new `payer_rejected` lane
alongside the existing four.
## Validation rules Cyclone enforces
See [837p.md](./837p.md#validation-rules-cyclone-enforces) and the
`cyclone.parsers.validator` / `cyclone.parsers.validator_835` modules.
## Adding a new payer
The pattern is:
1. Add a `PayerConfig` (837P) and `PayerConfig835` (835) factory in
`cyclone/parsers/payer.py` — usually thin wrappers that override only
the differing fields (allowed codes, payer IDs).
2. Register the factory in the `PAYER_FACTORIES` / `PAYER_FACTORIES_835`
dicts in `cyclone/api.py` and `cyclone/cli.py`.
3. Add a smoke fixture under `backend/tests/fixtures/` (a real or
synthetic file matching the new payer's rules) and a parser test that
loads it.
4. Update this note + the 837p/835 notes with the new payer's specifics.