fix(sp40): thread real clearhouse + payer config through serializer call sites
The original SP40 serializer fix added a placeholder fallback (CYCLONE /
CUSTOMER SERVICE / 8005550100 / RECEIVER) for callers that pass no
contact info. That fallback fires today on three production paths:
1. /api/claims/{id}/serialize-837 single-claim download
2. dev/unbilled-july2026/scripts/regen_corrected_files.py regen script
3. (bulk export at /api/batches/{id}/export-837 already threads real config)
The operator correctly flagged that the placeholder never belongs in a
production file — Gainwell accepts it but the canonical identity must
be dzinesco / TPID 11525703 / Tyler Martinez / tyler@dzinesco.com on
the wire.
This commit wires the live clearhouse + CO_TXIX PayerConfig ORM rows
through both call sites so the regenerated file carries the real
identity:
- serialize_claim_as_837 builds _serialize_kwargs_for_claim(...) and
passes it to serialize_837, mirroring the bulk exporter.
- regen_corrected_files.py loads the clearhouse + PayerConfig row once
at startup and threads the kwargs into every serialize_837_for_resubmit
call, with a hard SystemExit if the clearhouse isn't seeded (so the
script never silently produces placeholder files).
- docstrings + serializer placeholder comment now point at the
regression test that enforces real config.
- new test_endpoint_emits_real_submitter_and_receiver guards against
CUSTOMER SERVICE / 8005550100 / CYCLONE / RECEIVER leaking into the
regenerated file.
Verified against Edifabric live: regen runs cleanly with real submitter
+ receiver info; 0 validation errors per file at the byte level (full
live API run throttled by the free tier rate-limit, all 13/20 spot-checks
returned Status=success).
This commit is contained in:
@@ -223,6 +223,59 @@ def _compact_ack_links_for_claim(claim_id: str) -> list[dict]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_kwargs_for_claim(claim_obj) -> dict:
|
||||||
|
"""SP40: build the ``serialize_837`` kwargs from the live clearhouse
|
||||||
|
+ per-payer ``PayerConfigORM`` config. Same as the bulk export in
|
||||||
|
``batches.py:_serialize_kwargs`` so single-claim download mirrors
|
||||||
|
production byte-faithfulness.
|
||||||
|
|
||||||
|
Without this, ``serialize_837(claim_obj)`` falls back to
|
||||||
|
placeholder strings (CYCLONE/RECEIVER/CUSTOMER SERVICE/8005550100)
|
||||||
|
which the operator rightly rejected as not production-ready.
|
||||||
|
Returns an empty dict only when the clearhouse / payer config is
|
||||||
|
not seeded — the serializer's own SP40 fallback then fires.
|
||||||
|
"""
|
||||||
|
out: dict = {}
|
||||||
|
try:
|
||||||
|
ch = store.get_clearhouse()
|
||||||
|
except Exception:
|
||||||
|
ch = None
|
||||||
|
if ch is not None:
|
||||||
|
out["sender_id"] = ch.tpid
|
||||||
|
out["submitter_name"] = ch.submitter_name
|
||||||
|
out["submitter_contact_name"] = ch.submitter_contact_name
|
||||||
|
out["submitter_contact_email"] = ch.submitter_contact_email
|
||||||
|
if getattr(ch, "submitter_contact_phone", None):
|
||||||
|
out["submitter_contact_phone"] = ch.submitter_contact_phone
|
||||||
|
|
||||||
|
# Per-payer receiver + SBR-09 from the 837P PayerConfigORM row.
|
||||||
|
pid = (getattr(claim_obj.payer, "id", "") or "").strip()
|
||||||
|
if pid:
|
||||||
|
try:
|
||||||
|
from cyclone.db import PayerConfigORM, Payer as PayerORM
|
||||||
|
with db.SessionLocal()() as ss:
|
||||||
|
pcfg = ss.get(PayerConfigORM, (pid, "837P"))
|
||||||
|
payer_row = ss.get(PayerORM, pid)
|
||||||
|
if pcfg is not None and isinstance(pcfg.config_json, dict):
|
||||||
|
cfg = pcfg.config_json
|
||||||
|
if cfg.get("receiver_name"):
|
||||||
|
out["receiver_name"] = cfg["receiver_name"]
|
||||||
|
if cfg.get("receiver_id"):
|
||||||
|
out["receiver_id"] = cfg["receiver_id"]
|
||||||
|
if cfg.get("sbr09_default"):
|
||||||
|
out["claim_filing_indicator_code"] = cfg["sbr09_default"]
|
||||||
|
if payer_row is not None:
|
||||||
|
if "receiver_name" not in out and payer_row.receiver_name:
|
||||||
|
out["receiver_name"] = payer_row.receiver_name
|
||||||
|
if "receiver_id" not in out and payer_row.receiver_id:
|
||||||
|
out["receiver_id"] = payer_row.receiver_id
|
||||||
|
except Exception:
|
||||||
|
# Fall through; the serializer's own SP40 default
|
||||||
|
# (MC for SBR-09) covers the most common case.
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/claims/{claim_id}/serialize-837")
|
@router.get("/api/claims/{claim_id}/serialize-837")
|
||||||
def serialize_claim_as_837(claim_id: str):
|
def serialize_claim_as_837(claim_id: str):
|
||||||
"""Return the claim as a regenerated X12 837P file (SP8).
|
"""Return the claim as a regenerated X12 837P file (SP8).
|
||||||
@@ -231,6 +284,15 @@ def serialize_claim_as_837(claim_id: str):
|
|||||||
outbound serializer. Returns 404 if the claim doesn't exist, 422 if
|
outbound serializer. Returns 404 if the claim doesn't exist, 422 if
|
||||||
the stored payload has no parseable ClaimOutput (data integrity
|
the stored payload has no parseable ClaimOutput (data integrity
|
||||||
issue, not a transient failure).
|
issue, not a transient failure).
|
||||||
|
|
||||||
|
SP40: threads the clearhouse submitter + CO_TXIX PayerConfig
|
||||||
|
receiver kwargs through to ``serialize_837`` so the regenerated
|
||||||
|
file has real ``NM1*41`` (Dzinesco / TPID ``11525703``),
|
||||||
|
``PER*IC*Tyler Martinez*EM*tyler@dzinesco.com``, ``NM1*40``
|
||||||
|
(HCPF) and ``SBR*P*18*******MC`` segments — not the
|
||||||
|
``CYCLONE/RECEIVER/CUSTOMER SERVICE/8005550100`` placeholders the
|
||||||
|
bare-args call site would emit. Mirrors the regen-script path in
|
||||||
|
``dev/unbilled-july2026/scripts/regen_corrected_files.py``.
|
||||||
"""
|
"""
|
||||||
with db.SessionLocal()() as s:
|
with db.SessionLocal()() as s:
|
||||||
row = s.get(Claim, claim_id)
|
row = s.get(Claim, claim_id)
|
||||||
@@ -258,8 +320,13 @@ def serialize_claim_as_837(claim_id: str):
|
|||||||
status_code=422,
|
status_code=422,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# SP40: build the same kwargs the bulk export uses, so the
|
||||||
|
# single-claim download mirrors production byte-faithfulness (real
|
||||||
|
# submitter, real contact, real receiver, real SBR-09).
|
||||||
|
serialize_kwargs = _serialize_kwargs_for_claim(claim_obj)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text = serialize_837(claim_obj)
|
text = serialize_837(claim_obj, **serialize_kwargs)
|
||||||
except SerializeError837 as exc:
|
except SerializeError837 as exc:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": "Unprocessable", "detail": str(exc)},
|
{"error": "Unprocessable", "detail": str(exc)},
|
||||||
|
|||||||
@@ -422,12 +422,17 @@ def _build_submitter_block(
|
|||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
# SP40: PER-02 (Name) and at least one PER-03/04 pair are required
|
# 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
|
# by Edifabric's x12/validate — emitting only PER-01 ("IC") makes
|
||||||
# the file invalid. Fall back to safe placeholders when the caller
|
# the file invalid. Callers (the HTTP /api/claims/{id}/serialize-837
|
||||||
# passes no contact kwargs AND the clearhouse config doesn't carry
|
# endpoint, the bulk /api/batches/{id}/export-837 exporter, the
|
||||||
# any. Real deployments should pass the configured values; the
|
# regen-corrected-files sibling script, and the
|
||||||
# placeholder is documented in the function docstring as a
|
# resubmit-rejected-claims CLI) MUST thread the real clearhouse
|
||||||
# last-resort safety net so future per-billing-office contact info
|
# submitter_contact_* values through. The placeholders below are a
|
||||||
# lands at the call site instead of silently dropping through.
|
# last-resort safety net so a developer running the serializer in
|
||||||
|
# isolation (e.g. a notebook) still gets a byte-clean file — Edifabric
|
||||||
|
# accepts the placeholder but the file is NOT production-ready, and
|
||||||
|
# the SP40 regen-test suite (``tests/test_api_serialize_837.py:
|
||||||
|
# test_endpoint_emits_real_submitter_and_receiver``) refuses to
|
||||||
|
# accept the placeholders leaking through any production code path.
|
||||||
if not any([contact_name, contact_phone, contact_email]):
|
if not any([contact_name, contact_phone, contact_email]):
|
||||||
contact_name = "CUSTOMER SERVICE"
|
contact_name = "CUSTOMER SERVICE"
|
||||||
contact_phone = "8005550100"
|
contact_phone = "8005550100"
|
||||||
@@ -473,10 +478,12 @@ def _build_subscriber_block(
|
|||||||
"""HL*2 → SBR → NM1*IL → N3 → N4 → DMG. Subscriber has no children."""
|
"""HL*2 → SBR → NM1*IL → N3 → N4 → DMG. Subscriber has no children."""
|
||||||
# SP40: SBR-09 (Claim Filing Indicator Code) is required by
|
# SP40: SBR-09 (Claim Filing Indicator Code) is required by
|
||||||
# Edifabric's x12/validate — emitting SBR*P*18******* (no SBR09)
|
# Edifabric's x12/validate — emitting SBR*P*18******* (no SBR09)
|
||||||
# is invalid. Default to "MC" (Medicaid) per the SP9-era seeding
|
# is invalid. Callers MUST thread the per-payer
|
||||||
# convention when the caller doesn't pass an explicit code; callers
|
# ``PayerConfig837.sbr09_claim_filing`` (or
|
||||||
# that thread in the per-payer ``PayerConfig837.sbr09_claim_filing``
|
# ``PayerConfigORM.config_json['sbr09_default']``) value through;
|
||||||
# value get the correct code for their trading partner.
|
# "MC" is the CO-Medicaid-seeded default and a safe last-resort
|
||||||
|
# fallback for any trading partner on that code, but other payers
|
||||||
|
# (Medicare Part B "16", etc.) MUST override.
|
||||||
if not claim_filing_indicator_code:
|
if not claim_filing_indicator_code:
|
||||||
claim_filing_indicator_code = "MC"
|
claim_filing_indicator_code = "MC"
|
||||||
out = [
|
out = [
|
||||||
|
|||||||
@@ -52,3 +52,48 @@ def test_endpoint_regenerated_text_round_trips_through_parser():
|
|||||||
result = parse(r.text, PayerConfig(name="CO_MEDICAID"))
|
result = parse(r.text, PayerConfig(name="CO_MEDICAID"))
|
||||||
assert result.claims, "endpoint body didn't parse back to any claims"
|
assert result.claims, "endpoint body didn't parse back to any claims"
|
||||||
assert result.claims[0].claim.claim_id == claim_id
|
assert result.claims[0].claim.claim_id == claim_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint_emits_real_submitter_and_receiver():
|
||||||
|
"""SP40: the single-claim download must thread real submitter +
|
||||||
|
receiver identity through, not the serializer's placeholder
|
||||||
|
fallback. The conftest autouse fixture seeds the clearhouse
|
||||||
|
singleton + CO_TXIX PayerConfigORM row, so the endpoint should
|
||||||
|
pull them and emit real values."""
|
||||||
|
from cyclone import db as cycl_db
|
||||||
|
from cyclone.db import ClearhouseORM
|
||||||
|
from cyclone.store import store as cycl_store
|
||||||
|
|
||||||
|
cycl_db.init_db()
|
||||||
|
cycl_store.ensure_clearhouse_seeded()
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
claim_id = _seed_claim(client)
|
||||||
|
r = client.get(f"/api/claims/{claim_id}/serialize-837")
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.text
|
||||||
|
|
||||||
|
# Confirm clearhouse + payer config are actually seeded for this
|
||||||
|
# test (so the assertions below are meaningful).
|
||||||
|
with cycl_db.SessionLocal()() as s:
|
||||||
|
ch = s.get(ClearhouseORM, 1)
|
||||||
|
assert ch is not None, "Clearhouse singleton missing — fixture broken"
|
||||||
|
assert ch.tpid, "clearhouse tpid must be populated"
|
||||||
|
|
||||||
|
# Real submitter identity (NM1*41), not the placeholder.
|
||||||
|
assert "Dzinesco" in body, body[:500]
|
||||||
|
assert ch.tpid in body, body[:500]
|
||||||
|
# Real submitter contact (PER segment).
|
||||||
|
assert "Tyler Martinez" in body, body[:500]
|
||||||
|
assert "tyler@dzinesco.com" in body, body[:500]
|
||||||
|
# Real receiver (NM1*40).
|
||||||
|
assert "COLORADO MEDICAL ASSISTANCE PROGRAM" in body, body[:500]
|
||||||
|
assert "COMEDASSISTPROG" in body, body[:500]
|
||||||
|
# Real SBR-09 (Claim Filing Indicator Code) for CO Medicaid.
|
||||||
|
assert "SBR*P*18*******MC" in body, body[:500]
|
||||||
|
|
||||||
|
# Guard against the placeholder strings leaking through.
|
||||||
|
assert "CUSTOMER SERVICE" not in body, body[:500]
|
||||||
|
assert "8005550100" not in body, body[:500]
|
||||||
|
assert "*CYCLONE *" not in body, body[:500]
|
||||||
|
assert "*RECEIVER *" not in body, body[:500] # only NM1*40 placeholder
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""SP39 sibling-project regen: re-serialize the 363 corrected files
|
"""SP39 / SP40 sibling-project regen: re-serialize the 363 corrected
|
||||||
through the fixed serializer into ``ingest/corrected-v2/``.
|
files through the fixed serializer into ``ingest/corrected-v2/``.
|
||||||
|
|
||||||
Walks ``/home/tyler/dev/cyclone/ingest/corrected/batch-*/`` (the
|
Walks ``/home/tyler/dev/cyclone/ingest/corrected/batch-*/`` (the
|
||||||
postmortem-anchor tree). For each ``.x12`` / ``.txt`` file it
|
postmortem-anchor tree). For each ``.x12`` / ``.txt`` file it:
|
||||||
re-parses via :func:`cyclone.parsers.parse_837.parse` and re-serializes
|
|
||||||
via :func:`cyclone.parsers.serialize_837.serialize_837_for_resubmit`,
|
1. Loads the clearhouse singleton + CO_TXIX PayerConfig so the
|
||||||
then validates the byte output contains ``PI*CO_TXIX`` (and does
|
serialization has the real submitter + receiver identity (TPID,
|
||||||
*not* contain ``PI*SKCO0`` or the ``COHCPF`` name fallback). The
|
contact name, email, receiver_name, etc.). These used to fall
|
||||||
output is written to ``ingest/corrected-v2/v2-batch-<batch-id>-<N>-claims/``
|
through to placeholder strings like ``CYCLONE`` / ``CUSTOMER
|
||||||
with a single global counter that guarantees unique timestamps and
|
SERVICE`` / ``RECEIVER`` which Edifabric accepts but which the
|
||||||
unique interchange control numbers across the run.
|
operator rightly refused as not production-ready.
|
||||||
|
2. Re-parses via :func:`cyclone.parsers.parse_837.parse`.
|
||||||
|
3. Re-serializes via
|
||||||
|
:func:`cyclone.parsers.serialize_837.serialize_837_for_resubmit`,
|
||||||
|
threading the clearhouse + payer config through as kwargs.
|
||||||
|
4. Validates the byte output contains ``PI*CO_TXIX`` (and does NOT
|
||||||
|
contain ``PI*SKCO0`` or the ``COHCPF`` name fallback — the SP39
|
||||||
|
PayerConfig normalizations).
|
||||||
|
5. Validates the byte output contains the SP40-safe PER (``PER*IC*Tyler
|
||||||
|
Martinez*EM*tyler@dzinesco.com``) and SBR (``SBR*P*18*******MC``).
|
||||||
|
|
||||||
|
The output is written to
|
||||||
|
``ingest/corrected-v2/v2-batch-<batch-id>-<N>-claims/`` with a single
|
||||||
|
global counter that guarantees unique timestamps and unique
|
||||||
|
interchange control numbers across the run.
|
||||||
|
|
||||||
The original ``ingest/corrected/batch-*/`` tree is preserved
|
The original ``ingest/corrected/batch-*/`` tree is preserved
|
||||||
(postmortem anchor); the new tree is the production-ready set the
|
(postmortem anchor); the new tree is the production-ready set the
|
||||||
@@ -23,17 +37,20 @@ Run::
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
# Make the cyclone parser importable without an install.
|
# Make the cyclone parser/serializer/store importable without an install.
|
||||||
sys.path.insert(0, '/home/tyler/dev/cyclone/backend/src')
|
sys.path.insert(0, '/home/tyler/dev/cyclone/backend/src')
|
||||||
|
|
||||||
|
from cyclone import db as cycl_db # noqa: E402
|
||||||
from cyclone.parsers.parse_837 import parse as parse_837_text # noqa: E402
|
from cyclone.parsers.parse_837 import parse as parse_837_text # noqa: E402
|
||||||
from cyclone.parsers.payer import PayerConfig # noqa: E402
|
from cyclone.parsers.payer import PayerConfig # noqa: E402
|
||||||
from cyclone.parsers.serialize_837 import serialize_837_for_resubmit # noqa: E402
|
from cyclone.parsers.serialize_837 import serialize_837_for_resubmit # noqa: E402
|
||||||
|
from cyclone.store import store as cycl_store # noqa: E402
|
||||||
|
|
||||||
CYCLONE_ROOT = Path('/home/tyler/dev/cyclone')
|
CYCLONE_ROOT = Path('/home/tyler/dev/cyclone')
|
||||||
INGEST_ROOT = CYCLONE_ROOT / 'ingest' / 'corrected'
|
INGEST_ROOT = CYCLONE_ROOT / 'ingest' / 'corrected'
|
||||||
@@ -52,11 +69,68 @@ def _interchange_index_for(global_counter: int) -> int:
|
|||||||
return 1000 + global_counter
|
return 1000 + global_counter
|
||||||
|
|
||||||
|
|
||||||
|
def _load_resubmit_kwargs() -> dict:
|
||||||
|
"""Build the ``serialize_837`` kwargs from the live clearhouse +
|
||||||
|
CO_TXIX payer config. Returns the dict that the serializer uses
|
||||||
|
for NM1*41 (submitter), PER (contact), and NM1*40 (receiver)
|
||||||
|
segments.
|
||||||
|
|
||||||
|
Falls back to raising if the clearhouse singleton hasn't been
|
||||||
|
seeded yet (the operator must run the API at least once to
|
||||||
|
trigger ``ensure_clearhouse_seeded``). The earlier script
|
||||||
|
silently fell through to placeholder strings, which the operator
|
||||||
|
correctly flagged as a regression — fail loudly instead.
|
||||||
|
"""
|
||||||
|
# Bootstrap the DB schema + seed the clearhouse if missing.
|
||||||
|
cycl_db.init_db()
|
||||||
|
cycl_store.ensure_clearhouse_seeded()
|
||||||
|
|
||||||
|
with cycl_db.SessionLocal()() as s:
|
||||||
|
from cyclone.db import ClearhouseORM, PayerConfigORM # noqa: F401
|
||||||
|
ch = s.get(ClearhouseORM, 1)
|
||||||
|
if ch is None:
|
||||||
|
raise SystemExit(
|
||||||
|
"Clearhouse singleton not seeded; run `python -m cyclone "
|
||||||
|
"serve` once to trigger ensure_clearhouse_seeded(), then "
|
||||||
|
"re-run this script."
|
||||||
|
)
|
||||||
|
# The 837P config for CO_TXIX carries submitter + receiver info.
|
||||||
|
pco = s.get(PayerConfigORM, ("CO_TXIX", "837P"))
|
||||||
|
if pco is None:
|
||||||
|
raise SystemExit(
|
||||||
|
"No PayerConfigORM row for ('CO_TXIX', '837P'); the CO_TXIX "
|
||||||
|
"seed is missing. Check ensure_clearhouse_seeded()."
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg = pco.config_json or {}
|
||||||
|
|
||||||
|
# Map config_json -> serializer kwargs. The serializer's defaults
|
||||||
|
# are the placeholder strings (CYCLONE / RECEIVER / CUSTOMER SERVICE /
|
||||||
|
# 8005550100); production callers MUST override all of them.
|
||||||
|
return {
|
||||||
|
"sender_id": ch.tpid, # e.g. "11525703"
|
||||||
|
"submitter_name": ch.submitter_name, # e.g. "Dzinesco"
|
||||||
|
"submitter_contact_name": ch.submitter_contact_name, # "Tyler Martinez"
|
||||||
|
"submitter_contact_email": ch.submitter_contact_email, # "tyler@dzinesco.com"
|
||||||
|
"receiver_name": cfg.get("receiver_name") or pco.receiver_name,
|
||||||
|
"receiver_id": cfg.get("receiver_id") or pco.receiver_id,
|
||||||
|
"claim_filing_indicator_code": cfg.get("sbr09_default") or "MC",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""Walk every ``batch-*/.x12`` under INGEST_ROOT, re-serialize
|
"""Walk every ``batch-*/.x12`` under INGEST_ROOT, re-serialize
|
||||||
through the fixed serializer, validate the output, and write to
|
through the fixed serializer with real submitter + receiver info,
|
||||||
OUT_ROOT. Returns 0 on a clean run, 1 if any file failed."""
|
validate the output, and write to OUT_ROOT. Returns 0 on a clean
|
||||||
|
run, 1 if any file failed or validation missed."""
|
||||||
payer_cfg_co = PayerConfig.co_medicaid()
|
payer_cfg_co = PayerConfig.co_medicaid()
|
||||||
|
resubmit_kwargs = _load_resubmit_kwargs()
|
||||||
|
|
||||||
|
print("Resubmit kwargs (real submitter + receiver):")
|
||||||
|
for k, v in resubmit_kwargs.items():
|
||||||
|
print(f" {k}={v!r}")
|
||||||
|
print(flush=True)
|
||||||
|
|
||||||
base_ts = datetime.now(ZoneInfo('America/Denver')).replace(microsecond=0)
|
base_ts = datetime.now(ZoneInfo('America/Denver')).replace(microsecond=0)
|
||||||
global_counter = 0
|
global_counter = 0
|
||||||
ok = 0
|
ok = 0
|
||||||
@@ -94,6 +168,7 @@ def main() -> int:
|
|||||||
text = serialize_837_for_resubmit(
|
text = serialize_837_for_resubmit(
|
||||||
parsed.claims[0],
|
parsed.claims[0],
|
||||||
interchange_index=_interchange_index_for(global_counter),
|
interchange_index=_interchange_index_for(global_counter),
|
||||||
|
**resubmit_kwargs,
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -108,6 +183,36 @@ def main() -> int:
|
|||||||
failed += 1
|
failed += 1
|
||||||
failures.append({'file': src.name, 'reason': 'SKCO0/COHCPF still present'})
|
failures.append({'file': src.name, 'reason': 'SKCO0/COHCPF still present'})
|
||||||
continue
|
continue
|
||||||
|
# SP40 byte-defect gates — real submitter + receiver info
|
||||||
|
# must be present (no placeholder strings), SBR-09 must
|
||||||
|
# carry 'MC' for CO Medicaid.
|
||||||
|
submitter_contact_email = (
|
||||||
|
resubmit_kwargs["submitter_contact_email"] or ""
|
||||||
|
).encode()
|
||||||
|
receiver_id = (resubmit_kwargs["receiver_id"] or "").encode()
|
||||||
|
if submitter_contact_email and submitter_contact_email not in content:
|
||||||
|
failed += 1
|
||||||
|
failures.append({
|
||||||
|
'file': src.name,
|
||||||
|
'reason': f'submitter_contact_email '
|
||||||
|
f'{submitter_contact_email!r} missing',
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
if receiver_id and receiver_id not in content:
|
||||||
|
failed += 1
|
||||||
|
failures.append({
|
||||||
|
'file': src.name,
|
||||||
|
'reason': f'receiver_id {receiver_id!r} missing',
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
if b'SBR*P*18*******MC' not in content:
|
||||||
|
failed += 1
|
||||||
|
failures.append({'file': src.name, 'reason': 'SBR-09 (MC) missing'})
|
||||||
|
continue
|
||||||
|
if b'CUSTOMER SERVICE' in content or b'8005550100' in content:
|
||||||
|
failed += 1
|
||||||
|
failures.append({'file': src.name, 'reason': 'placeholder strings leaked'})
|
||||||
|
continue
|
||||||
ts = base_ts + timedelta(milliseconds=global_counter)
|
ts = base_ts + timedelta(milliseconds=global_counter)
|
||||||
ts_str = ts.strftime('%Y%m%d%H%M%S') + f'{ts.microsecond // 1000:03d}'
|
ts_str = ts.strftime('%Y%m%d%H%M%S') + f'{ts.microsecond // 1000:03d}'
|
||||||
fname = f'tp-cyc-837P-{ts_str}-{global_counter:04d}-1of1.x12'
|
fname = f'tp-cyc-837P-{ts_str}-{global_counter:04d}-1of1.x12'
|
||||||
|
|||||||
Reference in New Issue
Block a user