"""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_lowercase_tp_prefix_999(): # Gainwell's production filer uses lowercase `tp` for inbound 999/TA1. # The inbound regex must accept both casings on the TP prefix. 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.file_type == "999" assert parsed.ext == "x12" def test_parse_inbound_lowercase_tp_prefix_ta1(): name = "tp11525703-837P_M019044969-20260520180505477-1of1_TA1.x12" parsed = parse_inbound_filename(name) assert parsed.file_type == "TA1" def test_is_inbound_filename_accepts_both_cases(): # is_inbound_filename() is the fast path used by the scheduler to # filter the listing. It must accept both casings. upper = "TP11525703-837P_M019048402-20260520231513488-1of1_999.x12" lower = "tp11525703-837P_M019048402-20260520231513488-1of1_999.x12" assert is_inbound_filename(upper) assert is_inbound_filename(lower) def test_parse_inbound_rejects_mixed_case_tracking(): # Tracking value must stay uppercase alnum; the case-insensitive # flag is intentionally scoped to the TP prefix by the file_type # and timestamp constraints, so a mixed-case tracking should still # be rejected (it'd be invalid HCPF). # We exercise the obvious "totally lowercase" rejection to confirm # the rest of the pattern is still strict. with pytest.raises(ValueError, match="Not a valid HCPF inbound"): # Lowercase orig_tx; the orig_tx class is [A-Z0-9]+ so it # must be uppercase. parse_inbound_filename( "tp11525703-837p_M019048402-20260520231513488-1of1_999.x12" ) 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 now accepted too — Gainwell's filer has # used both casings on inbound 999/TA1 files. assert is_inbound_filename("tp11525703-837P_M019048402-20260520231513488-1of1_999.x12") # Outbound shape is still rejected (no tracking/ts/file_type). 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