Files
cyclone/docs/reference/co-medicaid.md
Tyler d54c44f04a feat(sp12): SQLCipher encryption at rest (optional)
- New cyclone.db_crypto module:
  * is_sqlcipher_available() — capability check
  * is_encryption_enabled() — Keychain key + sqlcipher3 present
  * get_db_key() — reads 'cyclone.db.key' from Keychain
  * make_sqlcipher_connect_creator(url, key) — SQLAlchemy creator
- db._make_engine() now switches to SQLCipher when key is present
- pyproject.toml: optional 'sqlcipher' extra (sqlcipher3>=0.6,<1)
- Fallback: without Keychain key, DB stays plain SQLite (no surprise
  behavior for operators who haven't set up encryption yet)
- Verified: encrypted file is unreadable as plain SQLite, wrong key
  raises on first query, migrations + ORM work transparently
- HIPAA §164.312(a)(2)(iv) compliance note in docs

Tests: 705 -> 717 (12 new for SQLCipher). All 717 backend tests pass.
2026-06-20 23:52:41 -06:00

275 lines
9.0 KiB
Markdown
Raw Permalink 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.
## Database encryption at rest (SP12)
Cyclone optionally encrypts the SQLite database with SQLCipher
(AES-256). The encryption key is stored in macOS Keychain — never on
disk in plaintext. Without the key, the DB falls back to plain SQLite.
### One-time operator setup
```bash
# 1. Install SQLCipher (C library) and the Python binding.
brew install sqlcipher
pip install -e backend[sqlcipher]
# 2. Generate a random 32-byte key and store it in Keychain.
python3 -c "import secrets; print(secrets.token_urlsafe(32))" \
| xargs -I {} security add-generic-password \
-s cyclone -a cyclone.db.key -w "{}"
# 3. Restart Cyclone. The DB is now encrypted.
```
### Verification
```bash
# The DB file should be unreadable as plain SQLite.
sqlite3 ~/.local/share/cyclone/cyclone.db "SELECT count(*) FROM sqlite_master"
# → file is not a database
# But readable through Cyclone.
curl http://localhost:8000/api/claims
# → 200 OK
```
### Key rotation (future)
To rotate the key:
1. Decrypt with old key, dump to SQL
2. Re-encrypt with new key, import SQL
3. Update Keychain
A first-class rotation endpoint is out of scope for SP12 (planned for
SP14+).
## 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.