Reconciling duty refunds after reclassification
When an HTS reclassification lowers the applicable rate on an entry that has already paid duty, the difference is refundable — but only a reconciliation that ties each refunded cent back to a specific original payment survives a CBP audit. This page builds that reconciliation in Python: it recomputes duty under the corrected classification, subtracts it from the duty originally paid, and posts an auditable ledger entry that references the reclassification source, the original entry line, and the exact refund delta. All arithmetic uses Decimal with ROUND_HALF_UP so the refund figure reconciles to the cent against what was actually paid. A reclassification that does not lower the rate must net to zero or negative and never fabricate a refund; the ledger is append-only so no refund is ever silently revised.
Prerequisites
This solution assumes a completed reclassification and a persisted record of the original payment. Confirm each before applying it:
- Python 3.10+ with
Decimalfor every currency figure. A refund is the difference of two rounded duty amounts;floatsubtraction introduces representational error that will not reconcile against the penny CBP recorded as paid. Never usefloatfor duty, entered value, or rates. - The corrected classification and its source. The refund is only defensible if it references why the rate changed. That decision comes from the classification pipeline — a resolved code from Fallback Routing for Unmapped Codes or a ruling-driven reclassification — carrying a source identifier the ledger can cite.
- The originally-paid duty per line, persisted immutably. You cannot compute a refund delta without the exact duty CBP accepted on the original entry summary. Read it from the historical record, not a mutable working table.
- A shared duty-computation function. Original and corrected duty must be computed by the same rounding rules, or the delta is an artifact of two different rounding conventions rather than a real refund. Use the ad valorem and compound-duty logic defined in Duty Formula Calculation Frameworks.
- An append-only refund ledger. This page posts to it; it does not create it. Rows are inserted and never updated.
Implementation
One reconciler recomputes duty under the corrected classification, diffs it against the original payment, and posts a ledger entry that carries the reclassification source and a reproducible audit hash. It posts a refundable entry only when the corrected duty is genuinely lower.
import logging
import hashlib
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional
from decimal import Decimal, ROUND_HALF_UP
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler()],
)
logger = logging.getLogger("refund_reconciler")
CENT = Decimal("0.01")
def ad_valorem_duty(entered_value: Decimal, rate: Decimal) -> Decimal:
"""Single source of truth for duty rounding — CBP half-up to the cent."""
return (entered_value * rate).quantize(CENT, rounding=ROUND_HALF_UP)
class LedgerKind(Enum):
REFUND = "REFUND" # corrected duty is lower
NO_REFUND = "NO_REFUND" # corrected duty equal or higher
ADDITIONAL_DUTY = "ADDITIONAL_DUTY" # corrected duty higher — owed, not refunded
@dataclass(frozen=True)
class OriginalPayment:
entry_no: str
line_no: int
hts_code: str
entered_value: Decimal # Decimal, never float
duty_paid: Decimal # the exact cent CBP recorded as paid
@dataclass(frozen=True)
class Reclassification:
line_no: int
corrected_hts: str
corrected_rate: Decimal
source_ref: str # ruling no., resolver decision id — ties the change to a cause
@dataclass(frozen=True)
class RefundLedgerEntry:
entry_no: str
line_no: int
kind: LedgerKind
original_hts: str
corrected_hts: str
duty_paid: Decimal
corrected_duty: Decimal
refund_delta: Decimal # positive = refundable to importer
source_ref: str
audit_hash: str
posted_at: datetime = field(default_factory=datetime.utcnow)
class RefundReconciler:
"""Reconcile a reclassification against the original payment. Pure and deterministic."""
def _audit(self, pay: OriginalPayment, rc: Reclassification,
corrected_duty: Decimal) -> str:
payload = (
f"{pay.entry_no}|{pay.line_no}|{pay.duty_paid}|"
f"{rc.corrected_hts}|{corrected_duty}|{rc.source_ref}"
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def reconcile(
self, pay: OriginalPayment, rc: Reclassification
) -> RefundLedgerEntry:
if rc.line_no != pay.line_no:
raise ValueError(
f"Reclassification line {rc.line_no} does not match "
f"payment line {pay.line_no} on entry {pay.entry_no}."
)
corrected_duty = ad_valorem_duty(pay.entered_value, rc.corrected_rate)
# Refund is what was overpaid: original minus corrected, both rounded.
refund_delta = (pay.duty_paid - corrected_duty).quantize(CENT)
if refund_delta > Decimal("0.00"):
kind = LedgerKind.REFUND
elif refund_delta == Decimal("0.00"):
kind = LedgerKind.NO_REFUND
else:
kind = LedgerKind.ADDITIONAL_DUTY
entry = RefundLedgerEntry(
entry_no=pay.entry_no,
line_no=pay.line_no,
kind=kind,
original_hts=pay.hts_code,
corrected_hts=rc.corrected_hts,
duty_paid=pay.duty_paid,
corrected_duty=corrected_duty,
refund_delta=refund_delta,
source_ref=rc.source_ref,
audit_hash=self._audit(pay, rc, corrected_duty),
)
logger.info(
"Reconciled entry %s line %s: %s | refund delta USD %s | src %s",
pay.entry_no, pay.line_no, kind.value, refund_delta, rc.source_ref,
)
return entry
def post_ledger(entry: RefundLedgerEntry, ledger: list[dict]) -> None:
"""Append-only: insert one immutable row, never update a prior refund."""
if entry.kind is not LedgerKind.REFUND:
logger.info(
"Entry %s line %s is %s; nothing to refund.",
entry.entry_no, entry.line_no, entry.kind.value,
)
ledger.append({
"entry_no": entry.entry_no,
"line_no": entry.line_no,
"kind": entry.kind.value,
"original_hts": entry.original_hts,
"corrected_hts": entry.corrected_hts,
"duty_paid": str(entry.duty_paid),
"corrected_duty": str(entry.corrected_duty),
"refund_delta": str(entry.refund_delta),
"source_ref": entry.source_ref,
"audit_hash": entry.audit_hash,
"posted_at": entry.posted_at.isoformat(),
})
Two invariants make the ledger auditable. First, original and corrected duty pass through the same ad_valorem_duty function, so refund_delta is a true difference and not a rounding artifact of two conventions. Second, the sign of refund_delta alone chooses the LedgerKind: a positive delta is a genuine overpayment refund, zero is a no-op, and a negative delta is additional duty owed — which is not a refund and must instead flow through a Post-Summary Correction, covered in filing post-summary corrections with Python. Every posted row cites source_ref, tying the refund back to the reclassification that justified it.
duty_paid − corrected_duty selects REFUND, NO_REFUND, or ADDITIONAL_DUTY, and every row cites its reclassification source.Verification steps
Run these checks against a representative entry before trusting the reconciler in production:
- Refund direction. Reconcile a payment of USD 1,250.00 against a corrected rate that lowers duty to USD 900.00. Assert
kind == REFUNDandrefund_delta == Decimal("350.00"). A negative or zero result here means the paid and corrected figures were transposed. - No-refund parity. Reconcile with a corrected rate equal to the original. Assert
refund_delta == Decimal("0.00")andkind == NO_REFUND, and confirmpost_ledgerstill writes a row for the audit trail rather than dropping the reconciliation. - Additional-duty routing. Reconcile with a corrected rate that raises duty. Assert
kind == ADDITIONAL_DUTYand a negativerefund_delta— this case must not post a refund; it belongs to the Post-Summary Correction path. - Shared-rounding checksum. Independently compute
ad_valorem_duty(entered_value, corrected_rate)and confirmcorrected_dutymatches to the cent. A divergence means a second rounding convention slipped into one of the two duty computations. - Line-match guard. Pass a
Reclassificationwhoseline_nodiffers from the payment. AssertreconcileraisesValueError— a refund must never be reconciled across mismatched entry lines. - Source traceability. Confirm every posted row carries a non-empty
source_refand that itsaudit_hashis reproducible fromentry_no,line_no,duty_paid,corrected_hts,corrected_duty, andsource_ref. A refund without a source reference is unauditable. - Ledger immutability. Post a refund, then reconcile and post again for the same line; confirm the ledger grows by new rows with a fresh
posted_atand never overwrites the prior entry.
Edge cases & gotchas
- Rounding both operands independently is mandatory. Duty paid was rounded to the cent at original filing; corrected duty must be rounded the same way before subtraction. Subtracting an unrounded corrected duty from a rounded paid figure yields a sub-cent delta that will not reconcile against CBP’s records.
- A lower rate does not always mean a refund. Compound and specific-rate lines can move in ways an ad valorem-only reconciler misreads. If the corrected classification carries a specific or compound duty, route it through the compound-duty logic in the duty framework rather than the
ad_valorem_dutyshortcut shown here. - Refund eligibility has a legal clock. A refund via Post-Summary Correction is only available while the entry is unliquidated; after liquidation the path is a protest under 19 USC 1514. The reconciler computes the delta, but posting an actual refund claim must be gated on liquidation status.
- Never net refunds across entries. Each
RefundLedgerEntryis scoped to one entry line. Aggregating refunds and additional duty across lines into a single net figure obscures the per-line trail CBP expects and breaks the line-level audit hash. - Timezone-naive
posted_at.datetime.utcnow()is naive; if downstream systems compare posting times against a liquidation or protest deadline, persist an explicit UTC offset so a reconciliation posted near midnight is not mis-dated. - Interest is out of scope for the delta. CBP may owe or assess interest on refunded or additional duty, but that is a separate computation. Keep
refund_deltaas the pure duty difference and post interest as its own ledger kind rather than folding it into the refund figure.