1942a22629
Adds pure local validators for the 10-digit NPI Luhn checksum (CMS- published algorithm with the '80840' NPPES prefix) and 9-digit EIN format (rejects reserved prefixes 00/07/80-89). No NPPES round-trip, no IRS e-file lookup — catches the 99% typo case at parse time. Surface: - cyclone.npi.is_valid_npi / is_valid_tax_id / normalize_tax_id - CLI: 'cyclone validate-npi <npi>' and 'cyclone validate-tax-id <ein>' - API: GET /api/admin/validate-provider?npi=&tax_id= - Parser validator: new R021_npi_checksum rule (warning, not error, to keep test fixtures with placeholder NPIs ingestible) - minimal_837p.txt fixture NPI updated from '1234567890' to the Luhn-valid '1993999998' so strict-mode CLI parses still pass Tests: - test_npi.py — 27 cases (Luhn math, valid/invalid NPIs, EIN cases, normalize_tax_id edge cases) - test_api_validate_provider.py — 4 cases (both valid, both invalid, omitted NPI, omitted tax_id) - test_cli_validate.py — 8 cases (valid/invalid for both subcommands, exit codes, malformed inputs) - test_validator.py — 4 new R021 cases (valid Luhn silent, bad Luhn warning, skipped when format bad, skipped when NPI missing) Total: 923 tests pass.
71 lines
2.5 KiB
Python
71 lines
2.5 KiB
Python
from datetime import date
|
|
from pathlib import Path
|
|
|
|
from cyclone.parsers.parse_837 import parse
|
|
from cyclone.parsers.payer import PayerConfig
|
|
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "minimal_837p.txt"
|
|
|
|
|
|
def test_parse_minimal_fixture_returns_one_claim():
|
|
text = FIXTURE.read_text()
|
|
result = parse(text, PayerConfig.co_medicaid())
|
|
assert len(result.claims) == 1
|
|
claim = result.claims[0]
|
|
assert claim.claim_id == "CLM001"
|
|
assert claim.billing_provider.npi == "1993999998"
|
|
assert claim.subscriber.last_name == "Doe"
|
|
assert claim.subscriber.first_name == "John"
|
|
assert claim.subscriber.member_id == "ABC123"
|
|
assert claim.payer.id == "SKCO0"
|
|
assert claim.claim.frequency_code == "1"
|
|
assert claim.claim.place_of_service == "12"
|
|
assert claim.claim.prior_auth == "PA123"
|
|
assert claim.diagnoses[0].code == "Z00"
|
|
assert claim.diagnoses[0].qualifier == "ABK"
|
|
assert len(claim.service_lines) == 1
|
|
sl = claim.service_lines[0]
|
|
assert sl.procedure.code == "99213"
|
|
assert sl.procedure.qualifier == "HC"
|
|
assert sl.charge == 100.00
|
|
|
|
|
|
def test_parse_envelope_captured():
|
|
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
|
|
assert result.envelope is not None
|
|
assert result.envelope.control_number == "991102977"
|
|
assert result.envelope.transaction_date == date(2026, 6, 11)
|
|
assert result.envelope.sender_id == "11525703"
|
|
|
|
|
|
def test_parse_summary_counts():
|
|
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
|
|
assert result.summary.total_claims == 1
|
|
assert result.summary.passed == 1
|
|
assert result.summary.failed == 0
|
|
|
|
|
|
def test_parse_continues_on_malformed_subscriber():
|
|
"""A bad subscriber record must not abort the whole batch."""
|
|
text = FIXTURE.read_text().replace("MI*ABC123", "MI*") # truncate member id segment
|
|
result = parse(text, PayerConfig.co_medicaid())
|
|
# Should still find the claim, but with errors
|
|
assert len(result.claims) == 1
|
|
assert result.claims[0].validation.passed is False
|
|
assert result.summary.failed == 1
|
|
|
|
|
|
def test_parse_preserves_raw_segments():
|
|
result = parse(FIXTURE.read_text(), PayerConfig.co_medicaid())
|
|
claim = result.claims[0]
|
|
assert any(s[0] == "CLM" for s in claim.raw_segments)
|
|
assert any(s[0] == "SV1" for s in claim.raw_segments)
|
|
|
|
|
|
def test_parse_uses_generic_config_when_requested():
|
|
result = parse(FIXTURE.read_text(), PayerConfig.generic_837p())
|
|
assert len(result.claims) == 1
|
|
# No patient-loop rule on generic config
|
|
assert result.claims[0].validation.passed is True
|