@teincfood/core 0.8.2 → 0.8.4
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/domain/orders/service.js +13 -1
- package/dist/hooks/fleet.hooks.js +15 -5
- package/dist/hooks/members.hooks.js +17 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reference/service.js +12 -0
- package/dist/sync/command-builder.js +3 -0
- package/dist/sync/command-queue.service.js +9 -0
- package/dist/sync/engine.js +5 -0
- package/dist/sync/full-sync.js +2 -4
- package/dist/sync/sync-guard.d.ts +17 -0
- package/dist/sync/sync-guard.js +26 -0
- package/dist/sync/sync-jobs.service.js +32 -20
- package/dist/utils/order-transitions.js +2 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import { canTransition } from "../../utils/order-transitions";
|
|
|
15
15
|
import { validateOrderCreateInput } from "../../validation/offline";
|
|
16
16
|
import { drainOne, pullRemoteOrders } from "../../sync/sync-jobs.service";
|
|
17
17
|
import { getAuthToken } from "../../adapters/auth";
|
|
18
|
+
import { canBusinessSync } from "../../sync/sync-guard";
|
|
18
19
|
import { syncLogger } from "../../utils/sync-logger";
|
|
19
20
|
import { getItem } from "../../adapters/kv";
|
|
20
21
|
let backfilled = new Set();
|
|
@@ -65,6 +66,9 @@ function isOnline() {
|
|
|
65
66
|
return isEffectivelyOnline();
|
|
66
67
|
}
|
|
67
68
|
export async function createLocalOrder(businessId, input) {
|
|
69
|
+
if (!businessId || !canBusinessSync(businessId)) {
|
|
70
|
+
throw new Error("Cannot create order without active business/login");
|
|
71
|
+
}
|
|
68
72
|
const isPos = true; // POS terminal always is_pos
|
|
69
73
|
// Online-required gates (single source: sync/online-gates). Cash needs no
|
|
70
74
|
// connectivity; prepaid/mobile-money and delivery do.
|
|
@@ -159,6 +163,9 @@ export async function createLocalOrder(businessId, input) {
|
|
|
159
163
|
return order;
|
|
160
164
|
}
|
|
161
165
|
export async function transitionLocalOrder(businessId, orderId, action) {
|
|
166
|
+
if (!businessId || !canBusinessSync(businessId)) {
|
|
167
|
+
throw new Error("Transition requires active business/login");
|
|
168
|
+
}
|
|
162
169
|
const db = await openSyncDatabase();
|
|
163
170
|
const order = await getOrderById(db, businessId, orderId);
|
|
164
171
|
if (!order)
|
|
@@ -196,12 +203,15 @@ export async function transitionLocalOrder(businessId, orderId, action) {
|
|
|
196
203
|
return updated ?? order;
|
|
197
204
|
}
|
|
198
205
|
export async function listLocalOrders(businessId, filters, page = 1, pageSize = 20) {
|
|
206
|
+
if (!businessId) {
|
|
207
|
+
return { data: [], total: 0, page, pageSize, totalPages: 0 };
|
|
208
|
+
}
|
|
199
209
|
await ensureOrdersBackfilledFromSnapshots(businessId);
|
|
200
210
|
const db = await openSyncDatabase();
|
|
201
211
|
const res = await listOrders(db, businessId, filters, page, pageSize);
|
|
202
212
|
if (res.total === 0) {
|
|
203
213
|
// blank-screen: snapshot fallback already tried — if still empty and online, fetch from cloud now so listing populates without manual refresh
|
|
204
|
-
if (isOnline() &&
|
|
214
|
+
if (isOnline() && canBusinessSync(businessId)) {
|
|
205
215
|
try {
|
|
206
216
|
await pullRemoteOrders(businessId);
|
|
207
217
|
}
|
|
@@ -223,6 +233,8 @@ export async function listLocalOrders(businessId, filters, page = 1, pageSize =
|
|
|
223
233
|
return res;
|
|
224
234
|
}
|
|
225
235
|
export async function getLocalOrder(businessId, idOrNumber) {
|
|
236
|
+
if (!businessId)
|
|
237
|
+
return null;
|
|
226
238
|
await ensureOrdersBackfilledFromSnapshots(businessId);
|
|
227
239
|
const db = await openSyncDatabase();
|
|
228
240
|
const found = await getOrderById(db, businessId, idOrNumber);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { useSyncExternalStore } from "react";
|
|
8
8
|
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
9
|
+
import { getAuthToken, subscribeAuth } from "../adapters/auth";
|
|
9
10
|
import { fleetService } from "../adapters/services";
|
|
10
11
|
import { assertOnlineFor } from "../sync/online-gates";
|
|
11
12
|
import { validateInviteInput } from "../validation/offline";
|
|
@@ -26,6 +27,10 @@ function useBusinessId() {
|
|
|
26
27
|
const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
27
28
|
return id ?? undefined;
|
|
28
29
|
}
|
|
30
|
+
function useIsAuthenticated() {
|
|
31
|
+
const token = useSyncExternalStore(subscribeAuth, () => getAuthToken(), () => null);
|
|
32
|
+
return !!token;
|
|
33
|
+
}
|
|
29
34
|
const OFFLINE_FIRST = { retry: false, networkMode: "offlineFirst" };
|
|
30
35
|
// Fleet has no SQLite mirror — offline source is the persisted query cache.
|
|
31
36
|
// Reads are cache-first (no online gate): the query is enabled offline so
|
|
@@ -37,10 +42,11 @@ function useOnline() {
|
|
|
37
42
|
// ─── Riders ───
|
|
38
43
|
export function useFleetRidersQuery(enabled = true, filters) {
|
|
39
44
|
const businessId = useBusinessId();
|
|
45
|
+
const isAuthenticated = useIsAuthenticated();
|
|
40
46
|
return useQuery({
|
|
41
47
|
queryKey: [...FLEET_KEYS.riders(businessId), filters],
|
|
42
48
|
queryFn: () => fleetService.fetchFleetRiders(businessId, 1, 50, filters),
|
|
43
|
-
enabled: enabled && Boolean(businessId),
|
|
49
|
+
enabled: enabled && Boolean(businessId) && isAuthenticated,
|
|
44
50
|
placeholderData: (prev) => prev,
|
|
45
51
|
...OFFLINE_FIRST,
|
|
46
52
|
});
|
|
@@ -121,10 +127,11 @@ export function useActivateRiderMutation() {
|
|
|
121
127
|
}
|
|
122
128
|
export function useJoinRequestsQuery(enabled = true) {
|
|
123
129
|
const businessId = useBusinessId();
|
|
130
|
+
const isAuthenticated = useIsAuthenticated();
|
|
124
131
|
return useQuery({
|
|
125
132
|
queryKey: FLEET_KEYS.joinRequests(businessId),
|
|
126
133
|
queryFn: () => fleetService.fetchJoinRequests(businessId),
|
|
127
|
-
enabled: enabled && Boolean(businessId),
|
|
134
|
+
enabled: enabled && Boolean(businessId) && isAuthenticated,
|
|
128
135
|
placeholderData: (prev) => prev,
|
|
129
136
|
...OFFLINE_FIRST,
|
|
130
137
|
});
|
|
@@ -158,10 +165,11 @@ export function useRejectJoinRequestMutation() {
|
|
|
158
165
|
}
|
|
159
166
|
export function useFleetInvitationsQuery(enabled = true) {
|
|
160
167
|
const businessId = useBusinessId();
|
|
168
|
+
const isAuthenticated = useIsAuthenticated();
|
|
161
169
|
return useQuery({
|
|
162
170
|
queryKey: FLEET_KEYS.invitations(businessId),
|
|
163
171
|
queryFn: () => fleetService.fetchInvitations(businessId),
|
|
164
|
-
enabled: enabled && Boolean(businessId),
|
|
172
|
+
enabled: enabled && Boolean(businessId) && isAuthenticated,
|
|
165
173
|
placeholderData: (prev) => prev,
|
|
166
174
|
...OFFLINE_FIRST,
|
|
167
175
|
});
|
|
@@ -182,10 +190,11 @@ export function useRevokeInvitationMutation() {
|
|
|
182
190
|
// ─── Vehicles ───
|
|
183
191
|
export function useFleetVehiclesQuery(enabled = true) {
|
|
184
192
|
const businessId = useBusinessId();
|
|
193
|
+
const isAuthenticated = useIsAuthenticated();
|
|
185
194
|
return useQuery({
|
|
186
195
|
queryKey: FLEET_KEYS.vehicles(businessId),
|
|
187
196
|
queryFn: () => fleetService.fetchFleetVehicles(businessId),
|
|
188
|
-
enabled: enabled && Boolean(businessId),
|
|
197
|
+
enabled: enabled && Boolean(businessId) && isAuthenticated,
|
|
189
198
|
placeholderData: (prev) => prev,
|
|
190
199
|
...OFFLINE_FIRST,
|
|
191
200
|
});
|
|
@@ -219,10 +228,11 @@ export function useRemoveVehicleMutation() {
|
|
|
219
228
|
// ─── Dashboard ───
|
|
220
229
|
export function useFleetDashboardQuery(enabled = true) {
|
|
221
230
|
const businessId = useBusinessId();
|
|
231
|
+
const isAuthenticated = useIsAuthenticated();
|
|
222
232
|
return useQuery({
|
|
223
233
|
queryKey: FLEET_KEYS.dashboard(businessId),
|
|
224
234
|
queryFn: () => fleetService.fetchFleetDashboard(businessId),
|
|
225
|
-
enabled: enabled && Boolean(businessId),
|
|
235
|
+
enabled: enabled && Boolean(businessId) && isAuthenticated,
|
|
226
236
|
placeholderData: (prev) => prev,
|
|
227
237
|
...OFFLINE_FIRST,
|
|
228
238
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { useSyncExternalStore } from "react";
|
|
8
8
|
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
9
|
+
import { getAuthToken, subscribeAuth } from "../adapters/auth";
|
|
9
10
|
import { businessService, rolesService, fleetService } from "../adapters/services";
|
|
10
11
|
import { assertOnlineFor } from "../sync/online-gates";
|
|
11
12
|
import { validateInviteInput } from "../validation/offline";
|
|
@@ -18,6 +19,10 @@ function useBusinessId() {
|
|
|
18
19
|
const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
|
|
19
20
|
return id ?? undefined;
|
|
20
21
|
}
|
|
22
|
+
function useIsAuthenticated() {
|
|
23
|
+
const token = useSyncExternalStore(subscribeAuth, () => getAuthToken(), () => null);
|
|
24
|
+
return !!token;
|
|
25
|
+
}
|
|
21
26
|
const OFFLINE_FIRST = { retry: false, networkMode: "offlineFirst" };
|
|
22
27
|
// ─── Keys ───
|
|
23
28
|
export const MEMBER_KEYS = {
|
|
@@ -42,10 +47,11 @@ export function useBusinessMembersQuery() {
|
|
|
42
47
|
const businessId = useBusinessId();
|
|
43
48
|
const cached = useReferenceData("members");
|
|
44
49
|
const online = useIsEffectivelyOnline();
|
|
50
|
+
const isAuthenticated = useIsAuthenticated();
|
|
45
51
|
const q = useQuery({
|
|
46
52
|
queryKey: MEMBER_KEYS.list(businessId ?? ""),
|
|
47
53
|
queryFn: () => businessService.fetchBusinessMembers(businessId),
|
|
48
|
-
enabled: Boolean(businessId) && online,
|
|
54
|
+
enabled: Boolean(businessId) && online && isAuthenticated,
|
|
49
55
|
staleTime: 30 * 1000,
|
|
50
56
|
...OFFLINE_FIRST,
|
|
51
57
|
placeholderData: cached.data?.payload,
|
|
@@ -94,12 +100,13 @@ export function useRemoveMemberMutation() {
|
|
|
94
100
|
}
|
|
95
101
|
export function useBusinessMemberInvitationsQuery() {
|
|
96
102
|
const businessId = useBusinessId();
|
|
97
|
-
// No invitations SQLite mirror — never fire while unreachable.
|
|
103
|
+
// No invitations SQLite mirror — never fire while unreachable or logged-out.
|
|
98
104
|
const online = useIsEffectivelyOnline();
|
|
105
|
+
const isAuthenticated = useIsAuthenticated();
|
|
99
106
|
return useQuery({
|
|
100
107
|
queryKey: MEMBER_KEYS.invitations(businessId ?? ""),
|
|
101
108
|
queryFn: () => businessService.fetchBusinessMemberInvitations(businessId),
|
|
102
|
-
enabled: Boolean(businessId) && online,
|
|
109
|
+
enabled: Boolean(businessId) && online && isAuthenticated,
|
|
103
110
|
...OFFLINE_FIRST,
|
|
104
111
|
});
|
|
105
112
|
}
|
|
@@ -143,12 +150,13 @@ export function useDeleteMemberInvitationMutation() {
|
|
|
143
150
|
}
|
|
144
151
|
// ─── User invitations ───
|
|
145
152
|
export function useUserInvitationsQuery() {
|
|
146
|
-
//
|
|
153
|
+
// User-scoped: requires login. Don't 401 while logged-out.
|
|
147
154
|
const online = useIsEffectivelyOnline();
|
|
155
|
+
const isAuthenticated = useIsAuthenticated();
|
|
148
156
|
return useQuery({
|
|
149
157
|
queryKey: INVITATION_KEYS.list(),
|
|
150
158
|
queryFn: () => businessService.fetchUserInvitations(),
|
|
151
|
-
enabled: online,
|
|
159
|
+
enabled: online && isAuthenticated,
|
|
152
160
|
staleTime: 15000,
|
|
153
161
|
...OFFLINE_FIRST,
|
|
154
162
|
});
|
|
@@ -206,11 +214,12 @@ export function useBusinessRolesQuery(enabled = true) {
|
|
|
206
214
|
const businessId = useBusinessId();
|
|
207
215
|
const cached = useReferenceData("roles");
|
|
208
216
|
const online = useIsEffectivelyOnline();
|
|
217
|
+
const isAuthenticated = useIsAuthenticated();
|
|
209
218
|
const q = useQuery({
|
|
210
219
|
queryKey: ROLES_KEYS.forBusiness(businessId),
|
|
211
220
|
queryFn: () => rolesService.fetchBusinessRoles(),
|
|
212
221
|
staleTime: Infinity,
|
|
213
|
-
enabled: enabled && !!businessId && online,
|
|
222
|
+
enabled: enabled && !!businessId && online && isAuthenticated,
|
|
214
223
|
...OFFLINE_FIRST,
|
|
215
224
|
placeholderData: cached.data?.payload,
|
|
216
225
|
});
|
|
@@ -221,10 +230,11 @@ export function useBusinessTaxRulesQuery(enabled = true) {
|
|
|
221
230
|
const businessId = useBusinessId();
|
|
222
231
|
const cached = useReferenceData("tax-rules");
|
|
223
232
|
const online = useIsEffectivelyOnline();
|
|
233
|
+
const isAuthenticated = useIsAuthenticated();
|
|
224
234
|
const q = useQuery({
|
|
225
235
|
queryKey: TAX_KEYS.list(businessId),
|
|
226
236
|
queryFn: () => businessService.fetchBusinessTaxRules(businessId),
|
|
227
|
-
enabled: enabled && Boolean(businessId) && online,
|
|
237
|
+
enabled: enabled && Boolean(businessId) && online && isAuthenticated,
|
|
228
238
|
staleTime: 30 * 1000,
|
|
229
239
|
...OFFLINE_FIRST,
|
|
230
240
|
placeholderData: cached.data?.payload,
|
package/dist/index.d.ts
CHANGED
|
@@ -136,6 +136,7 @@ export { getSyncJobsPendingCount, debugDumpSyncJobs, drainPending } from "./sync
|
|
|
136
136
|
export { performFullSync } from "./sync/full-sync";
|
|
137
137
|
export { stopHealthProbe } from "./sync/health-probe";
|
|
138
138
|
export { notifyAuthChanged, subscribeAuth } from "./adapters/auth";
|
|
139
|
+
export { isAuthenticated, hasActiveBusiness, canBusinessSync } from "./sync/sync-guard";
|
|
139
140
|
export { ACCESS_EXPIRED_CODE, SESSION_DEAD_CODES, SKIP_AUTH_RETRY, extractAuthFailure, isAccessExpired, isSessionDead, hasAuthRetrySkip, refreshSession, withAuthRetry, } from "./auth/session";
|
|
140
141
|
export type { AuthFailure, SessionTokens } from "./auth/session";
|
|
141
142
|
export { getCachedImagePath, ensureImageCacheDir, getCachedImageInfo, cacheRemoteImage, cacheImageBatch, clearImageCache, pruneImageCache, resolveImageSource, stableCacheUrl } from "./image-cache/service";
|
package/dist/index.js
CHANGED
|
@@ -151,6 +151,7 @@ export { getSyncJobsPendingCount, debugDumpSyncJobs, drainPending } from "./sync
|
|
|
151
151
|
export { performFullSync } from "./sync/full-sync";
|
|
152
152
|
export { stopHealthProbe } from "./sync/health-probe";
|
|
153
153
|
export { notifyAuthChanged, subscribeAuth } from "./adapters/auth";
|
|
154
|
+
export { isAuthenticated, hasActiveBusiness, canBusinessSync } from "./sync/sync-guard";
|
|
154
155
|
// ── Session lifecycle (access-expired → refresh → refresh-expired → logout)
|
|
155
156
|
export { ACCESS_EXPIRED_CODE, SESSION_DEAD_CODES, SKIP_AUTH_RETRY, extractAuthFailure, isAccessExpired, isSessionDead, hasAuthRetrySkip, refreshSession, withAuthRetry, } from "./auth/session";
|
|
156
157
|
// ── Image cache (durable) ──
|
|
@@ -13,6 +13,7 @@ import { generateUUID } from "../utils/uuid";
|
|
|
13
13
|
import { menuService, businessService, rolesService } from "../adapters/services";
|
|
14
14
|
import { cacheImageBatch, pruneImageCache } from "../adapters/image-cache";
|
|
15
15
|
import { getConnectivityState } from "../adapters/connectivity";
|
|
16
|
+
import { canBusinessSync } from "../sync/sync-guard";
|
|
16
17
|
import { getReferenceData, getReferenceVersion, mergeReferenceData, } from "./operations";
|
|
17
18
|
import { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, } from "./api";
|
|
18
19
|
const DEFAULT_SYNC_KEYS = [
|
|
@@ -33,6 +34,17 @@ class ReferenceSyncService {
|
|
|
33
34
|
this.isSyncing = false;
|
|
34
35
|
}
|
|
35
36
|
async sync(options) {
|
|
37
|
+
if (!canBusinessSync(options.businessId)) {
|
|
38
|
+
syncLogger.debug("ReferenceSync", "Skipping sync — no business/auth", {
|
|
39
|
+
businessId: options.businessId,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
synced: [],
|
|
43
|
+
skipped: options.keys ?? DEFAULT_SYNC_KEYS,
|
|
44
|
+
failed: [],
|
|
45
|
+
errors: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
36
48
|
if (this.isSyncing) {
|
|
37
49
|
return {
|
|
38
50
|
synced: [],
|
|
@@ -8,6 +8,9 @@ import { useAuthStore } from "../adapters/auth";
|
|
|
8
8
|
import { getAppInstanceId } from "../adapters/devices";
|
|
9
9
|
import { generateCommandId } from "../utils/uuid";
|
|
10
10
|
export function buildCommand(options) {
|
|
11
|
+
if (!options.businessId) {
|
|
12
|
+
throw new Error("buildCommand requires businessId — no active business/auth");
|
|
13
|
+
}
|
|
11
14
|
const user = useAuthStore.getState().user;
|
|
12
15
|
const userId = user?.id ?? "anonymous";
|
|
13
16
|
const deviceId = getAppInstanceId();
|
|
@@ -15,6 +15,7 @@ import { recordAttempt } from "../utils/attempt-stats";
|
|
|
15
15
|
import { generateEventId } from "../utils/uuid";
|
|
16
16
|
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrderNumber, } from "./order-number";
|
|
17
17
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
18
|
+
import { canBusinessSync } from "./sync-guard";
|
|
18
19
|
const MAX_RETRIES = 5;
|
|
19
20
|
const QUEUED_STATUS_BY_COMMAND = {
|
|
20
21
|
"order:accept": "accepted",
|
|
@@ -186,6 +187,8 @@ class CommandQueueService {
|
|
|
186
187
|
try {
|
|
187
188
|
subscribeConnectivity(() => {
|
|
188
189
|
const s = getConnectivityState();
|
|
190
|
+
if (!canBusinessSync())
|
|
191
|
+
return;
|
|
189
192
|
if (s.isCloudReachable === "reachable" && s.isOnline) {
|
|
190
193
|
this.replay().catch(() => { });
|
|
191
194
|
}
|
|
@@ -234,6 +237,10 @@ class CommandQueueService {
|
|
|
234
237
|
* Try to execute a single pending command through the current transport.
|
|
235
238
|
*/
|
|
236
239
|
async processCommand(command) {
|
|
240
|
+
if (!canBusinessSync(command.business_id)) {
|
|
241
|
+
// No active business/session — defer until login+selection.
|
|
242
|
+
throw new Error("ReplayDeferred: no business/auth, deferring queued command");
|
|
243
|
+
}
|
|
237
244
|
const db = getSyncDatabase();
|
|
238
245
|
const parsedCommand = {
|
|
239
246
|
command_id: command.command_id,
|
|
@@ -348,6 +355,8 @@ class CommandQueueService {
|
|
|
348
355
|
* ordering consistent; the next replay will retry from the failed command.
|
|
349
356
|
*/
|
|
350
357
|
async replay() {
|
|
358
|
+
if (!canBusinessSync())
|
|
359
|
+
return;
|
|
351
360
|
recordAttempt("sync_drain", "replay");
|
|
352
361
|
// Join an in-flight replay instead of no-op'ing: concurrent replays
|
|
353
362
|
// (connectivity auto-replay racing an explicit replay) must converge on
|
package/dist/sync/engine.js
CHANGED
|
@@ -21,6 +21,7 @@ import { CLOUD_COMMAND_TIMEOUT_MS } from "../utils/constants";
|
|
|
21
21
|
import { CAPABILITY_BY_COMMAND } from "../reference/capabilities";
|
|
22
22
|
import { getReferenceData } from "../reference/operations";
|
|
23
23
|
import { openSyncDatabase } from "../db/connection";
|
|
24
|
+
import { canBusinessSync } from "./sync-guard";
|
|
24
25
|
function withTimeout(promise, ms) {
|
|
25
26
|
return new Promise((resolve, reject) => {
|
|
26
27
|
const timer = setTimeout(() => {
|
|
@@ -53,6 +54,10 @@ export class SyncEngine {
|
|
|
53
54
|
* quickly instead of blocking the terminal for the full HTTP timeout.
|
|
54
55
|
*/
|
|
55
56
|
async execute(command, opts) {
|
|
57
|
+
if (!command.business_id || !canBusinessSync(command.business_id)) {
|
|
58
|
+
// No active business/auth — don't route to cloud/LAN or queue with empty bid.
|
|
59
|
+
throw new Error("Sync requires active business and login — business_id missing or not authenticated");
|
|
60
|
+
}
|
|
56
61
|
syncLogger.command(command, "issued");
|
|
57
62
|
// Perms guard (offline-capable): block before routing
|
|
58
63
|
const required = CAPABILITY_BY_COMMAND[command.command_type];
|
package/dist/sync/full-sync.js
CHANGED
|
@@ -4,21 +4,19 @@
|
|
|
4
4
|
* overwrites them; then pull merges; final drain pushes any remainder.
|
|
5
5
|
*/
|
|
6
6
|
import { getConnectivityState } from "../adapters/connectivity";
|
|
7
|
-
import { getAuthToken } from "../adapters/auth";
|
|
8
7
|
import { getActiveBusiness } from "../adapters/business";
|
|
9
8
|
import { syncLogger } from "../utils/sync-logger";
|
|
10
9
|
import { referenceSyncService } from "../reference/service";
|
|
11
10
|
import { pullRemoteOrders, drainPending } from "./sync-jobs.service";
|
|
11
|
+
import { canBusinessSync } from "./sync-guard";
|
|
12
12
|
let inFlight = null;
|
|
13
13
|
export async function performFullSync(businessId) {
|
|
14
14
|
const bid = businessId ?? getActiveBusiness()?.id;
|
|
15
|
-
if (!bid)
|
|
15
|
+
if (!bid || !canBusinessSync(bid))
|
|
16
16
|
return;
|
|
17
17
|
const s = getConnectivityState();
|
|
18
18
|
if (!s.isOnline || s.isCloudReachable === "unreachable")
|
|
19
19
|
return;
|
|
20
|
-
if (!getAuthToken())
|
|
21
|
-
return;
|
|
22
20
|
if (inFlight) {
|
|
23
21
|
try {
|
|
24
22
|
await inFlight;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Guard — single predicate for business-scoped sync.
|
|
3
|
+
*
|
|
4
|
+
* Option C (Hybrid): health probe stays `isOnline`-only (connectivity UX
|
|
5
|
+
* while logged-out). Every *business* sync path gates on both auth *and*
|
|
6
|
+
* active business: no drain, pull, replay, or reference fetch fires until
|
|
7
|
+
* the user has logged in and a business is selected. User-scoped queries
|
|
8
|
+
* (invitations, fleet) gate on auth only.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isAuthenticated(): boolean;
|
|
11
|
+
export declare function hasActiveBusiness(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Business sync may run only when both authenticated and a business is selected.
|
|
14
|
+
* Pass an explicit `businessId` to test that specific id; otherwise reads
|
|
15
|
+
* the active business store.
|
|
16
|
+
*/
|
|
17
|
+
export declare function canBusinessSync(businessId?: string | null): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Guard — single predicate for business-scoped sync.
|
|
3
|
+
*
|
|
4
|
+
* Option C (Hybrid): health probe stays `isOnline`-only (connectivity UX
|
|
5
|
+
* while logged-out). Every *business* sync path gates on both auth *and*
|
|
6
|
+
* active business: no drain, pull, replay, or reference fetch fires until
|
|
7
|
+
* the user has logged in and a business is selected. User-scoped queries
|
|
8
|
+
* (invitations, fleet) gate on auth only.
|
|
9
|
+
*/
|
|
10
|
+
import { getAuthToken } from "../adapters/auth";
|
|
11
|
+
import { getActiveBusiness } from "../adapters/business";
|
|
12
|
+
export function isAuthenticated() {
|
|
13
|
+
return !!getAuthToken();
|
|
14
|
+
}
|
|
15
|
+
export function hasActiveBusiness() {
|
|
16
|
+
return !!getActiveBusiness()?.id;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Business sync may run only when both authenticated and a business is selected.
|
|
20
|
+
* Pass an explicit `businessId` to test that specific id; otherwise reads
|
|
21
|
+
* the active business store.
|
|
22
|
+
*/
|
|
23
|
+
export function canBusinessSync(businessId) {
|
|
24
|
+
const bid = businessId ?? getActiveBusiness()?.id;
|
|
25
|
+
return !!bid && !!getAuthToken();
|
|
26
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { openSyncDatabase } from "../db/connection";
|
|
7
7
|
import { insertOrder } from "../db/operations";
|
|
8
8
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
9
|
-
import {
|
|
9
|
+
import { getRefreshToken, setAuthTokens, clearAuth, subscribeAuth } from "../adapters/auth";
|
|
10
10
|
import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
|
|
11
11
|
import { getHttpClient } from "../adapters/http";
|
|
12
12
|
import { phoenixSocket } from "../realtime/phoenix-socket";
|
|
@@ -16,6 +16,7 @@ import { BackoffTracker } from "./backoff";
|
|
|
16
16
|
import { generateOrderNumber } from "./order-number";
|
|
17
17
|
import { orderSnapshotsService } from "./order-snapshots.service";
|
|
18
18
|
import { getTransitionPath } from "./transition-endpoints";
|
|
19
|
+
import { canBusinessSync, isAuthenticated } from "./sync-guard";
|
|
19
20
|
const inflight = new Map();
|
|
20
21
|
const retryTimers = new Map();
|
|
21
22
|
const backoffTracker = new BackoffTracker();
|
|
@@ -33,6 +34,8 @@ function isOnlineForPhx() {
|
|
|
33
34
|
return s.isOnline && s.isCloudReachable === "reachable";
|
|
34
35
|
}
|
|
35
36
|
function scheduleRetry(businessId) {
|
|
37
|
+
if (!canBusinessSync(businessId))
|
|
38
|
+
return;
|
|
36
39
|
if (retryTimers.has(businessId))
|
|
37
40
|
return;
|
|
38
41
|
// Shared backoff curve: 2s doubling to a 5min cap with jitter. Attempts
|
|
@@ -43,7 +46,7 @@ function scheduleRetry(businessId) {
|
|
|
43
46
|
syncLogger.debug("SyncJobs", "scheduleRetry backoff", { businessId, delay });
|
|
44
47
|
const t = setTimeout(() => {
|
|
45
48
|
retryTimers.delete(businessId);
|
|
46
|
-
if (isOnlineForRest() &&
|
|
49
|
+
if (isOnlineForRest() && canBusinessSync(businessId)) {
|
|
47
50
|
syncLogger.debug("SyncJobs", "retry timer fired → drainPending", { businessId });
|
|
48
51
|
void drainPending(businessId);
|
|
49
52
|
}
|
|
@@ -184,14 +187,17 @@ async function tryRest(businessId, payload, commandType) {
|
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
export async function drainOne(businessId, entityId) {
|
|
190
|
+
if (!businessId || !canBusinessSync(businessId)) {
|
|
191
|
+
syncLogger.debug("SyncJobs", "drainOne early-return — no business/auth", { businessId, entityId });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
187
194
|
recordAttempt("sync_drain", `drainOne ${entityId.slice(0, 8)}`);
|
|
188
195
|
const onlineForRest = isOnlineForRest();
|
|
189
196
|
const onlineForPhx = isOnlineForPhx();
|
|
190
|
-
const token = getAuthToken();
|
|
191
197
|
const state = getConnectivityState();
|
|
192
|
-
syncLogger.debug("SyncJobs", "drainOne entry", { businessId, entityId, isOnlineForRest: onlineForRest, isOnlineForPhx: onlineForPhx, isCloudReachable: state.isCloudReachable, hasToken:
|
|
193
|
-
if (!onlineForRest
|
|
194
|
-
syncLogger.debug("SyncJobs", "drainOne early-return gate", { businessId, entityId, onlineForRest, hasToken:
|
|
198
|
+
syncLogger.debug("SyncJobs", "drainOne entry", { businessId, entityId, isOnlineForRest: onlineForRest, isOnlineForPhx: onlineForPhx, isCloudReachable: state.isCloudReachable, hasToken: isAuthenticated(), inflight: inflight.has(`${businessId}:${entityId}`) });
|
|
199
|
+
if (!onlineForRest) {
|
|
200
|
+
syncLogger.debug("SyncJobs", "drainOne early-return gate", { businessId, entityId, onlineForRest, hasToken: isAuthenticated(), connectivity: state });
|
|
195
201
|
return;
|
|
196
202
|
}
|
|
197
203
|
const key = `${businessId}:${entityId}`;
|
|
@@ -385,11 +391,14 @@ export async function drainOne(businessId, entityId) {
|
|
|
385
391
|
}
|
|
386
392
|
}
|
|
387
393
|
export async function pullRemoteOrders(businessId) {
|
|
394
|
+
if (!businessId || !canBusinessSync(businessId)) {
|
|
395
|
+
syncLogger.debug("SyncJobs", "pullRemoteOrders early-return — no business/auth", { businessId });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
388
398
|
const onlineForRest = isOnlineForRest();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
syncLogger.debug("SyncJobs", "pullRemoteOrders early-return gate", { businessId, onlineForRest, hasToken, connectivity: getConnectivityState() });
|
|
399
|
+
syncLogger.debug("SyncJobs", "pullRemoteOrders entry", { businessId, onlineForRest, hasToken: isAuthenticated() });
|
|
400
|
+
if (!onlineForRest) {
|
|
401
|
+
syncLogger.debug("SyncJobs", "pullRemoteOrders early-return gate", { businessId, onlineForRest, hasToken: isAuthenticated(), connectivity: getConnectivityState() });
|
|
393
402
|
return;
|
|
394
403
|
}
|
|
395
404
|
try {
|
|
@@ -450,12 +459,15 @@ export async function pullRemoteOrders(businessId) {
|
|
|
450
459
|
}
|
|
451
460
|
}
|
|
452
461
|
export async function drainPending(businessId) {
|
|
462
|
+
if (!businessId || !canBusinessSync(businessId)) {
|
|
463
|
+
syncLogger.debug("SyncJobs", "drainPending early-return — no business/auth", { businessId });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
453
466
|
recordAttempt("sync_drain", "drainPending");
|
|
454
467
|
const onlineForRest = isOnlineForRest();
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
syncLogger.debug("SyncJobs", "drainPending early-return gate", { businessId, onlineForRest, hasToken });
|
|
468
|
+
syncLogger.debug("SyncJobs", "drainPending entry", { businessId, onlineForRest, hasToken: isAuthenticated(), connectivity: getConnectivityState() });
|
|
469
|
+
if (!onlineForRest) {
|
|
470
|
+
syncLogger.debug("SyncJobs", "drainPending early-return gate", { businessId, onlineForRest, hasToken: isAuthenticated() });
|
|
459
471
|
return;
|
|
460
472
|
}
|
|
461
473
|
const db = await openSyncDatabase();
|
|
@@ -486,30 +498,30 @@ export function setupSyncJobsDrain() {
|
|
|
486
498
|
return;
|
|
487
499
|
}
|
|
488
500
|
drainSetup = true;
|
|
489
|
-
syncLogger.debug("SyncJobs", "setupSyncJobsDrain init", { hasBusiness: !!getActiveBusiness()?.id, connectivity: getConnectivityState(), hasToken:
|
|
501
|
+
syncLogger.debug("SyncJobs", "setupSyncJobsDrain init", { hasBusiness: !!getActiveBusiness()?.id, connectivity: getConnectivityState(), hasToken: isAuthenticated() });
|
|
490
502
|
const triggerFullSync = (b) => {
|
|
491
|
-
if (!b || !isOnlineForRest() || !
|
|
503
|
+
if (!b || !isOnlineForRest() || !canBusinessSync(b))
|
|
492
504
|
return;
|
|
493
505
|
void import("./full-sync").then((m) => m.performFullSync(b).catch(() => { }));
|
|
494
506
|
};
|
|
495
507
|
try {
|
|
496
508
|
subscribeConnectivity(() => {
|
|
497
509
|
const b = getActiveBusiness()?.id;
|
|
498
|
-
syncLogger.debug("SyncJobs", "subscribeConnectivity trigger", { businessId: b, connectivity: getConnectivityState(), hasToken:
|
|
510
|
+
syncLogger.debug("SyncJobs", "subscribeConnectivity trigger", { businessId: b, connectivity: getConnectivityState(), hasToken: isAuthenticated() });
|
|
499
511
|
triggerFullSync(b);
|
|
500
512
|
});
|
|
501
513
|
subscribeBusiness(() => {
|
|
502
514
|
const bid = getActiveBusiness()?.id;
|
|
503
|
-
syncLogger.debug("SyncJobs", "subscribeBusiness trigger", { businessId: bid, hasToken:
|
|
515
|
+
syncLogger.debug("SyncJobs", "subscribeBusiness trigger", { businessId: bid, hasToken: isAuthenticated(), connectivity: getConnectivityState() });
|
|
504
516
|
triggerFullSync(bid);
|
|
505
517
|
});
|
|
506
518
|
subscribeAuth(() => {
|
|
507
519
|
const b = getActiveBusiness()?.id;
|
|
508
|
-
syncLogger.debug("SyncJobs", "subscribeAuth trigger", { businessId: b, hasToken:
|
|
520
|
+
syncLogger.debug("SyncJobs", "subscribeAuth trigger", { businessId: b, hasToken: isAuthenticated(), connectivity: getConnectivityState() });
|
|
509
521
|
triggerFullSync(b);
|
|
510
522
|
});
|
|
511
523
|
const b0 = getActiveBusiness()?.id;
|
|
512
|
-
syncLogger.debug("SyncJobs", "setupSyncJobsDrain initial check", { b0, isOnlineForRest: isOnlineForRest(), hasToken:
|
|
524
|
+
syncLogger.debug("SyncJobs", "setupSyncJobsDrain initial check", { b0, isOnlineForRest: isOnlineForRest(), hasToken: isAuthenticated() });
|
|
513
525
|
triggerFullSync(b0);
|
|
514
526
|
}
|
|
515
527
|
catch (e) {
|
|
@@ -22,6 +22,8 @@ export const PIPELINE = ["pending", "accepted", "preparing", "ready", "delivered
|
|
|
22
22
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
23
23
|
export function isUnpaid(order) {
|
|
24
24
|
const o = order;
|
|
25
|
+
if (o.pos_payment_method === "cash")
|
|
26
|
+
return false;
|
|
25
27
|
return !o.is_paid && !o.paid_at && !o.payment?.is_paid;
|
|
26
28
|
}
|
|
27
29
|
export function isCancellable(status) {
|
package/package.json
CHANGED