@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.
@@ -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() && getAuthToken()) {
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
- // No invitations SQLite mirror — never fire while unreachable.
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
@@ -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];
@@ -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 { getAuthToken, getRefreshToken, setAuthTokens, clearAuth, subscribeAuth } from "../adapters/auth";
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() && getAuthToken() && getActiveBusiness()?.id === businessId) {
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: !!token, inflight: inflight.has(`${businessId}:${entityId}`) });
193
- if (!onlineForRest || !token) {
194
- syncLogger.debug("SyncJobs", "drainOne early-return gate", { businessId, entityId, onlineForRest, hasToken: !!token, connectivity: state });
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
- const hasToken = !!getAuthToken();
390
- syncLogger.debug("SyncJobs", "pullRemoteOrders entry", { businessId, onlineForRest, hasToken });
391
- if (!onlineForRest || !hasToken) {
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
- const hasToken = !!getAuthToken();
456
- syncLogger.debug("SyncJobs", "drainPending entry", { businessId, onlineForRest, hasToken, connectivity: getConnectivityState() });
457
- if (!onlineForRest || !hasToken) {
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: !!getAuthToken() });
501
+ syncLogger.debug("SyncJobs", "setupSyncJobsDrain init", { hasBusiness: !!getActiveBusiness()?.id, connectivity: getConnectivityState(), hasToken: isAuthenticated() });
490
502
  const triggerFullSync = (b) => {
491
- if (!b || !isOnlineForRest() || !getAuthToken())
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: !!getAuthToken() });
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: !!getAuthToken(), connectivity: getConnectivityState() });
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: !!getAuthToken(), connectivity: getConnectivityState() });
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: !!getAuthToken() });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",