Entry Summary Filing Schemas
An entry summary filing schema is the versioned data contract that turns a set of classified, duty-computed line items into the header-plus-line record CBP expects on Form 7501 and its ACE electronic equivalent, with every monetary field carried at the exact decimal precision the ABI accepts. It is the join point where the two upstream halves of a brokerage pipeline finally converge: the HTSUS 10-digit codes and duty figures produced by the tariff-mapping engines, and the entered values, quantities, and party details parsed out of commercial documents. Part of the Compliance Reporting & ACE Transmission reference architecture, this schema sits one step before transmission — after classification and duty calculation are settled, and before the entry is serialized for the Automated Broker Interface. For a customs broker, the 7501 is the legal declaration that fixes duties, taxes, and fees owed on an import; for the Python ETL team assembling it, that declaration must be a deterministic function of validated inputs, with money held in Decimal, fee math reproducible to the cent, and no line silently dropped or rounded into a rejection.
Problem Framing: The Line That Doesn’t Reconcile
The failure mode this schema exists to prevent is a non-reconciling entry — a 7501 whose header totals do not equal the sum of its lines, or whose fee arithmetic disagrees with what ACE recomputes on receipt, producing an ABI rejection or, worse, an accepted entry that under- or over-declares duty. Four defects recur in hand-assembled entry summaries. First, float contamination: entered value, duty, or the merchandise processing fee is computed in binary floating point, so a line that should read USD 1,204.37 serializes as 1204.3699999999999 and either truncates wrong or trips ACE’s decimal-precision edit. Second, fee-cap drift: the merchandise processing fee (MPF) is charged ad valorem at a statutory rate but bounded by a per-entry minimum and maximum, and a naive implementation applies the rate without clamping to the bracket, so formal entries over the cap threshold over-declare the fee. Third, HTS digit-length slippage: a 10-digit HTSUS statistical suffix is stored as an integer or a code that lost its leading zero, so 0101.21.00.10 becomes 101210010 and fails the ACE tariff edit. Fourth, header-line divergence: the header total duty is summed independently of the line records — from a different query, a different rounding step, or a cached figure — so the two disagree by a cent and the whole entry bounces.
Each defect turns a filing that looks correct in the broker’s system into an X12 824 application-advice rejection, or a liquidated entry that a later CBP review reopens for a supplemental duty bill plus interest. The schema and pipeline below close those gaps by holding every monetary field as a fixed-scale Decimal, computing header totals only by summing the validated lines, clamping fees to their statutory brackets, and rejecting any line whose HTSUS code is not exactly ten digits before the entry is ever handed to the ABI.
Schema / Data Contract
An entry summary is only as filable as the contract that guards its fields. The 7501 is a two-level structure: one header describing the entry as a whole — entry number, entry type code, importer of record, port, and the summed monetary totals — and one or more line items, each carrying an HTSUS 10-digit code, an entered value, a duty amount, and the calculated fees. ACE constrains monetary fields to fixed decimal scales and rejects codes that are not exactly ten digits, so the contract enforces those shapes at the boundary rather than discovering them in an 824 rejection. The Pydantic models below formalize both levels; note that every money field is typed as Decimal, the HTS code is pattern-constrained to ten digits, and the header exposes no total that is set independently of the lines.
from __future__ import annotations
import logging
from decimal import Decimal
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, field_validator
logging.basicConfig(
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
level=logging.INFO,
)
logger = logging.getLogger("entry_summary")
CENTS = Decimal("0.01")
class EntryType(str, Enum):
CONSUMPTION = "01" # formal consumption entry
INFORMAL = "11" # informal entry
WAREHOUSE = "21" # warehouse withdrawal for consumption
FTZ = "06" # foreign-trade-zone consumption
class EntrySummaryLine(BaseModel):
line_no: int = Field(..., ge=1, le=999)
hts_code: str = Field(..., pattern=r"^\d{10}$")
country_of_origin: str = Field(..., pattern=r"^[A-Z]{2}$")
entered_value: Decimal = Field(..., gt=0)
ad_valorem_rate: Decimal = Field(..., ge=0) # fraction, e.g. 0.032
duty_amount: Decimal = Field(..., ge=0)
mpf_amount: Decimal = Field(..., ge=0)
hmf_amount: Decimal = Field(..., ge=0)
@field_validator("entered_value", "duty_amount", "mpf_amount", "hmf_amount")
@classmethod
def two_decimal_scale(cls, v: Decimal) -> Decimal:
if v != v.quantize(CENTS):
raise ValueError(f"monetary field must carry exactly 2 decimal places: {v}")
return v
class EntrySummaryHeader(BaseModel):
entry_number: str = Field(..., pattern=r"^\d{3}-\d{7}-\d$")
entry_type: EntryType
importer_of_record: str = Field(..., min_length=1)
port_of_entry: str = Field(..., pattern=r"^\d{4}$")
mode_of_transport: str = Field(..., pattern=r"^(ocean|air|truck|rail)$")
total_entered_value: Decimal = Field(..., gt=0)
total_duty: Decimal = Field(..., ge=0)
total_mpf: Decimal = Field(..., ge=0)
total_hmf: Decimal = Field(..., ge=0)
class EntrySummary(BaseModel):
header: EntrySummaryHeader
lines: list[EntrySummaryLine] = Field(..., min_length=1)
The corresponding PostgreSQL DDL persists the same contract with the scale pinned in the column type, so a bad value cannot even land in the table. NUMERIC(14, 2) fixes the monetary scale at the storage layer, the CHECK constraint enforces the ten-digit HTS shape, and the foreign key ties each line to exactly one header.
CREATE TABLE entry_summary_header (
entry_number TEXT PRIMARY KEY,
entry_type TEXT NOT NULL,
importer_of_record TEXT NOT NULL,
port_of_entry CHAR(4) NOT NULL,
mode_of_transport TEXT NOT NULL,
total_entered_value NUMERIC(14, 2) NOT NULL CHECK (total_entered_value > 0),
total_duty NUMERIC(14, 2) NOT NULL DEFAULT 0,
total_mpf NUMERIC(11, 2) NOT NULL DEFAULT 0,
total_hmf NUMERIC(11, 2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE entry_summary_line (
entry_number TEXT NOT NULL REFERENCES entry_summary_header(entry_number),
line_no INTEGER NOT NULL CHECK (line_no BETWEEN 1 AND 999),
hts_code CHAR(10) NOT NULL CHECK (hts_code ~ '^\d{10}$'),
country_of_origin CHAR(2) NOT NULL,
entered_value NUMERIC(14, 2) NOT NULL CHECK (entered_value > 0),
ad_valorem_rate NUMERIC(7, 5) NOT NULL DEFAULT 0,
duty_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
mpf_amount NUMERIC(11, 2) NOT NULL DEFAULT 0,
hmf_amount NUMERIC(11, 2) NOT NULL DEFAULT 0,
PRIMARY KEY (entry_number, line_no)
);
The header totals in this design are derived, never authored: they exist as columns for query convenience and for the transmitted record, but they are written only by summing the persisted lines. That single rule eliminates the header-line divergence defect at its root — there is no independent path by which a header total can drift from the lines beneath it.
Step-by-Step Implementation
Assembly proceeds in fixed stages, each of which either enriches the record or refuses it. The entered value and HTSUS code arrive already settled from upstream — from the Duty Formula Calculation Frameworks for the duty figure and rate column, and from Commercial Invoice PDF Extraction for the entered value and party details. This pipeline’s job is not to recompute duty but to assemble, fee-augment, total, and validate.
Stage 1 — Compute the per-line fees
The merchandise processing fee and harbor maintenance fee are the two fields most often gotten wrong, because both are ad valorem but only one is capped and only one applies to a given mode of transport. MPF on a formal entry is charged at the statutory rate but clamped to a per-entry minimum and maximum; HMF is a flat ad valorem charge that applies to ocean shipments and not to air, truck, or rail. Both are computed in Decimal and quantized to the cent with explicit rounding.
from decimal import Decimal, ROUND_HALF_UP
CENTS = Decimal("0.01")
# Statutory parameters (illustrative; sync from the live fee schedule).
MPF_RATE = Decimal("0.003464") # 0.3464% ad valorem
MPF_MIN = Decimal("31.67") # per-entry floor
MPF_MAX = Decimal("614.35") # per-entry ceiling
HMF_RATE = Decimal("0.00125") # 0.125% ad valorem, ocean only
def compute_mpf(total_entered_value: Decimal) -> Decimal:
"""MPF is ad valorem, then clamped to the statutory per-entry bracket."""
raw = (total_entered_value * MPF_RATE).quantize(CENTS, rounding=ROUND_HALF_UP)
clamped = min(max(raw, MPF_MIN), MPF_MAX)
if clamped != raw:
logger.info("MPF clamped from %s to bracket value %s", raw, clamped)
return clamped
def compute_hmf(entered_value: Decimal, mode: str) -> Decimal:
"""HMF applies to ocean cargo only; other modes are exempt."""
if mode != "ocean":
return Decimal("0.00")
return (entered_value * HMF_RATE).quantize(CENTS, rounding=ROUND_HALF_UP)
The MPF minimum and maximum apply to the entry as a whole, not to each line, so the fee is computed once against the header’s total entered value and then, where a per-line breakdown is required by the schema, apportioned across lines in proportion to their entered value with the residual cent assigned to the largest line. That apportionment keeps the line-level MPF fields summing exactly to the entry-level fee.
Stage 2 — Assemble the validated line records
Each line is built from the upstream duty result plus the fees, and constructed through the Pydantic model so the decimal-scale and HTS-length validators fire before the record exists at all.
def build_line(
line_no: int,
hts_code: str,
country: str,
entered_value: Decimal,
ad_valorem_rate: Decimal,
duty_amount: Decimal,
apportioned_mpf: Decimal,
mode: str,
) -> EntrySummaryLine:
hts = hts_code.strip()
if len(hts) != 10 or not hts.isdigit():
logger.error("HTS code failed 10-digit check on line %d: %r", line_no, hts_code)
raise ValueError(f"line {line_no}: HTSUS code must be exactly 10 digits")
return EntrySummaryLine(
line_no=line_no,
hts_code=hts,
country_of_origin=country.upper(),
entered_value=entered_value.quantize(CENTS),
ad_valorem_rate=ad_valorem_rate,
duty_amount=duty_amount.quantize(CENTS),
mpf_amount=apportioned_mpf.quantize(CENTS),
hmf_amount=compute_hmf(entered_value, mode),
)
The explicit ten-digit guard is deliberately redundant with the Pydantic pattern: it produces a line-numbered log entry a broker can act on, rather than a generic validation stack trace that names no line.
Stage 3 — Derive the header from the lines
The header totals are computed only here, by summing the constructed lines. No total is passed in from outside; the function that builds the header cannot be handed a figure that disagrees with its lines.
def build_header(
entry_number: str,
entry_type: EntryType,
importer: str,
port: str,
mode: str,
lines: list[EntrySummaryLine],
) -> EntrySummaryHeader:
total_value = sum((ln.entered_value for ln in lines), Decimal("0.00"))
total_duty = sum((ln.duty_amount for ln in lines), Decimal("0.00"))
total_mpf = sum((ln.mpf_amount for ln in lines), Decimal("0.00"))
total_hmf = sum((ln.hmf_amount for ln in lines), Decimal("0.00"))
return EntrySummaryHeader(
entry_number=entry_number,
entry_type=entry_type,
importer_of_record=importer,
port_of_entry=port,
mode_of_transport=mode,
total_entered_value=total_value.quantize(CENTS),
total_duty=total_duty.quantize(CENTS),
total_mpf=total_mpf.quantize(CENTS),
total_hmf=total_hmf.quantize(CENTS),
)
Summing Decimal values with an explicit Decimal("0.00") start value keeps the accumulator in the decimal domain — a bare sum() would seed the total with the integer 0 and, on an empty edge case, return an int that breaks the quantize call. The result of these three stages is a fully assembled EntrySummary whose header provably reconciles to its lines, ready for the validation pass that guards transmission.
Validation & Determinism
Determinism is the property CBP’s edits and a later review both test: the same classified lines, entered values, and fee schedule must assemble to the same 7501 on every replay, cent for cent. Four cross-checks enforce it, and all four run before the entry is serialized for ABI. First, decimal precision: every monetary field is quantized to exactly two places with ROUND_HALF_UP, matching ACE’s decimal handling, and the Pydantic validator rejects any value that is not already at that scale — so a float that leaked in as 1204.3699999999999 fails here rather than at the interface. Second, HTS digit length: each line’s code is checked to be exactly ten digits, because the ACE tariff edit rejects a truncated or leading-zero-stripped statistical suffix. Third, fee-cap math: the MPF is verified to sit within its statutory minimum and maximum, and the HMF is verified to be zero on any non-ocean mode. Fourth, header-line reconciliation: each header total is re-summed from the lines and compared to the stored header, so a divergence of even one cent is caught before transmission.
def validate_entry(entry: EntrySummary) -> list[str]:
"""Return a list of blocking errors; an empty list means filable."""
errors: list[str] = []
h = entry.header
recomputed = {
"total_entered_value": sum((ln.entered_value for ln in entry.lines), Decimal("0.00")),
"total_duty": sum((ln.duty_amount for ln in entry.lines), Decimal("0.00")),
"total_mpf": sum((ln.mpf_amount for ln in entry.lines), Decimal("0.00")),
"total_hmf": sum((ln.hmf_amount for ln in entry.lines), Decimal("0.00")),
}
for field, expected in recomputed.items():
actual = getattr(h, field)
if actual.quantize(CENTS) != expected.quantize(CENTS):
errors.append(f"header {field} {actual} != sum of lines {expected}")
if not (MPF_MIN <= h.total_mpf <= MPF_MAX):
errors.append(f"MPF {h.total_mpf} outside bracket [{MPF_MIN}, {MPF_MAX}]")
if h.mode_of_transport != "ocean" and h.total_hmf != Decimal("0.00"):
errors.append(f"HMF {h.total_hmf} charged on non-ocean mode {h.mode_of_transport}")
seen: set[int] = set()
for ln in entry.lines:
if len(ln.hts_code) != 10:
errors.append(f"line {ln.line_no}: HTS {ln.hts_code!r} is not 10 digits")
if ln.line_no in seen:
errors.append(f"duplicate line number {ln.line_no}")
seen.add(ln.line_no)
if errors:
logger.warning("entry %s failed validation with %d error(s)", h.entry_number, len(errors))
return errors
Because the header totals are re-derived from the same Decimal lines using the same quantization, the check is a genuine reconciliation rather than a comparison of two independently rounded figures. An entry that returns a non-empty error list is never transmitted; it is routed to correction with the specific blocking reasons attached, so a broker resolves a named line defect instead of decoding a generic interface rejection after the fact.
Downstream Integration
Once an entry clears validation, it is serialized into the ABI record and transmitted into ACE. That transmission is not the end of the story — CBP responds asynchronously, and a syntactically valid entry can still be rejected on a business edit the local schema cannot see, such as a bond insufficiency or a quota that closed between assembly and receipt. Those responses arrive as X12 824 application advice and are decoded and remediated by the ACE and ABI Rejection Handling workflow, which maps each error condition back to the offending line and either auto-corrects or escalates. A clean assembly upstream keeps that queue small, because the four validation checks above already eliminate the malformed-record rejections; what remains for rejection handling is the genuinely external conditions.
Upstream, the entry summary is only as correct as the figures it assembles. The duty amount and rate column on each line come directly from the Duty Formula Calculation Frameworks, which apply the ad valorem, specific, or compound rate under CBP rounding rules; if that stage drifts, the entered-value-to-duty relationship on the 7501 will not survive an ACE recomputation. The entered value, quantities, and party details are normalized out of the source documents by Commercial Invoice PDF Extraction, including the multi-currency conversion that fixes the entered value in USD before it ever reaches this schema. Binding the assembled entry to the specific duty-engine run and document-extraction batch that produced its inputs lets a later review reconstruct exactly which figures were filed, even after the underlying rate tables or source documents have been superseded.
Scaling & Resilience
A brokerage assembling entries at clearance volume processes thousands of line items across hundreds of entries in a single peak window, and the assembly pipeline has to hold its footprint and its determinism under that load. Four controls keep it inside budget:
- Per-entry batching with
asyncpgbulkCOPYfor the line records, so an entry with several hundred lines writes in one round trip rather than per-line inserts that saturate the connection pool. - Fee computation once per entry — the MPF bracket clamp runs against the header total a single time and is apportioned, never recomputed per line, which keeps the arithmetic both cheap and reconcilable.
- Idempotent assembly keyed on entry number, so a retried assembly after a transient failure replaces the prior draft rather than creating a duplicate entry, and the header-line reconciliation is re-verified on every write.
- Bounded async dispatch with a semaphore cap on concurrent assemblies, so a burst of filings cannot exhaust the pooled database connections that the tariff-schedule and fee-schedule lookups also depend on.
For resilience, a stale or partially loaded fee schedule is treated as a hard stop rather than a figure to guess against: if the MPF rate, minimum, or maximum cannot be resolved for the entry’s filing date, assembly pauses and the entry is held rather than filed with a fabricated fee. Transient database or upstream errors are retried with backoff, but a validated entry is never transmitted twice, because the transmission step is guarded by the same entry-number idempotency key that guards assembly. These patterns hold assembly latency low even when a batch window pushes tens of thousands of line items through the pipeline.
Compliance Obligations
Every transmitted entry summary is a legal declaration of duties, taxes, and fees owed, so the assembled record and its provenance are retained as evidence. Each filed entry stores the full header and line payload, the fee schedule version and filing date used for the MPF and HMF math, the identifiers of the duty-engine run and document-extraction batch that supplied its inputs, and the validation result that cleared it for transmission. Retention follows the CBP recordkeeping horizon — at least five years from the date of entry — held on an immutable tier so a Focused Assessment or a post-entry review can be answered from primary records rather than reconstructed after the fact.
Change is handled as first-class input. When CBP adjusts the merchandise processing fee bracket, the harbor maintenance fee rate, or an entry-type requirement, the affected schedule version is superseded rather than overwritten, and entries already filed against the prior version remain bound to the parameters that were in force on their filing date. Any entry the pipeline cannot assemble deterministically — an unresolved fee schedule, an HTS code that fails the ten-digit edit, a header that will not reconcile to its lines — is escalated to a broker through a human-in-the-loop gate rather than filed on a guess. This combination of fixed-scale decimal money, derived-not-authored header totals, statutory fee clamping, versioned schedule binding, and mandatory escalation is what turns a stack of classified line items into an entry summary that clears ACE and survives review instead of merely passing a local check.
Related
- Assembling CBP 7501 line items from classified entries — the line-level build, MPF apportionment, and header derivation in full
- Validating entered value with ACE decimal precision — the decimal-scale and rounding checks ACE enforces on every monetary field
- ACE and ABI Rejection Handling — decoding X12 824 application advice and remediating rejected entries
- Duty Formula Calculation Frameworks — supplies the per-line duty amount and rate column the entry summary declares
- Commercial Invoice PDF Extraction — normalizes the entered value, quantities, and parties the schema assembles