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
Decimaleverywhere for money. PSC duty deltas are the arithmetic difference of two rounded currency figures; a singlefloatanywhere in the chain produces sub-cent drift that ACE will reject on entered-value validation. Never usefloatfor 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.
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:
- Window boundary parity. Assemble the same correction with
as_ofone day before and one day after the liquidation date. The first must returnFILABLE; the second must returnWINDOW_CLOSED. Confirm the assembler uses the feed’sliquidation_datewhen present and only falls back toentry_date + 314 dayswhen it is absent. - Delta sign correctness. For a reclassification that lowers the rate, assert
net_duty_deltais negative; for one that raises it, assert positive. A flipped sign here silently misstates whether the filer owes CBP or is owed a refund. - No-op suppression. Feed a corrected summary identical to the original. The outcome must be
NO_CHANGEwith an emptyline_deltas, andappend_auditmust write zero rows. A phantom correction here pollutes the audit trail. - Reason-code coverage. Every emitted
PscLineDeltamust carry aPscReason. Assert no line defaults incorrectly — an HTS change must map toCLASSIFICATION, a value change toVALUATION, unless an explicitreasonsoverride is supplied. - Cent-level reconciliation. Independently sum each line’s
corrected_duty − original_dutyand confirm it equalsnet_duty_deltaexactly underROUND_HALF_UP. A mismatch means afloatleaked intoentered_valueorduty_rate. - Audit immutability. After
append_audit, re-run the assembler and append again; confirm the store grows by new rows with a freshprepared_atand never mutates a prior row. Theaudit_hashmust be reproducible from the corrected lines alone. - Missing-line safety. Include a corrected line whose
line_nois 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_datefrom 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
FILABLEpackets with a positivenet_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()onEntryLine, 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 onduty()andhts_codetogether 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.