113 lines
4.1 KiB
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);
|
|
}
|
|
}
|
|
}
|