feat(parsers+api+ui+db): 999 ACK transaction set end-to-end (SP3 P3)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Tests for the 999 ACK Pydantic models.
|
||||
|
||||
SP3 Phase 3 (T10): minimal typed shape needed by the parser (T11),
|
||||
serializer (T12), and auto-ACK builder (T15). The spec defines a richer
|
||||
``FunctionalGroupResponse`` model with nested ``SetHeader`` /
|
||||
``ElementError`` types, but the actual code paths only need the
|
||||
``AK1``/``AK2`` headers + ``AK5`` accept/reject + ``AK9`` counts. We keep
|
||||
the leaner models in this file; richer shapes can be added later without
|
||||
breaking the existing public API.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from cyclone.parsers.models import BatchSummary, Envelope
|
||||
from cyclone.parsers.models_999 import (
|
||||
AcknowledgmentHeader,
|
||||
FunctionalGroupAck,
|
||||
ParseResult999,
|
||||
SegmentContext,
|
||||
SegmentError,
|
||||
SetAcceptReject,
|
||||
SetFunctionalGroupResponse,
|
||||
)
|
||||
|
||||
|
||||
def _build_result() -> ParseResult999:
|
||||
return ParseResult999(
|
||||
envelope=Envelope(
|
||||
sender_id="RECEIVERID",
|
||||
receiver_id="SUBMITTERID",
|
||||
control_number="000000001",
|
||||
transaction_date=date(2024, 1, 1),
|
||||
implementation_guide="005010X231A1",
|
||||
),
|
||||
functional_group_acks=[
|
||||
FunctionalGroupAck(
|
||||
ak1=AcknowledgmentHeader(
|
||||
functional_id_code="HC",
|
||||
group_control_number="0001",
|
||||
),
|
||||
received_count=1,
|
||||
accepted_count=1,
|
||||
rejected_count=0,
|
||||
ack_code="A",
|
||||
),
|
||||
],
|
||||
set_responses=[
|
||||
SetFunctionalGroupResponse(
|
||||
ak2=AcknowledgmentHeader(
|
||||
functional_id_code="837",
|
||||
group_control_number="0001",
|
||||
),
|
||||
set_control_number="0001",
|
||||
transaction_set_identifier="837",
|
||||
segment_errors=[],
|
||||
set_accept_reject=SetAcceptReject(code="A"),
|
||||
),
|
||||
],
|
||||
summary=BatchSummary(
|
||||
input_file="minimal_999.txt",
|
||||
control_number="000000001",
|
||||
transaction_date=date(2024, 1, 1),
|
||||
total_claims=1,
|
||||
passed=1,
|
||||
failed=0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_parse_result_999_minimal_round_trip():
|
||||
"""Build a minimal ParseResult999 and round-trip via JSON."""
|
||||
r = _build_result()
|
||||
blob = json.loads(r.model_dump_json())
|
||||
assert blob["envelope"]["control_number"] == "000000001"
|
||||
assert blob["functional_group_acks"][0]["ack_code"] == "A"
|
||||
assert blob["set_responses"][0]["set_accept_reject"]["code"] == "A"
|
||||
# Round-trip back to the model
|
||||
r2 = ParseResult999.model_validate(blob)
|
||||
assert r2.functional_group_acks[0].received_count == 1
|
||||
assert r2.set_responses[0].ak2.functional_id_code == "837"
|
||||
|
||||
|
||||
def test_set_accept_reject_codes():
|
||||
"""A / R / E / W / X all parse cleanly on SetAcceptReject."""
|
||||
for code in ("A", "R", "E", "W", "X"):
|
||||
sar = SetAcceptReject(code=code)
|
||||
assert sar.code == code
|
||||
# round-trip via model_dump
|
||||
assert SetAcceptReject.model_validate(sar.model_dump()).code == code
|
||||
|
||||
|
||||
def test_functional_group_ack_counts_serialize():
|
||||
"""accepted/rejected counts on FunctionalGroupAck round-trip."""
|
||||
fg = FunctionalGroupAck(
|
||||
ak1=AcknowledgmentHeader(functional_id_code="HC", group_control_number="1"),
|
||||
received_count=5,
|
||||
accepted_count=3,
|
||||
rejected_count=2,
|
||||
ack_code="P", # "partial" — surfaced in AK909
|
||||
)
|
||||
blob = json.loads(fg.model_dump_json())
|
||||
assert blob["received_count"] == 5
|
||||
assert blob["accepted_count"] == 3
|
||||
assert blob["rejected_count"] == 2
|
||||
assert blob["ack_code"] == "P"
|
||||
# default values populated
|
||||
rebuilt = FunctionalGroupAck.model_validate(blob)
|
||||
assert rebuilt.ak1.functional_id_code == "HC"
|
||||
assert rebuilt.received_count == 5
|
||||
|
||||
|
||||
def test_segment_error_carries_optional_element_ref():
|
||||
"""SegmentError.element_position is optional; defaults to None when absent."""
|
||||
seg_err = SegmentError(
|
||||
context=SegmentContext(
|
||||
segment_id="CLM",
|
||||
segment_position=12,
|
||||
loop_id="2300",
|
||||
implementation_convention_ref=None,
|
||||
),
|
||||
error_code="8", # "Segment Exceeds Maximum Use"
|
||||
)
|
||||
assert seg_err.element_position is None
|
||||
assert seg_err.element_reference is None
|
||||
# Round-trip
|
||||
blob = json.loads(seg_err.model_dump_json())
|
||||
assert blob["error_code"] == "8"
|
||||
assert blob["element_position"] is None
|
||||
rebuilt = SegmentError.model_validate(blob)
|
||||
assert rebuilt.context.segment_id == "CLM"
|
||||
Reference in New Issue
Block a user