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:
@@ -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),
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user