DEV Community

life
life

Posted on

A Damage Claim Is a Table With Evidence Columns and a Clock

A claim does not fail because the product broke. It fails because the record of what was packed was never stored anywhere retrievable, and by the time anyone wants it, the only remaining source is the memory of whoever was on that bench eleven days ago.

So the feature is a table with the right columns, written at the moment the carton closes, plus a clock attached to it. Not a nicer support workflow.

What has to be captured at pack-out

The scan that seals a carton is already producing most of this. It is usually thrown away.

create table pack_record (
  carton_id        text primary key,
  order_ref        text not null,
  station_id       text not null,
  operator_id      text not null,
  packed_at        timestamptz not null,
  scale_weight_g   integer not null,
  label_id         text,
  label_voided_at  timestamptz,
  manifest_scan_at timestamptz,
  dunnage_profile  text not null,   -- resolved from the product record, not typed
  building         text not null
);

create table pack_item (
  carton_id   text not null references pack_record,
  sku         text not null,
  qty         integer not null,
  unit_scan   boolean not null default false,
  primary key (carton_id, sku)
);
Enter fullscreen mode Exit fullscreen mode

The column people skip is dunnage_profile. Packaging choice is a property of the product, and if it is not recorded per carton then a claim cannot distinguish "this bottle was packed wrong" from "this bottle was packed to spec and still broke", which is exactly the distinction the carrier and the supplier both want.

unit_scan matters for a different reason. A carton packed by weight alone can contain the wrong unit and still be correct on every other field.

The claim row, and the clock that lives on it

create table damage_claim (
  id             bigserial primary key,
  carton_id      text references pack_record,
  opened_at      timestamptz not null default now(),
  filed_by       text not null,           -- shipper_of_record | seller | partner
  evidence       jsonb not null default '[]',
  disposition    text not null default 'pending',
  written_off_qty integer not null default 0,
  deadline_at    timestamptz,             -- carrier window, from the dated rule row
  closed_at      timestamptz
);
Enter fullscreen mode Exit fullscreen mode

Two design notes that decide whether this works.

The first is filed_by. The party with standing is normally whoever is named on the label, and in a third-party setup that is a real question with a real answer that has to be written down before the parcel moves. Left blank, both sides wait for the other to file and the claim never exists. This column is the difference between a process and a rumor.

The second is deadline_at, and where its value comes from. Carrier claim windows and default liability ceilings are published terms that change, and declared value coverage is bought when the label is purchased rather than retrofitted afterwards. Store the deadline as data with a source date, in the same dated-rule-table shape you would use for any other external constraint, and never compute it from a constant in code. A hard-coded window is a claim that quietly expires six months before anybody notices the terms moved.

The bug that outlives the claim

Here is the part that keeps costing money after the refund is issued. A damaged unit that is refunded and never dispositioned is still sitting in your available quantity.

The write-off has to be a transaction, not a status on the claim row:

begin
  update damage_claim set disposition='written_off', closed_at=now() where id=$1
  insert into stock_adjustment (sku, building, qty, reason, claim_id)
       values ($2, $3, -$4, 'damage', $1)
  -- available recomputes from on_hand + committed + adjustment
commit
Enter fullscreen mode Exit fullscreen mode

If the adjustment is a note instead of a row, the storefront will sell the phantom unit, the next order will fail at pick, and the exception will be filed under a completely different problem. Every damage claim needs a disposition with a quantity effect, and the quantity effect has to reach the number your channels are allowed to sell.

What to ask a partner, in order

Can you pull the pack record for this carton, by carton id, ten days later. Does it include the dunnage profile and the weight the scale read. Who is named as shipper of record on my labels. Who files, and what does the evidence pack contain when it leaves your building. And when a unit is written off, which system's available number changes.

Those five questions map one to one onto the columns above, which is the point. A partner who can answer them is describing a schema. A partner who answers with a promise is describing a habit, and habits do not survive a busy week or a staff change.

FulfillNexa by SBT (fulfillnexa.com) keeps the pack record with the carton and the write-off with the claim, on floors in Suzhou with 13,000 square meters, Shenzhen with 3,000 and Dongguan with 8,000. We do not publish rates or transit commitments, and we do not decide disposition policy on a seller's behalf. What we will commit to is that the row exists, that it names the building the carton left, and that a damaged unit does not stay sellable because nobody pressed a button.

Top comments (0)