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:
143
test/unit/staff_import_test.dart
Normal file
143
test/unit/staff_import_test.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:nearle_pos/data/datasources/local_store.dart';
|
||||
import 'package:nearle_pos/data/local/staff_dao.dart';
|
||||
import 'package:nearle_pos/domain/entities/store_account.dart';
|
||||
|
||||
/// The till shipped with three names and three PINs compiled into it —
|
||||
/// Suriya/4821, Divya/5093, Rahul/6274 — identical on every install and
|
||||
/// readable by anyone with the APK. They existed because a shop with nothing in
|
||||
/// its back office still has to trade on day one, and that is still true: only
|
||||
/// 116 of 596 accounts on the platform have a PIN, and the outlet this build
|
||||
/// ships pointed at has none at all.
|
||||
///
|
||||
/// So they stay, as a last resort, and these cover the thing that makes that
|
||||
/// safe: real staff must *retire* them rather than sit alongside them.
|
||||
StaffDao get staff => LocalStore.instance.staff;
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() async {
|
||||
await LocalStore.instance.reset();
|
||||
});
|
||||
|
||||
test('a fresh till has the seeded accounts and they work', () async {
|
||||
final seeded = await staff.all();
|
||||
|
||||
expect(seeded, hasLength(3));
|
||||
expect(await staff.authenticate('4821'), isNotNull);
|
||||
// Every one is flagged, so the first person in is made to change it.
|
||||
expect(seeded.every((s) => s.mustChangePin), isTrue);
|
||||
});
|
||||
|
||||
test('real staff retire the built-in PINs', () async {
|
||||
// The whole point. Without this the hardcoded logins would survive next to
|
||||
// the real ones for ever, on every terminal in the fleet.
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1229',
|
||||
name: 'Selvapuram',
|
||||
role: StaffRole.manager,
|
||||
pin: '7391',
|
||||
),
|
||||
]);
|
||||
|
||||
expect(await staff.authenticate('7391'), isNotNull,
|
||||
reason: 'the imported account must work');
|
||||
expect(await staff.authenticate('4821'), isNull,
|
||||
reason: 'Suriya was compiled into the app and must be gone');
|
||||
expect(await staff.authenticate('5093'), isNull);
|
||||
expect(await staff.authenticate('6274'), isNull);
|
||||
});
|
||||
|
||||
test('an empty answer leaves a working till alone', () async {
|
||||
// The common case: most outlets have nobody recorded. Wiping the logins
|
||||
// because the back office has not been filled in yet would close a shop.
|
||||
final written = await staff.replaceFromBackOffice(const []);
|
||||
|
||||
expect(written, 0);
|
||||
expect(await staff.authenticate('4821'), isNotNull);
|
||||
});
|
||||
|
||||
test('a leaver loses the till on the next sign-in', () async {
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'Asha', role: StaffRole.cashier, pin: '7391'),
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-2', name: 'Ravi', role: StaffRole.cashier, pin: '8402'),
|
||||
]);
|
||||
expect(await staff.authenticate('8402'), isNotNull);
|
||||
|
||||
// Ravi is removed in the back office.
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'Asha', role: StaffRole.cashier, pin: '7391'),
|
||||
]);
|
||||
|
||||
expect(await staff.authenticate('7391'), isNotNull);
|
||||
expect(await staff.authenticate('8402'), isNull);
|
||||
});
|
||||
|
||||
test('re-syncing the same person updates rather than duplicates', () async {
|
||||
// Keyed on the back office user id, so a shop that changes somebody's PIN
|
||||
// gets one account with a new PIN, not two accounts with one each.
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'Asha', role: StaffRole.cashier, pin: '7391'),
|
||||
]);
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'Asha Kumar', role: StaffRole.manager, pin: '8402'),
|
||||
]);
|
||||
|
||||
final all = await staff.all();
|
||||
expect(all, hasLength(1));
|
||||
expect(all.single.name, 'Asha Kumar');
|
||||
expect(all.single.role, StaffRole.manager);
|
||||
expect(await staff.authenticate('8402'), isNotNull);
|
||||
expect(await staff.authenticate('7391'), isNull);
|
||||
});
|
||||
|
||||
test('an imported PIN is not flagged for change', () async {
|
||||
// It was set by the shop in the back office, so it is already theirs. The
|
||||
// flag is for the seeds, which everyone shares.
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'Asha', role: StaffRole.cashier, pin: '7391'),
|
||||
]);
|
||||
|
||||
final imported = await staff.authenticate('7391');
|
||||
expect(imported!.mustChangePin, isFalse);
|
||||
});
|
||||
|
||||
test('an unusable PIN is skipped rather than written', () async {
|
||||
// A name on screen that nobody can sign in as reads as a broken terminal.
|
||||
// `pin = 0` is the single most common value in app_users.
|
||||
await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'No PIN', role: StaffRole.cashier, pin: '0'),
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-2', name: 'Blank', role: StaffRole.cashier, pin: ''),
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-3', name: 'Usable', role: StaffRole.cashier, pin: '7391'),
|
||||
]);
|
||||
|
||||
final all = await staff.all();
|
||||
expect(all, hasLength(1));
|
||||
expect(all.single.name, 'Usable');
|
||||
});
|
||||
|
||||
test('an answer of nothing usable does not strand the till', () async {
|
||||
// Every row unusable is not the same as a deliberate empty list, but it has
|
||||
// to behave the same way — otherwise a back office full of `pin = 0` rows
|
||||
// would deactivate the seeds and leave nobody able to sign in.
|
||||
final written = await staff.replaceFromBackOffice(const [
|
||||
StaffImportRecord(
|
||||
localId: 'boffice-1', name: 'No PIN', role: StaffRole.cashier, pin: '0'),
|
||||
]);
|
||||
|
||||
expect(written, 0);
|
||||
expect(await staff.authenticate('4821'), isNotNull,
|
||||
reason: 'the seeds must survive an import that wrote nobody');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user