Filing post-summary corrections with Python

A Post-Summary Correction (PSC) is the mechanism CBP gives a filer to amend an already-accepted entry summary while the entry is still unliquidated, and getting it wrong in software means either a rejected transmission or an unauditable change to a legally filed duty figure. This page builds the PSC assembly path in Python: it detects whether the entry is still inside the correctable window, computes the corrected entry-summary delta against the original filing, attaches the mandatory PSC reason code, and writes an append-only audit record before anything is transmitted to ACE. Duty deltas are computed with Decimal and CBP half-up rounding so the corrected duty owed or refundable reconciles to the cent. The goal is a single deterministic function that either produces a filable correction packet or refuses — never a partial, unlogged amendment.

Prerequisites

This solution assumes an established transmission pipeline and a persisted record of what was originally filed. Confirm each before applying it:

  • Python 3.10+ with Decimal everywhere for money. PSC duty deltas are the arithmetic difference of two rounded currency figures; a single float anywhere in the chain produces sub-cent drift that ACE will reject on entered-value validation. Never use float for entered value, duty, or rates.
  • The original accepted entry summary, persisted immutably. You cannot compute a delta without the exact values CBP already accepted — original entered value, duty paid, HTS lines, and the entry number. These are the CBP 7501 line items produced upstream by Entry Summary Filing Schemas. Read them from a read-only historical record, not from a mutable working table.
  • A liquidation-status feed. The correctable window closes at liquidation. You need the scheduled liquidation date (typically 314 days from the entry date, though CBP may extend or suspend it) so the resolver can decide whether a PSC is still permissible or whether the amendment must instead route to a protest under 19 USC 1514.
  • A PSC reason-code table. Every PSC line carries a reason for the change. Provision the CBP reason-code enumeration so the assembler flags each corrected field rather than transmitting a bare value.
  • An append-only audit store. This page writes to it; it does not create it. A row is inserted per correction and never updated in place.

Implementation

One assembler owns the whole PSC decision: window check, delta computation, reason flagging, and the append-only audit write. It returns a filable packet only when the entry is genuinely correctable and the numbers reconcile.

import logging
import hashlib
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
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("psc_assembler")

CENT = Decimal("0.01")
LIQUIDATION_DAYS = 314  # statutory default; extensions/suspensions override


class PscReason(Enum):
    CLASSIFICATION = "01"   # HTS reclassification
    VALUATION = "02"        # entered value correction
    QUANTITY = "03"         # quantity / UOM correction
    ORIGIN = "04"           # country of origin correction


class PscOutcome(Enum):
    FILABLE = "FILABLE"
    NO_CHANGE = "NO_CHANGE"
    WINDOW_CLOSED = "WINDOW_CLOSED"


@dataclass(frozen=True)
class EntryLine:
    line_no: int
    hts_code: str
    entered_value: Decimal   # Decimal, never float
    duty_rate: Decimal       # ad valorem fraction, e.g. Decimal("0.0350")

    def duty(self) -> Decimal:
        # CBP computes duty per line, rounded half-up to the cent.
        return (self.entered_value * self.duty_rate).quantize(
            CENT, rounding=ROUND_HALF_UP
        )


@dataclass(frozen=True)
class EntrySummary:
    entry_no: str
    entry_date: date
    lines: tuple[EntryLine, ...]
    liquidation_date: Optional[date] = None  # from the liquidation feed

    def total_duty(self) -> Decimal:
        return sum((ln.duty() for ln in self.lines), Decimal("0.00"))


@dataclass(frozen=True)
class PscLineDelta:
    line_no: int
    field_changed: str
    reason: PscReason
    original_duty: Decimal
    corrected_duty: Decimal

    @property
    def duty_delta(self) -> Decimal:
        # Positive = additional duty owed; negative = refundable.
        return (self.corrected_duty - self.original_duty).quantize(CENT)


@dataclass(frozen=True)
class PscPacket:
    entry_no: str
    outcome: PscOutcome
    line_deltas: tuple[PscLineDelta, ...]
    net_duty_delta: Decimal
    audit_hash: str
    prepared_at: datetime = field(default_factory=datetime.utcnow)


class PscAssembler:
    """Assemble a filable PSC or refuse. Pure function of (original, corrected, as_of)."""

    def _window_open(self, original: EntrySummary, as_of: date) -> bool:
        close = original.liquidation_date or (
            original.entry_date + timedelta(days=LIQUIDATION_DAYS)
        )
        # PSC must be filed and accepted before liquidation.
        return as_of < close

    def _audit(self, entry_no: str, deltas: tuple[PscLineDelta, ...]) -> str:
        payload = entry_no + "|" + "|".join(
            f"{d.line_no}:{d.field_changed}:{d.corrected_duty}" for d in deltas
        )
        return hashlib.sha256(payload.encode("utf-8")).hexdigest()

    def assemble(
        self,
        original: EntrySummary,
        corrected: EntrySummary,
        as_of: Optional[date] = None,
        reasons: Optional[dict[int, PscReason]] = None,
    ) -> PscPacket:
        entry_date = as_of or date.today()
        reasons = reasons or {}

        if not self._window_open(original, entry_date):
            logger.warning(
                "PSC window closed for entry %s; route to protest (19 USC 1514).",
                original.entry_no,
            )
            return PscPacket(
                original.entry_no, PscOutcome.WINDOW_CLOSED, (),
                Decimal("0.00"), self._audit(original.entry_no, ()),
            )

        orig_by_line = {ln.line_no: ln for ln in original.lines}
        deltas: list[PscLineDelta] = []

        for corr in corrected.lines:
            base = orig_by_line.get(corr.line_no)
            if base is None:
                logger.error(
                    "Corrected line %s absent from original entry %s; skipping.",
                    corr.line_no, original.entry_no,
                )
                continue
            if base.duty() == corr.duty() and base.hts_code == corr.hts_code:
                continue  # unchanged line — never emit a no-op correction
            field_changed = (
                "hts_code" if base.hts_code != corr.hts_code else "entered_value"
            )
            reason = reasons.get(
                corr.line_no,
                PscReason.CLASSIFICATION if field_changed == "hts_code"
                else PscReason.VALUATION,
            )
            deltas.append(
                PscLineDelta(
                    corr.line_no, field_changed, reason,
                    base.duty(), corr.duty(),
                )
            )

        if not deltas:
            logger.info("No material change for entry %s; PSC suppressed.",
                        original.entry_no)
            return PscPacket(
                original.entry_no, PscOutcome.NO_CHANGE, (),
                Decimal("0.00"), self._audit(original.entry_no, ()),
            )

        net = sum((d.duty_delta for d in deltas), Decimal("0.00"))
        frozen = tuple(deltas)
        packet = PscPacket(
            original.entry_no, PscOutcome.FILABLE, frozen,
            net.quantize(CENT), self._audit(original.entry_no, frozen),
        )
        logger.info(
            "PSC filable: entry %s | %d line(s) | net duty delta USD %s",
            original.entry_no, len(frozen), packet.net_duty_delta,
        )
        return packet


def append_audit(packet: PscPacket, store: list[dict]) -> None:
    """Append-only: insert one immutable row per correction, never update."""
    for d in packet.line_deltas:
        store.append({
            "entry_no": packet.entry_no,
            "line_no": d.line_no,
            "field": d.field_changed,
            "reason": d.reason.value,
            "original_duty": str(d.original_duty),
            "corrected_duty": str(d.corrected_duty),
            "duty_delta": str(d.duty_delta),
            "audit_hash": packet.audit_hash,
            "prepared_at": packet.prepared_at.isoformat(),
        })

The load-bearing rule lives in two places: _window_open refuses any correction once liquidation has passed — after that point a PSC is not the legal instrument, a protest is — and the base.duty() == corr.duty() guard suppresses no-op lines so the audit store never accumulates phantom corrections. A net positive net_duty_delta means additional duty is owed with the PSC; a negative value flags a refund that then flows into reconciling duty refunds after reclassification.

The PSC assembly decision and correction packet An original accepted entry summary and a corrected entry summary enter the assembler. First gate: is the entry still before its liquidation date, i.e. inside the correctable window? If no, the assembler returns WINDOW_CLOSED and routes the amendment to a protest under 19 USC 1514. If yes, each corrected line is diffed against the original line by duty and HTS code. Unchanged lines are dropped so no no-op correction is emitted. Each materially changed line becomes a PscLineDelta carrying the changed field, a PSC reason code, the original duty, and the corrected duty; its duty delta is corrected minus original. If no deltas remain the outcome is NO_CHANGE; otherwise the outcome is FILABLE with a net duty delta, where positive means additional duty owed and negative means refundable. Every filable packet is written once to an append-only audit store keyed by a SHA-256 hash of the corrected lines. original entry summary CBP-accepted 7501 corrected entry summary reclassified / revalued before liquidation? no WINDOW_CLOSED route to protest yes diff each line: duty & HTS drop unchanged · no no-op PscLineDelta field · reason code · orig duty corrected duty delta = corrected − original no deltas NO_CHANGE PSC suppressed FILABLE net delta: + owed / − refund PSC reason per line append-only audit store
A PSC is assembled only inside the correctable window; each changed line becomes a reason-coded PscLineDelta, no-op lines are dropped, and every filable packet is written once to an append-only audit store.

Verification steps

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

  1. Window boundary parity. Assemble the same correction with as_of one day before and one day after the liquidation date. The first must return FILABLE; the second must return WINDOW_CLOSED. Confirm the assembler uses the feed’s liquidation_date when present and only falls back to entry_date + 314 days when it is absent.
  2. Delta sign correctness. For a reclassification that lowers the rate, assert net_duty_delta is negative; for one that raises it, assert positive. A flipped sign here silently misstates whether the filer owes CBP or is owed a refund.
  3. No-op suppression. Feed a corrected summary identical to the original. The outcome must be NO_CHANGE with an empty line_deltas, and append_audit must write zero rows. A phantom correction here pollutes the audit trail.
  4. Reason-code coverage. Every emitted PscLineDelta must carry a PscReason. Assert no line defaults incorrectly — an HTS change must map to CLASSIFICATION, a value change to VALUATION, unless an explicit reasons override is supplied.
  5. Cent-level reconciliation. Independently sum each line’s corrected_duty − original_duty and confirm it equals net_duty_delta exactly under ROUND_HALF_UP. A mismatch means a float leaked into entered_value or duty_rate.
  6. Audit immutability. After append_audit, re-run the assembler and append again; confirm the store grows by new rows with a fresh prepared_at and never mutates a prior row. The audit_hash must be reproducible from the corrected lines alone.
  7. Missing-line safety. Include a corrected line whose line_no is absent from the original. Assert it is logged and skipped, not silently invented — a PSC cannot add a line that CBP never accepted.

Edge cases & gotchas

  • Liquidation suspension resets the window. When CBP suspends liquidation (for an antidumping/countervailing case, for example), the 314-day default is wrong and the window may stay open far longer. Always prefer the liquidation_date from the feed; never hard-code the statutory default as the sole source of truth.
  • A PSC that increases duty is not optional. If reasonable care surfaces an underpayment, the correction is a legal obligation, not a discretionary refund pursuit. Do not let a UI or business rule suppress FILABLE packets with a positive net_duty_delta.
  • Aggregate vs. per-line duty rounding. CBP rounds duty per line, then sums. Computing duty on an aggregated entered value and rounding once produces cent-level divergence that ACE flags against your line detail. Keep duty() on EntryLine, as shown, and sum the rounded results.
  • HTS change without a value change still needs a duty diff. A reclassification can move the rate even when entered value is unchanged, so diff on computed duty(), not on entered value alone. The assembler keys the change decision on duty() and hts_code together for exactly this reason.
  • Naive utcnow() timestamps. datetime.utcnow() is timezone-naive; if your audit store expects timezone-aware values, persist an explicit UTC offset so a correction prepared near midnight is not mis-dated relative to the liquidation clock.
  • Reconciliation entries are a different instrument. A PSC amends a specific entry summary. Value or classification changes tracked under CBP’s Reconciliation program flag a different transmission path — do not overload the PSC assembler to cover flagged reconciliation entries.

Up: Post-Entry Amendment Workflows