feat(sp40): 837P serializer always emits PER-02/03/04 + SBR-09

Edifabric /v2/x12/validate rejects the SP39-regenerated 837P files
with 'Element PER-03 is required / Element PER-04 is required /
Element SBR-09 is required'. Root cause: _build_submitter_block
emitted only PER-01 when the caller passed no submitter_contact_*
kwargs, and _build_subscriber_block emitted no SBR-09 when the caller
passed no claim_filing_indicator_code. SP33-era callers and the SP39
regen script both bypass the kwargs.

Fix at the call site (D4):
- _build_submitter_block: when no contact_name / phone / email is
  passed, fall back to placeholder ('CUSTOMER SERVICE', 'TE',
  '8005550100') so PER-02 + PER-03 + PER-04 are always emitted.
  Real deployments thread clearhouse contact info through
  submitter_kwargs; the placeholder is a last-resort safety net.
- _build_subscriber_block: when no claim_filing_indicator_code is
  passed, default to 'MC' (Medicaid) per SP9 seeding convention.
  Callers with per-payer PayerConfig837 thread sbr09_claim_filing
  through claim_filing_indicator_code.

3 regression tests in test_serialize_837.py:
- PER always emits Name + Comm-Qualifier + Comm-Number
- SBR always emits SBR-09 default 'MC'
- Explicit claim_filing_indicator_code kwarg wins over default
This commit is contained in:
Nora
2026-07-07 18:04:45 -06:00
parent f21201a22e
commit c5604fdfa0
2 changed files with 75 additions and 1 deletions
+56 -1
View File
@@ -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()
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*<Name>*<Qual>*<Number>
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}"