c8f8f5d3c6
Behaviour-preserving structural split of the 2,995-LOC store.py into a 14-module subpackage with a thin facade. CycloneStore class keeps its full method surface as 1-line delegations to module functions. 14 modules: exceptions, records, orm_builders, ui, write, batches, claim_detail, kpis, acks, backups, inbox, providers (+ __init__.py facade) Facade re-exports 12 public symbols (CycloneStore, store, BatchRecord*, BatchKind, AlreadyMatchedError/NotMatchedError/InvalidStateError, utcnow, dashboard_kpis, check_matched_pair_drift) + 3 private helpers (_claim_status_from_validation, _persist_835_remit, _remittance_835_row) to preserve the 4 test files that import them. Zero public API changes, zero test changes, zero importer changes. CycloneStore._lock and CycloneStore._batches.clear() remain intact for the 7 test files still using the cleanup idiom. Verified: 1,176 / 1 (pre-existing isolation flake) / 10 tests pass — identical to baseline.
308 lines
12 KiB
Python
308 lines
12 KiB
Python
"""Provider, payer, and clearhouse configuration reads and upserts.
|
|
|
|
The ``providers`` and ``payers`` modules in ``cyclone`` provide the
|
|
ORM-row DTOs; this module is the read/upsert surface over them.
|
|
``ensure_clearhouse_seeded`` is called at startup to insert the
|
|
default Clearhouse row if missing.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from cyclone import db
|
|
from cyclone.providers import Clearhouse, Payer, Provider
|
|
|
|
from . import utcnow
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# SP9: providers / payers / payer_configs / clearhouse
|
|
# ------------------------------------------------------------------
|
|
|
|
def list_providers(*, is_active: bool | None = True) -> list[Provider]:
|
|
"""List providers. ``is_active=None`` returns all."""
|
|
from cyclone.db import Provider as ProviderORM
|
|
from cyclone.providers import Provider
|
|
with db.SessionLocal()() as s:
|
|
q = s.query(ProviderORM)
|
|
if is_active is not None:
|
|
q = q.filter(ProviderORM.is_active == (1 if is_active else 0))
|
|
rows = q.order_by(ProviderORM.label).all()
|
|
return [Provider.model_validate(_provider_orm_to_dict(r)) for r in rows]
|
|
|
|
def get_provider(npi: str) -> Provider | None:
|
|
from cyclone.db import Provider as ProviderORM
|
|
from cyclone.providers import Provider
|
|
with db.SessionLocal()() as s:
|
|
row = s.get(ProviderORM, npi)
|
|
return Provider.model_validate(_provider_orm_to_dict(row)) if row else None
|
|
|
|
def upsert_provider(provider: Provider) -> Provider:
|
|
from cyclone.db import Provider as ProviderORM
|
|
with db.SessionLocal()() as s:
|
|
row = s.get(ProviderORM, provider.npi)
|
|
now = utcnow().isoformat()
|
|
if row is None:
|
|
row = ProviderORM(
|
|
npi=provider.npi, label=provider.label,
|
|
legal_name=provider.legal_name, tax_id=provider.tax_id,
|
|
taxonomy_code=provider.taxonomy_code,
|
|
address_line1=provider.address_line1,
|
|
address_line2=provider.address_line2,
|
|
city=provider.city, state=provider.state, zip=provider.zip,
|
|
is_active=1 if provider.is_active else 0,
|
|
created_at=provider.created_at.isoformat(),
|
|
updated_at=now,
|
|
)
|
|
s.add(row)
|
|
else:
|
|
row.label = provider.label
|
|
row.legal_name = provider.legal_name
|
|
row.tax_id = provider.tax_id
|
|
row.taxonomy_code = provider.taxonomy_code
|
|
row.address_line1 = provider.address_line1
|
|
row.address_line2 = provider.address_line2
|
|
row.city = provider.city
|
|
row.state = provider.state
|
|
row.zip = provider.zip
|
|
row.is_active = 1 if provider.is_active else 0
|
|
row.updated_at = now
|
|
s.commit()
|
|
return get_provider(provider.npi) # type: ignore[return-value]
|
|
|
|
def list_payers(*, is_active: bool | None = True) -> list[Payer]:
|
|
from cyclone.db import Payer as PayerORM
|
|
from cyclone.providers import Payer
|
|
with db.SessionLocal()() as s:
|
|
q = s.query(PayerORM)
|
|
if is_active is not None:
|
|
q = q.filter(PayerORM.is_active == (1 if is_active else 0))
|
|
rows = q.order_by(PayerORM.payer_id).all()
|
|
return [Payer.model_validate(_payer_orm_to_dict(r)) for r in rows]
|
|
|
|
def get_payer_config(payer_id: str, transaction_type: str) -> dict | None:
|
|
from cyclone.db import PayerConfigORM
|
|
with db.SessionLocal()() as s:
|
|
row = s.get(PayerConfigORM, (payer_id, transaction_type))
|
|
return dict(row.config_json) if row else None
|
|
|
|
def get_clearhouse() -> Clearhouse | None:
|
|
from cyclone.db import ClearhouseORM
|
|
from cyclone.providers import Clearhouse
|
|
with db.SessionLocal()() as s:
|
|
row = s.get(ClearhouseORM, 1)
|
|
if row is None:
|
|
return None
|
|
return Clearhouse.model_validate({
|
|
"id": 1,
|
|
"name": row.name,
|
|
"tpid": row.tpid,
|
|
"submitter_name": row.submitter_name,
|
|
"submitter_id_qual": row.submitter_id_qual,
|
|
"submitter_contact_name": row.submitter_contact_name,
|
|
"submitter_contact_email": row.submitter_contact_email,
|
|
"filename_block": dict(row.filename_block_json),
|
|
"sftp_block": dict(row.sftp_block_json),
|
|
"updated_at": row.updated_at,
|
|
})
|
|
|
|
def update_clearhouse(block: Clearhouse) -> Clearhouse:
|
|
"""Replace the singleton clearhouse row. SP25.
|
|
|
|
Used by ``PATCH /api/clearhouse`` to flip ``sftp_block.stub``
|
|
and adjust host/port/paths without touching SQLite directly.
|
|
Raises ``LookupError`` if the singleton row is missing — the
|
|
caller is expected to run the lifespan seed first.
|
|
"""
|
|
from cyclone.db import ClearhouseORM
|
|
with db.SessionLocal()() as s:
|
|
row = s.get(ClearhouseORM, 1)
|
|
if row is None:
|
|
raise LookupError(
|
|
"clearhouse singleton row missing; run the lifespan "
|
|
"seed (ensure_clearhouse_seeded) before PATCH /api/clearhouse"
|
|
)
|
|
row.name = block.name
|
|
row.tpid = block.tpid
|
|
row.submitter_name = block.submitter_name
|
|
row.submitter_id_qual = block.submitter_id_qual
|
|
row.submitter_contact_name = block.submitter_contact_name
|
|
row.submitter_contact_email = block.submitter_contact_email
|
|
row.filename_block_json = json.loads(
|
|
json.dumps(block.filename_block.model_dump())
|
|
)
|
|
row.sftp_block_json = json.loads(
|
|
json.dumps(block.sftp_block.model_dump())
|
|
)
|
|
row.updated_at = datetime.now(timezone.utc).isoformat()
|
|
s.commit()
|
|
return Clearhouse.model_validate({
|
|
"id": 1,
|
|
"name": row.name,
|
|
"tpid": row.tpid,
|
|
"submitter_name": row.submitter_name,
|
|
"submitter_id_qual": row.submitter_id_qual,
|
|
"submitter_contact_name": row.submitter_contact_name,
|
|
"submitter_contact_email": row.submitter_contact_email,
|
|
"filename_block": dict(row.filename_block_json),
|
|
"sftp_block": dict(row.sftp_block_json),
|
|
"updated_at": row.updated_at,
|
|
})
|
|
|
|
def ensure_clearhouse_seeded() -> None:
|
|
"""Insert the default clearhouse singleton + 3 providers + CO_TXIX payer
|
|
if they don't exist. Idempotent. Called from the API lifespan."""
|
|
from cyclone.db import ClearhouseORM, Payer as PayerORM, PayerConfigORM, Provider as ProviderORM
|
|
from cyclone.providers import Clearhouse
|
|
with db.SessionLocal()() as s:
|
|
if s.get(ClearhouseORM, 1) is None:
|
|
ch = Clearhouse(
|
|
id=1,
|
|
name="dzinesco",
|
|
tpid="11525703",
|
|
submitter_name="Dzinesco",
|
|
submitter_id_qual="46",
|
|
submitter_contact_name="Tyler Martinez",
|
|
submitter_contact_email="tyler@dzinesco.com",
|
|
filename_block={
|
|
"tz": "America/Denver",
|
|
"outbound_template": "tp{tpid}-{tx}-{ts_mt}-1of1.{ext}",
|
|
"inbound_template": "TP{tpid}-{orig_tx}_M{tracking}-{ts}-1of1_{file_type}.x12",
|
|
},
|
|
sftp_block={
|
|
"host": "mft.gainwelltechnologies.com",
|
|
"port": 22,
|
|
"username": "colorado-fts\\coxix_prod_11525703",
|
|
"paths": {
|
|
"outbound": "/CO XIX/PROD/coxix_prod_11525703/ToHPE",
|
|
"inbound": "/CO XIX/PROD/coxix_prod_11525703/FromHPE",
|
|
},
|
|
"stub": True,
|
|
"staging_dir": "./var/sftp/staging",
|
|
"poll_seconds": 300,
|
|
"auth": {"method": "keychain", "secret_ref": "sftp.gainwell.password"},
|
|
},
|
|
updated_at=utcnow(),
|
|
)
|
|
s.add(ClearhouseORM(
|
|
id=1,
|
|
name=ch.name,
|
|
tpid=ch.tpid,
|
|
submitter_name=ch.submitter_name,
|
|
submitter_id_qual=ch.submitter_id_qual,
|
|
submitter_contact_name=ch.submitter_contact_name,
|
|
submitter_contact_email=ch.submitter_contact_email,
|
|
filename_block_json=ch.filename_block.model_dump(),
|
|
sftp_block_json=ch.sftp_block.model_dump(),
|
|
updated_at=ch.updated_at.isoformat(),
|
|
))
|
|
|
|
# Seed 3 providers (idempotent)
|
|
from cyclone.providers import Provider
|
|
now = utcnow().isoformat()
|
|
for npi, label in [
|
|
("1881068062", "Montrose"),
|
|
("1851446637", "Delta"),
|
|
("1467507269", "Salida"),
|
|
]:
|
|
if s.get(ProviderORM, npi) is None:
|
|
s.add(ProviderORM(
|
|
npi=npi,
|
|
label=label,
|
|
legal_name="TOC, Inc.",
|
|
tax_id="721587149",
|
|
taxonomy_code="251E00000X",
|
|
address_line1="1100 East Main St",
|
|
address_line2="Suite A",
|
|
city="Montrose",
|
|
state="CO",
|
|
zip="814014063",
|
|
is_active=1,
|
|
created_at=now,
|
|
updated_at=now,
|
|
))
|
|
|
|
# Seed CO_TXIX payer (idempotent)
|
|
if s.get(PayerORM, "CO_TXIX") is None:
|
|
s.add(PayerORM(
|
|
payer_id="CO_TXIX",
|
|
name="Colorado Medical Assistance Program",
|
|
receiver_name="COLORADO MEDICAL ASSISTANCE PROGRAM",
|
|
receiver_id="COMEDASSISTPROG",
|
|
is_active=1,
|
|
created_at=now,
|
|
updated_at=now,
|
|
))
|
|
# 837P config block
|
|
s.add(PayerConfigORM(
|
|
payer_id="CO_TXIX",
|
|
transaction_type="837P",
|
|
config_json={
|
|
"submitter_name": "Dzinesco",
|
|
"submitter_contact_name": "Tyler Martinez",
|
|
"submitter_contact_email": "tyler@dzinesco.com",
|
|
"receiver_name": "COLORADO MEDICAL ASSISTANCE PROGRAM",
|
|
"receiver_id_qualifier": "46",
|
|
"receiver_id": "COMEDASSISTPROG",
|
|
"bht06_allowed": ["CH", "RP"],
|
|
"bht06_default": "CH",
|
|
"sbr09_default": "MC",
|
|
"sbr09_allowed": ["MC", "16", "MA", "MB", "ZZ"],
|
|
"payer_id_qualifier": "PI",
|
|
"payer_id": "CO_TXIX",
|
|
"pwk_supported": False,
|
|
"cas_2320_group_allowed": False,
|
|
"claim_type_codes": {"11": "Office", "12": "Home", "99": "Other"},
|
|
},
|
|
updated_at=now,
|
|
))
|
|
# 835 config block
|
|
s.add(PayerConfigORM(
|
|
payer_id="CO_TXIX",
|
|
transaction_type="835",
|
|
config_json={
|
|
"expected_payer_tax_ids": [
|
|
"81-1725341", "811725341", "84-0644739",
|
|
"840644739", "1811725341",
|
|
],
|
|
"expected_payer_health_plan_id": "7912900843",
|
|
"payer_name_pattern": "^CO_(TXIX|BHA)$",
|
|
},
|
|
updated_at=now,
|
|
))
|
|
|
|
s.commit()
|
|
|
|
|
|
|
|
|
|
def _provider_orm_to_dict(row) -> dict:
|
|
return {
|
|
"npi": row.npi,
|
|
"label": row.label,
|
|
"legal_name": row.legal_name,
|
|
"tax_id": row.tax_id,
|
|
"taxonomy_code": row.taxonomy_code,
|
|
"address_line1": row.address_line1,
|
|
"address_line2": row.address_line2,
|
|
"city": row.city,
|
|
"state": row.state,
|
|
"zip": row.zip,
|
|
"is_active": bool(row.is_active),
|
|
"created_at": row.created_at,
|
|
"updated_at": row.updated_at,
|
|
}
|
|
|
|
|
|
def _payer_orm_to_dict(row) -> dict:
|
|
return {
|
|
"payer_id": row.payer_id,
|
|
"name": row.name,
|
|
"receiver_name": row.receiver_name,
|
|
"receiver_id": row.receiver_id,
|
|
"is_active": bool(row.is_active),
|
|
"created_at": row.created_at,
|
|
"updated_at": row.updated_at,
|
|
}
|
|
|