import 'package:uuid/uuid.dart'; import 'app_database.dart'; import 'catalogue_dao.dart'; /// Who this till is, across a fleet. /// /// Everything that has to be distinguishable between 100 installed terminals /// hangs off this: MQTT topics, presence records, invoice numbers, and which /// cashier's shift a bill belongs to. Before it existed every device called /// itself `TERM-01`, so their topics collided, the fleet board showed one /// terminal, and 100 tills minted the same invoice number. class TerminalIdentity { const TerminalIdentity({ required this.deviceId, required this.code, required this.name, required this.storeId, }); /// Minted once, on first run, and never changed. The stable machine identity /// — reinstalling the app on the same till keeps it, because it lives in the /// database rather than in memory. final String deviceId; /// Short, unique, and safe inside an invoice number: `T4A9`. final String code; /// What a person calls it. Free text, and duplicates are the shop's problem, /// not the system's — nothing keys on it. final String name; final String storeId; /// Used as the MQTT client id and in every topic. Stable across restarts so /// the broker can resume a session rather than treating each launch as a new /// client. String get clientId => 'pos-$storeId-$code'; @override String toString() => '$name ($code)'; } /// Reads the terminal's identity from the database, minting it on first run. class TerminalIdentityStore { const TerminalIdentityStore(this._catalogue); final CatalogueDao _catalogue; static const _uuid = Uuid(); /// Loads the identity, creating one the first time this device is started. /// /// The mint is idempotent: an existing device id is never replaced, so a /// terminal cannot silently change identity and orphan its own history. Future load({String defaultStoreId = 'store-01'}) async { var deviceId = await _catalogue.meta(MetaKeys.deviceId); var code = await _catalogue.meta(MetaKeys.terminalCode); if (deviceId == null || deviceId.isEmpty) { deviceId = _uuid.v4(); await _catalogue.setMeta(MetaKeys.deviceId, deviceId); } if (code == null || code.isEmpty) { code = codeFor(deviceId); await _catalogue.setMeta(MetaKeys.terminalCode, code); } final name = await _catalogue.meta(MetaKeys.terminalName); final storeId = await _catalogue.meta(MetaKeys.storeId); return TerminalIdentity( deviceId: deviceId, code: code, name: (name == null || name.isEmpty) ? 'Terminal $code' : name, storeId: (storeId == null || storeId.isEmpty) ? defaultStoreId : storeId, ); } /// `T` plus four hex characters of the device id. /// /// Short enough to read off a screen and repeat over the phone, and with /// 65,536 values a 100-terminal fleet has roughly a 7% chance of a collision /// somewhere in it — which is why [rename] exists and why the back office /// should reject a duplicate rather than assume uniqueness. static String codeFor(String deviceId) { final hex = deviceId.replaceAll('-', ''); return 'T${hex.substring(0, 4).toUpperCase()}'; } /// Re-codes a terminal, for when head office wants readable numbers or two /// devices in one shop happened to collide. /// /// The device id is deliberately untouched: history already written under the /// old code keeps pointing at the same physical till. Future rename({String? code, String? name, String? storeId}) async { if (code != null && code.isNotEmpty) { await _catalogue.setMeta(MetaKeys.terminalCode, code.toUpperCase()); } if (name != null) await _catalogue.setMeta(MetaKeys.terminalName, name); if (storeId != null && storeId.isNotEmpty) { await _catalogue.setMeta(MetaKeys.storeId, storeId); } } }