---
name: evolution-invoice-staffid-masterid
description: "invoices.staffid stores the evolution USER id (= staff.masterid), not staff.id; staff.masterid is not unique — badge/name resolution keys on it"
metadata: 
  node_type: memory
  type: project
  originSessionId: 8bc83c34-5fba-450e-9fef-b4b2bfed9058
---

`invoices.staffid` does NOT hold `staff.id`. `NewInvoice()`/invoiceaddsave.php write `$_SESSION['userid']` into it, and `$_SESSION['userid']` = `evolution.users.id` (set in login.php) = the value stored in `staff.masterid`. So `invoices.staffid` lives in the **masterid namespace**, and to get the entering staff you join/lookup on `staff.masterid = invoices.staffid`, NOT `staff.id`.

Gotcha: **`staff.masterid` is NOT unique** (only `staff.id` is). A user can end up with >1 staff row sharing a masterid (re-added under a second email, or an old soft-deleted row left behind). The invoice register badge (`invoiceaddsave.php ?call=fetch`, `$nstaff` keyed by masterid) then binds to whichever row iterates last → wrong name badge. Seen live: user 107 had staff id 4 (deleted) + id 76 (active), both masterid 107.

Fix shipped 2026-07-15 — structural (fix at source + dedup + enforce), NOT a runtime resolver:
- staffaddsave.php register path refuses a 2nd non-deleted staff row for an existing masterid (server-side guard; JS checkEmail already warned).
- cron/staff_masterid_dedupe.php (one-shot, idempotent, all tenants): dedupes then adds the unique index per tenant. Keep-preference a) active status=1 b) staff.email = evolution.users.email for that masterid c) lowest id (original). Detaches non-keepers to masterid=0 via MyISAM-safe self-join (LEFT JOIN evolution.users; tenants+evolution share sqlHost). Safety gate skips+logs the index for any tenant still holding a real-masterid dupe (two-active clash → human decision).
- Migration `migrations/staff_masterid_dedupe_unique.sql`: same logic single-DB (PREVIEW + dedup + DDL) for manual runs.
- ENFORCE: PERSISTENT generated col `masterid_uq = IF(masterid=0,NULL,masterid)` + `UNIQUE KEY` — plain UNIQUE(masterid) impossible because masterid=0 (no-evolution-access sentinel) repeats.

We DELIBERATELY did NOT patch the invoiceaddsave.php badge resolver (`$nstaff[$masterid]` last-wins + `staff.id=invoices.staffid` JOIN are untouched/original): once source is stopped + all tenants deduped + unique index enforces 1:1, masterid→staff is unambiguous, so a runtime resolver would be dead code. (Still requires the cron to actually run on all tenants to hold.)

Keep-preference (active-first, then email-match, then lowest id) is deliberate: active beats a deleted original; email match to the evolution.users row is a strong "real record" signal; among equals the earliest is the original. See [[evolution_permission_system]] (permissions.staffid also = masterid).
