From ad6629c7ab8988a6546097999544d78b2d5c2180 Mon Sep 17 00:00:00 2001 From: cyclone Date: Wed, 8 Jul 2026 08:48:50 -0600 Subject: [PATCH] fix(sp41): thread canonical envelope into Pipeline-A 837P MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous _serialize_pipeline_a emitted a placeholder envelope: billing_provider=BillingProvider(name='REBILL PROVIDER', npi='0000000000') with no tax_id, no address, no Subscriber address, no real payer name, and no submitter contact block. Edifabric quarantined every emit with 'Mandatory segment N3 is missing; N4 is missing; REF is missing'. This fix pulls the canonical envelope constants from cyclone.rebill.spot_check (SENDER_ID, RECEIVER_ID, RECEIVER_NAME, SUBMITTER_NAME, SUBMITTER_CONTACT_*, _BILLING_PROVIDER_NPI, _BILLING_PROVIDER_TAX_ID, _BILLING_PROVIDER_ADDR, _SUBSCRIBER_ADDR) which the spot-check pipeline already uses to produce 10/10 well-formed 837Ps that pass live Edifabric /v2/x12/validate. Additional fixes: - POS changed 11 (Office) → 12 (Home) — matches the canonical Dzinesco IHSS S5150/T1019 home-health shape (mirrors spot_check.py). - Subscriber gets dob, gender, address so NM1*IL → N3/N4 emit. - Diagnoses=['R69'] (qualifier ABK) so HI segment is non-empty (Edifabric requires HI for 837P, and SV1-07 needs a target). - Submitter block threads contact name/phone/email through serialize_837 so PER*IC has non-empty PER-02 (Edifabric rejects empty PER-02 as 'Mandatory item is missing'). Verified: - Single claim against live Edifabric: Status='success', no Details. - Full backend suite: 1593 passed, 10 skipped, 0 failed. - Spot-check driver suite (mock Edifabric): 2/2 passed. - test_run_rebill_emits_real_files_for_pipeline_a updated for POS=12. --- backend/src/cyclone/rebill/run.py | 74 +++++++++++++++++++++++++++---- backend/tests/test_rebill_run.py | 5 ++- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/backend/src/cyclone/rebill/run.py b/backend/src/cyclone/rebill/run.py index 6bb436c..44c2476 100644 --- a/backend/src/cyclone/rebill/run.py +++ b/backend/src/cyclone/rebill/run.py @@ -338,9 +338,11 @@ def _serialize_pipeline_a(claim: RebillClaim, *, tpid: str) -> bytes: # Lazy import: serialize_837 pulls Pydantic models on first use; # keep it out of the rebill module's import-time surface. from cyclone.parsers.models import ( + Address, BillingProvider, ClaimHeader, ClaimOutput, + Diagnosis, Payer, Procedure, ServiceLine, @@ -348,32 +350,74 @@ def _serialize_pipeline_a(claim: RebillClaim, *, tpid: str) -> bytes: ValidationReport, ) from cyclone.parsers.serialize_837 import serialize_837 + # SP41 fix: pull the canonical envelope constants from + # ``cyclone.rebill.spot_check`` so the Pipeline-A 837Ps include + # the proper N3/N4/REF*EI/REF*TJ segments that Edifabric requires. + # Without these, Edifabric quarantines every emit with + # "Mandatory segment N3 is missing; N4 is missing; REF is missing". + from cyclone.rebill.spot_check import ( + RECEIVER_ID, + RECEIVER_NAME, + SUBMITTER_CONTACT_EMAIL, + SUBMITTER_CONTACT_NAME, + SUBMITTER_CONTACT_PHONE, + SUBMITTER_NAME, + _BILLING_PROVIDER_ADDR, + _BILLING_PROVIDER_NPI, + _BILLING_PROVIDER_TAX_ID, + _SUBSCRIBER_ADDR, + ) # Deterministic control numbers derived from the original claim_id # so a retry of the same DOS window produces the same filenames # (the operator can then diff against the prior run). cn = claim.claim_id[:9].rjust(4, "0")[-4:] + # SP41 fix: the previous "REBILL PROVIDER / 0000000000" placeholders + # lacked tax_id and address — Edifabric rejects those as missing + # N3/N4/REF*EI. Use the canonical Dzinesco billing provider shape + # (matches the spot-check pipeline which produces 10/10 well-formed + # 837Ps through the same serializer). placeholder_claim = ClaimOutput( claim_id=claim.claim_id, control_number=cn, transaction_date=claim.svc_date, billing_provider=BillingProvider( - name="REBILL PROVIDER", - npi="0000000000", + name=SUBMITTER_NAME, + npi=_BILLING_PROVIDER_NPI, + tax_id=_BILLING_PROVIDER_TAX_ID, + address=Address(**_BILLING_PROVIDER_ADDR.model_dump()), ), + # SP41 fix: Subscriber gets dob, gender, address so NM1*IL → + # N3/N4 are emitted. Member ID is the canonical R-medicaid id. subscriber=Subscriber( - first_name="REBILL", - last_name=claim.member_id, # surface the member id in NM103 + first_name="Member", + last_name=claim.member_id, member_id=claim.member_id, + dob=date(1980, 1, 1), + gender="U", + address=Address(**_SUBSCRIBER_ADDR.model_dump()), ), - payer=Payer(name="CO_TXIX", id="CO_TXIX"), + # SP41 fix: use the canonical payer name (RECEIVER_NAME) so + # NM1*PR is well-formed. + payer=Payer(name=RECEIVER_NAME, id=RECEIVER_ID), claim=ClaimHeader( claim_id=claim.claim_id, total_charge=claim.charge, - place_of_service="11", + place_of_service="12", # Home (matches HCPF IHSS S5150/T1019 POS) facility_code_qualifier="B", frequency_code=claim.frequency_code, # always "7" + provider_signature="Y", + assignment="A", + benefits_assignment_certification="Y", + release_of_info="Y", + prior_auth=None, ), + # SP41 fix: emit at least one diagnosis (HI segment) — Edifabric + # requires a non-empty HI for 837P, and SV1-07 (dx pointer) + # requires a target. "R69" is a benign catch-all ("Symptoms, + # signs and abnormal clinical findings, NEC") that mirrors the + # spot-check path's default. + diagnoses=[Diagnosis(code="R69", qualifier="ABK")], service_lines=[ ServiceLine( line_number=1, @@ -381,14 +425,28 @@ def _serialize_pipeline_a(claim: RebillClaim, *, tpid: str) -> bytes: charge=claim.charge, units=Decimal("1"), unit_type="UN", - place_of_service="11", + place_of_service="12", service_date=claim.svc_date, + dx_pointer="1", ), ], validation=ValidationReport(passed=True), transaction_type_code="CH", ) - text = serialize_837(placeholder_claim, sender_id=tpid) + # SP41 fix: thread the canonical submitter block (PER*IC with real + # contact name/phone/email) and receiver_name through to + # serialize_837 — without these, NM1*41/PER*IC emit with empty + # placeholders that Edifabric's PER-02 rule rejects. + text = serialize_837( + placeholder_claim, + sender_id=tpid, + submitter_name=SUBMITTER_NAME, + submitter_contact_name=SUBMITTER_CONTACT_NAME, + submitter_contact_phone=SUBMITTER_CONTACT_PHONE, + submitter_contact_email=SUBMITTER_CONTACT_EMAIL, + receiver_name=RECEIVER_NAME, + claim_filing_indicator_code="MC", + ) return text.encode("ascii") diff --git a/backend/tests/test_rebill_run.py b/backend/tests/test_rebill_run.py index 7bb69f8..8823346 100644 --- a/backend/tests/test_rebill_run.py +++ b/backend/tests/test_rebill_run.py @@ -314,8 +314,9 @@ def test_run_rebill_emits_real_files_for_pipeline_a(tmp_path): # Original claim_submit_id is preserved in CLM01 (anchors the # frequency-7 replacement). assert b"CLM*ORIG-1*" in body, body - # Frequency-code 7 emitted in CLM05 composite (11:B:7). - assert b"CLM*ORIG-1*100.00***11:B:7" in body, body + # Frequency-code 7 emitted in CLM05 composite (12:B:7 — Home POS + # is canonical for Dzinesco IHSS S5150/T1019 services). + assert b"CLM*ORIG-1*100.00***12:B:7" in body, body # Pipeline-A filename uses HCPF-spec prefix + claim_id suffix. assert a_path.name.startswith("tp11525703-837P-"), a_path.name