Files
cyclone/docs/reference/co-medicaid.md
T
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

9.0 KiB
Raw Blame History

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)

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

# 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 0199 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

# 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

# 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:

curl 'http://localhost:8000/api/admin/audit-log?entity_type=claim&entity_id=C-123'

To verify the chain (run nightly or on demand):

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:

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 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.