Skip to content
Dylan San GabrielDylan
Get in touch

GET /api/work/internal-erp/inventory

Counting what sells, not what exists

Live · · sole engineer

A nightly job decides which products are worth counting and reordering. It had been calling nearly everything important, which is the same as calling nothing important.A nightly classifier ranked all 27,215 products and made 1,979 of them class A. Ranking only what sells turned a fake Pareto into a real one.

  • TypeScript
  • PostgreSQL
  • Node.js
  • GS1
  • HIBC
revenue in the A class
89%
products in it
745
sellable catalog
27K
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/inventory"
source: src/content/projects/{id}

Response200 OK · 3.8 KB

{
  "id": "internal-erp/inventory",
  "title": "Counting what sells, not what exists",
  "status": "live",
  "year": 2026,
  "category": "work",
  "role": "sole engineer",
  "summary": "A nightly classifier ranked all 27,215 products and made 1,979 of them class A. Ranking only what sells turned a fake Pareto into a real one.",
  "summary_plain": "A nightly job decides which products are worth counting and reordering. It had been calling nearly everything important, which is the same as calling nothing important.",
  "stack": [
    "TypeScript",
    "PostgreSQL",
    "Node.js",
    "GS1",
    "HIBC"
  ],
  "metrics": {
    "revenue_in_the_a_class": "89%",
    "products_in_it": "745",
    "sellable_catalog": "27K"
  },
  "parent": {
    "id": "internal-erp",
    "title": "The internal ERP that runs a dental supply company",
    "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
  },
  "problem": "A nightly job sorts the catalog into A, B and C so the rest of the system knows what to count often and what to reorder carefully. It ranked the whole sellable catalog, 27,215 products, and handed the top 20% to class A. That's 5,443 slots against only 3,721 products with any demand at all, so basically everything that sold came out class A: 1,979 A against 2 B and 3 C. Nothing errored, and the number on the screen looked like a number. At the other end of the building the scanner was reading internal SKUs starting \"106-\" as a GS1 batch/lot field and putting a made-up lot number on screens people act on.",
  "unique": "Both halves are the same rule: a miss beats a wrong answer. The classifier now ranks only the products carrying demand, which turns the fake Pareto into a real one — 745 products carrying 89% of 30-day revenue, then 1,117 at 9.5% and 1,859 at 1.6%. Products with no demand floor to class C rather than null, which looks sloppy and is the opposite: null would empty the bucket the cycle-count schedules filter on, and send the replenishment fallback to a bigger safety cushion for products that have never sold a unit. On the scanning side every ambiguous barcode degrades to a literal lookup, because a missing lot number sends someone to look and a made-up one doesn't.",
  "ai": "The classifier was pulled out of the nightly job as a pure function so the rule could be tested without standing up the whole pipeline, and agents wrote that extraction and its tests. The calls were mine: rank only the demand-carrying population, floor no-demand to C not null, resolve classes by id not input order. The check that mattered wasn't a passing test, it was counting the output buckets, because 1,979 / 2 / 3 isn't a Pareto and no sorting assertion was ever going to say so.",
  "learned": [
    "A percentile is about the population you hand it. Twenty percent of 27,215 sellable products is 5,443 slots, but only 3,721 had any demand, so the A bucket was bigger than everything that could fill it. The arithmetic was never wrong. The population was.",
    "The output distribution is the test no unit test writes for you. 1,979 A against 2 B and 3 C passes every assertion about sorting and still can't happen naturally. Anything that ranks or scores now gets its buckets counted.",
    "Null is a routing decision. Flooring no-demand products to C keeps the C bucket the cycle-count schedules filter on from emptying, and stops the replenishment fallback from giving a bigger safety cushion to products that never sold. The safe-looking choice was the expensive one.",
    "A wrong expiry is worse than no expiry. Internal SKUs starting \"106-\" were parsed as a batch/lot field and put a made-up lot number on screens people act on. Ambiguous reads now degrade to a literal lookup, which is a miss someone checks instead of an answer nobody questions."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/inventory"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The problem

A nightly job sorts the catalog into A, B and C so the rest of the system knows what to count often and what to reorder carefully. It ranked the whole sellable catalog, 27,215 products, and handed the top 20% to class A. That's 5,443 slots against only 3,721 products with any demand at all, so basically everything that sold came out class A: 1,979 A against 2 B and 3 C. Nothing errored, and the number on the screen looked like a number. At the other end of the building the scanner was reading internal SKUs starting "106-" as a GS1 batch/lot field and putting a made-up lot number on screens people act on.

What was unique

Both halves are the same rule: a miss beats a wrong answer. The classifier now ranks only the products carrying demand, which turns the fake Pareto into a real one — 745 products carrying 89% of 30-day revenue, then 1,117 at 9.5% and 1,859 at 1.6%. Products with no demand floor to class C rather than null, which looks sloppy and is the opposite: null would empty the bucket the cycle-count schedules filter on, and send the replenishment fallback to a bigger safety cushion for products that have never sold a unit. On the scanning side every ambiguous barcode degrades to a literal lookup, because a missing lot number sends someone to look and a made-up one doesn't.

Where AI fit in

The classifier was pulled out of the nightly job as a pure function so the rule could be tested without standing up the whole pipeline, and agents wrote that extraction and its tests. The calls were mine: rank only the demand-carrying population, floor no-demand to C not null, resolve classes by id not input order. The check that mattered wasn't a passing test, it was counting the output buckets, because 1,979 / 2 / 3 isn't a Pareto and no sorting assertion was ever going to say so.

GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/inventory?fields=problem%2Cunique%2Cai"
source: src/content/projects/{id}

Response200 OK · 1.9 KB

{
  "problem": "A nightly job sorts the catalog into A, B and C so the rest of the system knows what to count often and what to reorder carefully. It ranked the whole sellable catalog, 27,215 products, and handed the top 20% to class A. That's 5,443 slots against only 3,721 products with any demand at all, so basically everything that sold came out class A: 1,979 A against 2 B and 3 C. Nothing errored, and the number on the screen looked like a number. At the other end of the building the scanner was reading internal SKUs starting \"106-\" as a GS1 batch/lot field and putting a made-up lot number on screens people act on.",
  "unique": "Both halves are the same rule: a miss beats a wrong answer. The classifier now ranks only the products carrying demand, which turns the fake Pareto into a real one — 745 products carrying 89% of 30-day revenue, then 1,117 at 9.5% and 1,859 at 1.6%. Products with no demand floor to class C rather than null, which looks sloppy and is the opposite: null would empty the bucket the cycle-count schedules filter on, and send the replenishment fallback to a bigger safety cushion for products that have never sold a unit. On the scanning side every ambiguous barcode degrades to a literal lookup, because a missing lot number sends someone to look and a made-up one doesn't.",
  "ai": "The classifier was pulled out of the nightly job as a pure function so the rule could be tested without standing up the whole pipeline, and agents wrote that extraction and its tests. The calls were mine: rank only the demand-carrying population, floor no-demand to C not null, resolve classes by id not input order. The check that mattered wasn't a passing test, it was counting the output buckets, because 1,979 / 2 / 3 isn't a Pareto and no sorting assertion was ever going to say so.",
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/inventory"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The story

A few hundred products earn nearly all the money and the rest sit on a shelf being occasionally useful. Knowing which is which tells the warehouse what to count this week and what to reorder before it runs out.

A job runs every night to mark each product A, B or C. It was picking the top 20% of everything we could sell, about 27,000 products, but only about 3,700 of those had sold anything. Twenty percent of 27,000 is more than 3,700, so every product that had ever sold got marked most important. Nearly two thousand top-priority products, and three that weren’t.

Nothing broke. No error, no alert. It just quietly made two other things wrong. The reordering maths gave every product the caution meant for the critical ones and rounded every suggested order up, so we bought more than we needed across the board. And “go count the important items” pointed the warehouse at five thousand products, most of which had never sold a unit.

The fix is one sentence: rank only the products that actually sell. That gives the list it was supposed to be, 745 products earning 89% of the money, then a middle group, then a long tail.

Same habit at the other end of the building. When the scanner reads a barcode it isn’t sure about, it says so instead of guessing. It used to sometimes read part of our own product code as a batch number and show one that didn’t exist. A missing batch number makes somebody go look at the box. An invented one doesn’t.

A percentile over the wrong population

The classifier is ordinary Pareto: sort by revenue velocity, top slice A, next B, rest C. The bug was in what got sorted. It ranked every sellable product, 27,215 of them, so a 20% A bucket meant 5,443 slots. Only 3,721 products had any demand in the lookback window. When the bucket is bigger than the population that can fill it, the boundary never binds, and everything with a pulse landed in A: 1,979 A, 2 B, 3 C. Every assertion you’d naturally write — sort is descending, buckets partition the input, percentages sum to 100 — passes on that.

What broke downstream

Nothing read the classes directly. Two things read them indirectly, and both failed in the direction that costs money without saying so.

Replenishment. The suggested-quantity SQL picks a service level (a Z score) from the class. With everything in A it pinned at 1.96 for the whole catalog and every suggestion rounded up. Over-ordering across the board, and each suggestion looked reasonable on its own.

Cycle counting. The scheduler’s rule is “count the A items often”. That pointed at 5,443 products, more than 3,700 of which had never sold a unit, so counting effort went to the shelves least likely to be wrong.

Ranking only products with demand gives the split the system was designed around:

Class Products Share of 30-day revenue
A 745 89.0%
B 1,117 9.5%
C 1,859 1.6%

Why zero demand lands in C, not null

Null looks like the honest choice for “wasn’t ranked” and it’s wrong in two ways. The cycle-count schedules filter on class, so a null empties the C bucket and the long tail goes from counted rarely to counted never. And the replenishment SQL’s fallback for a missing class routes to the B service level, 1.65, which is a bigger cushion than C. Null would’ve quietly ordered more of the products with no demand at all. So no-demand products floor to C, and the reasoning sits in a comment right above the line, because it looks like a shortcut.

One smaller edge: classes resolve by product id, not by index into the caller’s ordering. The function sorts internally, so a credit line with negative revenue sorting below the zeros can’t shift the boundaries under everything else.

Reading what’s physically on the shelf

The scanner reads GS1 Application Identifiers off cartons: GTIN, batch/lot, expiry. Twice, internal SKUs starting “106-” were read as AI (10), the batch/lot field, and a made-up lot number went onto screens people act on. The fix is an anchoring rule: an AI sequence is only trusted where the payload’s structure actually puts one, never inferred from a number that happens to start with the right digits. The HIBC $$ date layout isn’t inferred either, because a wrong expiry on a dental consumable is worse than none.

Every degradation lands in the same place, a literal-only lookup, which is a miss. A miss sends a person to look at the box. A made-up lot number sends nobody anywhere.

The classifier and the scan parser are public in code/, with the numbers and the traps in the comments.

What I learned

  1. A percentile is about the population you hand it. Twenty percent of 27,215 sellable products is 5,443 slots, but only 3,721 had any demand, so the A bucket was bigger than everything that could fill it. The arithmetic was never wrong. The population was.
  2. The output distribution is the test no unit test writes for you. 1,979 A against 2 B and 3 C passes every assertion about sorting and still can't happen naturally. Anything that ranks or scores now gets its buckets counted.
  3. Null is a routing decision. Flooring no-demand products to C keeps the C bucket the cycle-count schedules filter on from emptying, and stops the replenishment fallback from giving a bigger safety cushion to products that never sold. The safe-looking choice was the expensive one.
  4. A wrong expiry is worse than no expiry. Internal SKUs starting "106-" were parsed as a batch/lot field and put a made-up lot number on screens people act on. Ambiguous reads now degrade to a literal lookup, which is a miss someone checks instead of an answer nobody questions.
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/inventory?fields=learned"
source: src/content/projects/{id}

Response200 OK · 1.1 KB

{
  "learned": [
    "A percentile is about the population you hand it. Twenty percent of 27,215 sellable products is 5,443 slots, but only 3,721 had any demand, so the A bucket was bigger than everything that could fill it. The arithmetic was never wrong. The population was.",
    "The output distribution is the test no unit test writes for you. 1,979 A against 2 B and 3 C passes every assertion about sorting and still can't happen naturally. Anything that ranks or scores now gets its buckets counted.",
    "Null is a routing decision. Flooring no-demand products to C keeps the C bucket the cycle-count schedules filter on from emptying, and stops the replenishment fallback from giving a bigger safety cushion to products that never sold. The safe-looking choice was the expensive one.",
    "A wrong expiry is worse than no expiry. Internal SKUs starting \"106-\" were parsed as a batch/lot field and put a made-up lot number on screens people act on. Ambiguous reads now degrade to a literal lookup, which is a miss someone checks instead of an answer nobody questions."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/inventory"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.