@teincfood/core 0.1.1 → 0.1.2
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.
- package/dist/adapters/auth.js +2 -6
- package/dist/adapters/business.js +2 -6
- package/dist/adapters/connectivity.js +6 -14
- package/dist/adapters/devices.js +2 -6
- package/dist/adapters/http.js +4 -10
- package/dist/adapters/image-cache.js +2 -6
- package/dist/adapters/kv-types.js +1 -2
- package/dist/adapters/kv.js +11 -24
- package/dist/adapters/local-node.js +3 -7
- package/dist/adapters/pending-orders.js +1 -4
- package/dist/adapters/query-keys.js +4 -7
- package/dist/adapters/services.js +9 -14
- package/dist/adapters/sqlite.js +3 -8
- package/dist/db/connection.js +15 -20
- package/dist/db/migrations.js +1 -4
- package/dist/db/operations.js +19 -36
- package/dist/index.js +40 -93
- package/dist/reference/api.js +7 -12
- package/dist/reference/events.js +25 -33
- package/dist/reference/operations.js +6 -14
- package/dist/reference/service.js +36 -39
- package/dist/reference/types.js +1 -2
- package/dist/repositories/order.repository.js +15 -18
- package/dist/repositories/pos.repository.js +6 -9
- package/dist/sync/command-builder.js +8 -12
- package/dist/sync/command-queue.service.js +35 -38
- package/dist/sync/engine.js +38 -42
- package/dist/sync/order-number.js +6 -11
- package/dist/sync/order-snapshots.service.js +19 -22
- package/dist/sync/pending-orders.service.js +27 -33
- package/dist/sync/transport-local-node.js +16 -20
- package/dist/sync/transport-rest.js +18 -22
- package/dist/sync/types.js +1 -2
- package/dist/sync/use-sync-reconciliation.js +37 -40
- package/dist/types/api/auth.api.js +1 -2
- package/dist/types/api/business.api.js +1 -2
- package/dist/types/api/conversation.api.js +1 -2
- package/dist/types/api/error.api.js +1 -2
- package/dist/types/api/fleet.api.js +1 -2
- package/dist/types/api/index.js +11 -27
- package/dist/types/api/kiosk.api.js +1 -2
- package/dist/types/api/menu.api.js +1 -2
- package/dist/types/api/pagination.js +1 -2
- package/dist/types/api/rider.api.js +1 -2
- package/dist/types/api/seller.api.js +1 -2
- package/dist/types/api/wallet.api.js +1 -2
- package/dist/types/pos.types.js +1 -2
- package/dist/utils/constants.js +24 -27
- package/dist/utils/copyToClipBoard.js +2 -7
- package/dist/utils/currency.js +8 -16
- package/dist/utils/error.js +4 -11
- package/dist/utils/formatCurrency.js +1 -4
- package/dist/utils/formatDate.js +2 -6
- package/dist/utils/phone.js +6 -13
- package/dist/utils/responsive.js +1 -4
- package/dist/utils/storage.js +14 -28
- package/dist/utils/sync-logger.js +1 -4
- package/dist/utils/uuid.js +4 -10
- package/dist/utils/validation.js +11 -24
- package/package.json +12 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Durable order read-model.
|
|
4
3
|
*
|
|
@@ -14,11 +13,9 @@
|
|
|
14
13
|
* 2. Successful order list fetches are written through as snapshots so the
|
|
15
14
|
* exact list last seen while online is available offline.
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const operations_1 = require("../db/operations");
|
|
21
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
16
|
+
import { openSyncDatabase } from "../db/connection";
|
|
17
|
+
import { insertEvent, upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots, } from "../db/operations";
|
|
18
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
22
19
|
const RETAIN_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
23
20
|
const ORDER_EVENT_TYPES = new Set([
|
|
24
21
|
"order.created",
|
|
@@ -39,13 +36,13 @@ class OrderSnapshotsService {
|
|
|
39
36
|
if (!ORDER_EVENT_TYPES.has(event.event_type))
|
|
40
37
|
return false;
|
|
41
38
|
try {
|
|
42
|
-
const db = await
|
|
43
|
-
await
|
|
39
|
+
const db = await openSyncDatabase();
|
|
40
|
+
await insertEvent(db, { event, origin });
|
|
44
41
|
await this.applyOrderStatusToReadModel(db, event);
|
|
45
42
|
return true;
|
|
46
43
|
}
|
|
47
44
|
catch (error) {
|
|
48
|
-
|
|
45
|
+
syncLogger.error("OrderSnapshots", "Failed to persist order event", {
|
|
49
46
|
error: String(error),
|
|
50
47
|
event_type: event.event_type,
|
|
51
48
|
});
|
|
@@ -65,14 +62,14 @@ class OrderSnapshotsService {
|
|
|
65
62
|
return;
|
|
66
63
|
}
|
|
67
64
|
try {
|
|
68
|
-
const lists = await
|
|
65
|
+
const lists = await getEntitySnapshotsByType(db, "seller_orders");
|
|
69
66
|
for (const list of lists) {
|
|
70
67
|
const data = list.payload.data ?? [];
|
|
71
68
|
const idx = data.findIndex((o) => o.id === orderId);
|
|
72
69
|
if (idx === -1)
|
|
73
70
|
continue;
|
|
74
71
|
const updated = data.map((o, i) => i === idx ? { ...o, status: toStatus } : o);
|
|
75
|
-
await
|
|
72
|
+
await upsertEntitySnapshot(db, {
|
|
76
73
|
entityType: "seller_orders",
|
|
77
74
|
entityId: list.entity_id,
|
|
78
75
|
businessId: list.business_id,
|
|
@@ -83,7 +80,7 @@ class OrderSnapshotsService {
|
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
catch (error) {
|
|
86
|
-
|
|
83
|
+
syncLogger.error("OrderSnapshots", "Failed to apply status to read model", {
|
|
87
84
|
error: String(error),
|
|
88
85
|
});
|
|
89
86
|
}
|
|
@@ -97,8 +94,8 @@ class OrderSnapshotsService {
|
|
|
97
94
|
if (!businessId)
|
|
98
95
|
return;
|
|
99
96
|
try {
|
|
100
|
-
const db = await
|
|
101
|
-
await
|
|
97
|
+
const db = await openSyncDatabase();
|
|
98
|
+
await upsertEntitySnapshot(db, {
|
|
102
99
|
entityType: "seller_orders",
|
|
103
100
|
entityId: scope,
|
|
104
101
|
businessId,
|
|
@@ -108,7 +105,7 @@ class OrderSnapshotsService {
|
|
|
108
105
|
});
|
|
109
106
|
}
|
|
110
107
|
catch (error) {
|
|
111
|
-
|
|
108
|
+
syncLogger.error("OrderSnapshots", "Failed to persist seller list", {
|
|
112
109
|
error: String(error),
|
|
113
110
|
businessId,
|
|
114
111
|
});
|
|
@@ -119,14 +116,14 @@ class OrderSnapshotsService {
|
|
|
119
116
|
*/
|
|
120
117
|
async getSellerList(businessId, scope) {
|
|
121
118
|
try {
|
|
122
|
-
const db = await
|
|
123
|
-
const snap = await
|
|
119
|
+
const db = await openSyncDatabase();
|
|
120
|
+
const snap = await getEntitySnapshot(db, "seller_orders", scope);
|
|
124
121
|
if (!snap || snap.business_id !== businessId)
|
|
125
122
|
return null;
|
|
126
123
|
return snap.payload.data ?? null;
|
|
127
124
|
}
|
|
128
125
|
catch (error) {
|
|
129
|
-
|
|
126
|
+
syncLogger.error("OrderSnapshots", "Failed to read seller list", {
|
|
130
127
|
error: String(error),
|
|
131
128
|
});
|
|
132
129
|
return null;
|
|
@@ -138,14 +135,14 @@ class OrderSnapshotsService {
|
|
|
138
135
|
async pruneOld() {
|
|
139
136
|
try {
|
|
140
137
|
const cutoff = new Date(Date.now() - RETAIN_MS).toISOString();
|
|
141
|
-
const db = await
|
|
142
|
-
await
|
|
138
|
+
const db = await openSyncDatabase();
|
|
139
|
+
await pruneEntitySnapshots(db, "seller_orders", cutoff);
|
|
143
140
|
}
|
|
144
141
|
catch (error) {
|
|
145
|
-
|
|
142
|
+
syncLogger.error("OrderSnapshots", "Failed to prune", {
|
|
146
143
|
error: String(error),
|
|
147
144
|
});
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
}
|
|
151
|
-
|
|
148
|
+
export const orderSnapshotsService = new OrderSnapshotsService();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Pending Orders Service.
|
|
4
3
|
*
|
|
@@ -15,16 +14,11 @@
|
|
|
15
14
|
* itself; `order.created` carries `local_order_number` matching the optimistic
|
|
16
15
|
* order's `order_number`.
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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");
|
|
17
|
+
import { openSyncDatabase } from "../db/connection";
|
|
18
|
+
import { insertEvent, getUnsyncedEvents, markEventsSynced, } from "../db/operations";
|
|
19
|
+
import { usePendingOrdersStore } from "../adapters/pending-orders";
|
|
20
|
+
import { orderSnapshotsService } from "./order-snapshots.service";
|
|
21
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
28
22
|
const PENDING_EVENT_TYPE = "order.create_queued";
|
|
29
23
|
/** Order/delivery state-change events that should refresh every terminal. */
|
|
30
24
|
const ORDER_STATE_EVENT_TYPES = new Set([
|
|
@@ -37,7 +31,7 @@ let invalidator = null;
|
|
|
37
31
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
38
32
|
* order is reconciled. Returns an unregister function.
|
|
39
33
|
*/
|
|
40
|
-
function registerPendingOrdersInvalidator(fn) {
|
|
34
|
+
export function registerPendingOrdersInvalidator(fn) {
|
|
41
35
|
invalidator = fn;
|
|
42
36
|
return () => {
|
|
43
37
|
if (invalidator === fn)
|
|
@@ -48,15 +42,15 @@ function registerPendingOrdersInvalidator(fn) {
|
|
|
48
42
|
* Load persisted unsynced order:create_queued events into the store.
|
|
49
43
|
* Idempotent; safe to call after app restart while still offline.
|
|
50
44
|
*/
|
|
51
|
-
async function hydratePendingOrders() {
|
|
52
|
-
const store =
|
|
45
|
+
export async function hydratePendingOrders() {
|
|
46
|
+
const store = usePendingOrdersStore.getState();
|
|
53
47
|
if (store.hydrated)
|
|
54
48
|
return;
|
|
55
49
|
try {
|
|
56
|
-
const db = await
|
|
50
|
+
const db = await openSyncDatabase();
|
|
57
51
|
const byNumber = new Map();
|
|
58
52
|
for (const origin of ["local", "lan"]) {
|
|
59
|
-
const events = await
|
|
53
|
+
const events = await getUnsyncedEvents(db, origin, Number.MAX_SAFE_INTEGER);
|
|
60
54
|
for (const event of events) {
|
|
61
55
|
if (event.event_type !== PENDING_EVENT_TYPE)
|
|
62
56
|
continue;
|
|
@@ -65,15 +59,15 @@ async function hydratePendingOrders() {
|
|
|
65
59
|
byNumber.set(order.order_number, order);
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
|
-
|
|
62
|
+
usePendingOrdersStore.setState({ orders: [...byNumber.values()] });
|
|
69
63
|
}
|
|
70
64
|
catch (error) {
|
|
71
|
-
|
|
65
|
+
syncLogger.error("PendingOrders", "Hydration failed", {
|
|
72
66
|
error: String(error),
|
|
73
67
|
});
|
|
74
68
|
}
|
|
75
69
|
finally {
|
|
76
|
-
|
|
70
|
+
usePendingOrdersStore.getState().setHydrated(true);
|
|
77
71
|
}
|
|
78
72
|
}
|
|
79
73
|
/**
|
|
@@ -92,36 +86,36 @@ async function hydratePendingOrders() {
|
|
|
92
86
|
* pending-orders store (only queued creations are); this only fires the
|
|
93
87
|
* invalidator. Idempotent — duplicate deliveries are harmless.
|
|
94
88
|
*/
|
|
95
|
-
async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
89
|
+
export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
96
90
|
try {
|
|
97
91
|
if (!ORDER_STATE_EVENT_TYPES.has(event.event_type))
|
|
98
92
|
return;
|
|
99
|
-
|
|
93
|
+
syncLogger.debug("OrderState", "Order state changed", {
|
|
100
94
|
origin,
|
|
101
95
|
event_type: event.event_type,
|
|
102
96
|
entity_id: event.entity_id,
|
|
103
97
|
});
|
|
104
98
|
// Persist the event into the durable journal and patch the offline read
|
|
105
99
|
// model so every terminal keeps the same order states after a restart.
|
|
106
|
-
await
|
|
100
|
+
await orderSnapshotsService.persistOrderEvent(event, origin);
|
|
107
101
|
invalidator?.();
|
|
108
102
|
}
|
|
109
103
|
catch (error) {
|
|
110
|
-
|
|
104
|
+
syncLogger.error("OrderState", "Failed to apply order state change", {
|
|
111
105
|
error: String(error),
|
|
112
106
|
event_type: event.event_type,
|
|
113
107
|
});
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
|
-
async function applyPendingOrderEvent(event, origin = "local") {
|
|
110
|
+
export async function applyPendingOrderEvent(event, origin = "local") {
|
|
117
111
|
try {
|
|
118
112
|
if (event.event_type === PENDING_EVENT_TYPE) {
|
|
119
113
|
const order = event.payload;
|
|
120
114
|
if (!order?.order_number)
|
|
121
115
|
return;
|
|
122
|
-
|
|
123
|
-
const db = await
|
|
124
|
-
await
|
|
116
|
+
usePendingOrdersStore.getState().addOrder(order);
|
|
117
|
+
const db = await openSyncDatabase();
|
|
118
|
+
await insertEvent(db, { event, origin });
|
|
125
119
|
return;
|
|
126
120
|
}
|
|
127
121
|
if (event.event_type === "order.created") {
|
|
@@ -129,16 +123,16 @@ async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
129
123
|
const localOrderNumber = payload?.local_order_number;
|
|
130
124
|
if (typeof localOrderNumber !== "string")
|
|
131
125
|
return;
|
|
132
|
-
|
|
126
|
+
usePendingOrdersStore.getState().removeByOrderNumber(localOrderNumber);
|
|
133
127
|
// Persist the cloud acknowledgment into the durable journal + read model.
|
|
134
|
-
await
|
|
135
|
-
const db = await
|
|
128
|
+
await orderSnapshotsService.persistOrderEvent(event, "cloud");
|
|
129
|
+
const db = await openSyncDatabase();
|
|
136
130
|
// Mark every persisted optimistic event for this local order number as
|
|
137
131
|
// synced so it does not rehydrate as pending after a restart. Check both
|
|
138
132
|
// origins: the creating terminal persisted "local", LAN clients "lan".
|
|
139
133
|
const ids = [];
|
|
140
134
|
for (const origin of ["local", "lan"]) {
|
|
141
|
-
const unsynced = await
|
|
135
|
+
const unsynced = await getUnsyncedEvents(db, origin, Number.MAX_SAFE_INTEGER);
|
|
142
136
|
for (const e of unsynced) {
|
|
143
137
|
if (e.event_type !== PENDING_EVENT_TYPE)
|
|
144
138
|
continue;
|
|
@@ -149,13 +143,13 @@ async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
149
143
|
}
|
|
150
144
|
}
|
|
151
145
|
if (ids.length > 0) {
|
|
152
|
-
await
|
|
146
|
+
await markEventsSynced(db, ids);
|
|
153
147
|
}
|
|
154
148
|
invalidator?.();
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
151
|
catch (error) {
|
|
158
|
-
|
|
152
|
+
syncLogger.error("PendingOrders", "Failed to apply event", {
|
|
159
153
|
error: String(error),
|
|
160
154
|
event_type: event.event_type,
|
|
161
155
|
});
|
|
@@ -1,67 +1,63 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Local Node Transport.
|
|
4
3
|
*
|
|
5
4
|
* Routes commands to the elected Local Node over the LAN and receives events
|
|
6
5
|
* broadcast by the Local Node.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const sync_logger_1 = require("../utils/sync-logger");
|
|
12
|
-
class LocalNodeTransport {
|
|
7
|
+
import { useLocalNodeStore, localNodeService } from "../adapters/local-node";
|
|
8
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
9
|
+
export class LocalNodeTransport {
|
|
13
10
|
constructor() {
|
|
14
11
|
this.name = "local_node";
|
|
15
12
|
}
|
|
16
13
|
isAvailable() {
|
|
17
|
-
const store =
|
|
14
|
+
const store = useLocalNodeStore.getState();
|
|
18
15
|
if (!store.isLocalNodeEnabled)
|
|
19
16
|
return false;
|
|
20
17
|
// If this device is the Local Node, it is always available.
|
|
21
|
-
if (
|
|
18
|
+
if (localNodeService.isLocalNode())
|
|
22
19
|
return true;
|
|
23
20
|
// Otherwise, we must be connected to the elected node.
|
|
24
|
-
return
|
|
21
|
+
return localNodeService.isConnectedToNode();
|
|
25
22
|
}
|
|
26
23
|
subscribe() {
|
|
27
24
|
// Events are received through the LocalNodeService event pipeline.
|
|
28
25
|
return () => undefined;
|
|
29
26
|
}
|
|
30
27
|
async execute(command) {
|
|
31
|
-
|
|
28
|
+
syncLogger.transport(this, "executing", {
|
|
32
29
|
command_type: command.command_type,
|
|
33
30
|
command_id: command.command_id,
|
|
34
31
|
});
|
|
35
|
-
if (
|
|
32
|
+
if (localNodeService.isLocalNode()) {
|
|
36
33
|
// This device is the Local Node: execute locally.
|
|
37
|
-
|
|
34
|
+
syncLogger.info("LocalNodeTransport", "Executing on local node (self)", {
|
|
38
35
|
command_type: command.command_type,
|
|
39
36
|
command_id: command.command_id,
|
|
40
37
|
});
|
|
41
|
-
const event = await
|
|
42
|
-
|
|
38
|
+
const event = await localNodeService.handleNodeCommand(command);
|
|
39
|
+
syncLogger.transport(this, "completed", {
|
|
43
40
|
command_type: command.command_type,
|
|
44
41
|
event_type: event?.event_type,
|
|
45
42
|
});
|
|
46
43
|
return event ? [event] : [];
|
|
47
44
|
}
|
|
48
45
|
// Client: send command to the Local Node.
|
|
49
|
-
const client =
|
|
46
|
+
const client = localNodeService.getClient();
|
|
50
47
|
if (!client) {
|
|
51
48
|
throw new Error("LocalNodeTransport: no client connected to Local Node");
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
syncLogger.info("LocalNodeTransport", "Sending command to Local Node", {
|
|
54
51
|
command_type: command.command_type,
|
|
55
52
|
command_id: command.command_id,
|
|
56
|
-
node_device_id:
|
|
53
|
+
node_device_id: localNodeService.getElectedNode()?.deviceId,
|
|
57
54
|
});
|
|
58
55
|
const events = await client.execute(command);
|
|
59
|
-
|
|
56
|
+
syncLogger.transport(this, "completed", {
|
|
60
57
|
command_type: command.command_type,
|
|
61
58
|
event_count: events.length,
|
|
62
59
|
});
|
|
63
60
|
return events;
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
exports.localNodeTransport = new LocalNodeTransport();
|
|
63
|
+
export const localNodeTransport = new LocalNodeTransport();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* REST transport implementation.
|
|
4
3
|
*
|
|
@@ -6,12 +5,10 @@
|
|
|
6
5
|
* resulting domain events. This transport is used when the cloud API is
|
|
7
6
|
* reachable.
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const uuid_1 = require("../utils/uuid");
|
|
14
|
-
class RestTransport {
|
|
8
|
+
import { posService, sellerService, kioskService } from "../adapters/services";
|
|
9
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
10
|
+
import { generateEventId } from "../utils/uuid";
|
|
11
|
+
export class RestTransport {
|
|
15
12
|
constructor() {
|
|
16
13
|
this.name = "rest";
|
|
17
14
|
}
|
|
@@ -25,20 +22,20 @@ class RestTransport {
|
|
|
25
22
|
return () => undefined;
|
|
26
23
|
}
|
|
27
24
|
async execute(command) {
|
|
28
|
-
|
|
25
|
+
syncLogger.transport(this, "executing", {
|
|
29
26
|
command_type: command.command_type,
|
|
30
27
|
command_id: command.command_id,
|
|
31
28
|
});
|
|
32
29
|
try {
|
|
33
30
|
const event = await this.dispatchCommand(command);
|
|
34
|
-
|
|
31
|
+
syncLogger.transport(this, "completed", {
|
|
35
32
|
command_type: command.command_type,
|
|
36
33
|
event_type: event?.event_type,
|
|
37
34
|
});
|
|
38
35
|
return event ? [event] : [];
|
|
39
36
|
}
|
|
40
37
|
catch (error) {
|
|
41
|
-
|
|
38
|
+
syncLogger.transport(this, "error", {
|
|
42
39
|
command_type: command.command_type,
|
|
43
40
|
command_id: command.command_id,
|
|
44
41
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -56,39 +53,39 @@ class RestTransport {
|
|
|
56
53
|
command_id: command.command_id,
|
|
57
54
|
local_order_number: payload.order_number,
|
|
58
55
|
};
|
|
59
|
-
const response = await
|
|
56
|
+
const response = await posService.createPOSOrder(business_id, posPayload);
|
|
60
57
|
return this.wrapResponse("order.created", response.data, entity_id, timestamp, command);
|
|
61
58
|
}
|
|
62
59
|
case "order:accept": {
|
|
63
|
-
const response = await
|
|
60
|
+
const response = await sellerService.acceptOrder(entity_id);
|
|
64
61
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
65
62
|
}
|
|
66
63
|
case "order:reject": {
|
|
67
|
-
const response = await
|
|
64
|
+
const response = await sellerService.rejectOrder(entity_id);
|
|
68
65
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
69
66
|
}
|
|
70
67
|
case "order:start_preparing": {
|
|
71
|
-
const response = await
|
|
68
|
+
const response = await sellerService.startPreparingOrder(entity_id);
|
|
72
69
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
73
70
|
}
|
|
74
71
|
case "order:mark_ready": {
|
|
75
|
-
const response = await
|
|
72
|
+
const response = await sellerService.markOrderReady(entity_id);
|
|
76
73
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
77
74
|
}
|
|
78
75
|
case "order:complete_pickup": {
|
|
79
|
-
const response = await
|
|
76
|
+
const response = await sellerService.completeOrderPickup(entity_id);
|
|
80
77
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
81
78
|
}
|
|
82
79
|
case "order:cancel": {
|
|
83
|
-
const response = await
|
|
80
|
+
const response = await sellerService.cancelOrder(entity_id);
|
|
84
81
|
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
85
82
|
}
|
|
86
83
|
case "delivery:assign": {
|
|
87
|
-
const response = await
|
|
84
|
+
const response = await kioskService.assignKioskOrderToRider(entity_id, payload.rider_id);
|
|
88
85
|
return this.wrapResponse("delivery.assigned", response, entity_id, timestamp, command);
|
|
89
86
|
}
|
|
90
87
|
case "delivery:confirm_handover": {
|
|
91
|
-
const response = await
|
|
88
|
+
const response = await sellerService.confirmHandover(entity_id);
|
|
92
89
|
return this.wrapResponse("delivery.status_changed", response, entity_id, timestamp, command);
|
|
93
90
|
}
|
|
94
91
|
default:
|
|
@@ -97,7 +94,7 @@ class RestTransport {
|
|
|
97
94
|
}
|
|
98
95
|
wrapResponse(eventType, data, entityId, timestamp, command) {
|
|
99
96
|
return {
|
|
100
|
-
event_id:
|
|
97
|
+
event_id: generateEventId(),
|
|
101
98
|
event_type: eventType,
|
|
102
99
|
event_version: "1.0",
|
|
103
100
|
entity_id: entityId ?? command.entity_id ?? data.id ?? "unknown",
|
|
@@ -108,5 +105,4 @@ class RestTransport {
|
|
|
108
105
|
};
|
|
109
106
|
}
|
|
110
107
|
}
|
|
111
|
-
|
|
112
|
-
exports.restTransport = new RestTransport();
|
|
108
|
+
export const restTransport = new RestTransport();
|
package/dist/sync/types.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Core types for the offline-first, event-driven Sync Engine.
|
|
4
3
|
*
|
|
@@ -10,4 +9,4 @@
|
|
|
10
9
|
*
|
|
11
10
|
* All timestamps are ISO strings. Monetary amounts remain in minor units.
|
|
12
11
|
*/
|
|
13
|
-
|
|
12
|
+
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Sync reconciliation bridge.
|
|
4
3
|
*
|
|
@@ -8,18 +7,16 @@
|
|
|
8
7
|
* - Marks reconciled optimistic events as synced to cloud.
|
|
9
8
|
* - Invalidates TanStack Query caches for affected entities.
|
|
10
9
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const query_keys_1 = require("../adapters/query-keys");
|
|
22
|
-
const events_1 = require("../reference/events");
|
|
10
|
+
import { useEffect } from "react";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { syncEngine } from "./engine";
|
|
13
|
+
import { applyPendingOrderEvent, applyOrderStateChangeEvent, registerPendingOrdersInvalidator, } from "./pending-orders.service";
|
|
14
|
+
import { localNodeService } from "../adapters/local-node";
|
|
15
|
+
import { getSyncDatabase } from "../db/connection";
|
|
16
|
+
import { markEventsSynced } from "../db/operations";
|
|
17
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
18
|
+
import { POS_KEYS, SELLER_KEYS, MENU_KEYS, REFERENCE_KEYS } from "../adapters/query-keys";
|
|
19
|
+
import { applyReferenceChangeEvent, registerReferenceInvalidator, registerReferenceEventEngine, REFERENCE_CHANGED_EVENT_TYPE, } from "../reference/events";
|
|
23
20
|
function isLocalOrderEvent(event) {
|
|
24
21
|
const payload = event.payload;
|
|
25
22
|
if (!payload)
|
|
@@ -27,73 +24,73 @@ function isLocalOrderEvent(event) {
|
|
|
27
24
|
const localOrderNumber = payload.local_order_number;
|
|
28
25
|
return typeof localOrderNumber === "string" && localOrderNumber.startsWith("TFL-");
|
|
29
26
|
}
|
|
30
|
-
function useSyncReconciliation() {
|
|
31
|
-
const queryClient =
|
|
32
|
-
|
|
27
|
+
export function useSyncReconciliation() {
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
useEffect(() => {
|
|
33
30
|
// Reference changes re-sync offline data and refresh caches on this device
|
|
34
31
|
// and (via the LAN relay) on every connected terminal.
|
|
35
|
-
const unregisterReferenceInvalidator =
|
|
36
|
-
queryClient.invalidateQueries({ queryKey:
|
|
37
|
-
queryClient.invalidateQueries({ queryKey:
|
|
38
|
-
queryClient.invalidateQueries({ queryKey:
|
|
39
|
-
queryClient.invalidateQueries({ queryKey:
|
|
32
|
+
const unregisterReferenceInvalidator = registerReferenceInvalidator(() => {
|
|
33
|
+
queryClient.invalidateQueries({ queryKey: MENU_KEYS.all });
|
|
34
|
+
queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
|
|
35
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
36
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
40
37
|
});
|
|
41
38
|
// Pending-order reconciliation invalidates POS + seller order lists so the
|
|
42
39
|
// server-confirmed order replaces the optimistic one. Invalidate from the
|
|
43
40
|
// broadest key: POS_KEYS.orders() without a businessId would produce
|
|
44
41
|
// ['pos','orders',undefined] which does not prefix-match business-scoped
|
|
45
42
|
// keys.
|
|
46
|
-
const unregisterInvalidator =
|
|
47
|
-
queryClient.invalidateQueries({ queryKey:
|
|
48
|
-
queryClient.invalidateQueries({ queryKey:
|
|
43
|
+
const unregisterInvalidator = registerPendingOrdersInvalidator(() => {
|
|
44
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
49
46
|
});
|
|
50
47
|
// Reference-change events are published through the Sync Engine but are
|
|
51
48
|
// not backed by a command, so the engine has no transport dependency on
|
|
52
49
|
// the reference module. Register the singleton once.
|
|
53
|
-
|
|
54
|
-
const unsubscribe =
|
|
55
|
-
|
|
50
|
+
registerReferenceEventEngine(syncEngine);
|
|
51
|
+
const unsubscribe = syncEngine.subscribe(async (event) => {
|
|
52
|
+
syncLogger.event(event, "received");
|
|
56
53
|
// The node relays engine events (e.g. order.created when a queued
|
|
57
54
|
// command is replayed) to LAN clients so they reconcile their pending
|
|
58
55
|
// orders and refresh caches.
|
|
59
|
-
if (
|
|
60
|
-
|
|
56
|
+
if (localNodeService.isLocalNode()) {
|
|
57
|
+
localNodeService.broadcastToClients(event);
|
|
61
58
|
}
|
|
62
59
|
// Reference data changed on this device but we are a LAN client (not the
|
|
63
60
|
// node): forward the event to the node so it rebroadcasts it to every
|
|
64
61
|
// other terminal (the node also applies it locally via its onRelayEvent
|
|
65
62
|
// hook). The node does not echo it back to this device.
|
|
66
|
-
if (event.event_type ===
|
|
67
|
-
!
|
|
68
|
-
|
|
63
|
+
if (event.event_type === REFERENCE_CHANGED_EVENT_TYPE &&
|
|
64
|
+
!localNodeService.isLocalNode()) {
|
|
65
|
+
localNodeService.getClient()?.publishEvent?.(event);
|
|
69
66
|
}
|
|
70
67
|
// Reference data changed (locally or echoed from the cloud): re-sync the
|
|
71
68
|
// affected keys and refresh caches on this device.
|
|
72
|
-
await
|
|
69
|
+
await applyReferenceChangeEvent(event, "local");
|
|
73
70
|
// Order/delivery state changed: refresh order caches so this terminal
|
|
74
71
|
// shows the same order states as the device that made the change.
|
|
75
|
-
await
|
|
72
|
+
await applyOrderStateChangeEvent(event, "local");
|
|
76
73
|
// Route both queued (optimistic) and reconciled (created) order events
|
|
77
74
|
// into the pending-orders store.
|
|
78
75
|
if (event.event_type === "order.create_queued" ||
|
|
79
76
|
event.event_type === "order.created") {
|
|
80
|
-
await
|
|
77
|
+
await applyPendingOrderEvent(event, "local");
|
|
81
78
|
}
|
|
82
79
|
if (event.event_type === "order.created" && isLocalOrderEvent(event)) {
|
|
83
80
|
try {
|
|
84
|
-
const db =
|
|
85
|
-
await
|
|
81
|
+
const db = getSyncDatabase();
|
|
82
|
+
await markEventsSynced(db, [event.event_id]);
|
|
86
83
|
}
|
|
87
84
|
catch (error) {
|
|
88
|
-
|
|
85
|
+
syncLogger.error("Reconciliation", "Failed to mark event synced", {
|
|
89
86
|
error: String(error),
|
|
90
87
|
});
|
|
91
88
|
}
|
|
92
89
|
const payload = event.payload;
|
|
93
90
|
const businessId = payload.business_id;
|
|
94
91
|
if (businessId) {
|
|
95
|
-
queryClient.invalidateQueries({ queryKey:
|
|
96
|
-
queryClient.invalidateQueries({ queryKey:
|
|
92
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
|
|
93
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
});
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|