Document the customer create and merge

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-29 16:08:52 +05:30
parent e27eb8e927
commit e5a63cc412
2 changed files with 106 additions and 0 deletions

View File

@@ -2511,6 +2511,63 @@ non-zero, and `&&` short-circuited silently. Absolute paths and `;` instead of
third attempt was about to set something real. A command handed to somebody to
paste should contain nothing to edit and should fail loudly.
## A customer nobody has photographed, and the way back
`POST /api/customers` and `POST /api/visitors/{id}/merge`, shipped together
because the first creates the need for the second. A customer typed in at a
counter has **no face template**, so when a camera sees that person later the
matcher has nothing to compare against and enrols them as somebody new. That is
the design working, not failing — and it means every hand-created customer is a
duplicate waiting to happen. Shipping the create alone would have manufactured
duplicates into the state this file already flagged: *"there is no merge
endpoint server-side, so its duplicates would be unrecoverable."*
The number comes from `clients.visitor_seq`, taken exactly as `RecordVisit`
takes it. Two sources of visitor numbers that could disagree would be worse
than none — `V-42` has to mean one person whichever way they arrived.
**The merge is one transaction over five tables, and the count is the point.**
`visits`, `purchases`, `visitor_embeddings`, `consents` and `visitor_profiles`
all reference visitors `ON DELETE CASCADE`, so a table it forgets to re-point
is not an error: those rows are destroyed with the source and nobody finds out
until a customer's history is short.
Policies carried from the edge gallery's merge, which settled them once
already: a human name outranks an auto `Visitor N` whichever direction the
operator merged; `visit_count` is recomputed with `COUNT(*)` and never summed;
`first_seen_at` takes the earlier. Two that are this side's own: the source is
**deleted for real** (a tombstone would leave its number resolving to a record
holding nothing, which reads as *"exists and has never been here"*), and the
response names the **retired reference**, because staff write `V-42` on cards.
### It lost a phone number on its first live run
Found by walking the scenario against production, not by a test. Two records
each with a phone; the survivor kept its own and the source's stopped existing.
The first rule was *"fill the survivor's blanks, never overwrite"* — correct
about which value **wins** and silent about the one that loses. One person can
have two numbers. A merge that quietly deletes one is the same data loss this
file already refuses: *"silently turning Alice back into Visitor 3 is data loss
the operator cannot see happen."*
The profile is reconciled field by field in Go now, because the interesting
case was never the winner. Every losing value is returned in `discarded` **and**
appended to the survivor's notes — the response is read once, the record is read
forever. Notes are additive rather than a winner: two people writing about one
customer wrote two different true things.
And retained was not enough. `SearchVisitors` did not look at notes, so the
number was kept and **unfindable** — the letter of "nothing is lost" without the
point of it. Search covers notes now, which is what makes a customer reached by
their old number the one who comes back.
One bug caught in that same patch and worth the warning: the new clause was
written `ESCAPE` with two backslashes where the four beside it use one. In a Go
raw string that is two literal characters and Postgres requires exactly one —
it would have failed the **whole** customer search at runtime, on a query no
in-memory test executes.
## Setting up on a new machine
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds