Back to Feed
article 3m read

Supplier import could rewrite another supplier's products

When a spreadsheet row didn't match a supplier's own catalog, my import searched every product and wrote case sizes to whatever it found, and the schema let two suppliers share a product. I deleted the wide lookups, put an ownership check and a preflight in front of the writes, moved one-supplier-per-product into the database, and left a few loose ends I'm still not happy about.

Article DepthPractice

Flow

What now stands between an import and another supplier's product

  1. 1Wide lookups deleted

    The any-supplier ID, SKU and name lookups are gone. One brand-limited name search still looks at every product.

  2. 2Check before each write

    If another supplier holds a link to the product, the import stops with a conflict error.

  3. 3Catch looks for the conflict

    The per-row catch ends the import on that error instead of counting one more failed row.

  4. 4Check before any row

    If this supplier already shares a product with another one, the whole file is refused.

  5. 5Routes check the owner

    Edit and delete load the link first and say not found if it belongs to someone else.

  6. 6One supplier per product

    The migration clears doubles, moves cart lines to the kept link, and adds the database rule last.

The supplier import in the ordering system had a bad habit. When a row in one supplier's sheet didn't match that supplier's products, it went and searched everyone's. Then it wrote to whatever it found.

How the matching worked

The import reads a sheet for one supplier, one row at a time. It looked in that supplier's own products first, by ID, SKU and a loose name match. If those missed, it tried the ID and SKU again over every product in the system. Then it tried a name match across all products, limited by brand. If that missed too, it ran the loose name match over every product as well.

Whatever it found got a new case size. That number sits on the product itself, which every supplier linked to it shares.

Why it mattered

The database let two suppliers link the same product. It only stopped one supplier from linking it twice. So an import for one supplier could find a product that another supplier also used, and change a number that supplier counted on. I can see the path was open. I can't see that it ever actually happened.

What closed it

The any-supplier lookups are gone. A fallback whose job is to search outside the catalog isn't worth patching.

One wide search is still there: the brand-limited name match still looks at every product. So now, before the import writes to a product, it checks whether any other supplier holds a link to it. If one does, it stops with a conflict error. The same check guards hand edits to case size and new links.

That error almost got lost. Each row ran inside a catch that only counted failures, so the new error would have become one more failed row. The catch now checks for the conflict first and ends the import.

There's also a check before any row runs. If this supplier already shares a product with another one, the whole file is refused. Refusing up front matters, since as far as I can tell, stopping halfway leaves the earlier rows written. It won't catch everything, though: a row the brand search matches to another supplier's product still stops the run partway.

The edit and delete routes trusted the supplier ID in the URL without checking that the link was really that supplier's. Now they check and say "not found" when it isn't.

None of this came with tests, and it shipped alongside a big new order-grid screen and some unrelated tweaks. Nothing in it tries to write into another supplier's catalog to see whether the new check says no.

The database rule

Code checks only help the code that calls them. The database now allows just one supplier per product, so no path can add a second link. Before that rule could go on, the migration cleaned up existing doubles. It picked a winner for each product, active and recently updated first, moved cart lines over to it, deleted the rest, and added the rule last. If carts move first, nothing points at a deleted row.

The catch is that the losing links' prices and SKUs are thrown away, and the winner is picked by recency, not by who really owned the product.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward