import 'dart:async'; /// Raised when a batch could not be handed to the back office. /// /// Distinct from *rejection*: a transport failure means nobody knows whether /// the bills arrived, so every row stays pending and is tried again. A /// rejection means the back office looked at a bill and refused it, which /// retrying will not fix. class TransportException implements Exception { const TransportException(this.message, {this.retryable = true}); final String message; /// Whether trying again could plausibly succeed. A dropped connection is /// retryable; a rejected certificate or a bad credential is not, and the /// engine should stop rather than hammer the broker. final bool retryable; @override String toString() => message; } /// What the back office said about one batch. /// /// [accepted] is the contract that matters: only ids named here are marked /// synced. Anything absent stays pending, whatever the transport reported at /// its own layer. class PushReceipt { const PushReceipt({required this.accepted, this.rejected = const {}}); final List accepted; /// Order id → why the back office refused it. Retrying these unchanged will /// fail again, so the engine surfaces them instead of looping. final Map rejected; bool get isEmpty => accepted.isEmpty && rejected.isEmpty; } /// Something the cloud pushed down to this terminal. /// /// Only a transport with a live connection can deliver these; request/response /// transports expose an empty stream. class DownlinkMessage { const DownlinkMessage({required this.kind, this.payload = const {}}); final DownlinkKind kind; final Map payload; } enum DownlinkKind { /// The catalogue changed at head office — re-import rather than wait for /// tomorrow morning's pull. catalogueChanged, /// Head office is asking this terminal to upload now. syncRequested, /// Anything this build does not recognise. Kept rather than dropped so a /// newer server talking to an older terminal is visible in the events log /// instead of silently ignored. unknown, } /// How completed bills leave the terminal. /// /// The drain engine owns *when* to send and what to do when sending fails; /// this owns only the wire. Swapping HTTP for MQTT is a change of /// implementation here and nothing else. abstract class OrderTransport { /// Shown in the events log so a cashier reporting a problem can say which /// route the terminal was using. String get label; /// Opens the connection. Safe to call when already open. /// /// A request/response transport has nothing to open and returns at once. Future connect(); /// Hands a batch over and reports what the back office committed. /// /// Implementations must not report an id as accepted until the *application* /// has confirmed it. A broker acknowledging receipt of the bytes is not the /// back office confirming the sale. /// /// Throws [TransportException] when the outcome is unknown. Future pushOrders(List> orders); /// Hands over shoppers registered at this till. /// /// Same acceptance contract as [pushOrders] — only ids the back office names /// are marked sent — but the payload is a registration rather than a /// financial record, so the back office is expected to treat it as /// insert-if-absent on id. Replaying one it already holds must be a no-op, /// never an overwrite of a profile corrected at head office. /// /// Throws [TransportException] when the outcome is unknown. Future pushCustomers(List> customers); /// Cloud-initiated messages. Empty for transports that cannot receive. Stream get downlink; /// Whether the route is currently usable. Drives the header's live pill and /// wakes the drain engine when it flips to true. Stream get connectionState; bool get isConnected; /// Sends one heartbeat, and never throws. /// /// On the interface rather than on the broker transport alone, because it was /// on the broker transport alone and that was the bug: the reporter was /// started behind an `is MqttOrderTransport` check, so a shop on the HTTP /// route uploaded every bill correctly and never once appeared on the fleet /// board. Nothing logged it, because nothing had gone wrong — the feature /// simply did not exist on that route. /// /// A transport with nowhere to send it does nothing. That is a real answer, /// not a stub: the simulated route has no back office to tell. Future publishHealth(String payload); Future dispose(); }