Validating entered value with ACE decimal precision
ACE rejects an entry summary the instant a monetary field carries more decimal places than the schema allows, exceeds its field length, or was rounded with the wrong rule — and a rejection at transmission time is far more expensive than a validation failure inside your own pipeline. Entered value, duty, and the merchandise and harbor maintenance fees each have a fixed precision and a maximum magnitude in the ACE entry-summary record, and every one of them must be quantized with banker-proof half-up rounding to the cent before the payload leaves your process. The correct behavior is deterministic: take the assembled monetary fields, quantize each to its declared scale with ROUND_HALF_UP, assert it fits the field’s integer and fractional width, and reject the record locally — with a precise reason — before a single byte reaches ABI. This page gives you a single runnable validator that enforces that contract, plus the verification checklist and the rounding, scale, and field-length gotchas that break naive implementations.
Prerequisites
This validator assumes a specific upstream state and toolchain. Confirm each before applying it:
- Python 3.10+ — the validator uses
dataclass(frozen=True),Enum, andDecimalthroughout. Never usefloatfor any monetary field; float cannot represent0.005exactly, so a float value at a rounding boundary rounds unpredictably and ACE flags the mismatch. - Assembled 7501 line records. The validator runs on the output of Assembling CBP 7501 line items from classified entries — line records whose entered value and duty are already summed. It validates precision and magnitude; it does not compute or group.
- A field precision contract for the target ACE record. Each monetary field’s total digit width and fractional scale must be declared. The values below are illustrative — always confirm the widths against the current CBP ACE ABI CATAIR entry-summary record layout for your filing type before production.
- Decimal context wide enough for intermediate math. Set the process
decimalcontext precision high enough that no intermediate multiplication silently loses digits before you quantize; the default 28 is ample for entry-summary magnitudes. - Structured logging configured (stdlib
loggingwith the%(asctime)s | %(levelname)s | %(name)s | %(message)sformat, orstructlog) so every rejection carries a machine-readable field and reason for the audit trail.
Implementation
A single validator owns the entire pre-transmission money gate: quantize each field to its declared scale, verify it is non-negative and finite, and assert its integer and fractional widths fit the ACE field. Any violation returns a structured rejection rather than a coerced value — the record never silently mutates to fit.
import logging
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP, InvalidOperation
from enum import Enum
from typing import Optional
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler()],
)
logger = logging.getLogger("ace_precision_validator")
class RejectReason(Enum):
NOT_FINITE = "NOT_FINITE"
NEGATIVE = "NEGATIVE"
SCALE_OVERFLOW = "SCALE_OVERFLOW" # more fractional digits than allowed
INTEGER_OVERFLOW = "INTEGER_OVERFLOW" # magnitude exceeds field width
@dataclass(frozen=True)
class FieldSpec:
"""Precision contract for one ACE monetary field."""
name: str
int_digits: int # max digits left of the decimal point
frac_digits: int # fixed digits right of the decimal point (scale)
@property
def quantum(self) -> Decimal:
return Decimal(1).scaleb(-self.frac_digits) # e.g. Decimal("0.01")
@property
def max_magnitude(self) -> Decimal:
# Largest value the field can hold, e.g. 9,999,999,999.99
return Decimal(10) ** self.int_digits - self.quantum
@dataclass(frozen=True)
class FieldResult:
name: str
ok: bool
value: Optional[Decimal]
reason: Optional[RejectReason]
# Illustrative ACE entry-summary money fields — confirm against CATAIR.
ACE_FIELDS = {
"entered_value": FieldSpec("entered_value", int_digits=12, frac_digits=2),
"duty_amount": FieldSpec("duty_amount", int_digits=12, frac_digits=2),
"mpf": FieldSpec("mpf", int_digits=10, frac_digits=2),
"hmf": FieldSpec("hmf", int_digits=10, frac_digits=2),
}
class AcePrecisionValidator:
"""Quantizes and range-checks money fields before ABI transmission."""
def validate_field(self, spec: FieldSpec, raw: Decimal) -> FieldResult:
# 1. Reject NaN / Infinity outright — never transmit a non-finite money value.
try:
if not raw.is_finite():
return self._reject(spec, RejectReason.NOT_FINITE, raw)
except (InvalidOperation, AttributeError):
return self._reject(spec, RejectReason.NOT_FINITE, None)
# 2. Entered value, duty, and fees are never negative on a 7501 line.
if raw < 0:
return self._reject(spec, RejectReason.NEGATIVE, raw)
# 3. Quantize to the field's fixed scale with CBP half-up rounding.
quantized = raw.quantize(spec.quantum, rounding=ROUND_HALF_UP)
# 4. Guard against silent scale overflow: if the raw value carried more
# fractional precision than the field allows, quantizing changed it.
# That is data loss, not a formatting fix — reject and surface it.
if raw != quantized and _frac_len(raw) > spec.frac_digits:
return self._reject(spec, RejectReason.SCALE_OVERFLOW, raw)
# 5. Magnitude must fit the declared integer width.
if quantized > spec.max_magnitude:
return self._reject(spec, RejectReason.INTEGER_OVERFLOW, quantized)
logger.info("field %s OK: %s", spec.name, quantized)
return FieldResult(spec.name, True, quantized, None)
def validate_record(self, record: dict[str, Decimal]) -> list[FieldResult]:
results: list[FieldResult] = []
for name, spec in ACE_FIELDS.items():
raw = record.get(name, Decimal("0"))
results.append(self.validate_field(spec, raw))
rejects = [r for r in results if not r.ok]
if rejects:
logger.error(
"Record rejected before send: %s",
", ".join(f"{r.name}={r.reason.value}" for r in rejects),
)
return results
def _reject(self, spec: FieldSpec, reason: RejectReason,
value: Optional[Decimal]) -> FieldResult:
logger.warning("field %s rejected: %s (raw=%r)", spec.name, reason.value, value)
return FieldResult(spec.name, False, value, reason)
def _frac_len(value: Decimal) -> int:
"""Count significant fractional digits in a Decimal."""
exponent = value.normalize().as_tuple().exponent
return -exponent if isinstance(exponent, int) and exponent < 0 else 0
def record_is_transmittable(results: list[FieldResult]) -> bool:
"""Hard gate: every field must pass or the record does not go to ABI."""
return all(r.ok for r in results)
The critical distinction is in step 4: quantizing to fit the field is not the same as accepting a value that was always at the right scale. A duty figure of USD 1,528.0500 did not lose meaning when rounded to USD 1,528.05, but a value that arrives as USD 1,528.055 carried a third decimal place that the upstream duty engine should never have produced — that is a SCALE_OVERFLOW defect, and the validator surfaces it instead of silently rounding the evidence away.
SCALE_OVERFLOW, not a silent fix.Verification steps
Run these checks against a representative record before trusting the validator in production:
- Rounding-boundary correctness. Validate
Decimal("1528.055")against a 2-scale field. Assert it is rejected asSCALE_OVERFLOW— and separately, thatDecimal("1528.045")quantizes toUSD 1,528.05underROUND_HALF_UP, proving the half-up rule, not banker’s rounding, is in force. - Non-finite rejection. Feed
Decimal("NaN")andDecimal("Infinity"). Both must returnNOT_FINITE; a non-finite money value must never reach ABI, where it serializes to garbage. - Negative guard. Validate
Decimal("-0.01")for entered value. AssertNEGATIVE; a sign error upstream must fail here, not at CBP. Refund and correction deltas are handled through amendments, not a negative 7501 line. - Integer-width ceiling. Validate a value one quantum above
max_magnitudefor the field. AssertINTEGER_OVERFLOW; an entered value that overflows the field width is a data defect, most often a currency-conversion or unit error. - Clean-value passthrough. Validate
Decimal("24800.00")for a 12-integer, 2-scale field. Assertok is Trueand the returned value is bit-identicalDecimal("24800.00")— a clean value must survive untouched, with no scale drift. - Whole-record gate. Build a record where three fields pass and one fails. Assert
record_is_transmittablereturnsFalseand the log names the failing field and reason. A partial pass must block the entire record. - Zero-scale integrity. Confirm
Decimal("0")and a missing field both validate toUSD 0.00without triggering a falseSCALE_OVERFLOW. Duty-free and non-applicable fees are legitimately zero, not errors.
Edge cases & gotchas
- Half-up versus banker’s rounding. Python’s
Decimaldefaults toROUND_HALF_EVEN(banker’s rounding). CBP arithmetic expectsROUND_HALF_UP. If you omit the explicitrounding=argument anywhere in the money path, a value ending in exactly five in the discarded place rounds the wrong way half the time, and the mismatch surfaces only on a fraction of entries — the hardest kind of bug to reproduce. Always passROUND_HALF_UPexplicitly, in the assembler and here. - Float contamination is invisible until the boundary. A value that spent any part of its life as a
floatmay look correct —24800.00— yet be24800.000000000003internally, which quantizes fine but can trip scale checks elsewhere. Assertisinstance(value, Decimal)at the pipeline boundary and construct everyDecimalfrom a string, never from a float literal. - Quantizing is not validating. The most dangerous shortcut is to quantize every field and transmit, trusting rounding to make values fit. That silently discards precision the upstream engine should never have produced and destroys the evidence that something is wrong. Distinguish “reformatting a correctly scaled value” from “truncating an over-precise one,” as step 4 does.
- Field widths differ by record and filing type. The illustrative widths here are not universal. Entered value, duty, and fee fields can carry different integer and fractional widths across ACE record types and CATAIR versions. Load the widths from the current CATAIR layout for your filing type rather than hardcoding, and re-verify after every CBP schema release.
- Scale is fixed, not maximum. ACE money fields are fixed-scale:
USD 5.00must serialize with exactly two fractional digits, not5or5.0. When you format the quantizedDecimalfor the wire, preserve trailing zeros —str(Decimal("5.00"))already yields"5.00", but a naivefloat-based formatter would drop them and fail length validation. - Reject locally, then route the failure. A field that fails here should never be silently dropped; it is a data-quality signal. Feed the structured rejection into the same operational path that handles a returned entry so the correction is tracked, using ACE and ABI Rejection Handling, and only re-run validation once the assembler has produced corrected 7501 line items.