feat(sp25): move ack UI serializers to store/ui.py
This commit is contained in:
@@ -90,6 +90,7 @@ from cyclone.store import (
|
|||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
dashboard_kpis,
|
dashboard_kpis,
|
||||||
store,
|
store,
|
||||||
|
to_ui_two77ca_ack,
|
||||||
utcnow,
|
utcnow,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1036,7 +1037,7 @@ def list_277ca_acks_endpoint(
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
"""Return the list of persisted 277CA ACKs, newest first."""
|
"""Return the list of persisted 277CA ACKs, newest first."""
|
||||||
rows = store.list_277ca_acks()
|
rows = store.list_277ca_acks()
|
||||||
items = [_277ca_to_ui(r) for r in rows[:limit]]
|
items = [to_ui_two77ca_ack(r) for r in rows[:limit]]
|
||||||
return {"total": len(rows), "items": items}
|
return {"total": len(rows), "items": items}
|
||||||
|
|
||||||
|
|
||||||
@@ -1046,31 +1047,9 @@ def get_277ca_ack_endpoint(ack_id: int) -> dict:
|
|||||||
row = store.get_277ca_ack(ack_id)
|
row = store.get_277ca_ack(ack_id)
|
||||||
if row is None:
|
if row is None:
|
||||||
raise HTTPException(status_code=404, detail=f"277CA ACK {ack_id} not found")
|
raise HTTPException(status_code=404, detail=f"277CA ACK {ack_id} not found")
|
||||||
return {
|
body = to_ui_two77ca_ack(row)
|
||||||
"id": row.id,
|
body["raw_json"] = row.raw_json
|
||||||
"control_number": row.control_number,
|
return body
|
||||||
"accepted_count": row.accepted_count,
|
|
||||||
"rejected_count": row.rejected_count,
|
|
||||||
"paid_count": row.paid_count,
|
|
||||||
"pended_count": row.pended_count,
|
|
||||||
"source_batch_id": row.source_batch_id,
|
|
||||||
"parsed_at": row.parsed_at.isoformat() if row.parsed_at else None,
|
|
||||||
"raw_json": row.raw_json,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _277ca_to_ui(row) -> dict:
|
|
||||||
"""Render a 277caAck row for the UI (list endpoint shape)."""
|
|
||||||
return {
|
|
||||||
"id": row.id,
|
|
||||||
"control_number": row.control_number,
|
|
||||||
"accepted_count": row.accepted_count,
|
|
||||||
"rejected_count": row.rejected_count,
|
|
||||||
"paid_count": row.paid_count,
|
|
||||||
"pended_count": row.pended_count,
|
|
||||||
"source_batch_id": row.source_batch_id,
|
|
||||||
"parsed_at": row.parsed_at.isoformat() if row.parsed_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_ta1(result) -> str:
|
def _serialize_ta1(result) -> str:
|
||||||
|
|||||||
@@ -19,48 +19,16 @@ from fastapi.responses import StreamingResponse
|
|||||||
from cyclone.api_helpers import ndjson_stream_list, wants_ndjson
|
from cyclone.api_helpers import ndjson_stream_list, wants_ndjson
|
||||||
from cyclone.parsers.models_999 import ParseResult999
|
from cyclone.parsers.models_999 import ParseResult999
|
||||||
from cyclone.parsers.serialize_999 import serialize_999
|
from cyclone.parsers.serialize_999 import serialize_999
|
||||||
from cyclone.store import store
|
from cyclone.store import store, to_ui_ack
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _ack_to_ui(row) -> dict:
|
# SP25: ``_ack_to_ui`` moved to ``cyclone.store.ui.to_ui_ack`` so the
|
||||||
"""Map an ``Ack`` ORM row to the UI shape used by ``/api/acks``.
|
# live-tail event payload (``ack_received``) matches the list endpoint
|
||||||
|
# shape byte-for-byte.
|
||||||
Field names match the rest of the Cyclone API (snake_case). The
|
|
||||||
frontend ``useAcks`` hook re-shapes this to the camelCase ``Ack``
|
|
||||||
interface in ``src/types/index.ts``.
|
|
||||||
|
|
||||||
Adds ``patient_control_number`` pulled from ``raw_json`` so the
|
|
||||||
operator can correlate each 999 back to the original claim
|
|
||||||
batch. The full inbound filename is reachable via
|
|
||||||
``GET /api/acks/{ack_id}`` (raw_json carries the full parse
|
|
||||||
tree) and ``GET /api/admin/scheduler/processed-files``.
|
|
||||||
"""
|
|
||||||
body = {
|
|
||||||
"id": row.id,
|
|
||||||
"source_batch_id": row.source_batch_id,
|
|
||||||
"accepted_count": row.accepted_count,
|
|
||||||
"rejected_count": row.rejected_count,
|
|
||||||
"received_count": row.received_count,
|
|
||||||
"ack_code": row.ack_code,
|
|
||||||
"parsed_at": (
|
|
||||||
row.parsed_at.isoformat().replace("+00:00", "Z")
|
|
||||||
if row.parsed_at is not None
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"patient_control_number": None,
|
|
||||||
}
|
|
||||||
raw = row.raw_json or {}
|
|
||||||
try:
|
|
||||||
set_responses = raw.get("set_responses") or []
|
|
||||||
if set_responses:
|
|
||||||
body["patient_control_number"] = set_responses[0].get("set_control_number")
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
pass
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/acks")
|
@router.get("/api/acks")
|
||||||
@@ -80,7 +48,7 @@ def list_acks_endpoint(
|
|||||||
exceeds the page size.
|
exceeds the page size.
|
||||||
"""
|
"""
|
||||||
rows = store.list_acks()
|
rows = store.list_acks()
|
||||||
items = [_ack_to_ui(r) for r in rows[offset : offset + limit]]
|
items = [to_ui_ack(r) for r in rows[offset : offset + limit]]
|
||||||
total = len(rows)
|
total = len(rows)
|
||||||
returned = len(items)
|
returned = len(items)
|
||||||
has_more = offset + returned < total
|
has_more = offset + returned < total
|
||||||
@@ -117,7 +85,7 @@ def get_ack_endpoint(ack_id: int) -> dict:
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
detail={"error": "Not found", "detail": f"Ack {ack_id} not found"},
|
detail={"error": "Not found", "detail": f"Ack {ack_id} not found"},
|
||||||
)
|
)
|
||||||
body = _ack_to_ui(row)
|
body = to_ui_ack(row)
|
||||||
body["raw_json"] = row.raw_json
|
body["raw_json"] = row.raw_json
|
||||||
# Regenerate the X12 text from raw_json so the operator can download
|
# Regenerate the X12 text from raw_json so the operator can download
|
||||||
# the actual 999 file. (SP3 P3 follow-up: list endpoint doesn't carry
|
# the actual 999 file. (SP3 P3 follow-up: list endpoint doesn't carry
|
||||||
|
|||||||
@@ -15,26 +15,14 @@ from typing import Any
|
|||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
from cyclone import db
|
from cyclone import db
|
||||||
from cyclone.store import store
|
from cyclone.store import store, to_ui_ta1_ack
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _ta1_to_ui(row: db.Ta1Ack) -> dict:
|
# SP25: ``_ta1_to_ui`` moved to ``cyclone.store.ui.to_ui_ta1_ack`` so
|
||||||
"""Render a Ta1Ack row for the UI (list endpoint shape)."""
|
# the live-tail event payload (``ta1_ack_received``) matches the list
|
||||||
return {
|
# endpoint shape byte-for-byte.
|
||||||
"id": row.id,
|
|
||||||
"control_number": row.control_number,
|
|
||||||
"ack_code": row.ack_code,
|
|
||||||
"note_code": row.note_code,
|
|
||||||
"interchange_date": row.interchange_date.isoformat()
|
|
||||||
if row.interchange_date else None,
|
|
||||||
"interchange_time": row.interchange_time,
|
|
||||||
"sender_id": row.sender_id,
|
|
||||||
"receiver_id": row.receiver_id,
|
|
||||||
"source_batch_id": row.source_batch_id,
|
|
||||||
"parsed_at": row.parsed_at.isoformat() if row.parsed_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_ta1_from_row(row: db.Ta1Ack) -> str:
|
def _serialize_ta1_from_row(row: db.Ta1Ack) -> str:
|
||||||
@@ -57,7 +45,7 @@ def list_ta1_acks_endpoint(
|
|||||||
count regardless of the ``limit`` cap.
|
count regardless of the ``limit`` cap.
|
||||||
"""
|
"""
|
||||||
rows = store.list_ta1_acks()
|
rows = store.list_ta1_acks()
|
||||||
items = [_ta1_to_ui(r) for r in rows[:limit]]
|
items = [to_ui_ta1_ack(r) for r in rows[:limit]]
|
||||||
return {
|
return {
|
||||||
"total": len(rows),
|
"total": len(rows),
|
||||||
"items": items,
|
"items": items,
|
||||||
@@ -70,7 +58,7 @@ def get_ta1_ack_endpoint(ack_id: int) -> dict:
|
|||||||
row = store.get_ta1_ack(ack_id)
|
row = store.get_ta1_ack(ack_id)
|
||||||
if row is None:
|
if row is None:
|
||||||
raise HTTPException(status_code=404, detail=f"TA1 ACK {ack_id} not found")
|
raise HTTPException(status_code=404, detail=f"TA1 ACK {ack_id} not found")
|
||||||
body = _ta1_to_ui(row)
|
body = to_ui_ta1_ack(row)
|
||||||
body["raw_ta1_text"] = _serialize_ta1_from_row(row)
|
body["raw_ta1_text"] = _serialize_ta1_from_row(row)
|
||||||
body["raw_json"] = row.raw_json
|
body["raw_json"] = row.raw_json
|
||||||
return body
|
return body
|
||||||
|
|||||||
@@ -101,11 +101,14 @@ from .providers import (
|
|||||||
)
|
)
|
||||||
from .records import BatchKind, BatchRecord, BatchRecord837, BatchRecord835
|
from .records import BatchKind, BatchRecord, BatchRecord837, BatchRecord835
|
||||||
from .ui import (
|
from .ui import (
|
||||||
|
to_ui_ack,
|
||||||
to_ui_claim_detail,
|
to_ui_claim_detail,
|
||||||
to_ui_claim_from_orm,
|
to_ui_claim_from_orm,
|
||||||
to_ui_provider,
|
to_ui_provider,
|
||||||
to_ui_remittance_from_orm,
|
to_ui_remittance_from_orm,
|
||||||
to_ui_remittance_with_adjustments,
|
to_ui_remittance_with_adjustments,
|
||||||
|
to_ui_ta1_ack,
|
||||||
|
to_ui_two77ca_ack,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ shape change ripples to every API consumer.
|
|||||||
|
|
||||||
Also hosts ``utcnow()`` (the tz-aware UTC now) because it's a small
|
Also hosts ``utcnow()`` (the tz-aware UTC now) because it's a small
|
||||||
free function that several modules want and there's no better home.
|
free function that several modules want and there's no better home.
|
||||||
|
|
||||||
|
SP25: ``to_ui_ack`` / ``to_ui_ta1_ack`` / ``to_ui_two77ca_ack`` were
|
||||||
|
moved here from ``api_routers/acks.py`` / ``api_routers/ta1_acks.py``
|
||||||
|
/ ``api.py`` so the live-tail event payload can match the list endpoint
|
||||||
|
shape byte-for-byte. The seam between persistence and streaming
|
||||||
|
depends on the two halves staying in sync.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -189,6 +195,97 @@ def _iso_z(value: datetime | None) -> str:
|
|||||||
return value.isoformat().replace("+00:00", "Z")
|
return value.isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def to_ui_ack(row: db.Ack) -> dict:
|
||||||
|
"""Map an ``Ack`` ORM row to the UI shape used by ``/api/acks``.
|
||||||
|
|
||||||
|
Field names match the rest of the Cyclone API (snake_case). The
|
||||||
|
frontend ``useAcks`` hook re-shapes this to the camelCase ``Ack``
|
||||||
|
interface in ``src/types/index.ts``.
|
||||||
|
|
||||||
|
Adds ``patient_control_number`` pulled from ``raw_json`` so the
|
||||||
|
operator can correlate each 999 back to the original claim batch.
|
||||||
|
|
||||||
|
SP25: this was previously ``api_routers/acks._ack_to_ui``. Moved
|
||||||
|
here so the live-tail event payload (``ack_received``) matches the
|
||||||
|
list-endpoint shape — the seam between persistence and streaming
|
||||||
|
depends on byte-for-byte equality.
|
||||||
|
"""
|
||||||
|
body = {
|
||||||
|
"id": row.id,
|
||||||
|
"source_batch_id": row.source_batch_id,
|
||||||
|
"accepted_count": row.accepted_count,
|
||||||
|
"rejected_count": row.rejected_count,
|
||||||
|
"received_count": row.received_count,
|
||||||
|
"ack_code": row.ack_code,
|
||||||
|
"parsed_at": (
|
||||||
|
row.parsed_at.isoformat().replace("+00:00", "Z")
|
||||||
|
if row.parsed_at is not None
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"patient_control_number": None,
|
||||||
|
}
|
||||||
|
raw = row.raw_json or {}
|
||||||
|
try:
|
||||||
|
set_responses = raw.get("set_responses") or []
|
||||||
|
if set_responses:
|
||||||
|
body["patient_control_number"] = set_responses[0].get("set_control_number")
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
pass
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def to_ui_ta1_ack(row: db.Ta1Ack) -> dict:
|
||||||
|
"""Map a ``Ta1Ack`` ORM row to the UI shape used by ``/api/ta1-acks``.
|
||||||
|
|
||||||
|
SP25: this was previously ``api_routers/ta1_acks._ta1_to_ui``.
|
||||||
|
Moved here so the live-tail event payload (``ta1_ack_received``)
|
||||||
|
matches the list-endpoint shape byte-for-byte.
|
||||||
|
|
||||||
|
Note: ``parsed_at`` uses naive ``isoformat()`` (no ``Z`` suffix) —
|
||||||
|
preserved from the original implementation. SQLite strips tzinfo
|
||||||
|
on read, so the field comes back as a naive datetime; we deliberately
|
||||||
|
keep the original (non-``Z``) rendering rather than re-attaching
|
||||||
|
UTC, because changing the wire shape now would drift both halves
|
||||||
|
of the live-tail contract.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"control_number": row.control_number,
|
||||||
|
"ack_code": row.ack_code,
|
||||||
|
"note_code": row.note_code,
|
||||||
|
"interchange_date": row.interchange_date.isoformat()
|
||||||
|
if row.interchange_date else None,
|
||||||
|
"interchange_time": row.interchange_time,
|
||||||
|
"sender_id": row.sender_id,
|
||||||
|
"receiver_id": row.receiver_id,
|
||||||
|
"source_batch_id": row.source_batch_id,
|
||||||
|
"parsed_at": row.parsed_at.isoformat() if row.parsed_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_ui_two77ca_ack(row: db.Two77caAck) -> dict:
|
||||||
|
"""Map a ``Two77caAck`` ORM row to the UI shape used by ``/api/277ca-acks``.
|
||||||
|
|
||||||
|
SP25: this was previously ``api._277ca_to_ui`` inlined alongside
|
||||||
|
the 277CA list endpoint. Moved here so the live-tail event
|
||||||
|
payload (``two77ca_ack_received``) matches the list-endpoint
|
||||||
|
shape byte-for-byte.
|
||||||
|
|
||||||
|
Note: ``parsed_at`` uses naive ``isoformat()`` (no ``Z`` suffix) —
|
||||||
|
same SQLite-tzinfo caveat as :func:`to_ui_ta1_ack`.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": row.id,
|
||||||
|
"control_number": row.control_number,
|
||||||
|
"accepted_count": row.accepted_count,
|
||||||
|
"rejected_count": row.rejected_count,
|
||||||
|
"paid_count": row.paid_count,
|
||||||
|
"pended_count": row.pended_count,
|
||||||
|
"source_batch_id": row.source_batch_id,
|
||||||
|
"parsed_at": row.parsed_at.isoformat() if row.parsed_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _address_to_ui(addr: dict | None) -> dict:
|
def _address_to_ui(addr: dict | None) -> dict:
|
||||||
"""Render a raw ``Address`` dict in the spec's parties address shape.
|
"""Render a raw ``Address`` dict in the spec's parties address shape.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"""Tests for the moved-to-store UI serializers for 999 / TA1 / 277CA acks.
|
||||||
|
|
||||||
|
The serializers were previously inlined in ``api_routers/acks.py``,
|
||||||
|
``api_routers/ta1_acks.py``, and ``api.py`` (`_277ca_to_ui`). SP25
|
||||||
|
moves them to ``cyclone.store.ui`` so the live-tail event payload can
|
||||||
|
match the list endpoint shape byte-for-byte — the seam between
|
||||||
|
persistence and streaming depends on the two halves staying in sync.
|
||||||
|
|
||||||
|
Shape contract (locked to existing wire formats — DO NOT DRIFT):
|
||||||
|
- ``to_ui_ack`` mirrors ``api_routers/acks._ack_to_ui`` exactly
|
||||||
|
- ``to_ui_ta1_ack`` mirrors ``api_routers/ta1_acks._ta1_to_ui`` exactly
|
||||||
|
- ``to_ui_two77ca_ack`` mirrors ``api._277ca_to_ui`` exactly
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cyclone import db
|
||||||
|
from cyclone.store.ui import to_ui_ack, to_ui_ta1_ack, to_ui_two77ca_ack
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ack_row() -> db.Ack:
|
||||||
|
"""Insert a 999 ack row and return it."""
|
||||||
|
with db.SessionLocal()() as s:
|
||||||
|
row = db.Ack(
|
||||||
|
source_batch_id="999-TEST-1",
|
||||||
|
accepted_count=3,
|
||||||
|
rejected_count=1,
|
||||||
|
received_count=4,
|
||||||
|
ack_code="P",
|
||||||
|
parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
|
||||||
|
raw_json={
|
||||||
|
"envelope": {"control_number": "000000001"},
|
||||||
|
"set_responses": [
|
||||||
|
{"set_control_number": "PCN-42"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
s.add(row)
|
||||||
|
s.commit()
|
||||||
|
s.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ta1_row() -> db.Ta1Ack:
|
||||||
|
"""Insert a TA1 ack row and return it."""
|
||||||
|
with db.SessionLocal()() as s:
|
||||||
|
row = db.Ta1Ack(
|
||||||
|
source_batch_id="TA1-TEST-1",
|
||||||
|
control_number="000000007",
|
||||||
|
interchange_date=date(2026, 7, 2),
|
||||||
|
interchange_time="1200",
|
||||||
|
ack_code="A",
|
||||||
|
note_code="000",
|
||||||
|
ack_generated_date=None,
|
||||||
|
sender_id="SENDER-1",
|
||||||
|
receiver_id="RECEIVER-1",
|
||||||
|
parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
|
||||||
|
raw_json={"envelope": {"control_number": "000000007"}},
|
||||||
|
)
|
||||||
|
s.add(row)
|
||||||
|
s.commit()
|
||||||
|
s.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def two77ca_row() -> db.Two77caAck:
|
||||||
|
"""Insert a 277CA ack row and return it."""
|
||||||
|
with db.SessionLocal()() as s:
|
||||||
|
row = db.Two77caAck(
|
||||||
|
source_batch_id="277CA-TEST-1",
|
||||||
|
control_number="000000011",
|
||||||
|
accepted_count=2,
|
||||||
|
rejected_count=1,
|
||||||
|
paid_count=0,
|
||||||
|
pended_count=4,
|
||||||
|
parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
|
||||||
|
raw_json={"envelope": {"control_number": "000000011"}, "claim_statuses": []},
|
||||||
|
)
|
||||||
|
s.add(row)
|
||||||
|
s.commit()
|
||||||
|
s.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# to_ui_ack (999)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_ui_ack_round_trips_999_fields(ack_row):
|
||||||
|
"""to_ui_ack must produce the exact ``_ack_to_ui`` wire shape."""
|
||||||
|
body = to_ui_ack(ack_row)
|
||||||
|
assert body["id"] == ack_row.id
|
||||||
|
assert body["source_batch_id"] == "999-TEST-1"
|
||||||
|
assert body["accepted_count"] == 3
|
||||||
|
assert body["rejected_count"] == 1
|
||||||
|
assert body["received_count"] == 4
|
||||||
|
assert body["ack_code"] == "P"
|
||||||
|
# SQLite strips tzinfo from DateTime(timezone=True) values — the
|
||||||
|
# original `_ack_to_ui` serializer therefore produces a tz-naive
|
||||||
|
# ISO string in tests. Preserve the byte-for-byte format.
|
||||||
|
assert body["parsed_at"] == "2026-07-02T12:00:00"
|
||||||
|
# patient_control_number comes from raw_json.set_responses[0]
|
||||||
|
assert body["patient_control_number"] == "PCN-42"
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_ui_ack_handles_missing_set_responses(ack_row):
|
||||||
|
"""No set_responses in raw_json → patient_control_number is None."""
|
||||||
|
with db.SessionLocal()() as s:
|
||||||
|
row = db.Ack(
|
||||||
|
source_batch_id="999-TEST-NULL",
|
||||||
|
accepted_count=0,
|
||||||
|
rejected_count=0,
|
||||||
|
received_count=0,
|
||||||
|
ack_code="A",
|
||||||
|
parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
|
||||||
|
raw_json={"envelope": {}, "set_responses": []},
|
||||||
|
)
|
||||||
|
s.add(row)
|
||||||
|
s.commit()
|
||||||
|
fresh = row
|
||||||
|
body = to_ui_ack(fresh)
|
||||||
|
assert body["patient_control_number"] is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# to_ui_ta1_ack (TA1)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_ui_ta1_ack_round_trips_fields(ta1_row):
|
||||||
|
"""to_ui_ta1_ack must produce the exact ``_ta1_to_ui`` wire shape."""
|
||||||
|
body = to_ui_ta1_ack(ta1_row)
|
||||||
|
assert body["id"] == ta1_row.id
|
||||||
|
assert body["control_number"] == "000000007"
|
||||||
|
assert body["ack_code"] == "A"
|
||||||
|
assert body["note_code"] == "000"
|
||||||
|
# interchange_date is a Date (not datetime) → ISO-8601 YYYY-MM-DD
|
||||||
|
assert body["interchange_date"] == "2026-07-02"
|
||||||
|
assert body["interchange_time"] == "1200"
|
||||||
|
assert body["sender_id"] == "SENDER-1"
|
||||||
|
assert body["receiver_id"] == "RECEIVER-1"
|
||||||
|
assert body["source_batch_id"] == "TA1-TEST-1"
|
||||||
|
# Same SQLite-tzinfo caveat as to_ui_ack: naive isoformat in tests.
|
||||||
|
assert body["parsed_at"] == "2026-07-02T12:00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_ui_ta1_ack_handles_null_interchange_date():
|
||||||
|
"""Null interchange_date → None (matches original behavior)."""
|
||||||
|
with db.SessionLocal()() as s:
|
||||||
|
row = db.Ta1Ack(
|
||||||
|
source_batch_id="TA1-TEST-NULL",
|
||||||
|
control_number="000000099",
|
||||||
|
interchange_date=None,
|
||||||
|
interchange_time=None,
|
||||||
|
ack_code="R",
|
||||||
|
note_code="001",
|
||||||
|
ack_generated_date=None,
|
||||||
|
sender_id="S",
|
||||||
|
receiver_id="R",
|
||||||
|
parsed_at=datetime(2026, 7, 2, 12, 0, tzinfo=timezone.utc),
|
||||||
|
raw_json={"envelope": {}},
|
||||||
|
)
|
||||||
|
s.add(row)
|
||||||
|
s.commit()
|
||||||
|
s.refresh(row)
|
||||||
|
fresh = row
|
||||||
|
body = to_ui_ta1_ack(fresh)
|
||||||
|
assert body["interchange_date"] is None
|
||||||
|
assert body["interchange_time"] is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# to_ui_two77ca_ack (277CA)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_ui_two77ca_ack_round_trips_fields(two77ca_row):
|
||||||
|
"""to_ui_two77ca_ack must produce the exact ``_277ca_to_ui`` wire shape."""
|
||||||
|
body = to_ui_two77ca_ack(two77ca_row)
|
||||||
|
assert body["id"] == two77ca_row.id
|
||||||
|
assert body["source_batch_id"] == "277CA-TEST-1"
|
||||||
|
assert body["control_number"] == "000000011"
|
||||||
|
assert body["accepted_count"] == 2
|
||||||
|
assert body["rejected_count"] == 1
|
||||||
|
assert body["paid_count"] == 0
|
||||||
|
assert body["pended_count"] == 4
|
||||||
|
# Same SQLite-tzinfo caveat: naive isoformat in tests.
|
||||||
|
assert body["parsed_at"] == "2026-07-02T12:00:00"
|
||||||
Reference in New Issue
Block a user