Files
cyclone/backend/tests/test_clearhouse_api.py
Nora dd7da18279 fix(sftp): swap inbound/outbound paths — FromHPE is inbound, ToHPE is outbound
The dzinesco SP9 seed had `paths.inbound` and `paths.outbound` mapped to
the wrong Gainwell MFT directories:

  - paths.outbound  was  FromHPE/   (HPE sends files FROM here TO us — inbound)
  - paths.inbound   was  ToHPE/     (we send files TO here — outbound)

So `/api/clearhouse/submit` was writing 837P claims to FromHPE (where
HPE puts acks/835s) and the SP16 scheduler was polling ToHPE (where our
claims go). 999 / TA1 / 835 files in the real FromHPE inbox were
unreachable.

Semantics per operator (2026-06-24):
  - FromHPE = HPE/Gainwell → us  = 999, TA1, 835  (inbound)
  - ToHPE   = us → HPE/Gainwell = 837P claims    (outbound)

**Runtime code (the actual fix):**
  - backend/src/cyclone/store.py            — SP9 seed paths flipped
  - backend/src/cyclone/edi/filenames.py    — docstring corrected

**Test fixtures + assertions (would otherwise fail on the new seed):**
  - backend/tests/test_clearhouse_api.py
  - backend/tests/test_providers_seed.py
  - backend/tests/test_sftp_stub.py        — incl. inbound dir paths
  - backend/tests/test_sftp_paramiko.py
  - backend/tests/test_store_update_clearhouse.py
  - backend/tests/test_api_clearhouse_patch.py
  - backend/tests/test_scheduler.py
  - backend/tests/test_api_scheduler.py

**Docs (text-only — keeps the codebase self-consistent):**
  - README.md
  - docs/reference/co-medicaid.md
  - docs/superpowers/specs/2026-06-20-cyclone-multi-payer-npi-sftp-design.md

**Operator action required after merge:** the existing clearhouse row in
`~/.local/share/cyclone/cyclone.db` was seeded with the old wrong
paths. Easiest recovery:
  1. `rm ~/.local/share/cyclone/cyclone.db` and let `ensure_clearhouse_seeded` re-run on next boot, OR
  2. PATCH /api/clearhouse with the new `paths` block (the SP25
     reconfigure hook picks it up live, including by the running scheduler).

**Verification:**
  - 82 tests in the affected files pass (test_clearhouse_api, test_providers_seed,
    test_sftp_stub, test_sftp_paramiko, test_store_update_clearhouse,
    test_api_clearhouse_patch, test_scheduler, test_api_scheduler, test_filenames).
  - Full backend suite: 1029 pass + 36 pre-existing order-dependent flakes
    unrelated to this change (verified by running the same tests in isolation).
2026-06-24 22:05:55 -06:00

104 lines
3.1 KiB
Python

"""SP9 — clearhouse API endpoint tests."""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from cyclone import db as db_mod
from cyclone.api import app
@pytest.fixture
def client(tmp_path, monkeypatch):
monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db")
db_mod._reset_for_tests()
db_mod.init_db()
with TestClient(app) as c:
yield c
db_mod._reset_for_tests()
def test_get_clearhouse_seeded(client):
# Lifespan runs ensure_clearhouse_seeded()
r = client.get("/api/clearhouse")
assert r.status_code == 200
body = r.json()
assert body["name"] == "dzinesco"
assert body["tpid"] == "11525703"
assert body["sftp_block"]["stub"] is True
assert "ToHPE" in body["sftp_block"]["paths"]["outbound"]
assert "FromHPE" in body["sftp_block"]["paths"]["inbound"]
def test_list_providers(client):
r = client.get("/api/config/providers")
assert r.status_code == 200, r.text
body = r.json()
assert {p["label"] for p in body} == {"Montrose", "Delta", "Salida"}
assert {p["npi"] for p in body} == {"1881068062", "1851446637", "1467507269"}
def test_get_provider_by_npi(client):
r = client.get("/api/config/providers/1881068062")
assert r.status_code == 200
assert r.json()["label"] == "Montrose"
def test_get_provider_404(client):
r = client.get("/api/config/providers/9999999999")
assert r.status_code == 404
def test_list_payers(client):
r = client.get("/api/config/payers")
assert r.status_code == 200
body = r.json()
assert len(body) == 1
assert body[0]["payer_id"] == "CO_TXIX"
def test_list_payer_configs_for_co_txix(client):
r = client.get("/api/config/payers/CO_TXIX/configs")
assert r.status_code == 200
body = r.json()
# The lifespan loads config/payers.yaml; configs come from the registry
tx_types = {c["transaction_type"] for c in body}
assert "837P" in tx_types or "835" in tx_types
def test_reload_config(client):
r = client.post("/api/admin/reload-config")
assert r.status_code == 200
body = r.json()
assert body["ok"] is True
assert isinstance(body["loaded"], int)
def test_submit_clearhouse_rejects_empty_claim_ids(client):
r = client.post("/api/clearhouse/submit", json={"claim_ids": [], "payer_id": "CO_TXIX"})
assert r.status_code == 400
def test_submit_clearhouse_rejects_missing_payer_id(client):
r = client.post("/api/clearhouse/submit", json={"claim_ids": ["X"]})
assert r.status_code == 400
def test_submit_clearhouse_handles_unknown_claim_id(client):
r = client.post("/api/clearhouse/submit", json={
"claim_ids": ["DOES_NOT_EXIST"],
"payer_id": "CO_TXIX",
})
# 200 with per-claim ok:false entries (graceful degradation)
assert r.status_code == 200
body = r.json()
assert body["ok"] is True
assert body["stub"] is True
assert len(body["submitted"]) == 1
assert body["submitted"][0]["ok"] is False
assert "not found" in body["submitted"][0]["error"] or "cannot be re-serialized" in body["submitted"][0]["error"]