fix(store): spec compliance fixes per review

This commit is contained in:
Tyler
2026-06-19 19:13:52 -06:00
parent 0fa80e29d5
commit d0411b8186
3 changed files with 289 additions and 62 deletions
+52 -25
View File
@@ -16,6 +16,7 @@ from pydantic import BaseModel, ConfigDict, Field
from cyclone.parsers.models import ClaimOutput, ParseResult
from cyclone.parsers.models_835 import ClaimPayment, ParseResult835
from cyclone.parsers.payer import PayerConfig835
BatchKind = Literal["837p", "835"]
@@ -29,7 +30,7 @@ class BatchRecord(BaseModel):
id: str
kind: BatchKind
input_filename: str
parsed_at: str # ISO 8601 UTC, e.g. "2026-06-19T12:00:00Z"
parsed_at: datetime # tz-aware UTC
result: ParseResult | ParseResult835 = Field(discriminator=None)
@@ -121,6 +122,7 @@ class InMemoryStore:
for cp in result.claims:
all_remits.append(to_ui_remittance(
cp, batch_id=b.id, parsed_at=b.parsed_at,
payer_name=result.payer.name,
))
if payer is not None:
all_remits = [r for r in all_remits if r.get("payerName") == payer]
@@ -187,9 +189,9 @@ class InMemoryStore:
store = InMemoryStore()
def utcnow_iso() -> str:
"""ISO 8601 UTC timestamp, second precision, suffixed with 'Z'."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def utcnow() -> datetime:
"""tz-aware UTC `datetime` (replaces the old `utcnow_iso` string helper)."""
return datetime.now(timezone.utc)
# ---------------------------------------------------------------------------
@@ -203,26 +205,29 @@ def to_ui_claim(
claim: ClaimOutput,
*,
batch_id: str,
parsed_at: str,
parsed_at: datetime,
) -> dict:
"""Map a 837P ClaimOutput to the UI's `Claim` shape.
Status rules (per spec section 6.2, evaluated in priority order):
- !passed → denied
- passed and any warningspending
- passed and frequency_code == "1" → submitted
- otherwisedraft
Status rules (per spec section 6.2, first-match-wins):
- !passed and any R050_diagnosis_present error → draft
- !passed denied
- passed and frequency_code == "1" → submitted
- passed and any warnings pending
- otherwise → draft
"""
v = claim.validation
if not v.passed:
status = "denied"
elif v.warnings:
status = "pending"
has_r050 = any(e.rule == "R050_diagnosis_present" for e in v.errors)
status = "draft" if has_r050 else "denied"
elif claim.claim.frequency_code == "1":
status = "submitted"
elif v.warnings:
status = "pending"
else:
status = "draft"
parsed_iso = parsed_at.isoformat().replace("+00:00", "Z")
return {
"id": claim.claim_id,
"patientName": f"{claim.subscriber.first_name} {claim.subscriber.last_name}".strip(),
@@ -237,9 +242,9 @@ def to_ui_claim(
"receivedAmount": 0.0,
"status": status,
"denialReason": None,
"submissionDate": parsed_at,
"submissionDate": parsed_iso,
"batchId": batch_id,
"parsedAt": parsed_at,
"parsedAt": parsed_iso,
}
@@ -247,13 +252,17 @@ def to_ui_remittance(
cp: ClaimPayment,
*,
batch_id: str,
parsed_at: str,
parsed_at: datetime,
payer_config: PayerConfig835 | None = None,
payer_name: str = "",
) -> dict:
"""Map an 835 ClaimPayment to the UI's `Remittance` shape.
Status mapping (per spec section 6.3):
- status_code 21/22 → reconciled (reversal/correction)
- otherwise → received
Status mapping (per spec section 6.2):
- status_code 21/22 → reconciled (reversal/correction)
- status_code 4 → received + denialReason from CAS
- status_code outside allowlist → received + validationWarning
- otherwise → received
"""
code = cp.status_code
if code in {"21", "22"}:
@@ -261,17 +270,35 @@ def to_ui_remittance(
else:
status = "received"
denial_reason: str | None = None
if code == "4" and cp.service_payments:
sp = cp.service_payments[0]
if sp.adjustments:
adj = sp.adjustments[0]
denial_reason = (
f"{adj.group_code}-{adj.reason_code}: ${float(adj.amount):.2f}"
)
cfg = payer_config if payer_config is not None else PayerConfig835.generic_835()
validation_warnings: list[str] = []
if code not in cfg.allowed_status_codes:
validation_warnings.append(
f"CLP02 code {code} not in payer allowlist"
)
parsed_iso = parsed_at.isoformat().replace("+00:00", "Z")
return {
"id": cp.payer_claim_control_number,
"claimId": cp.original_claim_id or "",
"payerName": "",
"payerName": payer_name,
"paidAmount": float(cp.total_paid or 0.0),
"adjustmentAmount": 0.0,
"status": status,
"denialReason": None,
"receivedDate": parsed_at,
"denialReason": denial_reason,
"validationWarnings": validation_warnings,
"receivedDate": parsed_iso,
"batchId": batch_id,
"parsedAt": parsed_at,
"parsedAt": parsed_iso,
}
@@ -307,7 +334,7 @@ def to_activity_event(
id: str,
kind: str,
message: str,
timestamp: str,
timestamp: datetime,
npi: str | None = None,
amount: float | None = None,
) -> dict:
@@ -315,7 +342,7 @@ def to_activity_event(
"id": id,
"kind": kind,
"message": message,
"timestamp": timestamp,
"timestamp": timestamp.isoformat().replace("+00:00", "Z"),
"npi": npi,
"amount": amount,
}