Let an admin create till staff from the console, through the same code

An admin sets a shop up from a browser; a supervisor adds a cashier at the
counter. Both had to be possible, and only the second one was.

So the console gets createposuser / updateposuser / getposusers /
deleteposuser, under both /v1/web/tenants and /v1/mob/tenants — calling the
same service methods `/pos/users` calls. Not a parallel implementation: a
supervisor created from a browser is the same row, with the same PIN rules, the
same duplicate check and the same identity-column allocation, as one created at
a till. Two paths writing one table is precisely how the two stop matching, and
this codebase already had that happen once.

configid is inferred rather than asked for. It is a number nobody looks up, it
varies per tenant — 1087's accounts are spread across 1, 6 and 15 — and getting
it wrong creates somebody who cannot sign into the portal their colleagues use
and is invisible to half the platform's queries.

/posroles is served rather than left to the console to hardcode. A console that
knew supervisor was 7 would be wrong the day that changed and would have no way
to find out.

The outlet is the real difference between the two doors. A terminal proves it
with a signed token; the console asserts it, and is checked against the tenant
before anything is written. That is weaker, and it is worth being plain about:
these mint till credentials on an unauthenticated request, exactly like every
other route in the /v1/web and /v1/mob groups, because there is no auth
middleware on the web API at all. Documented as the weakest point in the design
and flagged to move behind a session guard once the console can hold one. The
terminal routes are untouched by it.

Proven in a rolled-back transaction against live data: the console creates a
supervisor at 1135, that supervisor signs in by PIN with can_manage_staff true,
the till's /pos/staff sees them alongside the two created at the counter, and
0451, 1234 and a duplicate PIN are each refused with the same message the
terminal gives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-06 20:42:27 +05:30
parent 6a62dbb9f3
commit cd2459dbb6
8 changed files with 285 additions and 0 deletions

View File

@@ -489,3 +489,22 @@ func (r *posRepository) StaffPinAvailable(tenantID, locationID int, pin int64, e
taken, err := posPinTaken(r.db, tenantID, locationID, pin, exceptUser)
return !taken, err
}
// PosConfigidFor returns the configid an outlet's people already use.
//
// The console cannot sensibly be asked for this. It is a number nobody looks
// up, it varies per tenant — live data has tenant 1087 spread across 1, 6 and
// 15 — and getting it wrong creates an account that cannot sign into the portal
// its colleagues use and is invisible to half the platform's queries.
//
// So it is inferred from whichever value that tenant's existing accounts most
// commonly carry. Returns 0 for a tenant with no accounts at all, which is
// simply what a fresh tenant looks like.
func (r *posRepository) PosConfigidFor(tenantID int) int {
var configID int
r.db.Raw(`SELECT COALESCE(configid, 0) FROM app_users
WHERE tenantid = ? AND COALESCE(configid, 0) > 0
GROUP BY configid ORDER BY COUNT(*) DESC, configid LIMIT 1`,
tenantID).Scan(&configID)
return configID
}