######################################################################
# Copyright
# John Holland <john@zoner.org>
# All rights reserved.
#
# This software is licensed as described in the file LICENSE.txt, which
# you should have received as part of this distribution.
#
######################################################################
"""
pyx12 error code registry.
Producers (validators in pyx12.map_if, the walker in pyx12.map_walker, and
the parser in pyx12.x12file) emit pyx12-internal error codes named
<LEVEL>_<X12_CODE>_<discriminator> (e.g. ELE_6_invalid_composite). Each
code maps to an ErrorCodeSpec in ERROR_CODES, which carries the level,
human-readable description, and the X12 4010 (AK) / 5010 (IK) codes to
emit in 997 / 999 / JSON output.
The table is the single source of truth: visitors look up the pyx12 code
to find the ack-output code, and any future remap (e.g. the historical
"SEG1" parser code routed to AK/IK "8") is a one-line table entry rather
than inline visitor logic.
This is the PR 1 slice — the table is fully populated but no producer
yet emits these codes. Visitors fall back to legacy raw X12-code lookup
when ERROR_CODES.get(err_cde) returns None. Producers migrate
incrementally in PR 2-4; PR 5 drops the legacy fallback.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
[docs]
LevelT = Literal["ISA", "GS", "ST", "SEG", "ELE"]
@dataclass(frozen=True, slots=True)
[docs]
class ErrorCodeSpec:
"""Single entry in ERROR_CODES.
ak_code and ik_code are the X12 acknowledgement codes for 4010 (AK)
and 5010 (IK) output respectively. None means the code is not
surfaced in that channel (e.g. parser HL1/HL2/LX bypass the ack
today; preserve that behavior with both = None).
x12_description is the official X12 spec definition for the error code
(e.g., "Required Data Element Missing" for IK4-03 code "1"). Provides
cross-reference to the X12 Implementation Guide.
"""
# Element-level pyx12 codes. Module-level constants pair with each
# ERROR_CODES entry for IDE auto-complete and CLI grep-ability.
# Sorted: numeric X12 codes (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13),
# then implementation-dependent codes (I6, I9, I10, I11, I12, I13).
[docs]
ELE_1_MANDATORY_MISSING = "ELE_1_mandatory_missing"
[docs]
ELE_2_CONDITIONAL_MISSING = "ELE_2_conditional_missing"
[docs]
ELE_3_TOO_MANY_ELEMENTS = "ELE_3_too_many_elements"
[docs]
ELE_4_TOO_SHORT = "ELE_4_too_short"
[docs]
ELE_5_TOO_LONG = "ELE_5_too_long"
[docs]
ELE_6_INVALID_COMPOSITE = "ELE_6_invalid_composite"
[docs]
ELE_6_TRAILING_SPACE = "ELE_6_trailing_space"
[docs]
ELE_6_CONTROL_CHAR = "ELE_6_control_char"
[docs]
ELE_6_INVALID_TYPE_CHAR = "ELE_6_invalid_type_char"
[docs]
ELE_7_INVALID_CODE = "ELE_7_invalid_code"
[docs]
ELE_7_REGEX_FAIL = "ELE_7_regex_fail"
[docs]
ELE_8_INVALID_DATE = "ELE_8_invalid_date"
[docs]
ELE_8_INVALID_DATE_RANGE = "ELE_8_invalid_date_range"
[docs]
ELE_9_INVALID_TIME = "ELE_9_invalid_time"
[docs]
ELE_9_INVALID_TIME_OF_DAY = "ELE_9_invalid_time_of_day"
[docs]
ELE_10_SYNTAX_EXCLUSIVE = "ELE_10_syntax_exclusive"
[docs]
ELE_12_TOO_MANY_REPETITIONS = "ELE_12_too_many_repetitions"
[docs]
ELE_13_TOO_MANY_COMPONENTS = "ELE_13_too_many_components"
[docs]
ELE_I10_NOT_USED = "ELE_I10_NOT_USED"
# Composite-level pyx12 codes. Composites historically emitted via
# EleError carrying the seg_error code semantics (see _composite.py).
# Sorted: numeric X12 codes (1, 3, 5).
[docs]
COMP_1_MANDATORY_MISSING = "COMP_1_mandatory_missing"
[docs]
COMP_3_TOO_MANY_SUBELEMENTS = "COMP_3_too_many_subelements"
[docs]
COMP_5_NOT_USED = "COMP_5_not_used"
# Segment-level pyx12 codes.
# Sorted: numeric X12 codes (1, 2, 3, 4, 5, 8, 10), then non-numeric.
# Codes 1-5 are validator / walker / parser sources; code 8 is
# synthetic (apply_segment_errors rollup + parser HL/LX series).
[docs]
SEG_1_SEGMENT_NOT_FOUND = "SEG_1_segment_not_found"
[docs]
SEG_1_INVALID_SEG_ID = "SEG_1_invalid_seg_id"
[docs]
SEG_1_LEADING_SPACE = "SEG_1_leading_space"
[docs]
SEG_2_SYNTAX_RELATIONAL = "SEG_2_syntax_relational"
[docs]
SEG_2_SEGMENT_NOT_USED = "SEG_2_segment_not_used"
[docs]
SEG_2_LOOP_NOT_USED = "SEG_2_loop_not_used"
[docs]
SEG_3_TOO_MANY_ELEMENTS = "SEG_3_too_many_elements"
[docs]
SEG_3_TOO_MANY_SUBELEMENTS = "SEG_3_too_many_subelements"
[docs]
SEG_3_MANDATORY_SEGMENT_MISSING = "SEG_3_mandatory_segment_missing"
[docs]
SEG_3_MANDATORY_LOOP_MISSING = "SEG_3_mandatory_loop_missing"
[docs]
SEG_4_LOOP_REPEAT_EXCEEDED = "SEG_4_loop_repeat_exceeded"
[docs]
SEG_5_SEGMENT_REPEAT_EXCEEDED = "SEG_5_segment_repeat_exceeded"
[docs]
SEG_8_HAS_DATA_ELEMENT_ERRORS = "SEG_8_has_data_element_errors"
[docs]
SEG_8_SEGMENT_EMPTY = "SEG_8_segment_empty"
[docs]
SEG_8_TRAILING_TERMINATORS = "SEG_8_trailing_terminators"
[docs]
SEG_8_HL_COUNT_MISMATCH = "SEG_8_hl_count_mismatch"
[docs]
SEG_8_HL_INVALID_PARENT = "SEG_8_hl_invalid_parent"
[docs]
SEG_8_LX_COUNT_MISMATCH = "SEG_8_lx_count_mismatch"
[docs]
SEG_10_SYNTAX_EXCLUSIVE = "SEG_10_syntax_exclusive"
[docs]
ERROR_CODES: dict[str, ErrorCodeSpec] = {
# --- Element-level: sorted by numeric X12 code (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13) ---
ELE_1_MANDATORY_MISSING: ErrorCodeSpec(
code=ELE_1_MANDATORY_MISSING,
level="ELE",
description="Mandatory data element missing",
ak_code="1",
ik_code="1",
x12_description="Required Data Element Missing",
),
ELE_2_CONDITIONAL_MISSING: ErrorCodeSpec(
code=ELE_2_CONDITIONAL_MISSING,
level="ELE",
description="Conditional required data element missing",
ak_code="2",
ik_code="2",
x12_description="Conditional Required Data Element Missing",
),
ELE_3_TOO_MANY_ELEMENTS: ErrorCodeSpec(
code=ELE_3_TOO_MANY_ELEMENTS,
level="ELE",
description="Too many data elements",
ak_code="3",
ik_code="3",
x12_description="Too Many Data Elements",
),
ELE_4_TOO_SHORT: ErrorCodeSpec(
code=ELE_4_TOO_SHORT,
level="ELE",
description="Data element too short",
ak_code="4",
ik_code="4",
x12_description="Data Element Too Short",
),
ELE_5_TOO_LONG: ErrorCodeSpec(
code=ELE_5_TOO_LONG,
level="ELE",
description="Data element too long",
ak_code="5",
ik_code="5",
x12_description="Data Element Too Long",
),
ELE_6_INVALID_COMPOSITE: ErrorCodeSpec(
code=ELE_6_INVALID_COMPOSITE,
level="ELE",
description="Composite element used at non-composite position",
ak_code="6",
ik_code="6",
x12_description="Invalid Character In Data Element",
),
ELE_6_TRAILING_SPACE: ErrorCodeSpec(
code=ELE_6_TRAILING_SPACE,
level="ELE",
description="Trailing space in data element",
ak_code="6",
ik_code="6",
x12_description="Invalid Character In Data Element",
),
ELE_6_CONTROL_CHAR: ErrorCodeSpec(
code=ELE_6_CONTROL_CHAR,
level="ELE",
description="Control character in data element",
ak_code="6",
ik_code="6",
x12_description="Invalid Character In Data Element",
),
ELE_6_INVALID_TYPE_CHAR: ErrorCodeSpec(
code=ELE_6_INVALID_TYPE_CHAR,
level="ELE",
description="Invalid character for declared element data type",
ak_code="6",
ik_code="6",
x12_description="Invalid Character In Data Element",
),
ELE_7_INVALID_CODE: ErrorCodeSpec(
code=ELE_7_INVALID_CODE,
level="ELE",
description="Data element value not in valid code list",
ak_code="7",
ik_code="7",
x12_description="Invalid Code Value",
),
ELE_7_REGEX_FAIL: ErrorCodeSpec(
code=ELE_7_REGEX_FAIL,
level="ELE",
description="Data element does not match required regex pattern",
ak_code="7",
ik_code="7",
x12_description="Invalid Code Value",
),
ELE_8_INVALID_DATE: ErrorCodeSpec(
code=ELE_8_INVALID_DATE,
level="ELE",
description="Data element contains an invalid date (D8/D6/DT)",
ak_code="8",
ik_code="8",
x12_description="Invalid Date",
),
ELE_8_INVALID_DATE_RANGE: ErrorCodeSpec(
code=ELE_8_INVALID_DATE_RANGE,
level="ELE",
description="Data element contains an invalid date range (RD8)",
ak_code="8",
ik_code="8",
x12_description="Invalid Date",
),
ELE_9_INVALID_TIME: ErrorCodeSpec(
code=ELE_9_INVALID_TIME,
level="ELE",
description="Data element contains an invalid time (TM)",
ak_code="9",
ik_code="9",
x12_description="Invalid Time",
),
ELE_9_INVALID_TIME_OF_DAY: ErrorCodeSpec(
code=ELE_9_INVALID_TIME_OF_DAY,
level="ELE",
description="Data element contains an invalid time-of-day value",
ak_code="9",
ik_code="9",
x12_description="Invalid Time",
),
ELE_10_SYNTAX_EXCLUSIVE: ErrorCodeSpec(
code=ELE_10_SYNTAX_EXCLUSIVE,
level="ELE",
description="Exclusion Condition Violated",
ak_code="10",
ik_code="10",
x12_description="Exclusion Condition Violated",
),
ELE_12_TOO_MANY_REPETITIONS: ErrorCodeSpec(
code=ELE_12_TOO_MANY_REPETITIONS,
level="ELE",
description="Too many repetitions of data element",
ak_code="12",
ik_code="12",
x12_description="Too Many Repetitions",
),
ELE_13_TOO_MANY_COMPONENTS: ErrorCodeSpec(
code=ELE_13_TOO_MANY_COMPONENTS,
level="ELE",
description="Too many components in data element",
ak_code="13",
ik_code="13",
x12_description="Too Many Components",
),
ELE_I10_NOT_USED: ErrorCodeSpec(
code=ELE_I10_NOT_USED,
level="ELE",
description="Implementation: data element marked Not Used (usage='N')",
ak_code=None,
ik_code="I10",
x12_description="Implementation 'Not Used' Data Element Present",
),
# --- Composite-level: sorted by numeric X12 code (1, 3, 5) ---
COMP_1_MANDATORY_MISSING: ErrorCodeSpec(
code=COMP_1_MANDATORY_MISSING,
level="ELE",
description="Mandatory composite element missing",
ak_code="1",
ik_code="1",
x12_description="Required Data Element Missing",
),
COMP_3_TOO_MANY_SUBELEMENTS: ErrorCodeSpec(
code=COMP_3_TOO_MANY_SUBELEMENTS,
level="ELE",
description="Composite has too many sub-elements",
ak_code="3",
ik_code="3",
x12_description="Too Many Data Elements",
),
COMP_5_NOT_USED: ErrorCodeSpec(
code=COMP_5_NOT_USED,
level="ELE",
description="Composite marked Not Used (usage='N')",
ak_code="5",
ik_code="I10",
x12_description="Implementation 'Not Used' Data Element Present (5010)",
),
# --- Segment-level: sorted by numeric X12 code (1, 2, 3, 4, 5, 8, 10),
# then non-numeric for code 8 (multiple sub-codes) ---
SEG_1_SEGMENT_NOT_FOUND: ErrorCodeSpec(
code=SEG_1_SEGMENT_NOT_FOUND,
level="SEG",
description="Unrecognized segment ID",
ak_code="1",
ik_code="1",
x12_description="Unrecognized segment ID",
),
SEG_1_INVALID_SEG_ID: ErrorCodeSpec(
code=SEG_1_INVALID_SEG_ID,
level="SEG",
description="Segment identifier is malformed (parser-time invalid seg ID)",
ak_code="1",
ik_code="1",
x12_description="Unrecognized segment ID",
),
SEG_1_LEADING_SPACE: ErrorCodeSpec(
code=SEG_1_LEADING_SPACE,
level="SEG",
description="Segment line started with leading whitespace (parser-time)",
ak_code="1",
ik_code="1",
x12_description="Unrecognized segment ID",
),
SEG_2_SYNTAX_RELATIONAL: ErrorCodeSpec(
code=SEG_2_SYNTAX_RELATIONAL,
level="SEG",
description="Segment syntax rule (relational) violated",
ak_code="2",
ik_code="2",
x12_description="Unexpected segment",
),
SEG_2_SEGMENT_NOT_USED: ErrorCodeSpec(
code=SEG_2_SEGMENT_NOT_USED,
level="SEG",
description="Segment marked Not Used (usage='N')",
ak_code="2",
ik_code="I4",
x12_description="Implementation 'Not Used' Segment Present",
),
SEG_2_LOOP_NOT_USED: ErrorCodeSpec(
code=SEG_2_LOOP_NOT_USED,
level="SEG",
description="Loop marked Not Used (usage='N')",
ak_code="2",
ik_code="I4",
x12_description="Implementation 'Not Used' Segment Present",
),
SEG_3_TOO_MANY_ELEMENTS: ErrorCodeSpec(
code=SEG_3_TOO_MANY_ELEMENTS,
level="SEG",
description="Segment has too many elements",
ak_code="3",
ik_code="3",
x12_description="Required Segment Missing",
),
SEG_3_TOO_MANY_SUBELEMENTS: ErrorCodeSpec(
code=SEG_3_TOO_MANY_SUBELEMENTS,
level="SEG",
description="Segment has too many sub-elements in a composite",
ak_code="3",
ik_code="3",
x12_description="Required Segment Missing",
),
SEG_3_MANDATORY_SEGMENT_MISSING: ErrorCodeSpec(
code=SEG_3_MANDATORY_SEGMENT_MISSING,
level="SEG",
description="Mandatory segment missing",
ak_code="3",
ik_code="3",
x12_description="Required Segment Missing",
),
SEG_3_MANDATORY_LOOP_MISSING: ErrorCodeSpec(
code=SEG_3_MANDATORY_LOOP_MISSING,
level="SEG",
description="Mandatory loop missing",
ak_code="3",
ik_code="3",
x12_description="Required Segment Missing",
),
SEG_4_LOOP_REPEAT_EXCEEDED: ErrorCodeSpec(
code=SEG_4_LOOP_REPEAT_EXCEEDED,
level="SEG",
description="Loop repeat count exceeded (loop occurs over maximum times)",
ak_code="4",
ik_code="4",
x12_description="Loop Occurs Over Maximum Times",
),
SEG_5_SEGMENT_REPEAT_EXCEEDED: ErrorCodeSpec(
code=SEG_5_SEGMENT_REPEAT_EXCEEDED,
level="SEG",
description="Segment repeat count exceeded (segment exceeds maximum use)",
ak_code="5",
ik_code="5",
x12_description="Segment Exceeds Maximum Use",
),
SEG_8_HAS_DATA_ELEMENT_ERRORS: ErrorCodeSpec(
code=SEG_8_HAS_DATA_ELEMENT_ERRORS,
level="SEG",
description=(
"Segment has data element errors. Used by apply_segment_errors "
"to roll up element-level validator errors (too-many-elements, "
"syntax violations, mandatory composite missing, etc.) into a "
"spec-correct IK3-04 / AK3-04 code per PR #161. The original "
"element-level pyx12 code is preserved in the err_str."
),
ak_code="8",
ik_code="8",
x12_description="Segment Has Data Element Errors",
),
SEG_8_SEGMENT_EMPTY: ErrorCodeSpec(
code=SEG_8_SEGMENT_EMPTY,
level="SEG",
description="Segment is empty (parser-time)",
ak_code="8",
ik_code="8",
x12_description="Segment Has Data Element Errors",
),
SEG_8_TRAILING_TERMINATORS: ErrorCodeSpec(
code=SEG_8_TRAILING_TERMINATORS,
level="SEG",
description="Segment has trailing element terminators (was 'SEG1')",
ak_code="8",
ik_code="8",
x12_description="Segment Has Data Element Errors",
),
SEG_8_HL_COUNT_MISMATCH: ErrorCodeSpec(
code=SEG_8_HL_COUNT_MISMATCH,
level="SEG",
description="HL count does not match self-reported HL01 (was 'HL1')",
ak_code=None,
ik_code=None,
x12_description="Segment Has Data Element Errors",
),
SEG_8_HL_INVALID_PARENT: ErrorCodeSpec(
code=SEG_8_HL_INVALID_PARENT,
level="SEG",
description="HL parent reference is not on the HL stack (was 'HL2')",
ak_code=None,
ik_code=None,
x12_description="Segment Has Data Element Errors",
),
SEG_8_LX_COUNT_MISMATCH: ErrorCodeSpec(
code=SEG_8_LX_COUNT_MISMATCH,
level="SEG",
description="2400/LX01 service line number does not match running count (was 'LX')",
ak_code=None,
ik_code=None,
x12_description="Segment Has Data Element Errors",
),
SEG_10_SYNTAX_EXCLUSIVE: ErrorCodeSpec(
code=SEG_10_SYNTAX_EXCLUSIVE,
level="SEG",
description="Segment syntax rule (exclusive) violated",
ak_code="10",
ik_code="10",
x12_description="Exclusion Condition Violated",
),
}
[docs]
def x12_code_for(err_cde: str, prefer_5010: bool = True) -> str | None:
"""Resolve a pyx12 error code to its X12 ack code.
Returns None for codes that do not surface in the requested ack
channel (e.g. the SEG_8_HL_* parser codes that historically were
filtered out).
Falls through to err_cde unchanged when err_cde is not a pyx12
code (legacy X12 codes like "6" / "8" emitted by un-migrated
producers during the PR 1-4 transition). PR 5 removes that
fallthrough — once all producers emit pyx12 codes only, an unknown
err_cde is an error.
"""
spec = ERROR_CODES.get(err_cde)
if spec is None:
# Legacy raw X12 code; pass through unchanged.
return err_cde
if prefer_5010:
return spec.ik_code if spec.ik_code is not None else spec.ak_code
return spec.ak_code if spec.ak_code is not None else spec.ik_code