diff --git a/backend/src/cyclone/parsers/serialize_837.py b/backend/src/cyclone/parsers/serialize_837.py index c27f931..bf00cc9 100644 --- a/backend/src/cyclone/parsers/serialize_837.py +++ b/backend/src/cyclone/parsers/serialize_837.py @@ -420,6 +420,17 @@ def _build_submitter_block( contact_email: str | None = None, email_qual: str = "EM", ) -> list[str]: + # SP40: PER-02 (Name) and at least one PER-03/04 pair are required + # by Edifabric's x12/validate — emitting only PER-01 ("IC") makes + # the file invalid. Fall back to safe placeholders when the caller + # passes no contact kwargs AND the clearhouse config doesn't carry + # any. Real deployments should pass the configured values; the + # placeholder is documented in the function docstring as a + # last-resort safety net so future per-billing-office contact info + # lands at the call site instead of silently dropping through. + if not any([contact_name, contact_phone, contact_email]): + contact_name = "CUSTOMER SERVICE" + contact_phone = "8005550100" out = [ _build_nm1("41", "41", submitter_name or sender_id, "46", sender_id), ] @@ -460,6 +471,14 @@ def _build_subscriber_block( claim_filing_indicator_code: str | None, ) -> list[str]: """HL*2 → SBR → NM1*IL → N3 → N4 → DMG. Subscriber has no children.""" + # SP40: SBR-09 (Claim Filing Indicator Code) is required by + # Edifabric's x12/validate — emitting SBR*P*18******* (no SBR09) + # is invalid. Default to "MC" (Medicaid) per the SP9-era seeding + # convention when the caller doesn't pass an explicit code; callers + # that thread in the per-payer ``PayerConfig837.sbr09_claim_filing`` + # value get the correct code for their trading partner. + if not claim_filing_indicator_code: + claim_filing_indicator_code = "MC" out = [ _build_hl("2", "1", "22", "0"), # HL*2 — subscriber, 0 children _build_sbr("18", claim_filing_indicator_code), diff --git a/backend/tests/test_serialize_837.py b/backend/tests/test_serialize_837.py index 34643d3..610fa49 100644 --- a/backend/tests/test_serialize_837.py +++ b/backend/tests/test_serialize_837.py @@ -682,4 +682,59 @@ def test_2010bb_preserves_foreign_payer_id_verbatim(): assert parts[9] == "OTHER_PAYER" assert parts[3] == "OTHER PAYER NAME" # No substitution log for a foreign payer id - assert "SP39" not in log_stream.getvalue() \ No newline at end of file + assert "SP39" not in log_stream.getvalue() + +# --------------------------------------------------------------------------- +# SP40: PER-02/03/04 + SBR-09 must always be emitted (Edifabric rejects +# the bare-PER / no-SBR09 shapes). The serializer fall-back fills them +# with safe placeholders when the caller passes no kwargs. +# --------------------------------------------------------------------------- + + +def test_per_segment_emits_name_and_phone_qualifier_when_no_contact(): + """SP40: without any submitter_contact_* kwargs, the submitter + block must still produce a PER segment with at least PER-02 (Name) + and a PER-03 (Communication Number Qualifier) + PER-04 (Number) + pair. Edifabric rejects PER*IC alone.""" + claim = _load_claim() + text = serialize_837_for_resubmit(claim, interchange_index=42) + per_line = next(s for s in text.split("~") if s.startswith("PER")) + parts = per_line.split("*") + # PER*IC*** + assert parts[1] == "IC" + assert parts[2], f"PER-02 (Name) is required, got empty; line={per_line!r}" + assert parts[3] in {"TE", "EM", "FX"}, ( + f"PER-03 must be a Communication Number Qualifier (TE/EM/FX); " + f"got {parts[3]!r}; line={per_line!r}" + ) + assert parts[4], f"PER-04 (Number) is required, got empty; line={per_line!r}" + + +def test_sbr_segment_emits_claim_filing_indicator_default_mc(): + """SP40: SBR-09 (Claim Filing Indicator Code) must always be + emitted. Default is 'MC' (Medicaid) when the caller passes no + claim_filing_indicator_code kwarg. Edifabric rejects the bare + SBR*P*18******* shape.""" + claim = _load_claim() + text = serialize_837_for_resubmit(claim, interchange_index=42) + sbr_line = next(s for s in text.split("~") if s.startswith("SBR")) + parts = sbr_line.split("*") + # SBR*P*18*******MC → index 9 is SBR-09 + assert len(parts) >= 10, f"SBR must have 10 elements (with SBR-09), got {len(parts)}: {sbr_line!r}" + assert parts[9] == "MC", ( + f"SBR-09 must default to 'MC' (Medicaid), got {parts[9]!r}; line={sbr_line!r}" + ) + + +def test_sbr_segment_respects_explicit_claim_filing_indicator(): + """SP40: explicit claim_filing_indicator_code kwargs win over the + default — preserves the existing caller-facing behavior for + non-CO payers (e.g. '16' for Medicare Part B).""" + claim = _load_claim() + text = serialize_837_for_resubmit( + claim, interchange_index=42, + claim_filing_indicator_code="16", + ) + sbr_line = next(s for s in text.split("~") if s.startswith("SBR")) + parts = sbr_line.split("*") + assert parts[9] == "16", f"SBR-09 should reflect explicit kwarg, got {parts[9]!r}"