Commit Graph

3 Commits

Author SHA1 Message Date
Suriya
c0a7fbc1b1 Stop the till and Nearle Daily from sharing accounts
app_users is the only thing the two products have in common, and the code was
treating it as though it were the whole relationship. Both directions leaked.

Back-office roles were leaking into the till. PosRoleCanManageStaff returned
true for roleid 1 to 6, on the reasoning that somebody who already administers a
shop from a browser is not made less privileged by standing at the counter. That
sounds fine and is wrong: measured against live data it handed till-supervisor
powers to 68 accounts, 59 of them Nearle Daily Super admins, not one of whom is
the administrator of anybody's POS. Meanwhile the actual shop accounts carry
roleid 0 and were refused, so the mapping was backwards from intent in both
halves at once.

Till accounts were leaking into the application. GetStaffs is WHERE tenantid
with no role filter, so a Counter Cashier appeared in the tenant staff list
beside the delivery riders — a row every action on that page would fail against,
since a cashier has no app login, no rider shift and no back-office screen.

So: eligibility for a till is now granted explicitly by provisioning a
Supervisor or a Cashier, never inherited from a back-office role, and roles 7
and 8 are excluded from every Nearle Daily lookup. The exclusion lives in the
queries rather than in a check after them, because a check bolted on afterwards
has to be repeated at six call sites and is one edit away from being forgotten
at one of them — and that one would be the hole. A till account is not rejected
by the app login; it is not found.

Two things this surfaced that were not visible before.

A Supervisor could not open a till. PIN sign-in needs a session that already
exists, so once back-office roles were refused, an outlet whose only POS
accounts were PIN-only had no way in at all. Supervisors are now provisioned
with a username and password as well as a PIN; cashiers deliberately get neither,
because they sign on at a counter somebody has already opened and a second
password would be one more credential to leak for no capability gained.

UpdatePosUser silently dropped authname. It wrote the password, reported
success, and left the account unreachable by either lookup — the failure
surfaced at a counter as "not recognised" rather than on the screen that caused
it. Contactno had the same gap.

Verified against live rows rather than asserted, by scratch/posseparation: a
provisioned supervisor signs in and gets the supervisor shell; five real
back-office accounts including Super admins are refused; the supervisor is
invisible to applogin, tenant weblogin and the password-setup lookup; and no
till account appears in getallusers, while asking for role 7 by name still
returns them so the console can read its own people.

All five outlets that stock products now have a Supervisor and a Cashier.

Also moves the loose markdown into docs/, which was already staged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:52:23 +05:30
Suriya
6a62dbb9f3 Hold web-created staff to the same rules the till applies
Two paths write `app_users`: the console's `tenants/createstaff`, and the
terminal's `/pos/users`. Only one of them checked anything.

`createstaff` wrote whatever it was handed. A cashier could be created there
with PIN "0451" — which a bigint column stores as 451 — and would then type four
digits at the counter and be refused for ever, with nothing on either screen to
explain it. Or with 1234, which live data already has on eleven accounts. Or
with a PIN somebody at the same outlet already had, which attributes a bill to
whichever row is read first. Or with no way to sign in at all.

None of that surfaced where it was caused. It surfaced at a counter, days later,
as "the new person cannot log in".

So the rules move into `ValidateStaffUser`, and both paths use it: a name, a
role that is actually a role, a PIN the schema can hold and nobody guesses
first, and at least one way to sign in. The duplicate-PIN check runs too, when
the row names an outlet.

The handler also stops answering 500 with a body claiming 409. Every one of
these is something the person filling in the form can fix, so it is a 400
carrying the reason.

`GetStaffs` now returns `rolename` alongside `roleid`, so a console can show
"Supervisor" without mapping ids itself — `app_roles` has six rows for four
back-office roles and most accounts carry an id absent from it, so any mapping
written client-side would be wrong.

This is what makes the two role systems one. A supervisor or cashier created
from the web behaves at the till exactly like one created at the till, because
there is now a single definition of what those are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:37:28 +05:30
Suriya
4b27b84b1f Let a shop run its own counter: supervisor and cashier, created from the till
A shop had no way to add the people who work in it. The terminal fell back to
three names and three PINs compiled into the app — the same three on every
install — because there was nothing for it to fall back *from*.

Two roles now exist in `app_roles`: Supervisor (7) runs the terminal and creates
staff, Cashier (8) bills. Fixed ids, written by hand, because that table has no
sequence and every id in it was assigned the same way. configid is left NULL
rather than duplicated per portal: a till is a till whichever portal a tenant
uses, and Admin already appears twice in that table for exactly that reason.

`/pos/users` is CRUD over them, and `/pos/login/pin` signs a cashier on at a
terminal a supervisor has already opened.

The rule every one of these follows: **tenant and outlet come from the caller's
token, never from the request.** There is no location field on the create body
to get wrong. A supervisor at Selvapuram cannot create staff at R mart, for the
same reason a till cannot bill into another shop's books — it is the same
inversion applied to people instead of sales.

PIN sign-in is deliberately behind the guard. Four digits is ten thousand
guesses, which is no barrier to an anonymous caller; requiring a session means a
real password opened the terminal first and the guesses are confined to one
outlet's own staff. The session it mints is fresh rather than derived, so a
cashier taking over from a supervisor drops their permissions instead of
inheriting them.

Three things the schema forced:

- A PIN cannot start with zero. `app_users.pin` is a bigint, so "0451" stores as
  451 and reads back as three digits — a cashier would type four and be refused
  for ever. Live data already holds one such account. Rendering refuses to show
  a PIN it cannot represent, rather than showing a short one nobody can type.
- `app_users` has no sequence either, so the next id is read and written inside
  one transaction behind an advisory lock. Two supervisors creating staff at the
  same moment would otherwise compute the same id and one insert would lose.
- 1234, 1111 and friends are refused outright. Live data has 1234 on eleven
  accounts and 1111 on nine.

Proven against outlet 1135, which had zero staff and was the reason the built-in
PINs were still load-bearing:

    created 9188  Store Supervisor  Supervisor  can_manage_staff=true
    created 9189  Counter Cashier   Cashier     can_manage_staff=false
    /pos/staff now returns 2        an unknown PIN is refused

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:22:16 +05:30