@teincfood/core 0.7.8 → 0.8.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.
- package/README.md +26 -21
- package/dist/adapters/auth.d.ts +10 -0
- package/dist/adapters/auth.js +43 -0
- package/dist/adapters/devices.d.ts +3 -1
- package/dist/adapters/devices.js +187 -3
- package/dist/adapters/filesystem.d.ts +4 -0
- package/dist/adapters/kv.d.ts +10 -0
- package/dist/adapters/kv.js +23 -7
- package/dist/adapters/local-node.d.ts +4 -6
- package/dist/adapters/local-node.js +9 -32
- package/dist/adapters/notify.d.ts +12 -0
- package/dist/adapters/notify.js +33 -0
- package/dist/adapters/query-keys.d.ts +4 -0
- package/dist/adapters/query-keys.js +4 -0
- package/dist/adapters/services.d.ts +99 -15
- package/dist/adapters/services.js +244 -4
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +172 -0
- package/dist/catalog/hooks.d.ts +10 -0
- package/dist/catalog/hooks.js +82 -11
- package/dist/db/migrations.js +274 -0
- package/dist/db/operations.d.ts +53 -2
- package/dist/db/operations.js +177 -8
- package/dist/domain/business/index.d.ts +5 -0
- package/dist/domain/business/index.js +5 -0
- package/dist/domain/orders/lifecycle.d.ts +6 -0
- package/dist/domain/orders/lifecycle.js +6 -0
- package/dist/domain/orders/service.d.ts +24 -0
- package/dist/domain/orders/service.js +242 -0
- package/dist/domain/payments/index.d.ts +6 -0
- package/dist/domain/payments/index.js +5 -0
- package/dist/domain/reference/index.d.ts +8 -0
- package/dist/domain/reference/index.js +8 -0
- package/dist/e2e/harness.d.ts +1 -0
- package/dist/e2e/harness.js +61 -0
- package/dist/hooks/analytics.hooks.d.ts +25 -0
- package/dist/hooks/analytics.hooks.js +214 -0
- package/dist/hooks/business-hours.hooks.d.ts +8 -0
- package/dist/hooks/business-hours.hooks.js +45 -0
- package/dist/hooks/dashboard.hooks.d.ts +30 -0
- package/dist/hooks/dashboard.hooks.js +132 -0
- package/dist/hooks/delivery.hooks.d.ts +225 -0
- package/dist/hooks/delivery.hooks.js +80 -0
- package/dist/hooks/fleet.hooks.d.ts +37 -0
- package/dist/hooks/fleet.hooks.js +229 -0
- package/dist/hooks/image.hooks.d.ts +20 -0
- package/dist/hooks/image.hooks.js +62 -0
- package/dist/hooks/kiosk.hooks.d.ts +22 -5
- package/dist/hooks/kiosk.hooks.js +74 -5
- package/dist/hooks/members.hooks.d.ts +48 -0
- package/dist/hooks/members.hooks.js +302 -0
- package/dist/hooks/offline-orders.hooks.d.ts +9 -0
- package/dist/hooks/offline-orders.hooks.js +51 -0
- package/dist/hooks/orders.hooks.d.ts +19 -39
- package/dist/hooks/orders.hooks.js +126 -321
- package/dist/hooks/places.hooks.d.ts +21 -0
- package/dist/hooks/places.hooks.js +112 -0
- package/dist/hooks/realtime-stubs.d.ts +28 -0
- package/dist/hooks/realtime-stubs.js +85 -0
- package/dist/hooks/rider.hooks.d.ts +470 -0
- package/dist/hooks/rider.hooks.js +373 -0
- package/dist/hooks/use-infinite-list.d.ts +243 -0
- package/dist/hooks/use-infinite-list.js +58 -0
- package/dist/hooks/wallet.hooks.d.ts +219 -0
- package/dist/hooks/wallet.hooks.js +56 -0
- package/dist/image-cache/service.d.ts +8 -0
- package/dist/image-cache/service.js +15 -3
- package/dist/index.d.ts +60 -5
- package/dist/index.js +78 -5
- package/dist/local-node/adapters.d.ts +87 -0
- package/dist/local-node/adapters.js +4 -0
- package/dist/local-node/index.d.ts +5 -0
- package/dist/local-node/index.js +5 -0
- package/dist/local-node/security.d.ts +25 -0
- package/dist/local-node/security.js +149 -0
- package/dist/local-node/service.d.ts +83 -0
- package/dist/local-node/service.js +706 -0
- package/dist/local-node/store.d.ts +50 -0
- package/dist/local-node/store.js +284 -0
- package/dist/local-node/types.d.ts +92 -0
- package/dist/local-node/types.js +4 -0
- package/dist/printer/receipt.d.ts +4 -0
- package/dist/printer/receipt.js +75 -0
- package/dist/printer/service.js +1 -1
- package/dist/providers/query-client.d.ts +13 -0
- package/dist/providers/query-client.js +58 -0
- package/dist/realtime/events.service.d.ts +18 -0
- package/dist/realtime/events.service.js +76 -0
- package/dist/realtime/order-events.service.d.ts +6 -0
- package/dist/realtime/order-events.service.js +17 -0
- package/dist/realtime/phoenix-socket.d.ts +55 -0
- package/dist/realtime/phoenix-socket.js +269 -0
- package/dist/realtime/useEventsConnection.d.ts +1 -0
- package/dist/realtime/useEventsConnection.js +50 -0
- package/dist/reference/api.d.ts +2 -0
- package/dist/reference/api.js +2 -0
- package/dist/reference/capabilities.d.ts +12 -7
- package/dist/reference/capabilities.js +28 -13
- package/dist/reference/hooks.d.ts +34 -0
- package/dist/reference/hooks.js +124 -0
- package/dist/reference/menu-apply.d.ts +29 -0
- package/dist/reference/menu-apply.js +289 -0
- package/dist/reference/menu-record.d.ts +69 -0
- package/dist/reference/menu-record.js +163 -0
- package/dist/reference/operations.d.ts +3 -0
- package/dist/reference/operations.js +35 -1
- package/dist/reference/service.js +36 -9
- package/dist/reference/types.d.ts +1 -1
- package/dist/repositories/order.repository.d.ts +8 -0
- package/dist/repositories/order.repository.js +24 -2
- package/dist/repositories/pos.repository.d.ts +4 -0
- package/dist/repositories/pos.repository.js +4 -0
- package/dist/sync/backoff.d.ts +21 -0
- package/dist/sync/backoff.js +42 -0
- package/dist/sync/command-builder.js +10 -2
- package/dist/sync/command-queue.service.d.ts +12 -0
- package/dist/sync/command-queue.service.js +115 -18
- package/dist/sync/device-lease.service.d.ts +3 -3
- package/dist/sync/device-lease.service.js +80 -20
- package/dist/sync/engine.d.ts +3 -1
- package/dist/sync/engine.js +44 -4
- package/dist/sync/full-sync.d.ts +6 -0
- package/dist/sync/full-sync.js +72 -0
- package/dist/sync/health-probe.d.ts +24 -0
- package/dist/sync/health-probe.js +138 -0
- package/dist/sync/offline-capable.js +25 -0
- package/dist/sync/online-gates.d.ts +17 -0
- package/dist/sync/online-gates.js +55 -0
- package/dist/sync/order-number.d.ts +4 -4
- package/dist/sync/order-number.js +33 -13
- package/dist/sync/order-snapshots.service.d.ts +1 -0
- package/dist/sync/order-snapshots.service.js +47 -2
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +148 -3
- package/dist/sync/reconciliation/index.d.ts +5 -0
- package/dist/sync/reconciliation/index.js +5 -0
- package/dist/sync/snapshots/entity.d.ts +6 -0
- package/dist/sync/snapshots/entity.js +6 -0
- package/dist/sync/sync-jobs.service.d.ts +18 -0
- package/dist/sync/sync-jobs.service.js +539 -0
- package/dist/sync/transition-endpoints.d.ts +11 -0
- package/dist/sync/transition-endpoints.js +30 -0
- package/dist/sync/transport-local-node.js +3 -2
- package/dist/sync/transport-rest.d.ts +7 -0
- package/dist/sync/transport-rest.js +90 -25
- package/dist/sync/use-sync-reconciliation.js +58 -16
- package/dist/test-helpers/memory-driver.js +182 -4
- package/dist/test-helpers/seed-hours.d.ts +1 -0
- package/dist/test-helpers/seed-hours.js +23 -0
- package/dist/types/api/business.api.d.ts +42 -0
- package/dist/types/api/menu.api.d.ts +15 -0
- package/dist/types/api/seller.api.d.ts +20 -0
- package/dist/types/places.d.ts +19 -0
- package/dist/types/places.js +2 -0
- package/dist/types/rider.d.ts +67 -0
- package/dist/types/rider.js +2 -0
- package/dist/utils/attempt-stats.d.ts +23 -0
- package/dist/utils/attempt-stats.js +61 -0
- package/dist/utils/business-hours.d.ts +37 -0
- package/dist/utils/business-hours.js +96 -0
- package/dist/utils/debug-log.d.ts +23 -0
- package/dist/utils/debug-log.js +153 -0
- package/dist/utils/order-filter.d.ts +1 -1
- package/dist/utils/order-filter.js +1 -1
- package/dist/utils/order-transitions.d.ts +30 -0
- package/dist/utils/order-transitions.js +185 -0
- package/dist/utils/sync-logger.js +5 -4
- package/dist/validation/offline.d.ts +46 -0
- package/dist/validation/offline.js +79 -0
- package/package.json +6 -1
- package/dist/pricing/engine.test.d.ts +0 -1
- package/dist/pricing/engine.test.js +0 -142
- package/dist/reference/lww.test.d.ts +0 -1
- package/dist/reference/lww.test.js +0 -23
- package/dist/reference/operations.test.d.ts +0 -1
- package/dist/reference/operations.test.js +0 -66
- package/dist/standalone.integration.test.d.ts +0 -1
- package/dist/standalone.integration.test.js +0 -235
- package/dist/sync/device-lease.test.d.ts +0 -1
- package/dist/sync/device-lease.test.js +0 -38
- package/dist/sync/offline-integration.test.d.ts +0 -1
- package/dist/sync/offline-integration.test.js +0 -52
- package/dist/sync/order-number.test.d.ts +0 -1
- package/dist/sync/order-number.test.js +0 -51
- package/dist/sync/perms.test.d.ts +0 -1
- package/dist/sync/perms.test.js +0 -15
- package/dist/utils/currency.test.d.ts +0 -1
- package/dist/utils/currency.test.js +0 -17
- package/dist/utils/hlc.test.d.ts +0 -1
- package/dist/utils/hlc.test.js +0 -29
- package/dist/utils/phone.test.d.ts +0 -1
- package/dist/utils/phone.test.js +0 -19
- package/dist/utils/responsive.d.ts +0 -13
- package/dist/utils/responsive.js +0 -18
- package/dist/utils/uuid.test.d.ts +0 -1
- package/dist/utils/uuid.test.js +0 -13
- package/dist/utils/validation.test.d.ts +0 -1
- package/dist/utils/validation.test.js +0 -23
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { SQLiteDatabase } from "../adapters/sqlite";
|
|
5
5
|
import type { ReferenceDataKey, ReferenceDataRecord } from "./types";
|
|
6
|
+
import type { OperatingHoursEntry } from "../types/api/business.api";
|
|
6
7
|
export declare function getReferenceData<T>(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<ReferenceDataRecord<T> | null>;
|
|
7
8
|
export declare function getReferenceVersion(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<string | null>;
|
|
8
9
|
export declare function setReferenceData<T>(db: SQLiteDatabase, businessId: string, record: ReferenceDataRecord<T>): Promise<void>;
|
|
@@ -10,3 +11,5 @@ export declare function mergeReferenceData<T extends Record<string, unknown>>(db
|
|
|
10
11
|
export declare function deleteReferenceData(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<void>;
|
|
11
12
|
export declare function deleteReferenceDataForBusiness(db: SQLiteDatabase, businessId: string): Promise<void>;
|
|
12
13
|
export declare function getAllReferenceDataKeys(db: SQLiteDatabase, businessId: string): Promise<ReferenceDataKey[]>;
|
|
14
|
+
export declare function setOperatingHoursLocal(businessId: string, entries: OperatingHoursEntry[]): Promise<void>;
|
|
15
|
+
export declare function setBusinessProfileLocal(businessId: string, profile: Record<string, unknown>): Promise<void>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQLite operations for the business-scoped reference data cache.
|
|
3
3
|
*/
|
|
4
|
+
import { compareHLCString, hlcToString, tickHLC, hlcFromString } from "../utils/hlc";
|
|
5
|
+
import { generateUUID } from "../utils/uuid";
|
|
6
|
+
import { openSyncDatabase } from "../db/connection";
|
|
7
|
+
import { getAppInstanceId } from "../adapters/devices";
|
|
8
|
+
import { notifyDbChange } from "../db/operations";
|
|
4
9
|
export async function getReferenceData(db, businessId, key) {
|
|
5
10
|
const row = await db.getFirstAsync("SELECT * FROM reference_data WHERE business_id = ? AND key = ?;", businessId, key);
|
|
6
11
|
if (!row)
|
|
@@ -29,9 +34,9 @@ export async function setReferenceData(db, businessId, record) {
|
|
|
29
34
|
last_event_id = excluded.last_event_id,
|
|
30
35
|
last_hlc = excluded.last_hlc;
|
|
31
36
|
`, businessId, record.key, record.version, JSON.stringify(record.payload), record.downloadedAt, record.lastEventId ?? null, record.lastHlc ?? null);
|
|
37
|
+
notifyDbChange(businessId, `reference_data:${record.key}`);
|
|
32
38
|
}
|
|
33
39
|
export async function mergeReferenceData(db, businessId, key, incoming, hlc, eventId) {
|
|
34
|
-
const { compareHLCString } = await import("../utils/hlc");
|
|
35
40
|
const existing = await getReferenceData(db, businessId, key);
|
|
36
41
|
if (!existing) {
|
|
37
42
|
await setReferenceData(db, businessId, {
|
|
@@ -68,3 +73,32 @@ export async function getAllReferenceDataKeys(db, businessId) {
|
|
|
68
73
|
const rows = await db.getAllAsync("SELECT key FROM reference_data WHERE business_id = ? ORDER BY key;", businessId);
|
|
69
74
|
return rows.map((r) => r.key);
|
|
70
75
|
}
|
|
76
|
+
async function enqueueReferenceSyncJob(businessId, key, hlc) {
|
|
77
|
+
try {
|
|
78
|
+
const db = await openSyncDatabase();
|
|
79
|
+
const id = generateUUID();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
await db.runAsync(`INSERT OR IGNORE INTO sync_jobs (id, business_id, command_type, entity_id, payload, status, created_at, updated_at, attempts, last_error) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, 0, NULL);`, id, businessId, "reference:sync", key, JSON.stringify({ key, hlc }), now, now);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
export async function setOperatingHoursLocal(businessId, entries) {
|
|
86
|
+
const db = await openSyncDatabase();
|
|
87
|
+
const payload = { data: entries };
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
// HLC ensures local edit wins over stale server pull via LWW
|
|
90
|
+
const existing = await getReferenceData(db, businessId, "operating-hours");
|
|
91
|
+
const lastHlc = existing?.lastHlc ? hlcFromString(existing.lastHlc) : null;
|
|
92
|
+
const hlcStr = hlcToString(tickHLC(Date.now(), lastHlc, getAppInstanceId()));
|
|
93
|
+
await mergeReferenceData(db, businessId, "operating-hours", payload, hlcStr, `evt_${hlcStr}`);
|
|
94
|
+
await enqueueReferenceSyncJob(businessId, "operating-hours", hlcStr);
|
|
95
|
+
}
|
|
96
|
+
export async function setBusinessProfileLocal(businessId, profile) {
|
|
97
|
+
const db = await openSyncDatabase();
|
|
98
|
+
const payload = { data: profile };
|
|
99
|
+
const existing = await getReferenceData(db, businessId, "business:profile");
|
|
100
|
+
const lastHlc = existing?.lastHlc ? hlcFromString(existing.lastHlc) : null;
|
|
101
|
+
const hlcStr = hlcToString(tickHLC(Date.now(), lastHlc, getAppInstanceId()));
|
|
102
|
+
await mergeReferenceData(db, businessId, "business:profile", payload, hlcStr, `evt_${hlcStr}`);
|
|
103
|
+
await enqueueReferenceSyncJob(businessId, "business:profile", hlcStr);
|
|
104
|
+
}
|
|
@@ -12,7 +12,8 @@ import { syncLogger } from "../utils/sync-logger";
|
|
|
12
12
|
import { generateUUID } from "../utils/uuid";
|
|
13
13
|
import { menuService, businessService, rolesService } from "../adapters/services";
|
|
14
14
|
import { cacheImageBatch, pruneImageCache } from "../adapters/image-cache";
|
|
15
|
-
import {
|
|
15
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
16
|
+
import { getReferenceData, getReferenceVersion, mergeReferenceData, } from "./operations";
|
|
16
17
|
import { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, } from "./api";
|
|
17
18
|
const DEFAULT_SYNC_KEYS = [
|
|
18
19
|
"menu:items",
|
|
@@ -22,6 +23,8 @@ const DEFAULT_SYNC_KEYS = [
|
|
|
22
23
|
"tax-rules",
|
|
23
24
|
"business:profile",
|
|
24
25
|
"operating-hours",
|
|
26
|
+
"delivery-settings",
|
|
27
|
+
"payment-settings",
|
|
25
28
|
"subscription:entitlements",
|
|
26
29
|
"capabilities",
|
|
27
30
|
];
|
|
@@ -38,6 +41,20 @@ class ReferenceSyncService {
|
|
|
38
41
|
errors: {},
|
|
39
42
|
};
|
|
40
43
|
}
|
|
44
|
+
const { isOnline, isCloudReachable } = getConnectivityState();
|
|
45
|
+
if (!isOnline || isCloudReachable === "unreachable") {
|
|
46
|
+
syncLogger.debug("ReferenceSync", "Skipping sync — cloud not reachable", {
|
|
47
|
+
businessId: options.businessId,
|
|
48
|
+
isOnline,
|
|
49
|
+
isCloudReachable,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
synced: [],
|
|
53
|
+
skipped: options.keys ?? DEFAULT_SYNC_KEYS,
|
|
54
|
+
failed: [],
|
|
55
|
+
errors: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
41
58
|
this.isSyncing = true;
|
|
42
59
|
syncLogger.info("ReferenceSync", "Starting reference data sync", {
|
|
43
60
|
businessId: options.businessId,
|
|
@@ -143,14 +160,9 @@ class ReferenceSyncService {
|
|
|
143
160
|
payload = fallback.payload;
|
|
144
161
|
version = fallback.version;
|
|
145
162
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
payload,
|
|
150
|
-
downloadedAt: new Date().toISOString(),
|
|
151
|
-
lastHlc: version ?? generateUUID(),
|
|
152
|
-
lastEventId: `evt_${version ?? generateUUID()}`,
|
|
153
|
-
});
|
|
163
|
+
// LWW: newer HLC wins — local edits that are newer than server are preserved.
|
|
164
|
+
const hlc = version ?? generateUUID();
|
|
165
|
+
await mergeReferenceData(db, businessId, key, payload, hlc, `evt_${hlc}`);
|
|
154
166
|
return true;
|
|
155
167
|
}
|
|
156
168
|
async fetchFallback(key, businessId) {
|
|
@@ -244,6 +256,21 @@ class ReferenceSyncService {
|
|
|
244
256
|
version: this.hashPayload(response),
|
|
245
257
|
};
|
|
246
258
|
}
|
|
259
|
+
case "delivery-settings": {
|
|
260
|
+
const response = await businessService.fetchDeliverySettings?.(businessId) ?? (await (await import("../adapters/http")).getHttpClient().get(`/businesses/${businessId}/delivery-settings`).then((r) => r.data));
|
|
261
|
+
return {
|
|
262
|
+
payload: response,
|
|
263
|
+
version: this.hashPayload(response),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case "payment-settings": {
|
|
267
|
+
const response = await businessService.fetchBusinessById(businessId);
|
|
268
|
+
const payload = response?.data?.paymentSettings ?? response;
|
|
269
|
+
return {
|
|
270
|
+
payload,
|
|
271
|
+
version: this.hashPayload(payload),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
247
274
|
default:
|
|
248
275
|
throw new Error(`No fallback fetch available for ${key}`);
|
|
249
276
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* It is synchronized separately from transactional data through the
|
|
9
9
|
* ReferenceSyncService.
|
|
10
10
|
*/
|
|
11
|
-
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements" | "capabilities";
|
|
11
|
+
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "delivery-settings" | "payment-settings" | "subscription:entitlements" | "capabilities";
|
|
12
12
|
export interface ReferenceDataRecord<T = unknown> {
|
|
13
13
|
key: ReferenceDataKey;
|
|
14
14
|
version: string;
|
|
@@ -24,6 +24,13 @@ export interface OrderActionInput {
|
|
|
24
24
|
businessId: string;
|
|
25
25
|
}
|
|
26
26
|
export declare const orderRepository: {
|
|
27
|
+
/**
|
|
28
|
+
* Single create path: build an `order:create` command and route it through
|
|
29
|
+
* the SyncEngine (cloud → LAN node → queue). Every path returns an
|
|
30
|
+
* order-shaped payload: cloud/LAN execute the real create, queue returns
|
|
31
|
+
* the optimistic order (same UUID + DD-RRR-CCC, replayed idempotently).
|
|
32
|
+
* Direct `createLocalOrder` bypasses sync — never call it from UI flows.
|
|
33
|
+
*/
|
|
27
34
|
createOrder(input: CreateOrderInput): Promise<SellerOrder>;
|
|
28
35
|
acceptOrder(input: OrderActionInput): Promise<SellerOrder>;
|
|
29
36
|
rejectOrder(input: OrderActionInput): Promise<SellerOrder>;
|
|
@@ -41,4 +48,5 @@ export declare const orderRepository: {
|
|
|
41
48
|
assignRider(input: AssignRiderInput & {
|
|
42
49
|
businessId: string;
|
|
43
50
|
}): Promise<unknown>;
|
|
51
|
+
confirmCashPayment(input: OrderActionInput): Promise<SellerOrder>;
|
|
44
52
|
};
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { syncEngine } from "../sync/engine";
|
|
11
11
|
import { buildCommand } from "../sync/command-builder";
|
|
12
|
+
import { generateUUID } from "../utils/uuid";
|
|
12
13
|
async function executeCommand(command) {
|
|
13
14
|
console.debug("[OrderRepository] Dispatching command", JSON.stringify({
|
|
14
15
|
command_id: command.command_id,
|
|
@@ -35,12 +36,24 @@ async function executeCommand(command) {
|
|
|
35
36
|
return event.payload;
|
|
36
37
|
}
|
|
37
38
|
export const orderRepository = {
|
|
39
|
+
/**
|
|
40
|
+
* Single create path: build an `order:create` command and route it through
|
|
41
|
+
* the SyncEngine (cloud → LAN node → queue). Every path returns an
|
|
42
|
+
* order-shaped payload: cloud/LAN execute the real create, queue returns
|
|
43
|
+
* the optimistic order (same UUID + DD-RRR-CCC, replayed idempotently).
|
|
44
|
+
* Direct `createLocalOrder` bypasses sync — never call it from UI flows.
|
|
45
|
+
*/
|
|
38
46
|
async createOrder(input) {
|
|
47
|
+
const entityId = input.entityId ?? generateUUID();
|
|
39
48
|
const command = buildCommand({
|
|
40
49
|
commandType: "order:create",
|
|
41
50
|
businessId: input.businessId,
|
|
42
|
-
entityId
|
|
43
|
-
payload:
|
|
51
|
+
entityId,
|
|
52
|
+
payload: {
|
|
53
|
+
...input.data,
|
|
54
|
+
id: entityId,
|
|
55
|
+
command_id: entityId,
|
|
56
|
+
},
|
|
44
57
|
});
|
|
45
58
|
return executeCommand(command);
|
|
46
59
|
},
|
|
@@ -126,4 +139,13 @@ export const orderRepository = {
|
|
|
126
139
|
}
|
|
127
140
|
return event.payload;
|
|
128
141
|
},
|
|
142
|
+
async confirmCashPayment(input) {
|
|
143
|
+
const command = buildCommand({
|
|
144
|
+
commandType: "payment:confirm_cash",
|
|
145
|
+
businessId: input.businessId,
|
|
146
|
+
entityId: input.orderId,
|
|
147
|
+
payload: {},
|
|
148
|
+
});
|
|
149
|
+
return executeCommand(command);
|
|
150
|
+
},
|
|
129
151
|
};
|
|
@@ -14,6 +14,10 @@ export interface POSOrderPreviewInput {
|
|
|
14
14
|
customerName?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const posRepository: {
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Single create path is `createLocalOrder` (domain/orders/service).
|
|
19
|
+
* Forwards via `orderRepository.createOrder`, which delegates there directly.
|
|
20
|
+
*/
|
|
17
21
|
createOrder(businessId: string, data: POSOrderCreateRequest, entityId?: string): Promise<SellerOrder>;
|
|
18
22
|
fetchPreview(input: POSOrderPreviewInput): Promise<POSOrderPreviewResponse>;
|
|
19
23
|
fetchOrders(businessId: string, params?: Record<string, string>): Promise<SellerOrdersResponse>;
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import { posService } from "../adapters/services";
|
|
8
8
|
import { orderRepository } from "./order.repository";
|
|
9
9
|
export const posRepository = {
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Single create path is `createLocalOrder` (domain/orders/service).
|
|
12
|
+
* Forwards via `orderRepository.createOrder`, which delegates there directly.
|
|
13
|
+
*/
|
|
10
14
|
async createOrder(businessId, data, entityId) {
|
|
11
15
|
return orderRepository.createOrder({ businessId, data, entityId });
|
|
12
16
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry backoff — one curve everywhere (sync retries, probes stay fixed).
|
|
3
|
+
*
|
|
4
|
+
* delay(attempts) = min(MAX, BASE * 2^attempts) + jitter(±20%).
|
|
5
|
+
* Attempts reset on any success or connectivity regain, so one bad night
|
|
6
|
+
* never permanently slows the device, and a fleet doesn't thunder-herd a
|
|
7
|
+
* recovering backend at the same millisecond.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BACKOFF_BASE_MS = 2000;
|
|
10
|
+
export declare const BACKOFF_MAX_MS: number;
|
|
11
|
+
export declare function backoffDelayMs(attempts: number): number;
|
|
12
|
+
/** Deterministic center of the curve (no jitter) — for tests and logging. */
|
|
13
|
+
export declare function backoffCenterMs(attempts: number): number;
|
|
14
|
+
/** Attempt counters keyed by scope (e.g. business id). Reset on success/regain. */
|
|
15
|
+
export declare class BackoffTracker {
|
|
16
|
+
private attempts;
|
|
17
|
+
next(scope: string): number;
|
|
18
|
+
reset(scope: string): void;
|
|
19
|
+
resetAll(): void;
|
|
20
|
+
count(scope: string): number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry backoff — one curve everywhere (sync retries, probes stay fixed).
|
|
3
|
+
*
|
|
4
|
+
* delay(attempts) = min(MAX, BASE * 2^attempts) + jitter(±20%).
|
|
5
|
+
* Attempts reset on any success or connectivity regain, so one bad night
|
|
6
|
+
* never permanently slows the device, and a fleet doesn't thunder-herd a
|
|
7
|
+
* recovering backend at the same millisecond.
|
|
8
|
+
*/
|
|
9
|
+
export const BACKOFF_BASE_MS = 2000;
|
|
10
|
+
export const BACKOFF_MAX_MS = 5 * 60000;
|
|
11
|
+
const JITTER_RATIO = 0.2;
|
|
12
|
+
export function backoffDelayMs(attempts) {
|
|
13
|
+
const safe = Math.max(0, Math.floor(attempts));
|
|
14
|
+
const capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, safe));
|
|
15
|
+
const jitter = capped * JITTER_RATIO * (Math.random() * 2 - 1);
|
|
16
|
+
return Math.max(0, Math.round(capped + jitter));
|
|
17
|
+
}
|
|
18
|
+
/** Deterministic center of the curve (no jitter) — for tests and logging. */
|
|
19
|
+
export function backoffCenterMs(attempts) {
|
|
20
|
+
const safe = Math.max(0, Math.floor(attempts));
|
|
21
|
+
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, safe));
|
|
22
|
+
}
|
|
23
|
+
/** Attempt counters keyed by scope (e.g. business id). Reset on success/regain. */
|
|
24
|
+
export class BackoffTracker {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.attempts = new Map();
|
|
27
|
+
}
|
|
28
|
+
next(scope) {
|
|
29
|
+
const n = this.attempts.get(scope) ?? 0;
|
|
30
|
+
this.attempts.set(scope, n + 1);
|
|
31
|
+
return backoffDelayMs(n);
|
|
32
|
+
}
|
|
33
|
+
reset(scope) {
|
|
34
|
+
this.attempts.delete(scope);
|
|
35
|
+
}
|
|
36
|
+
resetAll() {
|
|
37
|
+
this.attempts.clear();
|
|
38
|
+
}
|
|
39
|
+
count(scope) {
|
|
40
|
+
return this.attempts.get(scope) ?? 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -11,6 +11,14 @@ export function buildCommand(options) {
|
|
|
11
11
|
const user = useAuthStore.getState().user;
|
|
12
12
|
const userId = user?.id ?? "anonymous";
|
|
13
13
|
const deviceId = getAppInstanceId();
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
// Inject inserted_at / updated_at into payload so the backend preserves the
|
|
16
|
+
// original client creation time for offline-first sync.
|
|
17
|
+
const payload = {
|
|
18
|
+
...options.payload,
|
|
19
|
+
inserted_at: timestamp,
|
|
20
|
+
updated_at: timestamp,
|
|
21
|
+
};
|
|
14
22
|
return {
|
|
15
23
|
command_id: generateCommandId(),
|
|
16
24
|
command_type: options.commandType,
|
|
@@ -19,8 +27,8 @@ export function buildCommand(options) {
|
|
|
19
27
|
entity_id: options.entityId,
|
|
20
28
|
user_id: userId,
|
|
21
29
|
device_id: deviceId,
|
|
22
|
-
timestamp
|
|
23
|
-
payload
|
|
30
|
+
timestamp,
|
|
31
|
+
payload,
|
|
24
32
|
};
|
|
25
33
|
}
|
|
26
34
|
export { getAppInstanceId };
|
|
@@ -37,6 +37,18 @@ declare class CommandQueueService {
|
|
|
37
37
|
* Return the number of commands currently in the outbox.
|
|
38
38
|
*/
|
|
39
39
|
getPendingCount(): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Debug dump of the engine outbox for Diagnostics screens (all statuses,
|
|
42
|
+
* newest last). Mirrors debugDumpSyncJobs.
|
|
43
|
+
*/
|
|
44
|
+
debugDumpCommands(businessId: string): Promise<Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
command_type: string;
|
|
47
|
+
status: string;
|
|
48
|
+
retry_count: number;
|
|
49
|
+
error: string | null;
|
|
50
|
+
created_at: string;
|
|
51
|
+
}>>;
|
|
40
52
|
}
|
|
41
53
|
export declare const commandQueueService: CommandQueueService;
|
|
42
54
|
export {};
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { getSyncDatabase, openSyncDatabase } from "../db/connection";
|
|
12
12
|
import { insertCommand, getPendingCommands, markCommandInFlight, updateCommandStatus, deleteCommand, insertEvent, } from "../db/operations";
|
|
13
13
|
import { syncLogger } from "../utils/sync-logger";
|
|
14
|
+
import { recordAttempt } from "../utils/attempt-stats";
|
|
14
15
|
import { generateEventId } from "../utils/uuid";
|
|
15
|
-
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, } from "./order-number";
|
|
16
|
+
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrderNumber, } from "./order-number";
|
|
16
17
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
17
18
|
const MAX_RETRIES = 5;
|
|
18
19
|
const QUEUED_STATUS_BY_COMMAND = {
|
|
@@ -25,6 +26,10 @@ const QUEUED_STATUS_BY_COMMAND = {
|
|
|
25
26
|
"delivery:assign": "assigned",
|
|
26
27
|
"delivery:confirm_handover": "delivered",
|
|
27
28
|
};
|
|
29
|
+
const PAYMENT_QUEUED_PAYLOAD = {
|
|
30
|
+
is_paid: true,
|
|
31
|
+
to_is_paid: true,
|
|
32
|
+
};
|
|
28
33
|
export function getQueuedStatus(commandType) {
|
|
29
34
|
return QUEUED_STATUS_BY_COMMAND[commandType] ?? null;
|
|
30
35
|
}
|
|
@@ -42,6 +47,24 @@ function buildOptimisticEvent(command) {
|
|
|
42
47
|
timestamp: new Date().toISOString(),
|
|
43
48
|
};
|
|
44
49
|
}
|
|
50
|
+
if (command.command_type === "payment:confirm_cash") {
|
|
51
|
+
return {
|
|
52
|
+
event_id: generateEventId(),
|
|
53
|
+
event_type: "payment.completed_queued",
|
|
54
|
+
event_version: "1.0",
|
|
55
|
+
entity_id: command.entity_id ?? command.command_id,
|
|
56
|
+
user_id: command.user_id,
|
|
57
|
+
device_id: command.device_id,
|
|
58
|
+
payload: {
|
|
59
|
+
...payload,
|
|
60
|
+
command_id: command.command_id,
|
|
61
|
+
is_paid: true,
|
|
62
|
+
paid_at: new Date().toISOString(),
|
|
63
|
+
order_id: command.entity_id,
|
|
64
|
+
},
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
45
68
|
const queuedStatus = getQueuedStatus(command.command_type);
|
|
46
69
|
return {
|
|
47
70
|
event_id: generateEventId(),
|
|
@@ -58,11 +81,13 @@ function buildOptimisticEvent(command) {
|
|
|
58
81
|
to_status: queuedStatus,
|
|
59
82
|
order_id: command.entity_id,
|
|
60
83
|
delivery_id: command.entity_id,
|
|
84
|
+
...(queuedStatus ? {} : {}),
|
|
61
85
|
},
|
|
62
86
|
timestamp: new Date().toISOString(),
|
|
63
87
|
};
|
|
64
88
|
}
|
|
65
89
|
function buildOptimisticOrder(command, payload) {
|
|
90
|
+
const snapshots = payload.item_snapshots ?? null;
|
|
66
91
|
const items = payload.items ?? [];
|
|
67
92
|
const totalMinor = payload.total_minor ?? 0;
|
|
68
93
|
const subtotalMinor = payload.subtotal_minor ?? totalMinor;
|
|
@@ -74,14 +99,14 @@ function buildOptimisticOrder(command, payload) {
|
|
|
74
99
|
return {
|
|
75
100
|
id: command.entity_id ?? command.command_id,
|
|
76
101
|
order_number: orderNumber,
|
|
77
|
-
callout_number: generateLocalCalloutNumber(),
|
|
102
|
+
callout_number: generateLocalCalloutNumber(command.business_id),
|
|
78
103
|
order_type: payload.order_type ?? "takeout",
|
|
79
104
|
is_pos: true,
|
|
80
105
|
customer_name: payload.customer_name ?? null,
|
|
81
106
|
customer_phone: payload.customer_phone ?? null,
|
|
82
107
|
pos_payment_method: payload.pos_payment_method ?? "cash",
|
|
83
108
|
paid_at: null,
|
|
84
|
-
status: "
|
|
109
|
+
status: "accepted",
|
|
85
110
|
delivery_id: null,
|
|
86
111
|
delivery_status: null,
|
|
87
112
|
is_paid: payload.pos_payment_method === "cash",
|
|
@@ -109,14 +134,22 @@ function buildOptimisticOrder(command, payload) {
|
|
|
109
134
|
delivery_lat: null,
|
|
110
135
|
delivery_lng: null,
|
|
111
136
|
user: null,
|
|
112
|
-
items:
|
|
137
|
+
items: (snapshots ? snapshots.map((s, index) => ({
|
|
138
|
+
id: `tmp-${command.command_id}-${index}`,
|
|
139
|
+
menu_item: { id: s.menu_item_id, name: s.name },
|
|
140
|
+
quantity: s.quantity,
|
|
141
|
+
unit_price: s.unit_price_minor,
|
|
142
|
+
subtotal: s.unit_price_minor * s.quantity,
|
|
143
|
+
variant_name: s.variant_name,
|
|
144
|
+
addons: (s.addons ?? []).map((a) => ({ name: a.name, price_minor: a.price_minor })),
|
|
145
|
+
})) : items.map((item, index) => ({
|
|
113
146
|
id: `tmp-${command.command_id}-${index}`,
|
|
114
147
|
menu_item: { id: item.menu_item_id, name: "" },
|
|
115
148
|
quantity: item.quantity,
|
|
116
149
|
unit_price: 0,
|
|
117
150
|
subtotal: 0,
|
|
118
151
|
addons: [],
|
|
119
|
-
})),
|
|
152
|
+
}))),
|
|
120
153
|
inserted_at: now,
|
|
121
154
|
};
|
|
122
155
|
}
|
|
@@ -142,7 +175,7 @@ function injectLocalOrderNumber(command) {
|
|
|
142
175
|
}
|
|
143
176
|
class CommandQueueService {
|
|
144
177
|
constructor() {
|
|
145
|
-
this.replaying =
|
|
178
|
+
this.replaying = null;
|
|
146
179
|
this.syncEngine = null;
|
|
147
180
|
}
|
|
148
181
|
setSyncEngine(engine) {
|
|
@@ -222,7 +255,7 @@ class CommandQueueService {
|
|
|
222
255
|
}
|
|
223
256
|
await markCommandInFlight(db, command.command_id);
|
|
224
257
|
try {
|
|
225
|
-
const events = await this.syncEngine.execute(parsedCommand);
|
|
258
|
+
const events = await this.syncEngine.execute(parsedCommand, { forReplay: true });
|
|
226
259
|
// Outbox is an ephemeral queue — remove the row on success so the
|
|
227
260
|
// Diagnostics outbox count drops to 0 and old completed rows never
|
|
228
261
|
// accumulate. The optimistic event stays in `events` (marked synced).
|
|
@@ -238,11 +271,18 @@ class CommandQueueService {
|
|
|
238
271
|
}
|
|
239
272
|
catch (error) {
|
|
240
273
|
const message = error instanceof Error ? error.message : String(error);
|
|
274
|
+
// Replay with no transport available: leave the row exactly as-is for
|
|
275
|
+
// the next trigger (do not mark, do not count). Must reset the
|
|
276
|
+
// in_flight flag first — rows stuck in_flight are invisible to the
|
|
277
|
+
// outbox query and would wedge the queue forever.
|
|
278
|
+
if (message.includes("ReplayDeferred")) {
|
|
279
|
+
await updateCommandStatus(db, command.command_id, "pending", "deferred:no-transport");
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
241
282
|
const apiErr = error;
|
|
242
283
|
// 409 ORDER_NUMBER_TAKEN is retryable once with new RRR
|
|
243
284
|
const code = apiErr?.code ?? apiErr?.data?.code;
|
|
244
285
|
if (apiErr?.status === 409 && String(code).includes("ORDER_NUMBER_TAKEN")) {
|
|
245
|
-
// Regenerate random part of order_number if this was order:create
|
|
246
286
|
if (command.command_type === "order:create") {
|
|
247
287
|
try {
|
|
248
288
|
const payload = command.payload;
|
|
@@ -250,12 +290,14 @@ class CommandQueueService {
|
|
|
250
290
|
const parsed = on ? on.match(/^([0-9]{2})-([0-9]{3})-([0-9]{3})$/) : null;
|
|
251
291
|
if (parsed) {
|
|
252
292
|
const deviceNumber = parsed[1];
|
|
253
|
-
const {
|
|
254
|
-
const { orderNumber: newOn } = generateOrderNumber(deviceNumber);
|
|
293
|
+
const { orderNumber: newOn } = generateOrderNumber(deviceNumber, command.business_id);
|
|
255
294
|
const newPayload = { ...payload, order_number: newOn };
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
295
|
+
await db.runAsync(`UPDATE commands SET payload = ?, error = ?, updated_at = ? WHERE command_id = ?;`, JSON.stringify(newPayload), `retry_random:${message}`, new Date().toISOString(), command.command_id);
|
|
296
|
+
syncLogger.info("CommandQueueService", "Regenerated order_number after 409", { command_id: command.command_id, new_order_number: newOn });
|
|
297
|
+
// keep retry_count unchanged so next replay retries; do not mark failed twice.
|
|
298
|
+
// Reset in_flight → pending so the row stays visible to the outbox query.
|
|
299
|
+
await updateCommandStatus(db, command.command_id, "pending", `retry_random:${message}`);
|
|
300
|
+
return;
|
|
259
301
|
}
|
|
260
302
|
}
|
|
261
303
|
catch { }
|
|
@@ -275,11 +317,29 @@ class CommandQueueService {
|
|
|
275
317
|
status: apiErr.status,
|
|
276
318
|
error: message,
|
|
277
319
|
});
|
|
320
|
+
// Loud toast + revert: the optimistic local write is rolled back so
|
|
321
|
+
// the UI never shows what the backend refused.
|
|
322
|
+
try {
|
|
323
|
+
const { notifyError } = await import("../adapters/notify");
|
|
324
|
+
notifyError(`Server rejected ${command.command_type}: ${message}`);
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
try {
|
|
328
|
+
if (command.command_type.startsWith("menu:") && command.entity_id) {
|
|
329
|
+
const { revertMenuCommand } = await import("../reference/menu-apply");
|
|
330
|
+
await revertMenuCommand(command.business_id, command.command_type, command.entity_id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch { }
|
|
278
334
|
// Do not throw — allow replay to continue to next command.
|
|
279
335
|
return;
|
|
280
336
|
}
|
|
281
|
-
|
|
282
|
-
|
|
337
|
+
// Network/timeout/5xx: NOT a rejection — keep the command retryable
|
|
338
|
+
// indefinitely (status back to pending, retry_count untouched) so a
|
|
339
|
+
// long outage never dead-letters the outbox via MAX_RETRIES. Replay
|
|
340
|
+
// stops here to preserve ordering; the next trigger retries.
|
|
341
|
+
await updateCommandStatus(db, command.command_id, "pending", `network:${message}`);
|
|
342
|
+
syncLogger.command(parsedCommand, "queued", { error: message, deferred: true });
|
|
283
343
|
throw error;
|
|
284
344
|
}
|
|
285
345
|
}
|
|
@@ -288,11 +348,21 @@ class CommandQueueService {
|
|
|
288
348
|
* ordering consistent; the next replay will retry from the failed command.
|
|
289
349
|
*/
|
|
290
350
|
async replay() {
|
|
351
|
+
recordAttempt("sync_drain", "replay");
|
|
352
|
+
// Join an in-flight replay instead of no-op'ing: concurrent replays
|
|
353
|
+
// (connectivity auto-replay racing an explicit replay) must converge on
|
|
354
|
+
// the same outcome, otherwise callers observe a drained queue while work
|
|
355
|
+
// is still flying.
|
|
291
356
|
if (this.replaying) {
|
|
292
|
-
syncLogger.debug("CommandQueueService", "Replay already in progress");
|
|
357
|
+
syncLogger.debug("CommandQueueService", "Replay already in progress, joining");
|
|
358
|
+
try {
|
|
359
|
+
await this.replaying;
|
|
360
|
+
}
|
|
361
|
+
catch { }
|
|
293
362
|
return;
|
|
294
363
|
}
|
|
295
|
-
|
|
364
|
+
let resolveReplay;
|
|
365
|
+
this.replaying = new Promise((r) => { resolveReplay = r; });
|
|
296
366
|
try {
|
|
297
367
|
// Await open (not getSyncDatabase) so a cold-start replay triggered by a
|
|
298
368
|
// connectivity event that fires before the DB open completes does not
|
|
@@ -324,7 +394,11 @@ class CommandQueueService {
|
|
|
324
394
|
}
|
|
325
395
|
}
|
|
326
396
|
finally {
|
|
327
|
-
this.replaying =
|
|
397
|
+
this.replaying = null;
|
|
398
|
+
try {
|
|
399
|
+
resolveReplay();
|
|
400
|
+
}
|
|
401
|
+
catch { }
|
|
328
402
|
}
|
|
329
403
|
}
|
|
330
404
|
/**
|
|
@@ -345,5 +419,28 @@ class CommandQueueService {
|
|
|
345
419
|
const commands = await getPendingCommands(db, Number.MAX_SAFE_INTEGER);
|
|
346
420
|
return commands.length;
|
|
347
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Debug dump of the engine outbox for Diagnostics screens (all statuses,
|
|
424
|
+
* newest last). Mirrors debugDumpSyncJobs.
|
|
425
|
+
*/
|
|
426
|
+
async debugDumpCommands(businessId) {
|
|
427
|
+
try {
|
|
428
|
+
const db = getSyncDatabase();
|
|
429
|
+
const rows = await getPendingCommands(db, Number.MAX_SAFE_INTEGER);
|
|
430
|
+
return rows
|
|
431
|
+
.filter((c) => !businessId || c.business_id === businessId)
|
|
432
|
+
.map((c) => ({
|
|
433
|
+
id: c.command_id,
|
|
434
|
+
command_type: c.command_type,
|
|
435
|
+
status: c.status,
|
|
436
|
+
retry_count: c.retry_count,
|
|
437
|
+
error: c.error ?? null,
|
|
438
|
+
created_at: c.created_at ?? "",
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
348
445
|
}
|
|
349
446
|
export const commandQueueService = new CommandQueueService();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Backend authority: POST /businesses/:id/devices/lease
|
|
4
4
|
* KV persists device_number per businessId.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
import { getAppInstanceIdAsync } from "../adapters/devices";
|
|
7
7
|
export declare function getDeviceNumber(businessId: string): string | null;
|
|
8
8
|
export declare function isOnlineOnly(businessId: string): boolean;
|
|
9
9
|
export declare function ensureLease(businessId: string): Promise<string | null>;
|
|
@@ -14,6 +14,6 @@ export declare const deviceLeaseService: {
|
|
|
14
14
|
isOnlineOnly: typeof isOnlineOnly;
|
|
15
15
|
ensureLease: typeof ensureLease;
|
|
16
16
|
refreshLease: typeof refreshLease;
|
|
17
|
-
getAppInstanceId: typeof
|
|
17
|
+
getAppInstanceId: typeof getAppInstanceIdAsync;
|
|
18
|
+
getAppInstanceIdAsync: typeof getAppInstanceIdAsync;
|
|
18
19
|
};
|
|
19
|
-
export {};
|