@teincfood/core 0.7.9 → 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 +8 -0
- package/dist/adapters/auth.js +36 -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 +96 -15
- package/dist/adapters/services.js +241 -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 +18 -39
- package/dist/hooks/orders.hooks.js +124 -343
- 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 +56 -6
- package/dist/index.js +73 -6
- 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/phoenix-socket.js +30 -1
- package/dist/realtime/useEventsConnection.js +11 -2
- 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 +27 -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 +7 -0
- package/dist/repositories/order.repository.js +15 -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 +81 -16
- 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.js +18 -3
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +67 -1
- 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 +84 -26
- package/dist/sync/use-sync-reconciliation.js +15 -1
- 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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Health Probe — periodic cloud reachability check.
|
|
3
|
+
*
|
|
4
|
+
* Probes `GET /health` using the injected HttpClient with a short timeout.
|
|
5
|
+
* On success: marks cloud reachable (triggers sync replay via connectivity
|
|
6
|
+
* subscribers). On failure: marks unreachable and schedules a retry with
|
|
7
|
+
* exponential backoff (2s → 5min cap, ±20% jitter).
|
|
8
|
+
*
|
|
9
|
+
* Started automatically by `createTeincCore()`. Host apps should not need
|
|
10
|
+
* to call this directly.
|
|
11
|
+
*/
|
|
12
|
+
import { getConnectivityState, setCloudReachable, subscribeConnectivity } from "../adapters/connectivity";
|
|
13
|
+
import { getHttpClient } from "../adapters/http";
|
|
14
|
+
import { API_BASE_URL } from "../utils/constants";
|
|
15
|
+
import { syncLogger } from "../utils/sync-logger";
|
|
16
|
+
import { BackoffTracker } from "./backoff";
|
|
17
|
+
import { getActiveBusiness } from "../adapters/business";
|
|
18
|
+
import { getAuthToken } from "../adapters/auth";
|
|
19
|
+
const PROBE_URL = "/health";
|
|
20
|
+
const PROBE_TIMEOUT_MS = 4000;
|
|
21
|
+
const PROBE_INTERVAL_MS = 60000;
|
|
22
|
+
const UNREACHABLE_COOLDOWN_MS = 15000;
|
|
23
|
+
const INITIAL_DELAY_MS = 3000;
|
|
24
|
+
let probeTimer = null;
|
|
25
|
+
let cooldownTimer = null;
|
|
26
|
+
let stopFn = null;
|
|
27
|
+
const backoff = new BackoffTracker();
|
|
28
|
+
/**
|
|
29
|
+
* Single probe attempt. Returns true if cloud is reachable.
|
|
30
|
+
*/
|
|
31
|
+
export async function probeCloudHealth() {
|
|
32
|
+
const http = (() => { try {
|
|
33
|
+
return getHttpClient();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
} })();
|
|
38
|
+
if (!http)
|
|
39
|
+
return false;
|
|
40
|
+
const { isOnline } = getConnectivityState();
|
|
41
|
+
if (!isOnline)
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
46
|
+
await http.get(PROBE_URL, { signal: controller.signal });
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
backoff.reset("health");
|
|
49
|
+
const was = getConnectivityState().isCloudReachable;
|
|
50
|
+
setCloudReachable("reachable");
|
|
51
|
+
if (was !== "reachable") {
|
|
52
|
+
syncLogger.info("HealthProbe", "Cloud reachable", { previous: was });
|
|
53
|
+
// Kick off pull→merge→push without blocking probe.
|
|
54
|
+
try {
|
|
55
|
+
const bid = getActiveBusiness()?.id;
|
|
56
|
+
if (bid && getAuthToken()) {
|
|
57
|
+
void import("./full-sync").then((m) => m.performFullSync(bid).catch(() => { }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Don't mark unreachable here — the sync engine already handles that
|
|
66
|
+
// when a command times out. This probe only detects recovery.
|
|
67
|
+
// Log and backoff so we re-check later.
|
|
68
|
+
syncLogger.debug("HealthProbe", "Probe failed — will retry", {
|
|
69
|
+
previous: getConnectivityState().isCloudReachable,
|
|
70
|
+
});
|
|
71
|
+
scheduleRetry();
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function scheduleRetry() {
|
|
76
|
+
if (cooldownTimer)
|
|
77
|
+
return;
|
|
78
|
+
const delay = Math.max(UNREACHABLE_COOLDOWN_MS, backoff.next("health"));
|
|
79
|
+
syncLogger.debug("HealthProbe", "Scheduling retry", { delay });
|
|
80
|
+
cooldownTimer = setTimeout(() => {
|
|
81
|
+
cooldownTimer = null;
|
|
82
|
+
void probeCloudHealth();
|
|
83
|
+
}, delay);
|
|
84
|
+
}
|
|
85
|
+
function startPeriodicProbe() {
|
|
86
|
+
if (probeTimer)
|
|
87
|
+
return;
|
|
88
|
+
probeTimer = setInterval(() => void probeCloudHealth(), PROBE_INTERVAL_MS);
|
|
89
|
+
}
|
|
90
|
+
function stopPeriodicProbe() {
|
|
91
|
+
if (probeTimer) {
|
|
92
|
+
clearInterval(probeTimer);
|
|
93
|
+
probeTimer = null;
|
|
94
|
+
}
|
|
95
|
+
if (cooldownTimer) {
|
|
96
|
+
clearTimeout(cooldownTimer);
|
|
97
|
+
cooldownTimer = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Start the health probe. Safe to call multiple times (idempotent).
|
|
102
|
+
* Returns a stop function.
|
|
103
|
+
*/
|
|
104
|
+
export function startHealthProbe() {
|
|
105
|
+
if (stopFn)
|
|
106
|
+
return stopFn;
|
|
107
|
+
syncLogger.debug("HealthProbe", "Starting", { probeUrl: `${API_BASE_URL}${PROBE_URL}` });
|
|
108
|
+
// Initial probe (deferred so host bridges can set up first)
|
|
109
|
+
cooldownTimer = setTimeout(() => {
|
|
110
|
+
cooldownTimer = null;
|
|
111
|
+
void probeCloudHealth();
|
|
112
|
+
}, INITIAL_DELAY_MS);
|
|
113
|
+
// Periodic probe
|
|
114
|
+
startPeriodicProbe();
|
|
115
|
+
// Re-probe when network regains
|
|
116
|
+
let prevOnline = getConnectivityState().isOnline;
|
|
117
|
+
const unsub = subscribeConnectivity(() => {
|
|
118
|
+
const { isOnline } = getConnectivityState();
|
|
119
|
+
if (isOnline && !prevOnline) {
|
|
120
|
+
syncLogger.debug("HealthProbe", "Network regained — probing");
|
|
121
|
+
void probeCloudHealth();
|
|
122
|
+
}
|
|
123
|
+
prevOnline = isOnline;
|
|
124
|
+
});
|
|
125
|
+
stopFn = () => {
|
|
126
|
+
stopPeriodicProbe();
|
|
127
|
+
unsub();
|
|
128
|
+
stopFn = null;
|
|
129
|
+
};
|
|
130
|
+
return stopFn;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stop the health probe and clean up all timers.
|
|
134
|
+
*/
|
|
135
|
+
export function stopHealthProbe() {
|
|
136
|
+
if (stopFn)
|
|
137
|
+
stopFn();
|
|
138
|
+
}
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { syncEngine } from "./engine";
|
|
14
14
|
import { buildCommand } from "./command-builder";
|
|
15
|
+
import { assertOnlineFor } from "./online-gates";
|
|
15
16
|
import { syncLogger } from "../utils/sync-logger";
|
|
16
17
|
import { ApiError } from "../adapters/http";
|
|
18
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
17
19
|
import { CAPABILITY_BY_COMMAND } from "../reference/capabilities";
|
|
18
20
|
import { openSyncDatabase } from "../db/connection";
|
|
19
21
|
import { getReferenceData } from "../reference/operations";
|
|
@@ -21,6 +23,23 @@ export async function runOfflineCapableMutation(options) {
|
|
|
21
23
|
// Per-command capability guard before even attempting direct call
|
|
22
24
|
const required = CAPABILITY_BY_COMMAND[options.commandType];
|
|
23
25
|
if (required) {
|
|
26
|
+
// Online-required gate first: these commands can never succeed offline —
|
|
27
|
+
// throw loud immediately instead of queuing what no transport can fulfill.
|
|
28
|
+
assertOnlineFor(options.commandType, options.payload);
|
|
29
|
+
// Local-first for engine-routed domains (menu:*): commit locally via the
|
|
30
|
+
// engine and resolve immediately. Network is sync transport only — never a
|
|
31
|
+
// mutation delay. The engine cloud path carries the same 2.5s cap and
|
|
32
|
+
// returns the backend response when online.
|
|
33
|
+
if (options.commandType.startsWith("menu:")) {
|
|
34
|
+
const command = buildCommand({
|
|
35
|
+
commandType: options.commandType,
|
|
36
|
+
businessId: options.businessId,
|
|
37
|
+
entityId: options.entityId,
|
|
38
|
+
payload: options.payload,
|
|
39
|
+
});
|
|
40
|
+
const events = await syncEngine.execute(command);
|
|
41
|
+
return events[0]?.payload;
|
|
42
|
+
}
|
|
24
43
|
try {
|
|
25
44
|
const db = await openSyncDatabase();
|
|
26
45
|
const rec = await getReferenceData(db, options.businessId, "capabilities");
|
|
@@ -35,6 +54,12 @@ export async function runOfflineCapableMutation(options) {
|
|
|
35
54
|
}
|
|
36
55
|
}
|
|
37
56
|
try {
|
|
57
|
+
// Cloud already known unreachable (or no network): skip the direct REST
|
|
58
|
+
// attempt entirely — it would only burn a timeout before falling back.
|
|
59
|
+
const cs = getConnectivityState();
|
|
60
|
+
if (!cs.isOnline || cs.isCloudReachable === "unreachable") {
|
|
61
|
+
throw new Error("offline-capable: cloud known unreachable, engine route");
|
|
62
|
+
}
|
|
38
63
|
return await options.run();
|
|
39
64
|
}
|
|
40
65
|
catch (error) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Online-required allowlist — single source for what can NEVER work offline.
|
|
3
|
+
*
|
|
4
|
+
* Everything else is offline-first (SQLite read, local commit, engine sync).
|
|
5
|
+
* These commands throw immediately when the cloud is unreachable: queuing
|
|
6
|
+
* them would be dishonest because no transport (LAN or outbox replay) can
|
|
7
|
+
* ever fulfill them without the backend.
|
|
8
|
+
*/
|
|
9
|
+
/** Command types that require the cloud, unconditionally. */
|
|
10
|
+
export declare const ONLINE_REQUIRED: Record<string, string>;
|
|
11
|
+
export declare function isOnlineCapable(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Throw when a command cannot proceed offline. Handles payload-dependent
|
|
14
|
+
* cases: order:create with delivery type or prepaid/mobile-money payment,
|
|
15
|
+
* and delivery/order transitions the domain already gates.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertOnlineFor(commandType: string, payload?: Record<string, unknown>): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Online-required allowlist — single source for what can NEVER work offline.
|
|
3
|
+
*
|
|
4
|
+
* Everything else is offline-first (SQLite read, local commit, engine sync).
|
|
5
|
+
* These commands throw immediately when the cloud is unreachable: queuing
|
|
6
|
+
* them would be dishonest because no transport (LAN or outbox replay) can
|
|
7
|
+
* ever fulfill them without the backend.
|
|
8
|
+
*/
|
|
9
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
10
|
+
/** Command types that require the cloud, unconditionally. */
|
|
11
|
+
export const ONLINE_REQUIRED = {
|
|
12
|
+
"order:assign_rider": "Assign rider requires online",
|
|
13
|
+
"order:confirm_handover": "Handover confirmation requires online",
|
|
14
|
+
"chat:send": "Chat requires online",
|
|
15
|
+
"notification:register": "Notifications require online",
|
|
16
|
+
"member:invite": "Member invites require online",
|
|
17
|
+
"member:manage": "Member management requires online",
|
|
18
|
+
"fleet:manage": "Fleet changes require online",
|
|
19
|
+
"settings:manage": "Settings changes require online",
|
|
20
|
+
"kyc:submit": "KYC submission requires online",
|
|
21
|
+
"wallet:withdraw": "Withdrawals require online",
|
|
22
|
+
"wallet:payout": "Payouts require online",
|
|
23
|
+
"payment:charge_online": "Online payment requires online",
|
|
24
|
+
"device:lease": "Device lease requires online (first lease only)",
|
|
25
|
+
};
|
|
26
|
+
export function isOnlineCapable() {
|
|
27
|
+
const s = getConnectivityState();
|
|
28
|
+
return s.isOnline && s.isCloudReachable !== "unreachable";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Throw when a command cannot proceed offline. Handles payload-dependent
|
|
32
|
+
* cases: order:create with delivery type or prepaid/mobile-money payment,
|
|
33
|
+
* and delivery/order transitions the domain already gates.
|
|
34
|
+
*/
|
|
35
|
+
export function assertOnlineFor(commandType, payload) {
|
|
36
|
+
const fixed = ONLINE_REQUIRED[commandType];
|
|
37
|
+
if (fixed) {
|
|
38
|
+
if (!isOnlineCapable())
|
|
39
|
+
throw new Error(fixed);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (commandType === "order:create" && payload) {
|
|
43
|
+
const orderType = payload.order_type;
|
|
44
|
+
if (orderType === "delivery" || payload.delivery_params) {
|
|
45
|
+
if (!isOnlineCapable())
|
|
46
|
+
throw new Error("Delivery orders require online");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const method = payload.pos_payment_method;
|
|
50
|
+
if (typeof method === "string" && method !== "cash") {
|
|
51
|
+
if (!isOnlineCapable())
|
|
52
|
+
throw new Error("Online payment requires online — use cash while offline");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
* CCC = callout 001-999 global monotonic per device, wraps to 001 (new RRR on wrap)
|
|
6
6
|
* Example: "10-123-001"
|
|
7
7
|
*/
|
|
8
|
-
export declare function peekCallout(): number;
|
|
9
|
-
export declare function generateOrderNumber(deviceNumber: string): {
|
|
8
|
+
export declare function peekCallout(businessId?: string | null): number;
|
|
9
|
+
export declare function generateOrderNumber(deviceNumber: string, businessId?: string | null): {
|
|
10
10
|
orderNumber: string;
|
|
11
11
|
callout: number;
|
|
12
12
|
random: number;
|
|
13
13
|
};
|
|
14
14
|
export declare function generateLocalOrderNumber(): string;
|
|
15
15
|
export declare function generateOrderNumberForBusiness(businessId: string): string;
|
|
16
|
-
export declare function generateLocalCalloutNumber(): number;
|
|
16
|
+
export declare function generateLocalCalloutNumber(businessId?: string | null): number;
|
|
17
17
|
export declare function isLocalOrderNumber(orderNumber: string): boolean;
|
|
18
18
|
export declare function isOfflineOrderNumber(orderNumber: string): boolean;
|
|
19
19
|
export declare function parseOrderNumber(orderNumber: string): {
|
|
@@ -21,4 +21,4 @@ export declare function parseOrderNumber(orderNumber: string): {
|
|
|
21
21
|
random: string;
|
|
22
22
|
callout: string;
|
|
23
23
|
} | null;
|
|
24
|
-
export declare function resetCalloutForTests(): void;
|
|
24
|
+
export declare function resetCalloutForTests(businessId?: string | null): void;
|
|
@@ -16,23 +16,37 @@ function cryptoRandom100to999() {
|
|
|
16
16
|
}
|
|
17
17
|
return 100 + Math.floor(Math.random() * 900);
|
|
18
18
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
19
|
+
function calloutKeyForBusiness(businessId) {
|
|
20
|
+
return businessId ? `${CALLOUT_SEQ_KEY}:${businessId}` : CALLOUT_SEQ_KEY;
|
|
21
|
+
}
|
|
22
|
+
function getNextCallout(businessId) {
|
|
23
|
+
const key = calloutKeyForBusiness(businessId);
|
|
24
|
+
const stored = getItem(key);
|
|
21
25
|
let next = (stored ?? 0) + 1;
|
|
22
26
|
if (next > 999)
|
|
23
27
|
next = 1;
|
|
24
|
-
setItem(
|
|
28
|
+
setItem(key, next);
|
|
29
|
+
// migrate legacy global once: keep global in sync for backward compat
|
|
30
|
+
if (businessId) {
|
|
31
|
+
const legacy = getItem(CALLOUT_SEQ_KEY) ?? 0;
|
|
32
|
+
if (legacy < next)
|
|
33
|
+
setItem(CALLOUT_SEQ_KEY, next);
|
|
34
|
+
}
|
|
25
35
|
return next;
|
|
26
36
|
}
|
|
27
|
-
export function peekCallout() {
|
|
28
|
-
const
|
|
29
|
-
|
|
37
|
+
export function peekCallout(businessId) {
|
|
38
|
+
const key = calloutKeyForBusiness(businessId);
|
|
39
|
+
const v = getItem(key);
|
|
40
|
+
if (v != null)
|
|
41
|
+
return v;
|
|
42
|
+
const legacy = getItem(CALLOUT_SEQ_KEY);
|
|
43
|
+
return legacy ?? 0;
|
|
30
44
|
}
|
|
31
|
-
export function generateOrderNumber(deviceNumber) {
|
|
45
|
+
export function generateOrderNumber(deviceNumber, businessId) {
|
|
32
46
|
if (!/^(0[1-9]|[1-9][0-9])$/.test(deviceNumber)) {
|
|
33
47
|
throw new Error(`Invalid deviceNumber ${deviceNumber} — must be 01-99`);
|
|
34
48
|
}
|
|
35
|
-
const callout = getNextCallout();
|
|
49
|
+
const callout = getNextCallout(businessId);
|
|
36
50
|
let random = cryptoRandom100to999();
|
|
37
51
|
// On wrap to 001, ensure new random (already new per order, but keep hook)
|
|
38
52
|
if (callout === 1) {
|
|
@@ -53,11 +67,11 @@ export function generateOrderNumberForBusiness(businessId) {
|
|
|
53
67
|
if (!v || !/^(0[1-9]|[1-9][0-9])$/.test(v)) {
|
|
54
68
|
throw new Error(`No device lease for business ${businessId} — call ensureLease(businessId) first (online-only until leased)`);
|
|
55
69
|
}
|
|
56
|
-
return generateOrderNumber(v).orderNumber;
|
|
70
|
+
return generateOrderNumber(v, businessId).orderNumber;
|
|
57
71
|
}
|
|
58
|
-
export function generateLocalCalloutNumber() {
|
|
59
|
-
// kept for SellerOrder.callout_number compatibility — returns CCC
|
|
60
|
-
const next = peekCallout() + 1;
|
|
72
|
+
export function generateLocalCalloutNumber(businessId) {
|
|
73
|
+
// kept for SellerOrder.callout_number compatibility — returns next CCC
|
|
74
|
+
const next = peekCallout(businessId) + 1;
|
|
61
75
|
return next > 999 ? 1 : next;
|
|
62
76
|
}
|
|
63
77
|
// New canonical check — unified for DD-RRR-CCC
|
|
@@ -74,6 +88,12 @@ export function parseOrderNumber(orderNumber) {
|
|
|
74
88
|
return { deviceNumber: m[1], random: m[2], callout: m[3] };
|
|
75
89
|
}
|
|
76
90
|
// Test helpers
|
|
77
|
-
export function resetCalloutForTests() {
|
|
91
|
+
export function resetCalloutForTests(businessId) {
|
|
92
|
+
if (businessId) {
|
|
93
|
+
setItem(calloutKeyForBusiness(businessId), 0);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
78
96
|
setItem(CALLOUT_SEQ_KEY, 0);
|
|
97
|
+
// also clear per-business keys that tests may have touched
|
|
98
|
+
// caller should pass businessId if they used business-scoped sequences
|
|
79
99
|
}
|
|
@@ -74,7 +74,19 @@ class OrderSnapshotsService {
|
|
|
74
74
|
}
|
|
75
75
|
async applyOrderStatusToReadModelInternal(db, orderId, toStatus, event) {
|
|
76
76
|
try {
|
|
77
|
-
const
|
|
77
|
+
const payload = event.payload;
|
|
78
|
+
const businessId = payload?.business_id ?? payload?.businessId ?? null;
|
|
79
|
+
// Dual-write: keep canonical orders table in sync so POS/KDS reads converge instantly.
|
|
80
|
+
if (businessId) {
|
|
81
|
+
try {
|
|
82
|
+
const { updateOrderStatus } = await import("../db/operations");
|
|
83
|
+
await updateOrderStatus(db, businessId, orderId, toStatus, event.timestamp);
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
}
|
|
87
|
+
const lists = businessId
|
|
88
|
+
? await getEntitySnapshotsByType(db, "seller_orders", businessId)
|
|
89
|
+
: await getEntitySnapshotsByType(db, "seller_orders");
|
|
78
90
|
for (const list of lists) {
|
|
79
91
|
const data = list.payload.data ?? [];
|
|
80
92
|
const idx = data.findIndex((o) => o.id === orderId);
|
|
@@ -106,7 +118,10 @@ class OrderSnapshotsService {
|
|
|
106
118
|
try {
|
|
107
119
|
const db = await openSyncDatabase();
|
|
108
120
|
await insertEvent(db, { event, origin });
|
|
109
|
-
const
|
|
121
|
+
const businessId = payload?.business_id ?? payload?.businessId ?? null;
|
|
122
|
+
const lists = businessId
|
|
123
|
+
? await getEntitySnapshotsByType(db, "seller_orders", businessId)
|
|
124
|
+
: await getEntitySnapshotsByType(db, "seller_orders");
|
|
110
125
|
for (const list of lists) {
|
|
111
126
|
const data = list.payload.data ?? [];
|
|
112
127
|
const idx = data.findIndex((o) => o.id === orderId || o.order_number === payload?.order_number);
|
|
@@ -173,7 +188,7 @@ class OrderSnapshotsService {
|
|
|
173
188
|
async getSellerList(businessId, scope) {
|
|
174
189
|
try {
|
|
175
190
|
const db = await openSyncDatabase();
|
|
176
|
-
const snap = await getEntitySnapshot(db, "seller_orders", scope);
|
|
191
|
+
const snap = await getEntitySnapshot(db, "seller_orders", scope, businessId);
|
|
177
192
|
if (!snap || snap.business_id !== businessId)
|
|
178
193
|
return null;
|
|
179
194
|
return snap.payload.data ?? null;
|
|
@@ -15,11 +15,29 @@
|
|
|
15
15
|
* order's `order_number`.
|
|
16
16
|
*/
|
|
17
17
|
import type { DomainEvent } from "./types";
|
|
18
|
+
import type { SellerOrder } from "../types/api/seller.api";
|
|
18
19
|
/**
|
|
19
20
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
20
21
|
* order is reconciled. Returns an unregister function.
|
|
21
22
|
*/
|
|
22
23
|
export declare function registerPendingOrdersInvalidator(fn: () => void): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Fire the registered query invalidator (lists refetch from SQLite).
|
|
26
|
+
* Used after LAN-applied writes (order:sync) that bypass the engine event
|
|
27
|
+
* pipeline, so peers' UIs converge without waiting for a cloud echo.
|
|
28
|
+
*/
|
|
29
|
+
export declare function notifyPendingOrdersChanged(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Merge optimistic (queued, unconfirmed) state into a page of listed orders.
|
|
32
|
+
* Called by list queryFns (page 1 only) so offline work is visible:
|
|
33
|
+
* 1. Pending creations (order.create_queued, local or LAN) missing from the
|
|
34
|
+
* SQLite page are prepended — deduped by id/order_number.
|
|
35
|
+
* 2. Queued transitions only patch the snapshot read model, never the
|
|
36
|
+
* orders table — overlay snapshot statuses onto base rows so the list
|
|
37
|
+
* reflects the queued status immediately (converges on replay).
|
|
38
|
+
* Reactivity comes from the invalidator: every apply path above refetches.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mergePendingIntoList(base: SellerOrder[], businessId: string, page: number): Promise<SellerOrder[]>;
|
|
23
41
|
/**
|
|
24
42
|
* Load persisted unsynced order:create_queued events into the store.
|
|
25
43
|
* Idempotent; safe to call after app restart while still offline.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* order's `order_number`.
|
|
16
16
|
*/
|
|
17
17
|
import { openSyncDatabase } from "../db/connection";
|
|
18
|
-
import { insertEvent, getUnsyncedEvents, markEventsSynced, } from "../db/operations";
|
|
18
|
+
import { insertEvent, getUnsyncedEvents, markEventsSynced, getEntitySnapshotsByType, } from "../db/operations";
|
|
19
19
|
import { usePendingOrdersStore } from "../adapters/pending-orders";
|
|
20
20
|
import { orderSnapshotsService } from "./order-snapshots.service";
|
|
21
21
|
import { syncLogger } from "../utils/sync-logger";
|
|
@@ -49,6 +49,72 @@ export function registerPendingOrdersInvalidator(fn) {
|
|
|
49
49
|
invalidator = null;
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Fire the registered query invalidator (lists refetch from SQLite).
|
|
54
|
+
* Used after LAN-applied writes (order:sync) that bypass the engine event
|
|
55
|
+
* pipeline, so peers' UIs converge without waiting for a cloud echo.
|
|
56
|
+
*/
|
|
57
|
+
export function notifyPendingOrdersChanged() {
|
|
58
|
+
try {
|
|
59
|
+
invalidator?.();
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge optimistic (queued, unconfirmed) state into a page of listed orders.
|
|
65
|
+
* Called by list queryFns (page 1 only) so offline work is visible:
|
|
66
|
+
* 1. Pending creations (order.create_queued, local or LAN) missing from the
|
|
67
|
+
* SQLite page are prepended — deduped by id/order_number.
|
|
68
|
+
* 2. Queued transitions only patch the snapshot read model, never the
|
|
69
|
+
* orders table — overlay snapshot statuses onto base rows so the list
|
|
70
|
+
* reflects the queued status immediately (converges on replay).
|
|
71
|
+
* Reactivity comes from the invalidator: every apply path above refetches.
|
|
72
|
+
*/
|
|
73
|
+
export async function mergePendingIntoList(base, businessId, page) {
|
|
74
|
+
if (page !== 1)
|
|
75
|
+
return base;
|
|
76
|
+
let merged = base;
|
|
77
|
+
try {
|
|
78
|
+
const pending = usePendingOrdersStore.getState().orders;
|
|
79
|
+
if (pending.length) {
|
|
80
|
+
const seen = new Set(merged.map((o) => String(o.id ?? o.order_number)));
|
|
81
|
+
const fresh = pending.filter((p) => !seen.has(String(p.id ?? p.order_number)));
|
|
82
|
+
if (fresh.length)
|
|
83
|
+
merged = [...fresh, ...merged];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
try {
|
|
88
|
+
const db = await openSyncDatabase();
|
|
89
|
+
const snaps = await getEntitySnapshotsByType(db, "seller_orders", businessId);
|
|
90
|
+
if (!snaps.length)
|
|
91
|
+
return merged;
|
|
92
|
+
const statusById = new Map();
|
|
93
|
+
for (const s of snaps) {
|
|
94
|
+
const arr = s.payload?.data ?? [];
|
|
95
|
+
for (const o of arr) {
|
|
96
|
+
if (o?.id && o.status)
|
|
97
|
+
statusById.set(String(o.id), String(o.status));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!statusById.size)
|
|
101
|
+
return merged;
|
|
102
|
+
let patched = false;
|
|
103
|
+
merged = merged.map((o) => {
|
|
104
|
+
const want = statusById.get(String(o.id));
|
|
105
|
+
if (want && want !== o.status) {
|
|
106
|
+
patched = true;
|
|
107
|
+
return { ...o, status: want };
|
|
108
|
+
}
|
|
109
|
+
return o;
|
|
110
|
+
});
|
|
111
|
+
if (patched) {
|
|
112
|
+
syncLogger.debug("PendingOrders", "overlaid queued statuses onto list", { businessId, count: merged.length });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
52
118
|
/**
|
|
53
119
|
* Load persisted unsynced order:create_queued events into the store.
|
|
54
120
|
* Idempotent; safe to call after app restart while still offline.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity snapshots — local read model (§9 SQLite Model).
|
|
3
|
+
* Canonical in src/db/operations + src/sync/order-snapshots.service.
|
|
4
|
+
*/
|
|
5
|
+
export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
|
|
6
|
+
export { orderSnapshotsService } from "../order-snapshots.service";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity snapshots — local read model (§9 SQLite Model).
|
|
3
|
+
* Canonical in src/db/operations + src/sync/order-snapshots.service.
|
|
4
|
+
*/
|
|
5
|
+
export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
|
|
6
|
+
export { orderSnapshotsService } from "../order-snapshots.service";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Jobs FIFO — near real-time drain after local commit.
|
|
3
|
+
* Tries PHX business:{id} push first, then REST, queues on fail.
|
|
4
|
+
* Core-only, single in_flight per business, coalesced via isOnline gate.
|
|
5
|
+
*/
|
|
6
|
+
export declare function drainOne(businessId: string, entityId: string): Promise<void>;
|
|
7
|
+
export declare function pullRemoteOrders(businessId: string): Promise<void>;
|
|
8
|
+
export declare function drainPending(businessId: string): Promise<void>;
|
|
9
|
+
export declare function setupSyncJobsDrain(): void;
|
|
10
|
+
export declare function getSyncJobsPendingCount(businessId: string): Promise<number>;
|
|
11
|
+
export declare function debugDumpSyncJobs(businessId: string): Promise<Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
command_type: string;
|
|
14
|
+
status: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
attempts: number;
|
|
17
|
+
last_error: string | null;
|
|
18
|
+
}>>;
|