Files
nearle_pos/lib/data/local/terminal_identity.dart

113 lines
4.1 KiB
Dart

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.
///
/// [defaultStoreId] matches the store this build's default HTTP endpoint
/// serves — see `syncConfigProvider` — so a fresh terminal's first import
/// pulls that store's real catalogue without anyone visiting Settings
/// first. Settings → Connectivity & sync → Configure changes it per
/// terminal from there.
Future<TerminalIdentity> load({String defaultStoreId = '1135'}) 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<void> 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);
}
}
}