Files
cyclone/backend/tests/test_filenames.py
Tyler 9bca4b608a feat(release): v0.2.0 — batch 837 export, ClaimCard, theme tokens
Backend:

- New POST /api/batches/{id}/export-837: regenerate X12 837 files
  for a list of claim_ids into a ZIP using HCPF file naming standards,
  with a unique interchange/group control number per export. Wire
  the clearhouse Loop 1000A (NM1*41 + PER) and per-payer receiver
  (NM1*40) blocks so the serializer no longer falls back to
  CYCLONE / RECEIVER placeholders.
- /api/parse-837 and /api/parse-835 now surface the server-side
  batch_id in both JSON and NDJSON response shapes so the frontend
  can hit batch-scoped endpoints without an extra listBatches
  round-trip.
- Filename helpers and the 837 serializer updated to match the new
  HCPF envelope; tests cover batch export, parse batch_id, and the
  serializer's control-number uniqueness guarantee.

Frontend:
- New shared components: ClaimCard, ClaimCard837, DominantKpiCard,
  EditorialNote, ExportBar, TickerTape, and a charts/ set
  (BarChart, HBarChart, SegmentedBar, AgingBars).
- New useBatchExport hook driving ExportBar's download flow against
  the new endpoint.
- ClaimDrawer, Lane, and Layout migrated from raw CSS-variable
  colors to Tailwind theme tokens (bg-card, text-foreground,
  border/60, etc.) for consistency with the rest of the instrument
  chrome; the active tab indicator gains a subtle accent glow.
- Upload, Inbox, Batches, BatchDiff, Reconciliation, and Acks pages
  reworked to compose the new shared components and consume the new
  batch-scoped API surface (notably ExportBar wired into Batches).

Tooling / Docs:
- Add audit-uiux.mjs and a docs/goodclaim.x12 sample fixture.
- Update ClaimDrawer testids and add coverage for the new
  components and the useBatchExport hook.

Rolls up into the v0.2.0 release tag.
2026-06-22 11:01:58 -06:00

170 lines
6.0 KiB
Python

"""SP9 — HCPF X12 File Naming Standards helper tests."""
from __future__ import annotations
from datetime import datetime
from zoneinfo import ZoneInfo
import pytest
from cyclone.edi.filenames import (
ALLOWED_FILE_TYPES,
OUTBOUND_RE,
INBOUND_RE,
build_outbound_filename,
is_inbound_filename,
is_outbound_filename,
parse_inbound_filename,
)
from cyclone.providers import InboundFilename
MT = ZoneInfo("America/Denver")
# ----- build_outbound_filename --------------------------------------------
def test_build_outbound_with_explicit_mt():
now = datetime(2026, 6, 20, 13, 22, 43, 505_000, tzinfo=MT)
name = build_outbound_filename("11525703", "837P", now_mt=now)
# HCPF outbound format: tp prefix on the tpid
assert name == "tp11525703-837P-20260620132243505-1of1.x12"
def test_build_outbound_default_extension():
now = datetime(2026, 6, 20, 13, 22, 43, 505_000, tzinfo=MT)
name = build_outbound_filename("11525703", "837P", now_mt=now)
assert name.endswith(".x12")
def test_build_outbound_custom_extension():
now = datetime(2026, 6, 20, 13, 22, 43, 505_000, tzinfo=MT)
name = build_outbound_filename("11525703", "837P", ext="txt", now_mt=now)
assert name == "tp11525703-837P-20260620132243505-1of1.txt"
def test_build_outbound_uses_mt_when_no_arg():
# Snapshot test — the timestamp will be very recent; check format only
name = build_outbound_filename("11525703", "837P")
assert OUTBOUND_RE.match(name), name
# tp11525703-837P-YYYYMMDDhhmmssSSS-1of1.x12 — 4 dash-separated parts
parts = name.split("-")
assert len(parts) == 4
assert parts[0] == "tp11525703"
assert len(parts[2]) == 17 # yyyymmddhhmmssSSS
def test_build_outbound_rejects_non_numeric_tpid():
now = datetime(2026, 6, 20, tzinfo=MT)
with pytest.raises(ValueError, match="tpid must be digits"):
build_outbound_filename("abc123", "837P", now_mt=now)
def test_build_outbound_rejects_invalid_tx():
now = datetime(2026, 6, 20, tzinfo=MT)
with pytest.raises(ValueError, match="tx must be uppercase alnum"):
build_outbound_filename("11525703", "837-lower", now_mt=now)
def test_build_outbound_rejects_naive_dt():
with pytest.raises(ValueError, match="timezone-aware"):
build_outbound_filename("11525703", "837P", now_mt=datetime(2026, 6, 20))
def test_build_outbound_converts_other_tz_to_mt():
# 2026-06-20 19:22:43 UTC = 2026-06-20 13:22:43 MT (during DST)
from datetime import timezone
now_utc = datetime(2026, 6, 20, 19, 22, 43, 505_000, tzinfo=timezone.utc)
name = build_outbound_filename("11525703", "837P", now_mt=now_utc)
assert "20260620132243505" in name
# ----- parse_inbound_filename ---------------------------------------------
def test_parse_inbound_999_real_prodfile():
# Real production 999 filename from docs/prodfiles/FromHPE/
name = "TP11525703-837P_M019048402-20260520231513488-1of1_999.x12"
parsed = parse_inbound_filename(name)
assert parsed.tpid == "11525703"
assert parsed.orig_tx == "837P"
assert parsed.tracking == "M019048402"
assert parsed.ts == "20260520231513488"
assert parsed.file_type == "999"
assert parsed.ext == "x12"
def test_parse_inbound_ta1_real_prodfile():
name = "TP11525703-837P_M019044969-20260520180505477-1of1_TA1.x12"
parsed = parse_inbound_filename(name)
assert parsed.file_type == "TA1"
assert parsed.tracking == "M019044969"
def test_parse_inbound_277():
name = "TP11525703-837P_M019110219-20260601003507042-1of1_277.x12"
parsed = parse_inbound_filename(name)
assert parsed.file_type == "277"
def test_parse_inbound_rejects_missing_tp_prefix():
with pytest.raises(ValueError, match="Not a valid HCPF inbound"):
parse_inbound_filename("11525703-837P_M019048402-20260520231513488-1of1_999.x12")
def test_parse_inbound_rejects_wrong_segment_count():
with pytest.raises(ValueError):
parse_inbound_filename("TP11525703_837P_M019048402-1of1_999.x12")
def test_parse_inbound_rejects_unknown_file_type():
with pytest.raises(ValueError, match="not in allowed HCPF set"):
parse_inbound_filename("TP11525703-837P_M019048402-20260520231513488-1of1_XXX.x12")
def test_parse_inbound_rejects_non_x12_ext():
with pytest.raises(ValueError):
parse_inbound_filename("TP11525703-837P_M019048402-20260520231513488-1of1_999.txt")
# ----- round-trip ---------------------------------------------------------
def test_roundtrip_outbound_to_inbound():
# Outbound uses tp{...}, inbound uses TP{...} (case differs but both
# prefixes are required). The two regexes use different shapes —
# round-trip via tpid only.
now = datetime(2026, 6, 20, 13, 22, 43, 505_000, tzinfo=MT)
out = build_outbound_filename("11525703", "837P", now_mt=now)
assert OUTBOUND_RE.match(out)
assert "11525703" in out
assert "837P" in out
assert out.endswith("1of1.x12")
# ----- validators ---------------------------------------------------------
def test_is_outbound_filename():
# HCPF outbound always has the lowercase "tp" prefix
assert is_outbound_filename("tp11525703-837P-20260620132243505-1of1.x12")
# Bare tpid (no tp prefix) is no longer a valid outbound filename
assert not is_outbound_filename("11525703-837P-20260620132243505-1of1.x12")
# Uppercase TP prefix is the inbound shape, not outbound
assert not is_outbound_filename("TP11525703-837P-20260620132243505-1of1.x12")
assert not is_outbound_filename("not-a-filename")
def test_is_inbound_filename():
assert is_inbound_filename("TP11525703-837P_M019048402-20260520231513488-1of1_999.x12")
# Lowercase tp prefix is the outbound shape, not inbound
assert not is_inbound_filename("tp11525703-837P_M019048402-20260520231513488-1of1_999.x12")
assert not is_inbound_filename("11525703-837P-20260620132243505-1of1.x12")
def test_allowed_file_types_includes_277ca():
assert "277CA" in ALLOWED_FILE_TYPES
assert "TA1" in ALLOWED_FILE_TYPES
assert "999" in ALLOWED_FILE_TYPES
assert "835" in ALLOWED_FILE_TYPES