Assembling CBP 7501 line items from classified entries

Transforming a classified, duty-computed entry line into a filable CBP Form 7501 line record is where classification meets the wire format: each internal line item must become a tariff-line record carrying an HTSUS number, an entered value, a duty amount, a merchandise processing fee, a harbor maintenance fee, and a quantity in the unit of measure the schedule demands. Do it wrong and ACE rejects the entry summary — or worse, accepts an understated duty that surfaces months later as a liquidated-damages claim. The correct behavior is deterministic: read validated line items whose classification and duty are already settled, group them into 7501 tariff lines, compute the statutory fees with Decimal money and half-up rounding, and emit a structured line record that a transmission layer can serialize without further arithmetic. This page gives you a single runnable assembler that enforces that contract, plus the verification checklist and the grouping, rounding, and unit-of-measure gotchas that break naive implementations.

Prerequisites

This assembler assumes a specific upstream state and toolchain. Confirm each before applying it:

  • Python 3.10+ — the assembler uses dataclass(frozen=True), Enum, and Decimal throughout. Never use float for entered value, duty, or fees; ISO 4217 minor-unit rounding is not representable in binary floating point, and CBP reconciles to the cent.
  • Classified, duty-settled line items. Every input line must already carry a resolved HTSUS code and a duty figure produced by the Duty Formula Calculation Frameworks. The assembler is a projection, not a classifier — it never guesses a code and never recomputes an ad valorem rate from scratch.
  • A settled entered value per line. Entered value is the CBP-appraised transaction value (typically FOB, exclusive of international freight and insurance for most US imports). Currency conversion to USD must already be applied upstream; the assembler works only in ISO 4217 USD minor units.
  • Fee parameters for the entry date. The merchandise processing fee (MPF) ad valorem rate and its statutory floor and ceiling, plus the harbor maintenance fee (HMF) rate, must be supplied for the entry’s effective date. These change by regulation and are not hardcoded here.
  • Structured logging configured (stdlib logging with the %(asctime)s | %(levelname)s | %(name)s | %(message)s format, or structlog) so every assembled line and every fee computation is greppable for audit.

Implementation

A single assembler owns the entire projection: grouping classified lines by tariff line, summing entered value and quantity, computing duty and the statutory fees, and emitting an immutable 7501 line record. It performs no classification and no currency math — those are settled upstream.

import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
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("cbp7501_assembler")

CENT = Decimal("0.01")


def money(value: Decimal) -> Decimal:
    """Quantize to USD cents with CBP half-up rounding."""
    return value.quantize(CENT, rounding=ROUND_HALF_UP)


class DutyBasis(Enum):
    AD_VALOREM = "AD_VALOREM"
    SPECIFIC = "SPECIFIC"
    FREE = "FREE"


@dataclass(frozen=True)
class ClassifiedLine:
    """A settled input line: classification and duty already computed."""
    sku: str
    hts_code: str                 # 10-digit digit-only HTSUS number
    entered_value: Decimal        # appraised transaction value, USD
    duty_amount: Decimal          # computed by the duty engine, USD
    quantity: Decimal             # in the reporting unit of measure
    uom: str                      # UN/ECE Rec 20 code, e.g. "NO", "KG"
    basis: DutyBasis
    origin_country: str           # ISO 3166-1 alpha-2


@dataclass(frozen=True)
class FeeSchedule:
    """Statutory fee parameters valid for the entry date."""
    mpf_rate: Decimal             # e.g. Decimal("0.003464")
    mpf_min: Decimal              # per-entry floor
    mpf_max: Decimal              # per-entry ceiling
    hmf_rate: Decimal             # e.g. Decimal("0.00125")
    hmf_applies: bool             # only water-borne entries at HMF ports


@dataclass(frozen=True)
class Form7501Line:
    """One assembled 7501 tariff-line record, ready to serialize."""
    line_no: int
    hts_code: str
    entered_value: Decimal
    duty_amount: Decimal
    quantity: Decimal
    uom: str
    origin_country: str
    basis: DutyBasis
    sku_count: int
    assembled_at: datetime = field(default_factory=datetime.utcnow)


class Form7501Assembler:
    """Projects settled ClassifiedLine records into 7501 tariff lines."""

    def __init__(self, fees: FeeSchedule):
        self._fees = fees

    def _group_key(self, line: ClassifiedLine) -> tuple[str, str, str]:
        # Tariff-line identity: same HTSUS number, origin, and UOM merge into
        # one 7501 line. Mixing origins or units under one line misstates duty.
        return (line.hts_code, line.origin_country, line.uom)

    def assemble_lines(self, lines: list[ClassifiedLine]) -> list[Form7501Line]:
        buckets: dict[tuple[str, str, str], list[ClassifiedLine]] = defaultdict(list)
        for line in lines:
            if len(line.hts_code) != 10 or not line.hts_code.isdigit():
                raise ValueError(
                    f"SKU {line.sku}: non-filable HTSUS {line.hts_code!r}; "
                    "only 10-digit statistical suffixes may reach 7501."
                )
            buckets[self._group_key(line)].append(line)

        assembled: list[Form7501Line] = []
        # Deterministic order: sort keys so line numbers are reproducible.
        for line_no, key in enumerate(sorted(buckets), start=1):
            members = buckets[key]
            hts, origin, uom = key
            entered = money(sum((m.entered_value for m in members), Decimal("0")))
            duty = money(sum((m.duty_amount for m in members), Decimal("0")))
            qty = sum((m.quantity for m in members), Decimal("0"))
            assembled.append(
                Form7501Line(
                    line_no=line_no, hts_code=hts, entered_value=entered,
                    duty_amount=duty, quantity=qty, uom=uom,
                    origin_country=origin, basis=members[0].basis,
                    sku_count=len(members),
                )
            )
            logger.info(
                "7501 line %d: HTS %s origin %s qty %s %s | entered %s duty %s (%d SKUs)",
                line_no, hts, origin, qty, uom, entered, duty, len(members),
            )
        return assembled

    def compute_fees(self, lines: list[Form7501Line]) -> dict[str, Decimal]:
        """MPF and HMF are entry-level fees on the summed dutiable value."""
        total_entered = money(sum((l.entered_value for l in lines), Decimal("0")))

        mpf = money(total_entered * self._fees.mpf_rate)
        mpf = min(max(mpf, self._fees.mpf_min), self._fees.mpf_max)  # floor/ceiling
        hmf = (
            money(total_entered * self._fees.hmf_rate)
            if self._fees.hmf_applies else Decimal("0.00")
        )
        total_duty = money(sum((l.duty_amount for l in lines), Decimal("0")))

        logger.info(
            "Entry totals | entered %s duty %s MPF %s HMF %s",
            total_entered, total_duty, mpf, hmf,
        )
        return {
            "entered_value": total_entered,
            "total_duty": total_duty,
            "mpf": mpf,
            "hmf": hmf,
            "grand_total": money(total_duty + mpf + hmf),
        }

The critical contract is that assemble_lines refuses any code that is not a 10-digit HTSUS number: a 6- or 8-digit code is a valid subheading but is not filable on a 7501 line, so it raises rather than transmitting a truncated classification. Fees are computed once at the entry level — MPF is clamped to its statutory floor and ceiling, and HMF is applied only when the entry qualifies — never per line, which would over-collect.

Projecting classified lines into a CBP 7501 entry summary Classified, duty-settled line items enter on the left, each carrying an HTSUS code, entered value, duty amount, quantity and unit of measure. The assembler groups them by a composite key of HTSUS number, origin country, and unit of measure, so lines that share all three merge into a single 7501 tariff line. For each group the assembler sums entered value and duty in Decimal cents and sums quantity, producing a numbered 7501 line record with its HTSUS number, entered value, duty, quantity, UOM, and origin. Below the tariff lines, an entry-level fee stage computes the merchandise processing fee as an ad valorem charge on total entered value, clamped between a statutory floor and ceiling, and the harbor maintenance fee where it applies. Duty, MPF and HMF sum to the grand total transmitted to ACE. classified lines HTS 8517620000 value · duty · qty · UOM HTS 8517620000 same origin · same UOM HTS 6109100012 value · duty · qty · UOM group by HTS · origin · UOM sum Decimal 7501 tariff lines line 1 · 8517620000 entered USD 24,800.00 · duty USD 0.00 line 2 · 6109100012 entered USD 9,150.00 · duty USD 1,528.05 entry-level fees MPF = total entered x rate clamped to [floor, ceiling] HMF = total entered x rate (if water-borne) grand total = duty + MPF + HMF ACE entry summary
Classified lines that share HTSUS number, origin, and unit of measure merge into one 7501 tariff line; entered value and duty sum in Decimal cents, and MPF and HMF are computed once at the entry level before the grand total goes to ACE.

Verification steps

Run these checks against a representative entry before trusting the assembler in production:

  1. Grouping determinism. Feed the same line items in shuffled order twice. The two assemble_lines outputs must be identical, including line_no assignment — the sorted(buckets) key guarantees reproducible line numbering, which CBP amendment workflows depend on.
  2. Non-filable code rejection. Inject a line with a 6-digit or 8-digit HTSUS value. Assert assemble_lines raises ValueError; a truncated code must never reach a 7501 line.
  3. Origin and UOM separation. Give two lines the same HTSUS number but different origin_country (or different uom). Assert they produce two distinct 7501 lines, not one merged line — merging across origin misstates preferential duty and country-of-origin marking.
  4. Fee floor and ceiling. Run one entry with a tiny entered value and one with a very large value. Assert MPF equals the statutory floor in the first case and the ceiling in the second; an unclamped ad valorem MPF is the most common over- or under-collection bug.
  5. HMF gating. Assemble an air-freight entry with hmf_applies=False. Assert HMF is exactly USD 0.00 — HMF attaches only to water-borne entries through HMF ports, never universally.
  6. Cent-level reconciliation. Independently sum each line’s entered value and duty and compare against compute_fees. All figures must match to the cent under ROUND_HALF_UP; a mismatch means a float slipped into an upstream value or rate.
  7. Grand-total identity. Assert grand_total == total_duty + mpf + hmf exactly. This is the figure ACE validates against the payment tender, so any drift is a hard rejection at transmission.

Edge cases & gotchas

  • Summing before quantizing, not after. Quantize each line total once, then sum the already-quantized line values for the entry. Summing raw un-quantized values and quantizing only the grand total can drift by a cent against CBP’s line-by-line reconciliation. The assembler quantizes per line in assemble_lines and re-quantizes the entry total in compute_fees — keep that discipline.
  • Unit-of-measure mismatch. The HTSUS statistical suffix dictates the reporting UOM (for example NO for number, KG for kilograms, DOZ for dozens). If your source data reports pieces but the schedule expects dozens, the quantity is off by a factor of twelve and ACE flags it. Normalize to the UN/ECE Rec 20 code the schedule mandates before assembly, never after.
  • Duty-free lines still need a line record. A DutyBasis.FREE line has duty_amount of USD 0.00 but still carries entered value that feeds MPF. Do not drop free lines from the entry — their entered value is dutiable-value basis for the merchandise processing fee even when the duty is zero.
  • Specific and compound duty bases. The assembler sums a settled duty_amount and never re-derives it, so specific (per-unit) and compound duties pass through correctly only if the duty engine already resolved them. If you see a specific-rate line with duty that scales with value instead of quantity, the defect is upstream in the duty framework, not the assembler.
  • Entered value is not invoice total. Entered value is the CBP-appraised transaction value, which for most US imports excludes international freight and insurance. Passing a CIF invoice total inflates both duty and MPF. Confirm the value basis before it reaches this stage; if the entry is later corrected, route the delta through the rejection and amendment path in ACE and ABI Rejection Handling.
  • Line-count and value validation before send. Assembly produces the numbers, but the decimal precision and field lengths still have to satisfy ACE before transmission. Run the assembled lines through Validating entered value with ACE decimal precision as the gate between this projection and the wire.

Up: Entry Summary Filing Schemas