From 6e1f5428104eff0fabdfb15b87d7b6ac232b64c2 Mon Sep 17 00:00:00 2001 From: Russell Osborne Date: Fri, 27 Sep 2024 10:35:41 -0400 Subject: [PATCH] Adds LX and Ref for transactions This resolves all stdout warnings of Identifier: LX not handled in transaction loop. Identifier: REF not handled in transaction loop. While these fields don't seem super useful no warnings is quite nice. This also adds a nevada medicaid sample found here https://www.medicaid.nv.gov/Downloads/provider/Sample_835_File.pdf [AT-5936] Signed-off-by: Russell Osborne --- .vscode/settings.json | 7 +- .../elements/reference_qualifier.py | 9 +-- edi_835_parser/loops/transaction.py | 73 ++++++++++++------- edi_835_parser/segments/header_number.py | 27 +++++++ pyproject.toml | 2 + tests/conftest.py | 15 ++-- tests/loops/test_claim.py | 10 +++ tests/loops/test_transaction.py | 31 ++++++++ tests/test_edi_835_files/nevada_medicaid.txt | 1 + tests/test_edi_835_parser.py | 57 ++------------- tests/utils/__init__.py | 44 +++++++++++ 11 files changed, 180 insertions(+), 96 deletions(-) create mode 100644 edi_835_parser/segments/header_number.py create mode 100644 tests/loops/test_claim.py create mode 100644 tests/loops/test_transaction.py create mode 100644 tests/test_edi_835_files/nevada_medicaid.txt create mode 100644 tests/utils/__init__.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 4911468..97b6901 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { - "python.testing.pytestArgs": [ - "tests" - ], + "python.testing.pytestArgs": ["tests"], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "python.analysis.extraPaths": ["./tests"] } diff --git a/edi_835_parser/elements/reference_qualifier.py b/edi_835_parser/elements/reference_qualifier.py index 121e962..f780354 100644 --- a/edi_835_parser/elements/reference_qualifier.py +++ b/edi_835_parser/elements/reference_qualifier.py @@ -20,16 +20,11 @@ 'G2': 'Provider Commerical Number', 'HPI': 'Centers for Medicare and Medicaid Services National Provider Identifier', 'SY': 'Social Security Number', + # https://www.cms.gov/medicare/billing/electronicbillingeditrans/downloads/835-flatfile.pdf + 'EV': 'Reference Identification Number', } -# class ReferenceQualifier(Element): -# -# def parser(self, value: str) -> Code: -# description = reference_qualifiers.get(value, None) -# return Code(value, description) - - class ReferenceQualifier(Element): def parser(self, value: str) -> str: return reference_qualifiers.get(value, value) diff --git a/edi_835_parser/loops/transaction.py b/edi_835_parser/loops/transaction.py index ea5a632..9e8c66b 100644 --- a/edi_835_parser/loops/transaction.py +++ b/edi_835_parser/loops/transaction.py @@ -1,8 +1,10 @@ +import datetime from typing import Iterator, Tuple, Optional, List from edi_835_parser.loops.claim import Claim as ClaimLoop from edi_835_parser.loops.organization import Organization as OrganizationLoop +from edi_835_parser.segments.header_number import HeaderNumber from edi_835_parser.segments.utilities import find_identifier from edi_835_parser.segments.date import Date as DateSegment from edi_835_parser.segments.transaction import Transaction as TransactionSegment @@ -33,24 +35,25 @@ class Transaction: ] def __init__( - self, - transaction: TransactionSegment = None, - financial_information: FinancialInformationSegment = None, - trace_number: TraceNumberSegment = None, - provider_adjustments: List[ProviderAdjustmentSegment] = None, - provider_summary: ProviderSummarySegment = None, - date: DateSegment = None, - claims: List[ClaimLoop] = None, - organizations: List[OrganizationLoop] = None + self, + transaction: TransactionSegment = None, + financial_information: FinancialInformationSegment = None, + trace_number: TraceNumberSegment = None, + provider_adjustments: List[ProviderAdjustmentSegment] = None, + provider_summary: ProviderSummarySegment = None, + date: DateSegment = None, + claims: List[ClaimLoop] = None, + organizations: List[OrganizationLoop] = None, ): - self.transaction = transaction - self.financial_information = financial_information - self.trace_number = trace_number - self.provider_adjustments = provider_adjustments if provider_adjustments else [] - self.provider_summary = provider_summary - self.date = date - self.claims = claims if claims else [] - self.organizations = organizations if organizations else [] + self.transaction = transaction + self.financial_information = financial_information + self.trace_number = trace_number + self.provider_adjustments = provider_adjustments if provider_adjustments else [] + self.provider_summary = provider_summary + self.date = date + self.claims = claims if claims else [] + self.organizations = organizations if organizations else [] + self.references = [] def __repr__(self): return '\n'.join(str(item) for item in self.__dict__.items()) @@ -123,6 +126,16 @@ def other_payee_identification(self) -> Optional[ReferenceSegment]: if ref.qualifier == 'payee identification': return ref + @property + def reference_identification_number(self) -> Optional[ReferenceSegment]: + for ref in self.references: + if ref.qualifier == 'Reference Identification Number': + return ref + + @property + def production_date(self) -> datetime.datetime: + return self.date.date + @classmethod def build( cls, segment: str, segments: Iterator[str] @@ -156,9 +169,9 @@ def build( transaction.trace_number = trace_number segment = None elif identifier == ProviderAdjustmentSegment.identification: - provider_adjustment = ProviderAdjustmentSegment(segment) - transaction.provider_adjustments.append(provider_adjustment) - segment = None + provider_adjustment = ProviderAdjustmentSegment(segment) + transaction.provider_adjustments.append(provider_adjustment) + segment = None elif identifier == ProviderAdjustmentSegment.identification: provider_adjustment = ProviderAdjustmentSegment(segment) @@ -170,18 +183,28 @@ def build( transaction.provider_summary = provider_summary segment = None + elif identifier == HeaderNumber.identification: + header_number = HeaderNumber(segment) + transaction.header_number = header_number + segment = None + + elif identifier == ReferenceSegment.identification: + reference = ReferenceSegment(segment) + transaction.references.append(reference) + segment = None + elif identifier == DateSegment.identification: - date = DateSegment(segment) - transaction.date = date - segment = None + date = DateSegment(segment) + transaction.date = date + segment = None elif identifier in cls.terminating_identifiers: return transaction, segments, segment else: - segment = None - message = f'Identifier: {identifier} not handled in transaction loop.' + message = f'Identifier: {identifier} not handled in transaction loop. {segment}' Logger.logr.warning(message) + segment = None except StopIteration: return transaction, None, None diff --git a/edi_835_parser/segments/header_number.py b/edi_835_parser/segments/header_number.py new file mode 100644 index 0000000..c5d9365 --- /dev/null +++ b/edi_835_parser/segments/header_number.py @@ -0,0 +1,27 @@ +from edi_835_parser.elements.identifier import Identifier +from edi_835_parser.elements.integer import Integer +from edi_835_parser.segments.utilities import split_segment + + +class HeaderNumber: + identification = 'LX' + + identifier = Identifier() + number = Integer() + + def __init__(self, segment: str): + self.index = segment.split(':', 1)[0] + segment = segment.split(':', 1)[1] + + self.segment = segment + segment = split_segment(segment) + + self.identifier = segment[0] + self.number = segment[1] + + def __repr__(self): + return '\n'.join(str(item) for item in self.__dict__.items()) + + +if __name__ == '__main__': + pass diff --git a/pyproject.toml b/pyproject.toml index ff811d1..16763cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ python = "^3.9" pandas = "^2.0.3" pytest = "^8.1.1" +[tool.poetry.dev-dependencies] +ruff = "*" [build-system] requires = ["poetry-core"] diff --git a/tests/conftest.py b/tests/conftest.py index 7dc7d33..47a153f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import pytest sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +sys.path.append(os.path.join(os.path.dirname(__file__), 'utils')) import edi_835_parser # noqa: E402 @@ -34,13 +35,7 @@ def sample_935_with_interests(): return edi_835_parser.parse(path) -# @pytest.fixture -# def sample2_835(): -# path = current_path + '/test_edi_835_files/sample2_835.txt' -# return edi_835_parser.parse(path) -# -# -# @pytest.fixture -# def sample3_835(): -# path = current_path + '/test_edi_835_files/sample3_835.txt' -# return edi_835_parser.parse(path) +@pytest.fixture +def nevada_medicaid_sample(): + path = current_path + '/test_edi_835_files/nevada_medicaid.txt' + return edi_835_parser.parse(path) diff --git a/tests/loops/test_claim.py b/tests/loops/test_claim.py new file mode 100644 index 0000000..1c3d771 --- /dev/null +++ b/tests/loops/test_claim.py @@ -0,0 +1,10 @@ +from utils import count_claims, count_transactions, count_services, get_claim_by_control_number + + +def test_medicaid_sample(nevada_medicaid_sample): + assert count_claims(nevada_medicaid_sample) == 3 + assert count_transactions(nevada_medicaid_sample) == 3 + assert count_services(nevada_medicaid_sample) == 0 + + claim_loop = get_claim_by_control_number(nevada_medicaid_sample, '77777777') + assert claim_loop.claim.charge_amount == '72232' diff --git a/tests/loops/test_transaction.py b/tests/loops/test_transaction.py new file mode 100644 index 0000000..05339d8 --- /dev/null +++ b/tests/loops/test_transaction.py @@ -0,0 +1,31 @@ +import datetime +from edi_835_parser.segments.address import Address +from edi_835_parser.segments.location import Location + + +def test_transaction_properties(sample_835): + transaction = sample_835.transactions[0] + assert transaction.transaction.transaction_set_identifier_code == '835' + assert transaction.transaction.transaction_set_control_no == '1002' + + +def test_transaction_address(sample_835): + transaction = sample_835.transactions[0] + assert isinstance(transaction.payer_address, Address) + assert transaction.payer_address.address_line1 == '400 BROADWAY' + assert isinstance(transaction.payer_location, Location) + + +def test_transaction_production_date(sample_835): + transaction = sample_835.transactions[0] + assert transaction.production_date == datetime.datetime(2022, 1, 5, 0, 0) + + +def test_transaction_reference_identification_number(sample_835): + transaction = sample_835.transactions[0] + assert transaction.reference_identification_number.value == 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + + +def test_transaction_header_number(sample_835): + transaction = sample_835.transactions[0] + assert transaction.header_number.number == 1 diff --git a/tests/test_edi_835_files/nevada_medicaid.txt b/tests/test_edi_835_files/nevada_medicaid.txt new file mode 100644 index 0000000..e45ac00 --- /dev/null +++ b/tests/test_edi_835_files/nevada_medicaid.txt @@ -0,0 +1 @@ +ISA*00* *00* *ZZ*NVMED *ZZ*99999999*180613*1230*^*00501*100000300*0*P*:~GS*HP*NVMED*99999999*20180613*123021*100000300*X*005010X221A1~ST*835*0001~BPR*H*0*C*NON************20180615~TRN*1*100004762*1388600002~DTM*405*20180613~N1*PR*DIVISON OF HEALTH CARE FINANCING AND POLICY~N3*1100 East William Street Suite 101~N4*Carson*NV*89701~PER*BL*Nevada Medicaid*TE*8776383472*EM*nvmmis.edisupport@dxc.com~N1*PE*SUMMER*XX*6666666666~REF*TJ*111111111~LX*1~CLP*77777777*4*72232*0**MC*6666666666666~CAS*OA*147*50016*0~CAS*CO*26*22216*0~NM1*QC*1*TOM*SMITH****MR*77777777777~NM1*74*1*ALAN*PARKER****C*88888888888~NM1*PR*2*PACIFI*****PI* 9999~NM1*GB*1*BARRY*CARRY****MI*666666666~REF*EA*8888888~DTM*232*20180314~DTM*233*20180317~SE*22*0001~ST*835*0002~BPR*H*0*C*NON************20180615~TRN*1*100004765*5555555555~DTM*405*20180613~N1*PR*DIVISON OF HEALTH CARE FINANCING AND POLICY~N3*1100 East William Street Suite 101~N4*Carson*NV*89701~PER*BL*Nevada Medicaid*TE*8776383472*EM*nvmmis.edisupport@dxc.com~N1*PE*VALLEY*XX*6666666666~REF*TJ*530824679~LX*1~CLP*77777778*2*3002*0**MC*6666666666667~CAS*OA*176*3002*0~NM1*QC*1*BOB*THOMAS****MR*55555555555~NM1*74*1*ALAN*JACKSON****C*66666666666~REF*EA*8888888~DTM*232*20171001~DTM*233*20171002~CLP*77777779*4*41231.04*0**MC*6666666666668~CAS*OA*147*9365.04*0~CAS*CO*26*31866*0~NM1*QC*1*HELD*ALLEN****MR*77777777778~NM1*74*1*RYAN*LARRY****C*88888888889~NM1*PR*2*SENIOR*****PI* 8888~NM1*GB*1*MARY*JANE****MI*777777777~REF*EA*6047740~DTM*232*20180220~DTM*233*20180221~SE*29*0002~ST*835*0003~BPR*I*1812.27*C*CHK************20180727~TRN*1*000012382*5555555555~DTM*405*20180720~N1*PR*DIVISON OF HEALTH CARE FINANCING AND POLICY~N3*1100 East William Street Suite 101~N4*Carson*NV*89701~PER*BL*Nevada Medicaid*TE*8776383472*EM*nvmmis.edisupport@dxc.com~N1*PE*SILVER*XX*7777777777~REF*TJ*666666666~PLB*8888888888*20181231*CT:888888888*-1092.46*CT:888888888*-719.81*CS:8888888888887*-181.55*CS:8888888888887*181.55*CS:8888888888888*-130*CS:8888888888888*130~SE*12*0003~GE*3*100000300~IEA*1*100000301~ diff --git a/tests/test_edi_835_parser.py b/tests/test_edi_835_parser.py index aedef79..551fffc 100644 --- a/tests/test_edi_835_parser.py +++ b/tests/test_edi_835_parser.py @@ -1,30 +1,21 @@ from decimal import Decimal -def test_claim_count( - blue_cross_nc_sample, - emedny_sample, - sample_835 -): +from utils import count_claims, count_transactions, count_services, sum_interests + + +def test_claim_count(blue_cross_nc_sample, emedny_sample, sample_835): assert count_claims(blue_cross_nc_sample) == 1 assert count_claims(emedny_sample) == 3 assert count_claims(sample_835) == 6 -def test_transaction_count( - blue_cross_nc_sample, - emedny_sample, - sample_835 -): +def test_transaction_count(blue_cross_nc_sample, emedny_sample, sample_835): assert count_transactions(blue_cross_nc_sample) == 1 assert count_transactions(emedny_sample) == 1 assert count_transactions(sample_835) == 2 -def test_service_count( - blue_cross_nc_sample, - emedny_sample, - sample_835 -): +def test_service_count(blue_cross_nc_sample, emedny_sample, sample_835): assert count_services(blue_cross_nc_sample) == 3 assert count_services(emedny_sample) == 10 assert count_services(sample_835) == 12 @@ -51,39 +42,5 @@ def test_build_remit_service_lines(blue_cross_nc_sample, emedny_sample, sample_8 assert sample_835.build_remit_service_lines().shape[0] == 12 -def test_total_interests( - sample_935_with_interests -): +def test_total_interests(sample_935_with_interests): assert sum_interests(sample_935_with_interests) == round(Decimal(10.3), 2) - - -def count_claims(transaction_set) -> int: - count = 0 - for transaction in transaction_set.transactions: - count += len(transaction.claims) - return count - - -def count_transactions(transaction_set) -> int: - count = 0 - count += len(transaction_set.transactions) - - return count - -def count_services(transaction_set) -> int: - count = 0 - for transaction in transaction_set.transactions: - for claim in transaction.claims: - count += len(claim.services) - - return count - -def sum_interests(transaction_set): - total_interest = 0 - for transaction in transaction_set.transactions: - for claim in transaction.claims: - for amount in claim.amounts: - if amount.qualifier == "I": - total_interest += Decimal(amount.amount) - - return total_interest diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..6609031 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,44 @@ +from decimal import Decimal + + +def count_claims(transaction_set) -> int: + count = 0 + for transaction in transaction_set.transactions: + count += len(transaction.claims) + return count + + +def count_transactions(transaction_set) -> int: + count = 0 + count += len(transaction_set.transactions) + + return count + + +def count_services(transaction_set) -> int: + count = 0 + for transaction in transaction_set.transactions: + for claim in transaction.claims: + count += len(claim.services) + + return count + + +def sum_interests(transaction_set): + total_interest = 0 + for transaction in transaction_set.transactions: + for claim in transaction.claims: + for amount in claim.amounts: + if amount.qualifier == 'I': + total_interest += Decimal(amount.amount) + + return total_interest + + +def get_claim_by_control_number(transaction_set, claim_id): + for transaction in transaction_set.transactions: + for claim in transaction.claims: + if claim.claim.patient_control_number == claim_id: + return claim + + assert False, f'Claim with ID {claim_id} not found'