import 'package:uuid/uuid.dart'; import 'payment_method.dart'; /// Types of [TabEvent] the server understands. enum TabEventType { tabOpened('tab_opened'), tabRenamed('tab_renamed'), tabDeleted('tab_deleted'), lineAdded('line_added'), lineRemoved('line_removed'), settled('settled'), /// A stock count or tracking change made on the tablet. Not about a tab: /// it has no `tabId`. stockSet('stock_set'); final String value; const TabEventType(this.value); static TabEventType? fromValue(String value) { for (final type in TabEventType.values) { if (type.value == value) return type; } return null; } } /// Something that happened on a tab. Together the events form the operation /// log the server builds the venue's tabs from, and the tablet replays the /// ones the server hasn't applied yet on top of the server's tabs. [payload] /// is the JSON object that is stored in the outbox and sent as is. class TabEvent { final String id; final Map payload; const TabEvent({required this.id, required this.payload}); } /// One settled line in a `settled` event. typedef SettledLine = ({ String productId, String productName, int quantity, int unitPriceInCents, }); /// Builds the events, so their JSON shape lives in one place. class TabEventFactory { final _uuid = const Uuid(); final DateTime Function() _now; TabEventFactory({DateTime Function()? now}) : _now = now ?? DateTime.now; TabEvent tabOpened({ required String tabId, required String customerName, required DateTime openedAt, }) { return _event( TabEventType.tabOpened, tabId: tabId, customerName: customerName, fields: {'openedAt': openedAt.toUtc().toIso8601String()}, ); } /// [customerName] is the new name. TabEvent tabRenamed({required String tabId, required String customerName}) { return _event( TabEventType.tabRenamed, tabId: tabId, customerName: customerName, fields: const {}, ); } /// Written after a `line_removed` for every line still on the tab. TabEvent tabDeleted({required String tabId, required String customerName}) { return _event( TabEventType.tabDeleted, tabId: tabId, customerName: customerName, fields: const {}, ); } /// [quantity] is how many pieces were added; always positive. [lineId] is /// the local line, so the server can keep its id. TabEvent lineAdded({ required String tabId, required String customerName, required String lineId, required String productId, required String productName, required int unitPriceInCents, required int quantity, }) { return _lineEvent( TabEventType.lineAdded, tabId: tabId, customerName: customerName, lineId: lineId, productId: productId, productName: productName, unitPriceInCents: unitPriceInCents, quantity: quantity, ); } /// [quantity] is how many pieces were removed; always positive. TabEvent lineRemoved({ required String tabId, required String customerName, required String lineId, required String productId, required String productName, required int unitPriceInCents, required int quantity, }) { return _lineEvent( TabEventType.lineRemoved, tabId: tabId, customerName: customerName, lineId: lineId, productId: productId, productName: productName, unitPriceInCents: unitPriceInCents, quantity: quantity, ); } TabEvent settled({ required String tabId, required String customerName, required int amountInCents, required PaymentMethod paymentMethod, required List items, String? commandId, }) { return _event( TabEventType.settled, tabId: tabId, customerName: customerName, fields: { 'amountInCents': amountInCents, 'paymentMethod': paymentMethodValue(paymentMethod), 'commandId': ?commandId, 'items': [ for (final item in items) { 'productId': item.productId, 'productName': item.productName, 'quantity': item.quantity, 'unitPriceInCents': item.unitPriceInCents, }, ], }, ); } /// The stock of [productId] was set to [quantity] on this tablet (a count, /// or a new product's starting stock); null stops tracking it. TabEvent stockSet({required String productId, required int? quantity}) { final id = _uuid.v4(); return TabEvent( id: id, payload: { 'id': id, 'type': TabEventType.stockSet.value, 'productId': productId, 'quantity': quantity, 'occurredAt': _now().toUtc().toIso8601String(), }, ); } /// The server's payment method names: `cash`, `payconiq`, /// `payment_pending` or `other`. static String paymentMethodValue(PaymentMethod method) { return switch (method) { PaymentMethod.cash => 'cash', PaymentMethod.payconiq => 'payconiq', PaymentMethod.paymentPending => 'payment_pending', }; } TabEvent _lineEvent( TabEventType type, { required String tabId, required String customerName, required String lineId, required String productId, required String productName, required int unitPriceInCents, required int quantity, }) { if (quantity < 1) { throw ArgumentError.value(quantity, 'quantity', 'Must be at least 1'); } return _event( type, tabId: tabId, customerName: customerName, fields: { 'lineId': lineId, 'productId': productId, 'productName': productName, 'unitPriceInCents': unitPriceInCents, 'quantity': quantity, }, ); } TabEvent _event( TabEventType type, { required String tabId, required String customerName, required Map fields, }) { final id = _uuid.v4(); return TabEvent( id: id, payload: { 'id': id, 'type': type.value, 'tabId': tabId, 'customerName': customerName, 'occurredAt': _now().toUtc().toIso8601String(), ...fields, }, ); } }