0f1e609888
CycloneStore split precedent (SP21) — lift three helpers from scheduler.py + api.py into one module. Both callers will switch to this in Task 6. The new package also defines HandleResult and the HANDLERS registry; handle_999 / handle_ta1 / handle_277ca / handle_835 fill in over Tasks 2-5. The registry is lazy + best-effort: register_handlers() catches broad Exception so a partial-modification SyntaxError in one in-flight handler module can't break scheduler or API import. 11 tests added; existing suite unchanged (1 failed + 2 errors pre-existing in test_provider_extended_response.py are not introduced by this commit).
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""Ack ID helpers shared between the scheduler and the FastAPI
|
|
endpoints (SP27 Task 1).
|
|
|
|
Lifted from ``scheduler.py:_ack_count_summary`` +
|
|
``_ack_synthetic_source_batch_id`` + ``_277ca_synthetic_source_batch_id`` —
|
|
all three had inline copies in both ``scheduler.py`` and ``api.py``
|
|
prior to this SP. Both callers now import from this module.
|
|
|
|
Helpers
|
|
-------
|
|
``ack_count_summary(result)``
|
|
Aggregate ``(received, accepted, rejected, ack_code)`` from a
|
|
parsed ``ParseResult999``, trusting set-level IK5 over the
|
|
functional-group AK9. Gainwell's MFT ships AK9 segments that
|
|
contradict the per-set IK5 (e.g. ``AK9*A*1*1*1`` with
|
|
``IK5*A``), so trusting AK9's rejected count over-reports.
|
|
See scheduler commit ``6507a8c`` for the operational context.
|
|
|
|
``ack_synthetic_source_batch_id(icn, *, pcn, source_filename)``
|
|
Build a unique-per-file ``batches.id`` for a 999 that ships
|
|
without its own source batch. Falls back to a hash suffix of
|
|
the filename so daily pulls don't all collapse onto the same
|
|
ICN. Gainwell's MFT ships every 999 with the default ICN
|
|
``000000001`` (per the scheduler docstring).
|
|
|
|
``two77ca_synthetic_source_batch_id(icn)``
|
|
Same idea for a 277CA without its own source batch.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Any
|
|
|
|
|
|
def ack_count_summary(result: Any) -> tuple[int, int, int, str]:
|
|
"""Aggregate ``(received, accepted, rejected, ack_code)`` from
|
|
a ``ParseResult999``.
|
|
|
|
Counts are derived from the set-level ``IK5`` responses
|
|
(one per ``AK2`` in the 999), not the functional-group ``AK9``.
|
|
Gainwell's MFT ships contradictory AK9 segments; the per-set
|
|
IK5 is the authoritative per-claim accept/reject signal.
|
|
"""
|
|
sets = result.set_responses
|
|
received = len(sets)
|
|
accepted = sum(1 for s in sets if s.set_accept_reject.code == "A")
|
|
rejected = received - accepted
|
|
if rejected == 0:
|
|
code = "A"
|
|
elif accepted == 0:
|
|
code = "R"
|
|
else:
|
|
code = "P"
|
|
return (received, accepted, rejected, code)
|
|
|
|
|
|
def ack_synthetic_source_batch_id(
|
|
interchange_control_number: str,
|
|
*,
|
|
pcn: str | None = None,
|
|
source_filename: str | None = None,
|
|
) -> str:
|
|
"""Synthetic ``batches.id`` for a received 999 with no source batch.
|
|
|
|
Precedence for the human-readable part of the id (column is
|
|
``VARCHAR(32)``):
|
|
|
|
1. ``999-{pcn}-{hash8}`` if ``AK2`` set_control_number is
|
|
present (the common case). 4 + 9 + 1 + 8 = 22 chars max.
|
|
2. ``999-{icn}-{hash8}`` if no AK2 (envelope-only 999).
|
|
3. ``999-{hash12}`` if no filename either (shouldn't happen
|
|
in production).
|
|
"""
|
|
short_hash = ""
|
|
if source_filename:
|
|
short_hash = hashlib.sha1(source_filename.encode("utf-8")).hexdigest()[:8]
|
|
if pcn and pcn.strip():
|
|
return f"999-{pcn.strip()}-{short_hash}"
|
|
icn = (interchange_control_number or "").strip() or "000000001"
|
|
if short_hash:
|
|
return f"999-{icn}-{short_hash}"
|
|
return f"999-{short_hash or icn}"
|
|
|
|
|
|
def two77ca_synthetic_source_batch_id(interchange_control_number: str) -> str:
|
|
"""Synthetic ``batches.id`` for a received 277CA with no source batch."""
|
|
return f"277CA-{(interchange_control_number or '').strip() or '000000001'}"
|