A case study in one missing date

Jaddi's rabies shot had no expiry date.

This is the story of what a small grooming shop should do about that, and the system we built to answer it.

Chapter 1

It started with Jaddi

Jaddi is our founder's Shih Tzu. He gets groomed regularly, and like every dog at every grooming shop, he needs proof of a current rabies vaccination before he goes on the table.

The proof we had was an invoice from his vet. It named the rabies vaccine. It gave the date he got it. And nowhere on the page did it say when that vaccination expires.

One question, and a small business has to answer it every day: is this dog covered?

Chapter 2

The guess that becomes the record

A rabies vaccine can be good for one year or three. Both are legal. Which one Jaddi got is printed on the certificate, and the certificate is what the health department will want to see.

A grooming shop doesn't have a data team. It has a front counter, a folder of paper, and whoever is working today. So somebody looks at the invoice, makes a reasonable guess, and writes it down. From that moment on, the guess is the record. Nothing downstream will ever question it, because in a small business there is no downstream.

Chapter 3

So we asked an AI to read it

The obvious modern fix is to photograph the paperwork and let AI read it. So we tested that, field by field, against answer keys we labelled by hand.

492 / 492
fields right on clean PDFs
~15
confident dates invented from one phone photo

On clean documents it was perfect. On a phone photo, which is what a busy counter actually produces, it read a smudged Jan 2?, 2027 as a crisp Jan 29, 2027. It did that about fifteen times on one page, and a better prompt didn't fix it.

The AI wasn't failing. It was doing exactly what the person at the counter does: filling the gap with a confident guess. It was just doing it faster.

Chapter 4

A system that's allowed to say "I don't know"

So the rule we built everything around is simple: no expiry date, no record. Jaddi's invoice is kept, what was read from it is kept, and no vaccination record is created. He stays "not cleared" until the shop has the real certificate.

That rule doesn't live in a training manual or in the app's screens, where a busy day could route around it. It lives in the database itself, alongside the shop's other judgment calls:

  • A dog can't start a groom while a required vaccine is expired, missing, or disputed.
  • A date the AI read becomes a record only after a person checks it.
  • A date nobody can read becomes a request to the owner for a better copy, not a guess.
  • A shave-down can't be recorded without a coat assessment that justifies it.
  • A reminder can't go to an owner who opted out.

42 rules in all, each one proven by a test that checks the database says no: 517 of them, all passing.

Chapter 5

Now you're at the counter

The groomers never see any of that. They see one screen: who's grooming today, every dog with a green check when it's cleared, its usual haircut, its allergies, the note that it hates having its back feet touched, and a calendar of what's coming up.

The shop's check-in screen showing Jaddi's card: Can't groom today. Rabies: expired. Ask the owner for a current certificate.
What the counter says about Jaddi. No guess, no exception, and a clear next step.

The demo is that screen, running the shop's real database in your browser with invented dogs, and Jaddi. Try to book him in.

  • Jaddi: his rabies has expired. See what the counter tells you.
  • A new walk-in whose paperwork has no expiry date. Try to save the shot.
  • A regular client booked with a different groomer: it asks why.

Nothing leaves your browser, and a reload starts over. Admin, photos, and the AI reader need a server, so they're left out of the demo.

And when the counter says no, the shop isn't left guessing what to do next. That's the next story: the manual we wrote so the shop can run it without us.

Anyone can demo a clean extraction.
The harder story is a system that knows what it doesn't know.

PostgreSQL 16 · pgTAP · FastAPI · Next.js · Tailwind · Claude (vision) · Docker · PGlite