import '../../core/security/pin_hasher.dart'; import '../../domain/entities/store_account.dart'; import 'app_database.dart'; import 'catalogue_dao.dart'; import 'staff_dao.dart'; /// The PIN that authorises removing a rung item from a bill. /// /// Set once by an admin and handed to whoever is on the counter, so a cashier /// can void a line without an admin walking over. It is a *separate* secret /// from staff PINs on purpose: a staff PIN identifies a person and is what /// stamps a bill, and sharing one to allow voids would put every sale that /// shift under the wrong name. /// /// Stored hashed with its own salt, never in the clear. Until an admin sets /// one, [verify] falls back to any admin's staff PIN — a terminal that cannot /// void at all is worse than one that needs the admin present. class VoidPinStore { const VoidPinStore(this._meta, this._staff); final CatalogueDao _meta; final StaffDao _staff; /// Whether an admin has set a dedicated removal PIN on this terminal. /// /// Empty counts as absent: [clearPin] blanks the row rather than deleting /// it, so a null check alone would report a cleared PIN as still set. Future get isConfigured async { final hash = await _meta.meta(MetaKeys.voidPinHash); return hash != null && hash.isNotEmpty; } Future setPin(String pin) async { _assertAcceptable(pin); final salt = PinHasher.newSalt(); await _meta.setMeta(MetaKeys.voidPinHash, PinHasher.hash(pin, salt)); await _meta.setMeta(MetaKeys.voidPinSalt, salt); } /// Drops the dedicated PIN, returning the terminal to admin-PIN-only voids. Future clearPin() async { await _meta.setMeta(MetaKeys.voidPinHash, ''); await _meta.setMeta(MetaKeys.voidPinSalt, ''); } /// True when [pin] may authorise a removal. /// /// Checks the dedicated PIN first, then admin staff PINs. An admin's own PIN /// always works, so setting a removal PIN never locks the owner out of their /// own till. Future verify(String pin) async { final hash = await _meta.meta(MetaKeys.voidPinHash); final salt = await _meta.meta(MetaKeys.voidPinSalt); if (hash != null && hash.isNotEmpty && salt != null && salt.isNotEmpty) { if (PinHasher.verify(pin, salt: salt, hash: hash)) return true; } final user = await _staff.authenticate(pin); return user != null && user.role == StaffRole.admin; } /// Same rule the staff PINs use, for the same reason: these are typed on a /// keypad behind a counter, in front of a queue. static void _assertAcceptable(String pin) { if (pin.length < 4 || int.tryParse(pin) == null) { throw const VoidPinException('A PIN must be at least four digits.'); } const tooObvious = {'0000', '1111', '2222', '3333', '4444', '5555', '6666', '7777', '8888', '9999', '1234', '4321', '0123',}; if (tooObvious.contains(pin)) { throw const VoidPinException( 'That PIN is too easy to guess from across the counter. ' 'Choose another.', ); } } } class VoidPinException implements Exception { const VoidPinException(this.message); final String message; @override String toString() => message; }