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 Decimal for every currency figure. A refund is the difference of two rounded duty amounts; float subtraction introduces representational error that will not reconcile against the penny CBP recorded as paid. Never use float for 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.

Refund reconciliation from original versus corrected duty The original payment supplies entered value, original HTS code, and the exact duty paid. A reclassification supplies a corrected HTS code, a corrected rate, and a source reference. Both the original and corrected duty are computed through the same ad valorem rounding function, half-up to the cent, over the same entered value. The refund delta is duty paid minus corrected duty. The delta sign selects the ledger kind: a positive delta posts a REFUND row that is refundable to the importer; a zero delta posts NO_REFUND; a negative delta posts ADDITIONAL_DUTY, which is owed rather than refunded and routes to a Post-Summary Correction. Every row is written once to an append-only refund ledger and cites the reclassification source reference and a SHA-256 audit hash. original payment entered value · orig HTS duty paid reclassification corrected HTS · rate source_ref ad_valorem_duty() same ROUND_HALF_UP entered value corrected rate paid − corrected > 0 REFUND refundable to importer NO_REFUND = 0 < 0 ADDITIONAL_DUTY owed · route to PSC append-only refund ledger
Original and corrected duty pass through the same rounding function; the sign of 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:

  1. Refund direction. Reconcile a payment of USD 1,250.00 against a corrected rate that lowers duty to USD 900.00. Assert kind == REFUND and refund_delta == Decimal("350.00"). A negative or zero result here means the paid and corrected figures were transposed.
  2. No-refund parity. Reconcile with a corrected rate equal to the original. Assert refund_delta == Decimal("0.00") and kind == NO_REFUND, and confirm post_ledger still writes a row for the audit trail rather than dropping the reconciliation.
  3. Additional-duty routing. Reconcile with a corrected rate that raises duty. Assert kind == ADDITIONAL_DUTY and a negative refund_delta — this case must not post a refund; it belongs to the Post-Summary Correction path.
  4. Shared-rounding checksum. Independently compute ad_valorem_duty(entered_value, corrected_rate) and confirm corrected_duty matches to the cent. A divergence means a second rounding convention slipped into one of the two duty computations.
  5. Line-match guard. Pass a Reclassification whose line_no differs from the payment. Assert reconcile raises ValueError — a refund must never be reconciled across mismatched entry lines.
  6. Source traceability. Confirm every posted row carries a non-empty source_ref and that its audit_hash is reproducible from entry_no, line_no, duty_paid, corrected_hts, corrected_duty, and source_ref. A refund without a source reference is unauditable.
  7. 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_at and 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_duty shortcut 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 RefundLedgerEntry is 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_delta as the pure duty difference and post interest as its own ledger kind rather than folding it into the refund figure.

Up: Post-Entry Amendment Workflows