Evaluating compound duty formulas with CBP rounding
A compound duty rate charges two amounts at once — an ad valorem percentage of the entered value plus a specific charge per unit of quantity — and CBP expects the sum rounded half-up to the whole cent on the entry summary. Get either component or the rounding step wrong and the 7501 line total disagrees with the ACE-calculated figure, which triggers a rejection or a post-summary correction. This page shows how to parse a compound rate expression like 5% + 12.5¢/kg, evaluate both legs with Python Decimal end to end, and apply CBP ROUND_HALF_UP once at the line total — not per leg — so the filed duty matches the government’s math to the cent every time.
The trap that catches naive implementations is double rounding. If you round the ad valorem leg to the cent, then round the specific leg to the cent, then add, you can land a cent away from the true half-up total of the un-rounded sum. CBP rounds the composed duty, so the arithmetic must compose first and round last. Everything below enforces that ordering.
Prerequisites
This resolver is a pure function of an entry line and a parsed rate; wire it in only once the upstream state below holds:
- Python 3.10+ with
Decimalthroughout. Never usefloatfor entered value, per-unit charges, or percentages. Binary floating point cannot represent0.125or a half-cent boundary exactly, sofloatrounding is non-deterministic at exactly the tie cases CBP cares about. - A classified, in-window line item. Each line must already carry a valid, effective HTS code resolved upstream — see Handling missing HTS codes in ETL pipelines. A compound rate applied to a provisional or superseded classification is meaningless.
- A normalized rate string per HTS row. The HTSUS publishes compound rates as free text (
"5% + 12.5¢/kg","3.5% + 1.5¢/kg"). Your tariff snapshot must expose that raw text so it can be parsed deterministically; the sibling guide Computing ad valorem duty with decimal precision covers the pure-percentage case that this page generalizes. - Entered value and billable quantity in known units. The specific leg multiplies a per-unit charge by a quantity; the quantity’s unit of measure must match the rate’s unit (kg, liter, dozen, number) or the leg is silently wrong. Unit reconciliation happens before this step.
- Structured logging configured (stdlib
loggingwith the%(asctime)s | %(levelname)s | %(name)s | %(message)sformat) so every evaluated formula is greppable for audit.
The formula
For an entered value , an ad valorem rate , a per-unit specific charge , and a billable quantity , the compound duty is the half-up rounding of the composed sum:
Concretely, for an entered value of USD 4,000.00 at 5% + 12.5¢/kg on 320 kg: the ad valorem leg is USD 200.00, the specific leg is USD 40.00, and the composed duty is USD 240.00. The rounding only bites when the composed sum lands on a fraction of a cent — and it must be applied to that sum, once.
Implementation
CompoundRate parses the published rate text into two exact Decimal components. evaluate_compound_duty composes both legs and rounds the total exactly once. Neither ever sees a float.
import logging
import re
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler()],
)
logger = logging.getLogger("compound_duty")
CENT = Decimal("0.01")
# One percentage leg (e.g. "5%" or "3.5%") and one specific leg
# (e.g. "12.5¢/kg" or "1.5 cents/kg"). Either may be absent.
_PCT = re.compile(r"(?P<pct>\d+(?:\.\d+)?)\s*%")
_SPECIFIC = re.compile(
r"(?P<amt>\d+(?:\.\d+)?)\s*(?P<unit_kind>¢|cents?|\$|usd)\s*/\s*(?P<uom>[a-z]+)",
re.IGNORECASE,
)
@dataclass(frozen=True)
class CompoundRate:
"""A parsed HTSUS compound rate: ad valorem fraction + specific per-unit charge."""
ad_valorem: Decimal # fraction, e.g. Decimal("0.05") for 5%
specific_per_unit: Decimal # USD per unit, e.g. Decimal("0.125") for 12.5¢
uom: Optional[str] # unit of measure the specific leg bills against
raw: str
@classmethod
def parse(cls, text: str) -> "CompoundRate":
pct_match = _PCT.search(text)
spec_match = _SPECIFIC.search(text)
if pct_match is None and spec_match is None:
raise ValueError(f"Unparseable rate expression: {text!r}")
ad_valorem = (
Decimal(pct_match.group("pct")) / Decimal("100")
if pct_match else Decimal("0")
)
specific = Decimal("0")
uom: Optional[str] = None
if spec_match:
amt = Decimal(spec_match.group("amt"))
kind = spec_match.group("unit_kind").lower()
# Cents -> dollars; keep full precision, do NOT round here.
specific = amt / Decimal("100") if kind in ("¢", "cent", "cents") else amt
uom = spec_match.group("uom").lower()
return cls(ad_valorem=ad_valorem, specific_per_unit=specific, uom=uom, raw=text)
@dataclass(frozen=True)
class CompoundDuty:
ad_valorem_leg: Decimal # un-rounded USD
specific_leg: Decimal # un-rounded USD
total: Decimal # composed sum, rounded half-up to the cent
def evaluate_compound_duty(
rate: CompoundRate,
entered_value: Decimal,
quantity: Decimal,
) -> CompoundDuty:
"""Compose both legs at full precision, then round the SUM once (CBP order)."""
if not isinstance(entered_value, Decimal) or not isinstance(quantity, Decimal):
raise TypeError("entered_value and quantity must be Decimal, never float.")
ad_valorem_leg = entered_value * rate.ad_valorem
specific_leg = rate.specific_per_unit * quantity
# CRITICAL: add first, round last. Rounding each leg then summing can
# drift one cent off the half-up rounding of the true composed total.
total = (ad_valorem_leg + specific_leg).quantize(CENT, rounding=ROUND_HALF_UP)
logger.info(
"compound %s: adv=%s + spec=%s (%s @ %s/%s) -> %s",
rate.raw, ad_valorem_leg, specific_leg,
quantity, rate.specific_per_unit, rate.uom, total,
)
return CompoundDuty(ad_valorem_leg, specific_leg, total)
The single load-bearing line is the quantize on the summed legs. Because ad_valorem_leg and specific_leg are un-rounded Decimal values, their sum is exact, and ROUND_HALF_UP resolves the half-cent tie in the same direction CBP’s ACE engine does. Rounding either leg before the addition is the defect this design exists to prevent.
Decimal precision and summed; ROUND_HALF_UP is applied once to the composed total, never per leg.Verification steps
Run these checks against a representative set of compound-rated lines before trusting the evaluator:
- Round-once vs. round-each divergence. Pick an entered value and quantity where each leg has a fractional cent (e.g.
3.5% + 1.5¢/kgon USD 1,000.01 over 33 kg). Compute the duty both ways — rounding each leg then summing, versus summing then rounding. Assert your evaluator matches the sum-then-round result. If they ever differ, ACE will disagree with a per-leg implementation. - Parser round-trip. Feed
CompoundRate.parsethe raw HTSUS strings in your snapshot ("5% + 12.5¢/kg","3.5%","1.5¢/kg"). Confirm the ad valorem fraction and per-unitDecimalmatch hand-computed values, and that a pure-percentage or pure-specific string still parses with the missing leg atDecimal("0"). - Half-cent tie direction. Construct a composed sum that lands exactly on a half cent (total
...0.005). Assert it rounds up, matchingROUND_HALF_UP. Python’s defaultROUND_HALF_EVENwould round0.125to0.12; that banker’s rounding is wrong for CBP. - Type guard fires. Pass a
floatentered value and assertTypeError. Afloatthat reachesquantizeproduces a plausible but non-reproducible cent, which is the hardest class of duty bug to detect after the fact. - Unit-of-measure match. Assert the
uomparsed from the rate equals the unit thequantityis denominated in. A12.5¢/kgrate billed against a quantity in pounds is off by a factor of 2.2 with no exception raised. - Zero-quantity and zero-value edges. A line with
quantity == 0must yield only the ad valorem leg; an entered value of0must yield only the specific leg. Confirm neither degenerates to a spurious rounding artifact.
Edge cases & gotchas
- Double rounding is the whole point. The most common regression is a well-meaning refactor that rounds
ad_valorem_leg“for display” before summing. Keep both legs un-rounded until the composedquantize. If you must expose per-leg cents in a UI, compute them separately for display only and never feed them back into the total. - Cents vs. dollars in the specific leg. HTSUS mixes
¢/kgand$/kg(andcents/kgspelled out). The parser divides by 100 only for the cent forms; a misclassified unit inflates the specific leg 100-fold. Assert on the parsedspecific_per_unitmagnitude in tests, not just that parsing succeeded. - Unicode cent sign. The
¢glyph (U+00A2) arrives from some feeds as the ASCII fallback"cents"and from others as mojibake if a Latin-1 source was re-decoded as UTF-8. Normalize the rate text to UTF-8 witherrors="strict"at ingestion and quarantine on decode failure rather than letting a corrupted glyph slip past the regex. - Quantity unit conversion drift. If you convert quantity units (pounds to kilograms) with a
floatfactor before evaluation, you reintroduce the very imprecisionDecimalavoids. Convert with aDecimalfactor and only at full precision, mirroring the discipline in Computing ad valorem duty with decimal precision. - Preferential program overrides. Under a trade agreement, the ad valorem leg may drop to free while the specific leg still applies, or vice versa. The compound structure is correct, but the applicable rate depends on origin — determine eligibility in the Rule of Origin Logic Engines before selecting which
CompoundRateto evaluate. - The filed figure must survive to ACE unchanged. Once
evaluate_compound_dutyreturns a cent-exact total, pass that exactDecimalthrough to the entry summary — do not re-derive it downstream. The line-total contract for transmission lives in Entry Summary Filing Schemas; a re-computation there with different intermediate rounding is a classic source of a 7501-versus-ACE mismatch.
Related
- Computing ad valorem duty with decimal precision
- Handling missing HTS codes in ETL pipelines
- Rule of Origin Logic Engines
- Entry Summary Filing Schemas
Up: Duty Formula Calculation Frameworks
Authoritative references: WCO Harmonized System 2022; HTSUS General Rules of Interpretation and column-1 compound rate expressions (USITC); CBP ACE Entry Summary duty computation; 19 CFR Part 141; ISO 4217 currency minor units.