DEV Community

life
life

Posted on

Fiber composition is a percentage array with an ordering rule

Most product catalogs store fabric as a string. material: "cotton blend", or "95% Cotton 5% Spandex" typed by whoever opened the supplier sheet that week. The string is fine for a shopper scanning a page. It is useless for a system that has to produce a label, because a label is an ordering rule applied to numbers, with a floor, a remainder class and a placement constraint, which is not the same object as a string.

The Federal Trade Commission's textile rules at 16 CFR 303.16 require, on the label, the generic names and percentages by weight of constituent fibers present at 5 percent or more, in order of predominance by weight, with any portion that must be designated as other fiber appearing last. The same section requires the name or registered identification number of a responsible party, and the country where the product was processed or manufactured, all of it clearly legible, conspicuous and readily accessible, with the fiber content in type of equal size. 303.15 adds that the origin disclosure always appears on the front side of the label, and that for products with a neck the origin goes inside the center of the neck.

That is a surprising amount of logic for a tag. Here is how it lands in a schema.

Store the array, render the string

create table fiber_component (
  product_id      text not null,
  generic_name    text not null,          -- FTC generic name, not a marketing name
  pct_by_weight   numeric not null check (pct_by_weight > 0 and pct_by_weight <= 100),
  is_other_fiber  boolean not null default false,
  recorded_at     timestamptz not null,
  primary key (product_id, generic_name, recorded_at)
);

create table label_identity (
  product_id    text not null,
  party_kind    text not null check (party_kind in ('business_name','house_mark','rn')),
  party_value   text not null,
  rn_holder     text,                     -- required when party_kind = 'rn'
  primary key (product_id, party_kind)
);

create table label_origin (
  product_id        text not null,
  processed_country text not null,        -- where it was processed or manufactured
  has_neck          boolean not null,
  front_side        boolean not null default true,
  primary key (product_id)
);
Enter fullscreen mode Exit fullscreen mode

Three tables because three disclosures, and each one fails differently. The component rows are per measurement event, so a supplier change is a new set of rows rather than an update, and the audit trail is the point. A single updated_at on a product row cannot answer what the label said on the previous production run.

The render function is where the rule lives

type Comp = { name: string; pct: number; other: boolean };

// 303.16(a)(1) sets the disclosure floor at 5 percent by weight.
// Sub-floor components are governed by 303.3, which is a separate rule set
// with its own treatment; read it before you decide what this function does
// with the remainder. This version refuses to guess.
const FLOOR = 5;

export function renderFiberContent(comps: Comp[]): string {
  const sum = comps.reduce((a, c) => a + c.pct, 0);
  if (sum > 100.5) throw new Error(`components sum to ${sum}`);

  const below = comps.filter((c) => c.pct < FLOOR && !c.other);
  if (below.length) throw new Error('sub-floor components need 303.3 treatment');

  return comps
    .filter((c) => !c.other)
    .sort((a, b) => b.pct - a.pct)
    .concat(comps.filter((c) => c.other).sort((a, b) => a.pct - b.pct))
    .map((c) => `${c.name} ${c.pct}%`)
    .join(', ');
}
Enter fullscreen mode Exit fullscreen mode

The two sorts carry the rule. Order of predominance by weight is required, and any portion designated as other fiber appears last. Rendering "95% Cotton 5% Spandex" from a text field gets the right answer for the wrong reason, and it breaks the first time a composition has four components or a sub-floor fiber shows up.

has_neck and front_side are stored, not assumed, because placement is a property of the article. A validator that renders correct content into a position the rule does not accept has manufactured non-compliant labels faster than any person could.

The listing and the label are one fact

303.34 requires that when a textile product is advertised in mail order promotional material, the description states the product was made in the U.S.A., imported, or both, and that this statement must not be inconsistent with the origin labeling on the product. Treat that as a join with an assertion, not as two editable fields in two admin screens.

create view label_drift as
  select p.product_id,
         o.processed_country,
         l.listing_origin,
         case
           when o.processed_country is null then 'missing_origin_fact'
           when lower(coalesce(l.listing_origin,'')) = '' then 'listing_silent'
           when lower(l.listing_origin) = 'made in usa'
                and o.processed_country <> 'US' then 'listing_vs_label_conflict'
           when lower(l.listing_origin) <> 'made in usa'
                and o.processed_country = 'US' then 'listing_vs_label_conflict'
           else null
         end as problem
  from products p
  join label_origin o using (product_id)
  left join listings l using (product_id);
Enter fullscreen mode Exit fullscreen mode

listing_silent is the row most shops ignore, and it maps to a complaint about the listing rather than a letter about the goods.

Where a document may substitute, and where it may not

303.31 lets an invoice or another document stand where a textile product reaches a buyer in some form other than the one meant for the ultimate consumer to buy, receive or use, and that paper then carries the fiber content the rules provide for plus any other required information, on top of the issuer's name and address. That is a property of the goods at a moment, so it belongs in the model as a per-shipment state, not as a boolean on the SKU.

export function substituteDocumentErrors(ship: {
  formForConsumer: boolean;
  issuerName: string | null;
  issuerAddress: string | null;
  fiberContentOnDoc: string | null;
  otherRequiredOnDoc: boolean;
}): string[] {
  if (ship.formForConsumer) return ['substitute_not_available_for_finished_goods'];
  const e: string[] = [];
  if (!ship.issuerName) e.push('issuer_name_missing');
  if (!ship.issuerAddress) e.push('issuer_address_missing');
  if (!ship.fiberContentOnDoc) e.push('fiber_content_missing_from_document');
  if (!ship.otherRequiredOnDoc) e.push('other_required_info_missing');
  return e;
}
Enter fullscreen mode Exit fullscreen mode

A wholesale destination alone does not excuse the tag. A carton of finished leggings picked and mailed to shoppers is in the form intended for delivery to the consumer whoever paid for it, so the first line of that function is the one that fails most often in a system where a b2b channel code was read as permission to skip the tag.

One boundary. This is a model of what the published rules ask a system to hold and render. Whether a specific article, composition or label layout is lawful is not decided by a view in your database. FulfillNexa by SBT (fulfillnexa.com) runs seller stock out of three buildings in China, the Suzhou floor at 13,000 square meters, Dongguan at 8,000 and Shenzhen at 3,000, and ours is the smaller observation that these three facts live in different systems, a garment, a listing template and a label file, and that they agree only when somebody makes them agree.

Top comments (0)