Generating ARCOS Fixed-Width Transaction Files
Build byte-exact ARCOS fixed-width transaction records in Python — field widths, zero and space padding, justification, and truncation guards with a hash
A fixed-width file has no delimiters to save you. If a single field is one byte too wide, every column to its right shifts, and the DEA’s ARCOS processor either rejects the batch or — worse — silently reads a quantity where it expected a date. This is the specific failure mode that makes ARCOS serialization unforgiving: correctness is not “the values are right,” it is “every value occupies exactly its column range, padded on the correct side, truncated nowhere.” This recipe builds byte-exact ARCOS transaction records in Python, validates that each is the mandated width, and hashes the file for the audit trail. It implements the serialization step deferred by the parent topic, ARCOS Transaction Reporting, where the reportable events are selected and assembled; here we turn assembled records into the bytes ARCOS ingests.
The problem decomposes into three deterministic questions for every field: how wide is it, which side does it pad on, and what happens when the value is too long. Answer all three per field and the record is correct by construction; leave any one to chance and the batch fails inspection.
Prerequisites & Environment
- Python 3.11+ — the implementation uses
dataclasses(frozen=True),enum.Enum, and the union-type syntax. - Standard library only:
hashlib,logging,datetime,dataclasses,enum. Addpytestonly for the test block. - Encoding: ARCOS records are ASCII. The serializer encodes to strict ASCII so a stray non-ASCII byte fails loudly rather than widening a field.
- Regulatory context you should already hold:
21 CFR § 1304.33(ARCOS periodic transaction reporting) and21 CFR § 1304.04(two-year retrievable retention of the filing). The upstream selection and record assembly — which events are reportable and how they map to transaction codes — belong to the parent topic and are assumed done. Each record’s NDC must already be the canonical 11-digit identity produced by NDC parsing regex patterns for Python, because the serializer zero-pads on the left and would happily mask a lost leading zero.
Implementation: A Frozen Field Spec and a Fixed-Width Serializer
The design separates the layout (a frozen field specification: name, width, justification, pad character) from the serializer (a pure function that applies the spec). The spec is data, so it is auditable and testable on its own; the serializer is small and total. Every field declares whether it right-justifies with zeros (numeric) or left-justifies with spaces (text), and the serializer raises on any value that would exceed its width — the truncation hazard is converted into a hard failure.
from __future__ import annotations
import hashlib
import logging
from dataclasses import dataclass
from datetime import date
from enum import Enum
logger = logging.getLogger("pharmacy.reporting.arcos.fixedwidth")
class Justify(str, Enum):
LEFT = "LEFT" # text: pad on the right
RIGHT = "RIGHT" # numeric: pad on the left
@dataclass(frozen=True)
class Field:
"""One fixed-width column. Immutable so the layout cannot drift at runtime."""
name: str
width: int
justify: Justify
pad: str # single character: " " for text, "0" for numeric
# The ARCOS record layout, left to right. Widths are illustrative of the ARCOS
# fixed-width discipline; confirm exact widths against the current DEA ARCOS
# Registrant Handbook before production filing.
ARCOS_LAYOUT: tuple[Field, ...] = (
Field("reporter_dea", 9, Justify.LEFT, " "),
Field("action", 1, Justify.LEFT, " "),
Field("transaction_code", 2, Justify.LEFT, " "),
Field("ndc_11", 11, Justify.RIGHT, "0"),
Field("quantity", 10, Justify.RIGHT, "0"),
Field("associated_dea", 9, Justify.LEFT, " "),
Field("transaction_date", 8, Justify.RIGHT, "0"), # YYYYMMDD
)
RECORD_WIDTH = sum(f.width for f in ARCOS_LAYOUT) # 50
class ArcosFieldError(ValueError):
"""Raised when a value cannot fit its field — never truncate silently."""
@dataclass(frozen=True)
class ArcosTransaction:
reporter_dea: str
action: str
transaction_code: str
ndc_11: str
quantity: int # implied-decimal integer units
associated_dea: str
transaction_date: date
def _fit(field: Field, raw: str) -> str:
"""Pad `raw` to exactly field.width, or raise if it overflows."""
if len(raw) > field.width:
# A one-byte overflow would shift every downstream field — refuse it.
raise ArcosFieldError(
f"field '{field.name}' value width {len(raw)} exceeds {field.width}"
)
if field.justify is Justify.RIGHT:
return raw.rjust(field.width, field.pad)
return raw.ljust(field.width, field.pad)
def serialize_record(txn: ArcosTransaction) -> bytes:
"""Render one transaction to a byte-exact fixed-width ASCII record."""
raw_values = {
"reporter_dea": txn.reporter_dea,
"action": txn.action,
"transaction_code": txn.transaction_code,
# Digits only — a hyphen here would consume a column and shift the record.
"ndc_11": txn.ndc_11,
"quantity": str(txn.quantity),
"associated_dea": txn.associated_dea,
"transaction_date": f"{txn.transaction_date:%Y%m%d}",
}
line = "".join(_fit(f, raw_values[f.name]) for f in ARCOS_LAYOUT)
if len(line) != RECORD_WIDTH:
raise ArcosFieldError(f"assembled width {len(line)} != {RECORD_WIDTH}")
# Strict ASCII: a non-ASCII byte would widen the record — fail loudly.
return line.encode("ascii")
def serialize_batch(transactions: list[ArcosTransaction]) -> tuple[bytes, str]:
"""Concatenate records and return (file_bytes, sha256_hex) for the audit trail."""
records = [serialize_record(t) for t in transactions]
body = b"\n".join(records) + (b"\n" if records else b"")
audit_hash = hashlib.sha256(body).hexdigest()
logger.info(
"arcos_file_serialized records=%d bytes=%d audit_hash=%s",
len(records), len(body), audit_hash,
)
return body, audit_hash
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
txns = [
ArcosTransaction("PM1234567", "A", "P", "00093005801", 500, "PM9988776", date(2026, 7, 10)),
ArcosTransaction("PM1234567", "D", "S", "00093005801", 120, "PB5544332", date(2026, 7, 12)),
]
body, audit_hash = serialize_batch(txns)
for line in body.decode("ascii").splitlines():
print(f"[{line}] len={len(line)}")
print("audit_hash:", audit_hash)
The _fit helper is the whole safety story: it pads to the exact width when the value fits and raises ArcosFieldError the moment it does not, so a too-long DEA number or an over-range quantity stops the batch instead of corrupting it. Because ARCOS_LAYOUT is a frozen tuple of frozen Field objects, the layout is data an auditor can read and a test can assert against.
Verification & Testing
Two things must be proven: that every serialized record is exactly RECORD_WIDTH bytes with each field in its declared column, and that an overflowing value raises rather than truncates. The block below asserts exact widths, verifies zero- and space-padding on representative fields, and confirms the overflow guard fires.
import pytest
from datetime import date
def _sample() -> ArcosTransaction:
return ArcosTransaction("PM1234567", "A", "P", "00093005801", 500, "PM9988776", date(2026, 7, 10))
def test_record_is_exact_width():
rec = serialize_record(_sample())
assert len(rec) == RECORD_WIDTH == 50
def test_numeric_fields_zero_pad_left():
rec = serialize_record(_sample()).decode("ascii")
# NDC occupies columns [12:23) after reporter(9)+action(1)+code(2)=12.
assert rec[12:23] == "00093005801"
# Quantity 500 zero-pads to width 10, right-justified.
assert rec[23:33] == "0000000500"
def test_text_fields_space_pad_right():
rec = serialize_record(_sample()).decode("ascii")
assert rec[0:9] == "PM1234567" # exactly 9, no padding needed
short = ArcosTransaction("PB12", "A", "P", "00093005801", 1, "PB99", date(2026, 7, 1))
rec2 = serialize_record(short).decode("ascii")
assert rec2[0:9] == "PB12 " # left-justified, space-padded to 9
def test_overflow_raises_never_truncates():
bad = ArcosTransaction("PM1234567890", "A", "P", "00093005801", 1, "PB99", date(2026, 7, 1))
with pytest.raises(ArcosFieldError):
serialize_record(bad) # 12-char DEA cannot fit a 9-col field
def test_batch_hash_is_stable():
body_a, hash_a = serialize_batch([_sample()])
body_b, hash_b = serialize_batch([_sample()])
assert body_a == body_b and hash_a == hash_b
A passing run emits a PHI-free structured audit line that binds the record count, byte length, and file hash — the provenance an inspection under 21 CFR § 1304.04 relies on:
INFO pharmacy.reporting.arcos.fixedwidth arcos_file_serialized \
records=2 bytes=102 audit_hash=4e8f...b93c
Re-running serialize_batch on the same transactions reproduces both the bytes and the audit_hash; a mismatch against a stored hash proves the file was altered after generation, which is exactly the tamper signal the audit trail exists to surface.
Gotchas & Compliance Pitfalls
- A one-byte overflow shifts everything after it. Fixed-width has no delimiters, so a 10-character value in a 9-column field does not “spill” — it pushes every downstream field one column right and the ARCOS processor misreads the entire record. Always convert overflow into a hard error, as
_fitdoes; neverstr[:width]. - Leading-zero loss corrupts the NDC. If an NDC ever passed through an integer or a careless CSV reader,
00093005801becomes93005801and the serializer will zero-pad it back to eleven digits at the wrong positions, producing a valid-looking but wrong identifier. Carry NDCs as strings end to end and normalize upstream with the parser referenced in the prerequisites. - Justification is per field type, not global. Numeric fields (NDC, quantity, date) right-justify and zero-pad; text fields (DEA numbers, codes) left-justify and space-pad. Reversing either — right-padding a DEA number with zeros, or left-padding a quantity with spaces — yields a structurally valid width but a semantically wrong record.
- Encoding must be strict ASCII. A smart quote or accented character pasted into a code field can encode to multiple bytes under UTF-8, silently widening the record. Encoding with
ascii(notutf-8) makes such contamination raise instead of shift. - Quantity decimals are implied, not literal. ARCOS quantity fields carry implied decimals; writing a literal
.consumes a column and shifts the record. Represent quantity as an integer in the field’s implied-decimal convention and never emit a decimal point. - Do not hand-edit a rejected file. When ARCOS rejects a batch, fix the source data or the layout and regenerate, then re-hash. Editing the file by hand breaks the binding between the retained
audit_hashand the ledger events, destroying the reproducibility that makes the filing defensible.
Frequently Asked Questions
Why not use a CSV or delimited format instead of fixed-width?
Because ARCOS ingests fixed-width records; the format is dictated by the DEA processor, not chosen. Fixed-width trades delimiters for exact column positions, which is why width and justification are correctness properties rather than style choices. The upside is that a byte-exact record is trivially reproducible and hashable for the audit trail.
What width should each ARCOS field actually be?
The widths in ARCOS_LAYOUT illustrate the fixed-width discipline — right-justified zero-padding for numeric fields, left-justified space-padding for text — but you must confirm the exact column specification against the current DEA ARCOS Registrant Handbook before production filing. The design keeps the layout as data precisely so updating a width is a one-line change with test coverage.
How does hashing the file help during a DEA inspection?
The SHA-256 hash binds the exact bytes of the submitted file to the retained audit record. Re-running the serializer over the same transactions reproduces the identical hash, so you can demonstrate that the file on record is the file that was filed. A mismatch is proof of post-generation alteration, satisfying the tamper-evidence expectation behind 21 CFR § 1304.04 retention.
What happens if a quantity is larger than the field allows?
The _fit guard raises ArcosFieldError rather than truncating, so an out-of-range quantity stops the batch instead of silently reporting a wrong number. That is the intended behavior: an unrepresentable quantity signals a data or unit error upstream that must be corrected at the source, after which the batch is regenerated cleanly.
Related
- ARCOS Transaction Reporting — parent topic: selecting and assembling the records this recipe serializes
- Regulatory Reporting & DEA Submissions — the reporting area and its projection-of-the-ledger principle
- NDC parsing regex patterns for Python — canonical 11-digit NDC the serializer zero-pads
- NDC-11 vs NDC-10 Parsing Standards — why NDC-11 is the storage and reporting key