@teincfood/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +44 -0
  2. package/dist/adapters/auth.d.ts +13 -0
  3. package/dist/adapters/auth.js +14 -0
  4. package/dist/adapters/business.d.ts +13 -0
  5. package/dist/adapters/business.js +14 -0
  6. package/dist/adapters/connectivity.d.ts +17 -0
  7. package/dist/adapters/connectivity.js +38 -0
  8. package/dist/adapters/devices.d.ts +5 -0
  9. package/dist/adapters/devices.js +14 -0
  10. package/dist/adapters/http.d.ts +30 -0
  11. package/dist/adapters/http.js +37 -0
  12. package/dist/adapters/image-cache.d.ts +8 -0
  13. package/dist/adapters/image-cache.js +12 -0
  14. package/dist/adapters/kv-types.d.ts +5 -0
  15. package/dist/adapters/kv-types.js +2 -0
  16. package/dist/adapters/kv.d.ts +20 -0
  17. package/dist/adapters/kv.js +81 -0
  18. package/dist/adapters/local-node.d.ts +34 -0
  19. package/dist/adapters/local-node.js +40 -0
  20. package/dist/adapters/pending-orders.d.ts +17 -0
  21. package/dist/adapters/pending-orders.js +31 -0
  22. package/dist/adapters/query-keys.d.ts +17 -0
  23. package/dist/adapters/query-keys.js +20 -0
  24. package/dist/adapters/services.d.ts +62 -0
  25. package/dist/adapters/services.js +95 -0
  26. package/dist/adapters/sqlite.d.ts +20 -0
  27. package/dist/adapters/sqlite.js +23 -0
  28. package/dist/db/connection.d.ts +8 -0
  29. package/dist/db/connection.js +87 -0
  30. package/dist/db/migrations.d.ts +9 -0
  31. package/dist/db/migrations.js +106 -0
  32. package/dist/db/operations.d.ts +45 -0
  33. package/dist/db/operations.js +192 -0
  34. package/dist/index.d.ts +50 -0
  35. package/dist/index.js +118 -0
  36. package/dist/reference/api.d.ts +10 -0
  37. package/dist/reference/api.js +24 -0
  38. package/dist/reference/events.d.ts +54 -0
  39. package/dist/reference/events.js +144 -0
  40. package/dist/reference/operations.d.ts +11 -0
  41. package/dist/reference/operations.js +46 -0
  42. package/dist/reference/service.d.ts +27 -0
  43. package/dist/reference/service.js +270 -0
  44. package/dist/reference/types.d.ts +47 -0
  45. package/dist/reference/types.js +12 -0
  46. package/dist/repositories/order.repository.d.ts +44 -0
  47. package/dist/repositories/order.repository.js +132 -0
  48. package/dist/repositories/pos.repository.d.ts +20 -0
  49. package/dist/repositories/pos.repository.js +28 -0
  50. package/dist/sync/command-builder.d.ts +17 -0
  51. package/dist/sync/command-builder.js +30 -0
  52. package/dist/sync/command-queue.service.d.ts +41 -0
  53. package/dist/sync/command-queue.service.js +253 -0
  54. package/dist/sync/engine.d.ts +53 -0
  55. package/dist/sync/engine.js +216 -0
  56. package/dist/sync/order-number.d.ts +20 -0
  57. package/dist/sync/order-number.js +58 -0
  58. package/dist/sync/order-snapshots.service.d.ts +46 -0
  59. package/dist/sync/order-snapshots.service.js +151 -0
  60. package/dist/sync/pending-orders.service.d.ts +45 -0
  61. package/dist/sync/pending-orders.service.js +163 -0
  62. package/dist/sync/transport-local-node.d.ts +14 -0
  63. package/dist/sync/transport-local-node.js +67 -0
  64. package/dist/sync/transport-rest.d.ts +17 -0
  65. package/dist/sync/transport-rest.js +112 -0
  66. package/dist/sync/types.d.ts +102 -0
  67. package/dist/sync/types.js +13 -0
  68. package/dist/sync/use-sync-reconciliation.d.ts +10 -0
  69. package/dist/sync/use-sync-reconciliation.js +106 -0
  70. package/dist/types/api/auth.api.d.ts +61 -0
  71. package/dist/types/api/auth.api.js +3 -0
  72. package/dist/types/api/business.api.d.ts +137 -0
  73. package/dist/types/api/business.api.js +3 -0
  74. package/dist/types/api/conversation.api.d.ts +46 -0
  75. package/dist/types/api/conversation.api.js +2 -0
  76. package/dist/types/api/error.api.d.ts +8 -0
  77. package/dist/types/api/error.api.js +2 -0
  78. package/dist/types/api/fleet.api.d.ts +98 -0
  79. package/dist/types/api/fleet.api.js +3 -0
  80. package/dist/types/api/index.d.ts +11 -0
  81. package/dist/types/api/index.js +27 -0
  82. package/dist/types/api/kiosk.api.d.ts +4 -0
  83. package/dist/types/api/kiosk.api.js +2 -0
  84. package/dist/types/api/menu.api.d.ts +90 -0
  85. package/dist/types/api/menu.api.js +2 -0
  86. package/dist/types/api/pagination.d.ts +10 -0
  87. package/dist/types/api/pagination.js +2 -0
  88. package/dist/types/api/rider.api.d.ts +179 -0
  89. package/dist/types/api/rider.api.js +2 -0
  90. package/dist/types/api/seller.api.d.ts +164 -0
  91. package/dist/types/api/seller.api.js +2 -0
  92. package/dist/types/api/wallet.api.d.ts +27 -0
  93. package/dist/types/api/wallet.api.js +2 -0
  94. package/dist/types/pos.types.d.ts +33 -0
  95. package/dist/types/pos.types.js +5 -0
  96. package/dist/utils/constants.d.ts +85 -0
  97. package/dist/utils/constants.js +72 -0
  98. package/dist/utils/copyToClipBoard.d.ts +5 -0
  99. package/dist/utils/copyToClipBoard.js +23 -0
  100. package/dist/utils/currency.d.ts +9 -0
  101. package/dist/utils/currency.js +37 -0
  102. package/dist/utils/error.d.ts +4 -0
  103. package/dist/utils/error.js +73 -0
  104. package/dist/utils/formatCurrency.d.ts +4 -0
  105. package/dist/utils/formatCurrency.js +16 -0
  106. package/dist/utils/formatDate.d.ts +8 -0
  107. package/dist/utils/formatDate.js +42 -0
  108. package/dist/utils/phone.d.ts +10 -0
  109. package/dist/utils/phone.js +71 -0
  110. package/dist/utils/responsive.d.ts +13 -0
  111. package/dist/utils/responsive.js +21 -0
  112. package/dist/utils/storage.d.ts +15 -0
  113. package/dist/utils/storage.js +60 -0
  114. package/dist/utils/sync-logger.d.ts +22 -0
  115. package/dist/utils/sync-logger.js +79 -0
  116. package/dist/utils/uuid.d.ts +7 -0
  117. package/dist/utils/uuid.js +47 -0
  118. package/dist/utils/validation.d.ts +31 -0
  119. package/dist/utils/validation.js +170 -0
  120. package/package.json +49 -0
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * Durable order read-model.
4
+ *
5
+ * Bridges order events and fetched order lists into the SQLite
6
+ * `entity_snapshots` read model so a restaurant can see current + recent
7
+ * orders offline (survives restart and logout) and refresh them once a
8
+ * connection is re-established.
9
+ *
10
+ * Two sources feed the read model:
11
+ * 1. Incoming order/delivery events (cloud WebSocket or LAN relay) are
12
+ * persisted to the events journal and used to patch the latest status of
13
+ * a known order.
14
+ * 2. Successful order list fetches are written through as snapshots so the
15
+ * exact list last seen while online is available offline.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.orderSnapshotsService = void 0;
19
+ const connection_1 = require("../db/connection");
20
+ const operations_1 = require("../db/operations");
21
+ const sync_logger_1 = require("../utils/sync-logger");
22
+ const RETAIN_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
23
+ const ORDER_EVENT_TYPES = new Set([
24
+ "order.created",
25
+ "order.status_changed",
26
+ "delivery.assigned",
27
+ "delivery.status_changed",
28
+ "delivery.pickup_confirmed",
29
+ "payment.completed",
30
+ "payment.failed",
31
+ ]);
32
+ class OrderSnapshotsService {
33
+ /**
34
+ * Persist an order/delivery event into the durable event journal and patch
35
+ * the read model. Returns true if it was an order event (idempotent for
36
+ * duplicates).
37
+ */
38
+ async persistOrderEvent(event, origin) {
39
+ if (!ORDER_EVENT_TYPES.has(event.event_type))
40
+ return false;
41
+ try {
42
+ const db = await (0, connection_1.openSyncDatabase)();
43
+ await (0, operations_1.insertEvent)(db, { event, origin });
44
+ await this.applyOrderStatusToReadModel(db, event);
45
+ return true;
46
+ }
47
+ catch (error) {
48
+ sync_logger_1.syncLogger.error("OrderSnapshots", "Failed to persist order event", {
49
+ error: String(error),
50
+ event_type: event.event_type,
51
+ });
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Patch the latest status of a known order inside every list snapshot for the
57
+ * business, so offline devices see live status changes without a refetch.
58
+ */
59
+ async applyOrderStatusToReadModel(db, event) {
60
+ const payload = event.payload;
61
+ const orderId = payload?.order_id;
62
+ const toStatus = payload?.to_status ??
63
+ payload?.status;
64
+ if (!orderId || !toStatus || event.event_type !== "order.status_changed") {
65
+ return;
66
+ }
67
+ try {
68
+ const lists = await (0, operations_1.getEntitySnapshotsByType)(db, "seller_orders");
69
+ for (const list of lists) {
70
+ const data = list.payload.data ?? [];
71
+ const idx = data.findIndex((o) => o.id === orderId);
72
+ if (idx === -1)
73
+ continue;
74
+ const updated = data.map((o, i) => i === idx ? { ...o, status: toStatus } : o);
75
+ await (0, operations_1.upsertEntitySnapshot)(db, {
76
+ entityType: "seller_orders",
77
+ entityId: list.entity_id,
78
+ businessId: list.business_id,
79
+ lastEventId: event.event_id,
80
+ lastEventTimestamp: event.timestamp,
81
+ payload: { ...list.payload, data: updated },
82
+ });
83
+ }
84
+ }
85
+ catch (error) {
86
+ sync_logger_1.syncLogger.error("OrderSnapshots", "Failed to apply status to read model", {
87
+ error: String(error),
88
+ });
89
+ }
90
+ }
91
+ /**
92
+ * Write through a fetched order list so it is available offline. Used by the
93
+ * seller and POS order queries. `scope` distinguishes complementary lists
94
+ * (e.g. the unfiltered recent list) within a business.
95
+ */
96
+ async persistSellerOrders(businessId, scope, orders) {
97
+ if (!businessId)
98
+ return;
99
+ try {
100
+ const db = await (0, connection_1.openSyncDatabase)();
101
+ await (0, operations_1.upsertEntitySnapshot)(db, {
102
+ entityType: "seller_orders",
103
+ entityId: scope,
104
+ businessId,
105
+ lastEventId: `fetch-${Date.now()}`,
106
+ lastEventTimestamp: new Date().toISOString(),
107
+ payload: { data: orders, savedAt: new Date().toISOString() },
108
+ });
109
+ }
110
+ catch (error) {
111
+ sync_logger_1.syncLogger.error("OrderSnapshots", "Failed to persist seller list", {
112
+ error: String(error),
113
+ businessId,
114
+ });
115
+ }
116
+ }
117
+ /**
118
+ * Read a persisted order list from the offline read model.
119
+ */
120
+ async getSellerList(businessId, scope) {
121
+ try {
122
+ const db = await (0, connection_1.openSyncDatabase)();
123
+ const snap = await (0, operations_1.getEntitySnapshot)(db, "seller_orders", scope);
124
+ if (!snap || snap.business_id !== businessId)
125
+ return null;
126
+ return snap.payload.data ?? null;
127
+ }
128
+ catch (error) {
129
+ sync_logger_1.syncLogger.error("OrderSnapshots", "Failed to read seller list", {
130
+ error: String(error),
131
+ });
132
+ return null;
133
+ }
134
+ }
135
+ /**
136
+ * Prune snapshots outside the retention window. Call on startup.
137
+ */
138
+ async pruneOld() {
139
+ try {
140
+ const cutoff = new Date(Date.now() - RETAIN_MS).toISOString();
141
+ const db = await (0, connection_1.openSyncDatabase)();
142
+ await (0, operations_1.pruneEntitySnapshots)(db, "seller_orders", cutoff);
143
+ }
144
+ catch (error) {
145
+ sync_logger_1.syncLogger.error("OrderSnapshots", "Failed to prune", {
146
+ error: String(error),
147
+ });
148
+ }
149
+ }
150
+ }
151
+ exports.orderSnapshotsService = new OrderSnapshotsService();
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pending Orders Service.
3
+ *
4
+ * Bridges optimistic order events (queued while offline) into the
5
+ * Pending Orders Store so every terminal can render them, and cleans them up
6
+ * once the cloud acknowledges the order.
7
+ *
8
+ * Event sources:
9
+ * - The Sync Engine broadcasts `order.create_queued` events locally.
10
+ * - The Local Node broadcasts the same events to LAN clients.
11
+ * - The cloud returns `order.created` (echoing `local_order_number`).
12
+ *
13
+ * The optimistic event payload for `order.create_queued` is the SellerOrder
14
+ * itself; `order.created` carries `local_order_number` matching the optimistic
15
+ * order's `order_number`.
16
+ */
17
+ import type { DomainEvent } from "./types";
18
+ /**
19
+ * Register a callback used to invalidate TanStack Query caches when a pending
20
+ * order is reconciled. Returns an unregister function.
21
+ */
22
+ export declare function registerPendingOrdersInvalidator(fn: () => void): () => void;
23
+ /**
24
+ * Load persisted unsynced order:create_queued events into the store.
25
+ * Idempotent; safe to call after app restart while still offline.
26
+ */
27
+ export declare function hydratePendingOrders(): Promise<void>;
28
+ /**
29
+ * Apply a domain event to the pending-orders store and persistence layer.
30
+ *
31
+ * - order.create_queued → add to store + persist (origin local|lan).
32
+ * - order.created → remove matching pending order, mark the persisted
33
+ * optimistic event synced, and invalidate caches.
34
+ *
35
+ * Idempotent: duplicate events (local broadcast + LAN broadcast + direct
36
+ * enqueue) are safe.
37
+ */
38
+ /**
39
+ * Apply an order/delivery state-change event: refresh order caches so every
40
+ * terminal shows the same order states. Status changes are not stored in the
41
+ * pending-orders store (only queued creations are); this only fires the
42
+ * invalidator. Idempotent — duplicate deliveries are harmless.
43
+ */
44
+ export declare function applyOrderStateChangeEvent(event: DomainEvent, origin?: "local" | "lan"): Promise<void>;
45
+ export declare function applyPendingOrderEvent(event: DomainEvent, origin?: "local" | "lan"): Promise<void>;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Pending Orders Service.
4
+ *
5
+ * Bridges optimistic order events (queued while offline) into the
6
+ * Pending Orders Store so every terminal can render them, and cleans them up
7
+ * once the cloud acknowledges the order.
8
+ *
9
+ * Event sources:
10
+ * - The Sync Engine broadcasts `order.create_queued` events locally.
11
+ * - The Local Node broadcasts the same events to LAN clients.
12
+ * - The cloud returns `order.created` (echoing `local_order_number`).
13
+ *
14
+ * The optimistic event payload for `order.create_queued` is the SellerOrder
15
+ * itself; `order.created` carries `local_order_number` matching the optimistic
16
+ * order's `order_number`.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.registerPendingOrdersInvalidator = registerPendingOrdersInvalidator;
20
+ exports.hydratePendingOrders = hydratePendingOrders;
21
+ exports.applyOrderStateChangeEvent = applyOrderStateChangeEvent;
22
+ exports.applyPendingOrderEvent = applyPendingOrderEvent;
23
+ const connection_1 = require("../db/connection");
24
+ const operations_1 = require("../db/operations");
25
+ const pending_orders_1 = require("../adapters/pending-orders");
26
+ const order_snapshots_service_1 = require("./order-snapshots.service");
27
+ const sync_logger_1 = require("../utils/sync-logger");
28
+ const PENDING_EVENT_TYPE = "order.create_queued";
29
+ /** Order/delivery state-change events that should refresh every terminal. */
30
+ const ORDER_STATE_EVENT_TYPES = new Set([
31
+ "order.status_changed",
32
+ "delivery.status_changed",
33
+ "delivery.assigned",
34
+ ]);
35
+ let invalidator = null;
36
+ /**
37
+ * Register a callback used to invalidate TanStack Query caches when a pending
38
+ * order is reconciled. Returns an unregister function.
39
+ */
40
+ function registerPendingOrdersInvalidator(fn) {
41
+ invalidator = fn;
42
+ return () => {
43
+ if (invalidator === fn)
44
+ invalidator = null;
45
+ };
46
+ }
47
+ /**
48
+ * Load persisted unsynced order:create_queued events into the store.
49
+ * Idempotent; safe to call after app restart while still offline.
50
+ */
51
+ async function hydratePendingOrders() {
52
+ const store = pending_orders_1.usePendingOrdersStore.getState();
53
+ if (store.hydrated)
54
+ return;
55
+ try {
56
+ const db = await (0, connection_1.openSyncDatabase)();
57
+ const byNumber = new Map();
58
+ for (const origin of ["local", "lan"]) {
59
+ const events = await (0, operations_1.getUnsyncedEvents)(db, origin, Number.MAX_SAFE_INTEGER);
60
+ for (const event of events) {
61
+ if (event.event_type !== PENDING_EVENT_TYPE)
62
+ continue;
63
+ const order = event.payload;
64
+ if (order?.order_number)
65
+ byNumber.set(order.order_number, order);
66
+ }
67
+ }
68
+ pending_orders_1.usePendingOrdersStore.setState({ orders: [...byNumber.values()] });
69
+ }
70
+ catch (error) {
71
+ sync_logger_1.syncLogger.error("PendingOrders", "Hydration failed", {
72
+ error: String(error),
73
+ });
74
+ }
75
+ finally {
76
+ pending_orders_1.usePendingOrdersStore.getState().setHydrated(true);
77
+ }
78
+ }
79
+ /**
80
+ * Apply a domain event to the pending-orders store and persistence layer.
81
+ *
82
+ * - order.create_queued → add to store + persist (origin local|lan).
83
+ * - order.created → remove matching pending order, mark the persisted
84
+ * optimistic event synced, and invalidate caches.
85
+ *
86
+ * Idempotent: duplicate events (local broadcast + LAN broadcast + direct
87
+ * enqueue) are safe.
88
+ */
89
+ /**
90
+ * Apply an order/delivery state-change event: refresh order caches so every
91
+ * terminal shows the same order states. Status changes are not stored in the
92
+ * pending-orders store (only queued creations are); this only fires the
93
+ * invalidator. Idempotent — duplicate deliveries are harmless.
94
+ */
95
+ async function applyOrderStateChangeEvent(event, origin = "local") {
96
+ try {
97
+ if (!ORDER_STATE_EVENT_TYPES.has(event.event_type))
98
+ return;
99
+ sync_logger_1.syncLogger.debug("OrderState", "Order state changed", {
100
+ origin,
101
+ event_type: event.event_type,
102
+ entity_id: event.entity_id,
103
+ });
104
+ // Persist the event into the durable journal and patch the offline read
105
+ // model so every terminal keeps the same order states after a restart.
106
+ await order_snapshots_service_1.orderSnapshotsService.persistOrderEvent(event, origin);
107
+ invalidator?.();
108
+ }
109
+ catch (error) {
110
+ sync_logger_1.syncLogger.error("OrderState", "Failed to apply order state change", {
111
+ error: String(error),
112
+ event_type: event.event_type,
113
+ });
114
+ }
115
+ }
116
+ async function applyPendingOrderEvent(event, origin = "local") {
117
+ try {
118
+ if (event.event_type === PENDING_EVENT_TYPE) {
119
+ const order = event.payload;
120
+ if (!order?.order_number)
121
+ return;
122
+ pending_orders_1.usePendingOrdersStore.getState().addOrder(order);
123
+ const db = await (0, connection_1.openSyncDatabase)();
124
+ await (0, operations_1.insertEvent)(db, { event, origin });
125
+ return;
126
+ }
127
+ if (event.event_type === "order.created") {
128
+ const payload = event.payload;
129
+ const localOrderNumber = payload?.local_order_number;
130
+ if (typeof localOrderNumber !== "string")
131
+ return;
132
+ pending_orders_1.usePendingOrdersStore.getState().removeByOrderNumber(localOrderNumber);
133
+ // Persist the cloud acknowledgment into the durable journal + read model.
134
+ await order_snapshots_service_1.orderSnapshotsService.persistOrderEvent(event, "cloud");
135
+ const db = await (0, connection_1.openSyncDatabase)();
136
+ // Mark every persisted optimistic event for this local order number as
137
+ // synced so it does not rehydrate as pending after a restart. Check both
138
+ // origins: the creating terminal persisted "local", LAN clients "lan".
139
+ const ids = [];
140
+ for (const origin of ["local", "lan"]) {
141
+ const unsynced = await (0, operations_1.getUnsyncedEvents)(db, origin, Number.MAX_SAFE_INTEGER);
142
+ for (const e of unsynced) {
143
+ if (e.event_type !== PENDING_EVENT_TYPE)
144
+ continue;
145
+ if (e.payload?.order_number ===
146
+ localOrderNumber) {
147
+ ids.push(e.event_id);
148
+ }
149
+ }
150
+ }
151
+ if (ids.length > 0) {
152
+ await (0, operations_1.markEventsSynced)(db, ids);
153
+ }
154
+ invalidator?.();
155
+ }
156
+ }
157
+ catch (error) {
158
+ sync_logger_1.syncLogger.error("PendingOrders", "Failed to apply event", {
159
+ error: String(error),
160
+ event_type: event.event_type,
161
+ });
162
+ }
163
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Local Node Transport.
3
+ *
4
+ * Routes commands to the elected Local Node over the LAN and receives events
5
+ * broadcast by the Local Node.
6
+ */
7
+ import type { Command, DomainEvent, Transport } from "./types";
8
+ export declare class LocalNodeTransport implements Transport {
9
+ readonly name = "local_node";
10
+ isAvailable(): boolean;
11
+ subscribe(): () => void;
12
+ execute(command: Command): Promise<DomainEvent[]>;
13
+ }
14
+ export declare const localNodeTransport: LocalNodeTransport;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Local Node Transport.
4
+ *
5
+ * Routes commands to the elected Local Node over the LAN and receives events
6
+ * broadcast by the Local Node.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.localNodeTransport = exports.LocalNodeTransport = void 0;
10
+ const local_node_1 = require("../adapters/local-node");
11
+ const sync_logger_1 = require("../utils/sync-logger");
12
+ class LocalNodeTransport {
13
+ constructor() {
14
+ this.name = "local_node";
15
+ }
16
+ isAvailable() {
17
+ const store = local_node_1.useLocalNodeStore.getState();
18
+ if (!store.isLocalNodeEnabled)
19
+ return false;
20
+ // If this device is the Local Node, it is always available.
21
+ if (local_node_1.localNodeService.isLocalNode())
22
+ return true;
23
+ // Otherwise, we must be connected to the elected node.
24
+ return local_node_1.localNodeService.isConnectedToNode();
25
+ }
26
+ subscribe() {
27
+ // Events are received through the LocalNodeService event pipeline.
28
+ return () => undefined;
29
+ }
30
+ async execute(command) {
31
+ sync_logger_1.syncLogger.transport(this, "executing", {
32
+ command_type: command.command_type,
33
+ command_id: command.command_id,
34
+ });
35
+ if (local_node_1.localNodeService.isLocalNode()) {
36
+ // This device is the Local Node: execute locally.
37
+ sync_logger_1.syncLogger.info("LocalNodeTransport", "Executing on local node (self)", {
38
+ command_type: command.command_type,
39
+ command_id: command.command_id,
40
+ });
41
+ const event = await local_node_1.localNodeService.handleNodeCommand(command);
42
+ sync_logger_1.syncLogger.transport(this, "completed", {
43
+ command_type: command.command_type,
44
+ event_type: event?.event_type,
45
+ });
46
+ return event ? [event] : [];
47
+ }
48
+ // Client: send command to the Local Node.
49
+ const client = local_node_1.localNodeService.getClient();
50
+ if (!client) {
51
+ throw new Error("LocalNodeTransport: no client connected to Local Node");
52
+ }
53
+ sync_logger_1.syncLogger.info("LocalNodeTransport", "Sending command to Local Node", {
54
+ command_type: command.command_type,
55
+ command_id: command.command_id,
56
+ node_device_id: local_node_1.localNodeService.getElectedNode()?.deviceId,
57
+ });
58
+ const events = await client.execute(command);
59
+ sync_logger_1.syncLogger.transport(this, "completed", {
60
+ command_type: command.command_type,
61
+ event_count: events.length,
62
+ });
63
+ return events;
64
+ }
65
+ }
66
+ exports.LocalNodeTransport = LocalNodeTransport;
67
+ exports.localNodeTransport = new LocalNodeTransport();
@@ -0,0 +1,17 @@
1
+ /**
2
+ * REST transport implementation.
3
+ *
4
+ * Translates domain commands into existing service calls and returns the
5
+ * resulting domain events. This transport is used when the cloud API is
6
+ * reachable.
7
+ */
8
+ import type { Command, DomainEvent, Transport } from "./types";
9
+ export declare class RestTransport implements Transport {
10
+ readonly name = "rest";
11
+ isAvailable(): boolean;
12
+ subscribe(): () => void;
13
+ execute(command: Command): Promise<DomainEvent[]>;
14
+ private dispatchCommand;
15
+ private wrapResponse;
16
+ }
17
+ export declare const restTransport: RestTransport;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * REST transport implementation.
4
+ *
5
+ * Translates domain commands into existing service calls and returns the
6
+ * resulting domain events. This transport is used when the cloud API is
7
+ * reachable.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.restTransport = exports.RestTransport = void 0;
11
+ const services_1 = require("../adapters/services");
12
+ const sync_logger_1 = require("../utils/sync-logger");
13
+ const uuid_1 = require("../utils/uuid");
14
+ class RestTransport {
15
+ constructor() {
16
+ this.name = "rest";
17
+ }
18
+ isAvailable() {
19
+ // Availability is determined by the Sync Engine based on connectivity.
20
+ return true;
21
+ }
22
+ subscribe() {
23
+ // Events from the cloud arrive via Phoenix Channels, not push replies.
24
+ // This transport does not push events itself.
25
+ return () => undefined;
26
+ }
27
+ async execute(command) {
28
+ sync_logger_1.syncLogger.transport(this, "executing", {
29
+ command_type: command.command_type,
30
+ command_id: command.command_id,
31
+ });
32
+ try {
33
+ const event = await this.dispatchCommand(command);
34
+ sync_logger_1.syncLogger.transport(this, "completed", {
35
+ command_type: command.command_type,
36
+ event_type: event?.event_type,
37
+ });
38
+ return event ? [event] : [];
39
+ }
40
+ catch (error) {
41
+ sync_logger_1.syncLogger.transport(this, "error", {
42
+ command_type: command.command_type,
43
+ command_id: command.command_id,
44
+ error: error instanceof Error ? error.message : String(error),
45
+ });
46
+ throw error;
47
+ }
48
+ }
49
+ async dispatchCommand(command) {
50
+ const { command_type, business_id, entity_id, payload } = command;
51
+ const timestamp = new Date().toISOString();
52
+ switch (command_type) {
53
+ case "order:create": {
54
+ const posPayload = {
55
+ ...payload,
56
+ command_id: command.command_id,
57
+ local_order_number: payload.order_number,
58
+ };
59
+ const response = await services_1.posService.createPOSOrder(business_id, posPayload);
60
+ return this.wrapResponse("order.created", response.data, entity_id, timestamp, command);
61
+ }
62
+ case "order:accept": {
63
+ const response = await services_1.sellerService.acceptOrder(entity_id);
64
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
65
+ }
66
+ case "order:reject": {
67
+ const response = await services_1.sellerService.rejectOrder(entity_id);
68
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
69
+ }
70
+ case "order:start_preparing": {
71
+ const response = await services_1.sellerService.startPreparingOrder(entity_id);
72
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
73
+ }
74
+ case "order:mark_ready": {
75
+ const response = await services_1.sellerService.markOrderReady(entity_id);
76
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
77
+ }
78
+ case "order:complete_pickup": {
79
+ const response = await services_1.sellerService.completeOrderPickup(entity_id);
80
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
81
+ }
82
+ case "order:cancel": {
83
+ const response = await services_1.sellerService.cancelOrder(entity_id);
84
+ return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
85
+ }
86
+ case "delivery:assign": {
87
+ const response = await services_1.kioskService.assignKioskOrderToRider(entity_id, payload.rider_id);
88
+ return this.wrapResponse("delivery.assigned", response, entity_id, timestamp, command);
89
+ }
90
+ case "delivery:confirm_handover": {
91
+ const response = await services_1.sellerService.confirmHandover(entity_id);
92
+ return this.wrapResponse("delivery.status_changed", response, entity_id, timestamp, command);
93
+ }
94
+ default:
95
+ throw new Error(`RestTransport: unsupported command type ${command_type}`);
96
+ }
97
+ }
98
+ wrapResponse(eventType, data, entityId, timestamp, command) {
99
+ return {
100
+ event_id: (0, uuid_1.generateEventId)(),
101
+ event_type: eventType,
102
+ event_version: "1.0",
103
+ entity_id: entityId ?? command.entity_id ?? data.id ?? "unknown",
104
+ user_id: command.user_id,
105
+ device_id: command.device_id,
106
+ payload: data,
107
+ timestamp,
108
+ };
109
+ }
110
+ }
111
+ exports.RestTransport = RestTransport;
112
+ exports.restTransport = new RestTransport();
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Core types for the offline-first, event-driven Sync Engine.
3
+ *
4
+ * These types define the contract between:
5
+ * - UI / Hooks
6
+ * - Repositories
7
+ * - Sync Engine
8
+ * - Transports (REST, Local Node, queued)
9
+ *
10
+ * All timestamps are ISO strings. Monetary amounts remain in minor units.
11
+ */
12
+ export interface EventMetadata {
13
+ /** Globally unique event identifier (UUID v4). */
14
+ event_id: string;
15
+ /** Business event type, e.g. "order.created". */
16
+ event_type: string;
17
+ /** Event schema version, e.g. "1.0". */
18
+ event_version: string;
19
+ /** ISO 8601 timestamp when the event was originally emitted. */
20
+ timestamp: string;
21
+ /** Device that produced the event. */
22
+ device_id: string;
23
+ /** User that triggered the event. */
24
+ user_id: string;
25
+ /** ID of the aggregate/entity the event belongs to. */
26
+ entity_id: string;
27
+ }
28
+ export interface DomainEvent<T = unknown> extends EventMetadata {
29
+ payload: T;
30
+ }
31
+ export interface CommandMetadata {
32
+ /** Globally unique command identifier (UUID v4). Used for idempotency. */
33
+ command_id: string;
34
+ /** Command type, e.g. "create_order". */
35
+ command_type: string;
36
+ /** Command schema version, e.g. "1.0". */
37
+ command_version: string;
38
+ /** ISO 8601 timestamp when the command was issued. */
39
+ timestamp: string;
40
+ /** Device that issued the command. */
41
+ device_id: string;
42
+ /** User that issued the command. */
43
+ user_id: string;
44
+ /** Business that owns the command. */
45
+ business_id: string;
46
+ /** Aggregate/entity ID the command targets, when applicable. */
47
+ entity_id?: string;
48
+ }
49
+ export interface Command<T = unknown> extends CommandMetadata {
50
+ payload: T;
51
+ }
52
+ export type CommandStatus = "pending" | "in_flight" | "completed" | "failed" | "cancelled";
53
+ export interface PersistedCommand<T = Record<string, unknown>> extends Command<T> {
54
+ /** Local database row ID. */
55
+ id: number;
56
+ status: CommandStatus;
57
+ /** Error message if status === "failed". */
58
+ error?: string | null;
59
+ /** Retry count. */
60
+ retry_count: number;
61
+ /** ISO 8601 timestamp of last retry attempt. */
62
+ last_attempted_at?: string | null;
63
+ /** ISO 8601 timestamp when the command was persisted. */
64
+ created_at: string;
65
+ /** ISO 8601 timestamp when the command was last updated. */
66
+ updated_at: string;
67
+ }
68
+ export interface PersistedEvent<T = Record<string, unknown>> extends DomainEvent<T> {
69
+ /** Local database row ID. */
70
+ id: number;
71
+ /** Whether this event originated locally or from cloud/LAN. */
72
+ origin: "local" | "cloud" | "lan";
73
+ /** Whether the event has been synced to the cloud. */
74
+ synced_to_cloud: boolean;
75
+ /** ISO 8601 timestamp when the event was persisted. */
76
+ persisted_at: string;
77
+ }
78
+ export interface Transport {
79
+ /** Human-readable transport name for logging. */
80
+ readonly name: string;
81
+ /** True if the transport is currently available to accept commands. */
82
+ isAvailable(): boolean;
83
+ /**
84
+ * Execute a command and return resulting domain events.
85
+ * The transport is responsible for network serialization only.
86
+ */
87
+ execute(command: Command): Promise<DomainEvent[]>;
88
+ /**
89
+ * Subscribe to events pushed by the transport.
90
+ * Returns an unsubscribe function.
91
+ */
92
+ subscribe(handler: (event: DomainEvent) => void): () => void;
93
+ }
94
+ export interface SyncStrategy {
95
+ /** Decide how a command should be handled given current state. */
96
+ resolve(command: Command, context: {
97
+ isOnline: boolean;
98
+ isCloudReachable: boolean;
99
+ isLocalNodeAvailable: boolean;
100
+ queueDepth: number;
101
+ }): "cloud" | "local_node" | "queue" | "reject";
102
+ }