2c0afbe9c5
- Add cyclone.parsers.models_277ca + parse_277ca (X12 005010X214)
- Per-Patient HL ClaimStatus with REF*1K (PCN), REF*EJ (tax ID),
STC category code, amount, service date
- STC classifier: A1-A3 accepted, A4/A6/A7 rejected, A8/A9 pended,
P1-P5 paid, anything else unknown
- Multiple STCs per Patient HL: last wins (canonical pattern for
'first pended, then paid' acknowledgments)
- Subscriber-level STCs surface in unscoped_statuses
- Add apply_277ca_rejections — stamps Claim.payer_rejected_* fields on
matching rows (lookup by patient_control_number, mirrors 999 path)
- New /api/parse-277ca, /api/277ca-acks, /api/277ca-acks/{id} endpoints
- New two77ca_acks table + Two77caAck ORM model
- New Claim columns: payer_rejected_at, _reason, _status_code, _by_277ca_id
- New payer_rejected lane in /api/inbox/lanes (distinct from 999
envelope rejected lane)
- New PayerConfig277CA block in config/payers.yaml + Pydantic model
- Migration 0008 bumps user_version to 8
Tests: 654 -> 688 (parser 22 + apply 6 + API 8 + config 6 + adjustments).
All 688 backend tests pass; 1 pre-existing skipped test class unaffected.
200 lines
6.9 KiB
Markdown
200 lines
6.9 KiB
Markdown
# 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.
|
||
|
||
## 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) |
|
||
| P1–P5 | 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.
|