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:
49
API.md
49
API.md
@@ -47,6 +47,8 @@ user; the tenant is always taken from the session and never from the request.
|
|||||||
| `GET /api/visits` · `GET /api/visits/stream` | authed |
|
| `GET /api/visits` · `GET /api/visits/stream` | authed |
|
||||||
| `GET /api/visitors` · `GET /api/visitors/{id}/history` · `GET /api/visitors/{id}/image` · `GET /api/faces/{id}` | authed |
|
| `GET /api/visitors` · `GET /api/visitors/{id}/history` · `GET /api/visitors/{id}/image` · `GET /api/faces/{id}` | authed |
|
||||||
| `PUT /api/visitors/{id}/profile` · `POST /api/purchases` | staff |
|
| `PUT /api/visitors/{id}/profile` · `POST /api/purchases` | staff |
|
||||||
|
| `POST /api/customers` | staff |
|
||||||
|
| `POST /api/visitors/{id}/merge` — **irreversible** | manager |
|
||||||
| `DELETE /api/visitors/{id}` — erasure | manager |
|
| `DELETE /api/visitors/{id}` — erasure | manager |
|
||||||
| `GET /api/sites` · `GET /api/sites/{site}/check` · `GET /api/cameras` · `GET /api/cameras/{id}/snapshot.jpg` · `GET /api/cameras/{id}/live` | authed |
|
| `GET /api/sites` · `GET /api/sites/{site}/check` · `GET /api/cameras` · `GET /api/cameras/{id}/snapshot.jpg` · `GET /api/cameras/{id}/live` | authed |
|
||||||
| `POST /api/sites/{site}/cameras` · `PATCH` / `DELETE /api/cameras/{id}` · `POST /api/cameras/{id}/check` | manager |
|
| `POST /api/sites/{site}/cameras` · `PATCH` / `DELETE /api/cameras/{id}` · `POST /api/cameras/{id}/check` | manager |
|
||||||
@@ -776,6 +778,53 @@ half hours wrong in India.
|
|||||||
bars of a footfall report to get either.** `fraction_below_gate` travels with
|
bars of a footfall report to get either.** `fraction_below_gate` travels with
|
||||||
them because it is what says whether the count is a number or a floor.
|
them because it is what says whether the count is a number or a floor.
|
||||||
|
|
||||||
|
### `POST /api/customers` — staff and above
|
||||||
|
|
||||||
|
Register a customer before any camera has seen them — somebody standing at the
|
||||||
|
counter. Body is a profile; **at least a name or a phone** is required, since a
|
||||||
|
record with neither is a number nobody can search for.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "…", "ref": "V-7", "label": "Asha Menon", "visit_count": 0, "has_profile": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
They get a `V-` reference from the same counter an enrolled customer does, so a
|
||||||
|
hand-created record is indistinguishable from one the engine made.
|
||||||
|
|
||||||
|
**Know what this implies.** They have no face template, so when a camera sees
|
||||||
|
that person later the matcher has nothing to compare against and enrols them
|
||||||
|
again — by design, not by failure. Join the two with the merge below.
|
||||||
|
|
||||||
|
### `POST /api/visitors/{id}/merge` — manager and above
|
||||||
|
|
||||||
|
Fold the customer in the path **into** the one named in the body, and delete
|
||||||
|
the source. `into` takes a uuid or a `V-` reference.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "into": "V-12" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "visitor_id": "…", "ref": "V-12", "label": "Asha Menon",
|
||||||
|
"visits": 2, "purchases": 1, "embeddings": 1, "consents": 1,
|
||||||
|
"retired_ref": "V-7",
|
||||||
|
"discarded": ["phone: 9000000001"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Irreversible**, which is why it is manager-and-above. Two different people
|
||||||
|
welded together cannot be separated: nothing records which visit came from
|
||||||
|
whom. It logs at WARNING and writes an audit row.
|
||||||
|
- **`retired_ref` is the reference that has stopped resolving.** Staff write
|
||||||
|
these on cards; asking for it afterwards returns 404.
|
||||||
|
- **Nothing is discarded silently.** The survivor keeps its own profile values
|
||||||
|
and its blanks are filled from the source; anything that loses is listed in
|
||||||
|
`discarded` **and** appended to the survivor's notes — and `GET /api/visitors?q=`
|
||||||
|
searches notes, so a customer reached by their old phone number is still found.
|
||||||
|
- Visits, purchases, templates and consents all move. `visit_count` is
|
||||||
|
recomputed by counting rows, and `first_seen_at` takes the earlier of the two.
|
||||||
|
- Merging a customer into themselves is **400**; an unknown or another tenant's
|
||||||
|
customer is **404**.
|
||||||
|
|
||||||
### `DELETE /api/visitors/{id}` — **erasure**, manager and above
|
### `DELETE /api/visitors/{id}` — **erasure**, manager and above
|
||||||
|
|
||||||
Destroys the face template and the photo outright. Keeps the visit rows,
|
Destroys the face template and the photo outright. Keeps the visit rows,
|
||||||
|
|||||||
57
CLAUDE.md
57
CLAUDE.md
@@ -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
|
third attempt was about to set something real. A command handed to somebody to
|
||||||
paste should contain nothing to edit and should fail loudly.
|
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
|
## Setting up on a new machine
|
||||||
|
|
||||||
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds
|
1. Copy the `Behavision` folder **including `.env`** (gitignored, holds
|
||||||
|
|||||||
Reference in New Issue
Block a user