Take staff from the back office, and let the seeded PINs die when it has any

Suriya/4821, Divya/5093, Rahul/6274 were compiled into the app — the same three
logins on every install, readable by anyone with the APK, and unreplaceable.

Sign-in now imports the outlet's real staff and deactivates everything it
didn't import, so the built-in PINs stop working the moment a shop has anyone
recorded. That deactivation is the point: merging would have left the hardcoded
logins alive alongside the real ones for ever.

The seeds stay, and that is not a hedge. Only 116 of 596 accounts on the
platform have a PIN set, and outlet 1135 — the one this build ships pointed at
— has none at all. Deleting them would hand 33 of 34 tenants a till nobody can
sign in to. So: back office first, local database once synced, seeds only when
there is nothing else.

Rows are keyed on the back office user id, so a re-sync updates one account
rather than creating a second. A leaver removed upstream loses the till on the
next sign-in. Accounts are deactivated rather than deleted, because bills carry
the cashier's name and shifts settle against it.

An import that writes nobody is treated exactly like an empty answer — a back
office full of `pin = 0` rows must not deactivate the seeds and strand the
counter. That is a real shape in the data, not a hypothetical.

An imported PIN is not flagged for change; the shop already chose it. The flag
belongs to the seeds, which everyone shares.

Role names are mapped by name and fall back to cashier. `app_roles` holds six
rows for four roles — Admin and Manager appear twice each — and most accounts
carry a roleid absent from the table entirely, so an unrecognised role must not
quietly become an admin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Suriya
2026-08-06 16:02:04 +05:30
parent b5b2047bcd
commit 4f9a5c3d6b
4 changed files with 351 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app/providers.dart';
import '../../../data/local/staff_dao.dart';
import '../../../data/remote/pos_auth_api.dart';
import '../../../domain/entities/pos_session.dart';
import '../../../domain/entities/store_account.dart';
@@ -163,6 +164,19 @@ class AuthController extends StateNotifier<AuthState> {
phone: session.phone,
);
// Who may ring a bill here, per the back office.
//
// This is what retires the seeded logins. The till ships with three names
// and three PINs compiled into it — the same three on every install — and
// they exist only so a shop whose back office has no staff recorded can
// still trade on day one. The moment real staff arrive they are
// deactivated, which is the whole point of importing rather than merging.
//
// Empty is the common case rather than an error: most outlets have nobody
// recorded, including the one this build ships pointed at. The import
// no-ops, the seeds survive, and the shop keeps selling.
await _importStaff(session);
_ref.invalidate(storeAccountProvider);
final store = await _ref.read(storeAccountProvider.future);
final staff = store.staff;
@@ -184,6 +198,43 @@ class AuthController extends StateNotifier<AuthState> {
state = Authenticated(store: store, user: opener);
}
/// Writes the back office's staff over this terminal's.
///
/// Failures are swallowed. A shop must be able to open its till even when the
/// staff import fails — the seeded or previously-synced accounts are still
/// there, and refusing the sign-in would trade a working counter for a
/// tidier database.
Future<void> _importStaff(PosSession session) async {
if (session.staff.isEmpty) return;
try {
await _ref.read(localStoreProvider).staff.replaceFromBackOffice([
for (final member in session.staff)
StaffImportRecord(
localId: member.localId,
name: member.fullName,
role: _roleFor(member.role),
pin: member.pin,
),
]);
} on Object {
// Deliberately silent — see above.
}
}
/// Maps the back office's role names onto the till's three.
///
/// `app_roles` holds six rows for four distinct roles — Admin and Manager are
/// each in there twice — and most accounts carry a `roleid` that is not in
/// the table at all. So this matches on the name and falls back to the least
/// privileged answer: an unrecognised role must not silently become an admin.
StaffRole _roleFor(String backOfficeRole) =>
switch (backOfficeRole.trim().toLowerCase()) {
'super admin' || 'admin' => StaffRole.admin,
'manager' || 'operations' => StaffRole.manager,
_ => StaffRole.cashier,
};
/// Switches the active operator, checking their PIN.
///
/// Every bill is stamped with whoever is active, so this is the boundary that