@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.
Files changed (191) hide show
  1. package/README.md +26 -21
  2. package/dist/adapters/auth.d.ts +8 -0
  3. package/dist/adapters/auth.js +36 -0
  4. package/dist/adapters/devices.d.ts +3 -1
  5. package/dist/adapters/devices.js +187 -3
  6. package/dist/adapters/filesystem.d.ts +4 -0
  7. package/dist/adapters/kv.d.ts +10 -0
  8. package/dist/adapters/kv.js +23 -7
  9. package/dist/adapters/local-node.d.ts +4 -6
  10. package/dist/adapters/local-node.js +9 -32
  11. package/dist/adapters/notify.d.ts +12 -0
  12. package/dist/adapters/notify.js +33 -0
  13. package/dist/adapters/query-keys.d.ts +4 -0
  14. package/dist/adapters/query-keys.js +4 -0
  15. package/dist/adapters/services.d.ts +96 -15
  16. package/dist/adapters/services.js +241 -4
  17. package/dist/auth/session.d.ts +66 -0
  18. package/dist/auth/session.js +172 -0
  19. package/dist/catalog/hooks.d.ts +10 -0
  20. package/dist/catalog/hooks.js +82 -11
  21. package/dist/db/migrations.js +274 -0
  22. package/dist/db/operations.d.ts +53 -2
  23. package/dist/db/operations.js +177 -8
  24. package/dist/domain/business/index.d.ts +5 -0
  25. package/dist/domain/business/index.js +5 -0
  26. package/dist/domain/orders/lifecycle.d.ts +6 -0
  27. package/dist/domain/orders/lifecycle.js +6 -0
  28. package/dist/domain/orders/service.d.ts +24 -0
  29. package/dist/domain/orders/service.js +242 -0
  30. package/dist/domain/payments/index.d.ts +6 -0
  31. package/dist/domain/payments/index.js +5 -0
  32. package/dist/domain/reference/index.d.ts +8 -0
  33. package/dist/domain/reference/index.js +8 -0
  34. package/dist/e2e/harness.d.ts +1 -0
  35. package/dist/e2e/harness.js +61 -0
  36. package/dist/hooks/analytics.hooks.d.ts +25 -0
  37. package/dist/hooks/analytics.hooks.js +214 -0
  38. package/dist/hooks/business-hours.hooks.d.ts +8 -0
  39. package/dist/hooks/business-hours.hooks.js +45 -0
  40. package/dist/hooks/dashboard.hooks.d.ts +30 -0
  41. package/dist/hooks/dashboard.hooks.js +132 -0
  42. package/dist/hooks/delivery.hooks.d.ts +225 -0
  43. package/dist/hooks/delivery.hooks.js +80 -0
  44. package/dist/hooks/fleet.hooks.d.ts +37 -0
  45. package/dist/hooks/fleet.hooks.js +229 -0
  46. package/dist/hooks/image.hooks.d.ts +20 -0
  47. package/dist/hooks/image.hooks.js +62 -0
  48. package/dist/hooks/kiosk.hooks.d.ts +22 -5
  49. package/dist/hooks/kiosk.hooks.js +74 -5
  50. package/dist/hooks/members.hooks.d.ts +48 -0
  51. package/dist/hooks/members.hooks.js +302 -0
  52. package/dist/hooks/offline-orders.hooks.d.ts +9 -0
  53. package/dist/hooks/offline-orders.hooks.js +51 -0
  54. package/dist/hooks/orders.hooks.d.ts +18 -39
  55. package/dist/hooks/orders.hooks.js +124 -343
  56. package/dist/hooks/places.hooks.d.ts +21 -0
  57. package/dist/hooks/places.hooks.js +112 -0
  58. package/dist/hooks/realtime-stubs.d.ts +28 -0
  59. package/dist/hooks/realtime-stubs.js +85 -0
  60. package/dist/hooks/rider.hooks.d.ts +470 -0
  61. package/dist/hooks/rider.hooks.js +373 -0
  62. package/dist/hooks/use-infinite-list.d.ts +243 -0
  63. package/dist/hooks/use-infinite-list.js +58 -0
  64. package/dist/hooks/wallet.hooks.d.ts +219 -0
  65. package/dist/hooks/wallet.hooks.js +56 -0
  66. package/dist/image-cache/service.d.ts +8 -0
  67. package/dist/image-cache/service.js +15 -3
  68. package/dist/index.d.ts +56 -6
  69. package/dist/index.js +73 -6
  70. package/dist/local-node/adapters.d.ts +87 -0
  71. package/dist/local-node/adapters.js +4 -0
  72. package/dist/local-node/index.d.ts +5 -0
  73. package/dist/local-node/index.js +5 -0
  74. package/dist/local-node/security.d.ts +25 -0
  75. package/dist/local-node/security.js +149 -0
  76. package/dist/local-node/service.d.ts +83 -0
  77. package/dist/local-node/service.js +706 -0
  78. package/dist/local-node/store.d.ts +50 -0
  79. package/dist/local-node/store.js +284 -0
  80. package/dist/local-node/types.d.ts +92 -0
  81. package/dist/local-node/types.js +4 -0
  82. package/dist/printer/receipt.d.ts +4 -0
  83. package/dist/printer/receipt.js +75 -0
  84. package/dist/printer/service.js +1 -1
  85. package/dist/providers/query-client.d.ts +13 -0
  86. package/dist/providers/query-client.js +58 -0
  87. package/dist/realtime/phoenix-socket.js +30 -1
  88. package/dist/realtime/useEventsConnection.js +11 -2
  89. package/dist/reference/api.d.ts +2 -0
  90. package/dist/reference/api.js +2 -0
  91. package/dist/reference/capabilities.d.ts +12 -7
  92. package/dist/reference/capabilities.js +27 -13
  93. package/dist/reference/hooks.d.ts +34 -0
  94. package/dist/reference/hooks.js +124 -0
  95. package/dist/reference/menu-apply.d.ts +29 -0
  96. package/dist/reference/menu-apply.js +289 -0
  97. package/dist/reference/menu-record.d.ts +69 -0
  98. package/dist/reference/menu-record.js +163 -0
  99. package/dist/reference/operations.d.ts +3 -0
  100. package/dist/reference/operations.js +35 -1
  101. package/dist/reference/service.js +36 -9
  102. package/dist/reference/types.d.ts +1 -1
  103. package/dist/repositories/order.repository.d.ts +7 -0
  104. package/dist/repositories/order.repository.js +15 -2
  105. package/dist/repositories/pos.repository.d.ts +4 -0
  106. package/dist/repositories/pos.repository.js +4 -0
  107. package/dist/sync/backoff.d.ts +21 -0
  108. package/dist/sync/backoff.js +42 -0
  109. package/dist/sync/command-builder.js +10 -2
  110. package/dist/sync/command-queue.service.d.ts +12 -0
  111. package/dist/sync/command-queue.service.js +81 -16
  112. package/dist/sync/device-lease.service.d.ts +3 -3
  113. package/dist/sync/device-lease.service.js +80 -20
  114. package/dist/sync/engine.d.ts +3 -1
  115. package/dist/sync/engine.js +44 -4
  116. package/dist/sync/full-sync.d.ts +6 -0
  117. package/dist/sync/full-sync.js +72 -0
  118. package/dist/sync/health-probe.d.ts +24 -0
  119. package/dist/sync/health-probe.js +138 -0
  120. package/dist/sync/offline-capable.js +25 -0
  121. package/dist/sync/online-gates.d.ts +17 -0
  122. package/dist/sync/online-gates.js +55 -0
  123. package/dist/sync/order-number.d.ts +4 -4
  124. package/dist/sync/order-number.js +33 -13
  125. package/dist/sync/order-snapshots.service.js +18 -3
  126. package/dist/sync/pending-orders.service.d.ts +18 -0
  127. package/dist/sync/pending-orders.service.js +67 -1
  128. package/dist/sync/reconciliation/index.d.ts +5 -0
  129. package/dist/sync/reconciliation/index.js +5 -0
  130. package/dist/sync/snapshots/entity.d.ts +6 -0
  131. package/dist/sync/snapshots/entity.js +6 -0
  132. package/dist/sync/sync-jobs.service.d.ts +18 -0
  133. package/dist/sync/sync-jobs.service.js +539 -0
  134. package/dist/sync/transition-endpoints.d.ts +11 -0
  135. package/dist/sync/transition-endpoints.js +30 -0
  136. package/dist/sync/transport-local-node.js +3 -2
  137. package/dist/sync/transport-rest.d.ts +7 -0
  138. package/dist/sync/transport-rest.js +84 -26
  139. package/dist/sync/use-sync-reconciliation.js +15 -1
  140. package/dist/test-helpers/memory-driver.js +182 -4
  141. package/dist/test-helpers/seed-hours.d.ts +1 -0
  142. package/dist/test-helpers/seed-hours.js +23 -0
  143. package/dist/types/api/business.api.d.ts +42 -0
  144. package/dist/types/api/menu.api.d.ts +15 -0
  145. package/dist/types/api/seller.api.d.ts +20 -0
  146. package/dist/types/places.d.ts +19 -0
  147. package/dist/types/places.js +2 -0
  148. package/dist/types/rider.d.ts +67 -0
  149. package/dist/types/rider.js +2 -0
  150. package/dist/utils/attempt-stats.d.ts +23 -0
  151. package/dist/utils/attempt-stats.js +61 -0
  152. package/dist/utils/business-hours.d.ts +37 -0
  153. package/dist/utils/business-hours.js +96 -0
  154. package/dist/utils/debug-log.d.ts +23 -0
  155. package/dist/utils/debug-log.js +153 -0
  156. package/dist/utils/order-filter.d.ts +1 -1
  157. package/dist/utils/order-filter.js +1 -1
  158. package/dist/utils/order-transitions.d.ts +30 -0
  159. package/dist/utils/order-transitions.js +185 -0
  160. package/dist/utils/sync-logger.js +5 -4
  161. package/dist/validation/offline.d.ts +46 -0
  162. package/dist/validation/offline.js +79 -0
  163. package/package.json +6 -1
  164. package/dist/pricing/engine.test.d.ts +0 -1
  165. package/dist/pricing/engine.test.js +0 -142
  166. package/dist/reference/lww.test.d.ts +0 -1
  167. package/dist/reference/lww.test.js +0 -23
  168. package/dist/reference/operations.test.d.ts +0 -1
  169. package/dist/reference/operations.test.js +0 -66
  170. package/dist/standalone.integration.test.d.ts +0 -1
  171. package/dist/standalone.integration.test.js +0 -235
  172. package/dist/sync/device-lease.test.d.ts +0 -1
  173. package/dist/sync/device-lease.test.js +0 -38
  174. package/dist/sync/offline-integration.test.d.ts +0 -1
  175. package/dist/sync/offline-integration.test.js +0 -52
  176. package/dist/sync/order-number.test.d.ts +0 -1
  177. package/dist/sync/order-number.test.js +0 -51
  178. package/dist/sync/perms.test.d.ts +0 -1
  179. package/dist/sync/perms.test.js +0 -15
  180. package/dist/utils/currency.test.d.ts +0 -1
  181. package/dist/utils/currency.test.js +0 -17
  182. package/dist/utils/hlc.test.d.ts +0 -1
  183. package/dist/utils/hlc.test.js +0 -29
  184. package/dist/utils/phone.test.d.ts +0 -1
  185. package/dist/utils/phone.test.js +0 -19
  186. package/dist/utils/responsive.d.ts +0 -13
  187. package/dist/utils/responsive.js +0 -18
  188. package/dist/utils/uuid.test.d.ts +0 -1
  189. package/dist/utils/uuid.test.js +0 -13
  190. package/dist/utils/validation.test.d.ts +0 -1
  191. package/dist/utils/validation.test.js +0 -23
@@ -1,340 +1,149 @@
1
1
  /**
2
- * Order lifecycle hooks — offline-first, device-owned outbox.
3
- *
4
- * Device owns its partition: when offline it serves the durable
5
- * entity_snapshots + pending overlay immediately; the SyncEngine replays
6
- * queued commands when back online. No hook ever blocks on a cloud timeout
7
- * when offline.
8
- *
9
- * Host apps just re-export from "@teincfood/core" — no app-local duplication.
2
+ * Order hooks — SQLite is real DB (no pending overlay, no fetchAll).
3
+ * Apps just import from "@teincfood/core".
10
4
  */
11
- import { useEffect, useMemo, useSyncExternalStore } from "react";
5
+ import { useEffect } from "react";
12
6
  import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
7
+ import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
8
+ import { useSyncExternalStore } from "react";
9
+ import { listLocalOrders, getLocalOrder } from "../domain/orders/service";
10
+ import { getDeviceNumber, ensureLease } from "../sync/device-lease.service";
11
+ import { assertOnlineFor } from "../sync/online-gates";
12
+ import { SELLER_DASHBOARD_KEYS } from "../adapters/query-keys";
13
+ import { openSyncDatabase } from "../db/connection";
14
+ import { getReferenceData } from "../reference/operations";
15
+ import { calculatePricing } from "../pricing/engine";
16
+ import { generateUUID } from "../utils/uuid";
17
+ import { subscribePendingOrders, getPendingOrdersSnapshot } from "../adapters/pending-orders";
18
+ import { hydratePendingOrders, mergePendingIntoList } from "../sync/pending-orders.service";
13
19
  import { posRepository } from "../repositories/pos.repository";
14
20
  import { orderRepository } from "../repositories/order.repository";
15
- import { sellerService } from "../adapters/services";
16
- import { getActiveBusiness, subscribeBusiness } from "../adapters/business";
17
- import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
18
- import { getPendingOrdersSnapshot, subscribePendingOrders, } from "../adapters/pending-orders";
19
- import { orderSnapshotsService } from "../sync/order-snapshots.service";
20
- import { hydratePendingOrders } from "../sync/pending-orders.service";
21
- import { isLocalOrderNumber } from "../sync/order-number";
22
- import { filterOrdersOffline, paginateOffline } from "../utils/order-filter";
23
- // ─── Keys (host apps should import from core, not define their own) ─────
24
- export const POS_KEYS = {
25
- all: ["pos"],
26
- orders: (businessId) => ["pos", "orders", businessId],
27
- preview: (businessId) => ["pos", "preview", businessId],
28
- };
29
- export const SELLER_KEYS = {
30
- all: ["seller"],
31
- orders: (businessId, filters) => [...SELLER_KEYS.all, "orders", businessId, filters].filter((x) => x !== undefined),
32
- deliveries: (businessId, filters) => [...SELLER_KEYS.all, "deliveries", businessId, filters].filter((x) => x !== undefined),
33
- order: (id, businessId) => [...SELLER_KEYS.all, "order", id, businessId],
34
- };
35
- // ─── Helpers ───────────────────────────────────────────────────────────────
21
+ export const POS_KEYS = { all: ["pos"], orders: (bid) => ["pos", "orders", bid], preview: (bid) => ["pos", "preview", bid] };
22
+ export const SELLER_KEYS = { all: ["seller"], orders: (bid, f) => [...SELLER_KEYS.all, "orders", bid, f].filter((x) => x !== undefined), deliveries: (bid, f) => [...SELLER_KEYS.all, "deliveries", bid, f].filter((x) => x !== undefined), order: (id, bid) => [...SELLER_KEYS.all, "order", id, bid] };
36
23
  function useBusinessId() {
37
24
  const id = useSyncExternalStore(subscribeBusiness, () => getActiveBusiness()?.id ?? null, () => null);
38
25
  return id ?? undefined;
39
26
  }
40
- function useConnectivity() {
41
- const state = useSyncExternalStore(subscribeConnectivity, getConnectivityState, getConnectivityState);
42
- return state;
43
- }
44
- function useCorePendingOrders(businessId) {
45
- const snap = useSyncExternalStore(subscribePendingOrders, getPendingOrdersSnapshot, getPendingOrdersSnapshot);
46
- useEffect(() => {
47
- if (snap.hydrated)
48
- return;
49
- hydratePendingOrders().catch(() => { });
50
- }, [snap.hydrated]);
51
- return useMemo(() => (businessId ? snap.orders.filter((o) => o.business_id === businessId) : snap.orders), [snap.orders, businessId]);
52
- }
53
- function mergePendingOrders(pending, serverOrders) {
54
- if (pending.length === 0)
55
- return serverOrders ?? [];
56
- const seen = new Set();
57
- const merged = [];
58
- for (const order of pending) {
59
- seen.add(order.order_number);
60
- merged.push(order);
61
- }
62
- for (const order of serverOrders ?? []) {
63
- if (!seen.has(order.order_number))
64
- merged.push(order);
65
- }
66
- return merged.sort((a, b) => (b.inserted_at ?? "").localeCompare(a.inserted_at ?? ""));
67
- }
68
- function isOfflineState(s) {
69
- return !s.isOnline || s.isCloudReachable === "unreachable";
70
- }
71
- // ─── POS Orders Query (offline-first) ─────────────────────────────────────
72
27
  export function usePOSOrdersQuery(filters, enabled = true) {
73
- const businessId = useBusinessId();
74
- const pendingOrders = useCorePendingOrders(businessId);
75
- const connectivity = useConnectivity();
76
- const offline = isOfflineState(connectivity);
77
- const query = useInfiniteQuery({
78
- queryKey: [...POS_KEYS.orders(businessId), filters],
28
+ const bid = useBusinessId();
29
+ return useInfiniteQuery({
30
+ queryKey: [...POS_KEYS.orders(bid), filters],
79
31
  queryFn: async ({ pageParam = 1 }) => {
80
- const params = { page: String(pageParam) };
81
- if (filters?.status)
82
- params.status = filters.status;
83
- if (filters?.order_type)
84
- params.order_type = filters.order_type;
85
- if (filters?.is_pos)
86
- params.is_pos = filters.is_pos;
87
- if (filters?.date_from)
88
- params.date_from = filters.date_from;
89
- if (filters?.date_to)
90
- params.date_to = filters.date_to;
91
- if (filters?.q)
92
- params.q = filters.q;
93
- if (filters?.search)
94
- params.search = filters.search;
95
- if (offline) {
96
- const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
97
- const base = offlineList ?? [];
98
- const filtered = filterOrdersOffline(base, filters);
99
- // date_from/to are handled inside filterSellerOrders via inserted_at; apply if present
100
- const paged = paginateOffline(filtered, pageParam, 20);
101
- return paged;
102
- }
103
- try {
104
- const result = (await posRepository.fetchOrders(businessId, params));
105
- if (pageParam === 1 && businessId && Array.isArray(result.data)) {
106
- orderSnapshotsService.persistSellerOrders(businessId, businessId, result.data).catch(() => { });
107
- }
108
- return result;
109
- }
110
- catch (e) {
111
- const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
112
- if (offlineList) {
113
- const filtered = filterOrdersOffline(offlineList, filters);
114
- const paged = paginateOffline(filtered, pageParam, 20);
115
- return paged;
116
- }
117
- throw e;
118
- }
32
+ if (!bid)
33
+ return { data: [], pagination: { page: 1, page_size: 20, total_pages: 1, total_entries: 0 } };
34
+ const { data, total } = await listLocalOrders(bid, filters, pageParam, 20);
35
+ // Offline overlay: pending creations + queued statuses (page 1). Refetch
36
+ // is driven by the pending-orders invalidator on every apply path.
37
+ const merged = await mergePendingIntoList(data, bid, pageParam);
38
+ const totalPages = Math.max(1, Math.ceil(total / 20));
39
+ return { data: merged, pagination: { page: pageParam, page_size: 20, total_pages: totalPages, total_entries: total + (merged.length - data.length) } };
119
40
  },
120
41
  initialPageParam: 1,
121
42
  getNextPageParam: (lastPage) => {
122
43
  const p = lastPage.pagination;
123
- return p && p.page < p.total_pages ? p.page + 1 : undefined;
44
+ return p.page < p.total_pages ? p.page + 1 : undefined;
124
45
  },
125
- enabled: enabled && !!businessId,
126
- staleTime: 30 * 1000,
127
- retry: false,
46
+ enabled: enabled && !!bid,
47
+ staleTime: 5 * 1000,
128
48
  networkMode: "offlineFirst",
129
49
  select: (data) => data.pages.flatMap((p) => p.data),
130
50
  });
131
- const data = useMemo(() => mergePendingOrders(pendingOrders, query.data), [pendingOrders, query.data]);
132
- return { ...query, data };
133
51
  }
134
- // ─── Seller Orders Query (offline-first, shares same snapshot) ────────────
135
52
  export function useSellerOrdersQuery(enabled = true, filters) {
136
- const businessId = useBusinessId();
137
- const pendingOrders = useCorePendingOrders(businessId);
138
- const connectivity = useConnectivity();
139
- const offline = isOfflineState(connectivity);
140
- const query = useInfiniteQuery({
141
- queryKey: SELLER_KEYS.orders(businessId, filters),
53
+ const bid = useBusinessId();
54
+ return useInfiniteQuery({
55
+ queryKey: SELLER_KEYS.orders(bid, filters),
142
56
  queryFn: async ({ pageParam = 1 }) => {
143
- const params = { page: pageParam, page_size: 20 };
144
- if (filters?.status)
145
- params.status = filters.status;
146
- if (filters?.q)
147
- params.q = filters.q;
148
- if (filters?.search)
149
- params.search = filters.search;
150
- if (filters?.date_from)
151
- params.date_from = filters.date_from;
152
- if (filters?.date_to)
153
- params.date_to = filters.date_to;
154
- if (offline) {
155
- const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
156
- const base = offlineList ?? [];
157
- const filtered = filterOrdersOffline(base, filters);
158
- const paged = paginateOffline(filtered, pageParam, 20);
159
- return paged;
160
- }
161
- try {
162
- const result = (await sellerService.fetchBusinessOrders(businessId, params));
163
- if (pageParam === 1 && businessId && Array.isArray(result.data)) {
164
- orderSnapshotsService.persistSellerOrders(businessId, businessId, result.data).catch(() => { });
165
- }
166
- return result;
167
- }
168
- catch (e) {
169
- const offlineList = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
170
- if (offlineList) {
171
- const filtered = filterOrdersOffline(offlineList, filters);
172
- const paged = paginateOffline(filtered, pageParam, 20);
173
- return paged;
174
- }
175
- throw e;
176
- }
57
+ if (!bid)
58
+ return { data: [], pagination: { page: 1, page_size: 20, total_pages: 1, total_entries: 0 } };
59
+ const q = filters?.q ?? filters?.search;
60
+ const { data, total } = await listLocalOrders(bid, { status: filters?.status, q }, pageParam, 20);
61
+ const merged = await mergePendingIntoList(data, bid, pageParam);
62
+ const totalPages = Math.max(1, Math.ceil(total / 20));
63
+ return { data: merged, pagination: { page: pageParam, page_size: 20, total_pages: totalPages, total_entries: total + (merged.length - data.length) } };
177
64
  },
178
65
  initialPageParam: 1,
179
66
  getNextPageParam: (lastPage) => {
180
67
  const p = lastPage.pagination;
181
- return p && p.page < p.total_pages ? p.page + 1 : undefined;
68
+ return p.page < p.total_pages ? p.page + 1 : undefined;
182
69
  },
183
- enabled: enabled && !!businessId,
184
- staleTime: 30 * 1000,
185
- retry: false,
70
+ enabled: enabled && !!bid,
71
+ staleTime: 5 * 1000,
186
72
  networkMode: "offlineFirst",
187
73
  select: (data) => data.pages.flatMap((p) => p.data),
188
74
  });
189
- const data = useMemo(() => mergePendingOrders(pendingOrders, query.data), [pendingOrders, query.data]);
190
- return { ...query, data };
191
75
  }
192
- // ─── Pending orders hook (for screens that just need the overlay) ─────────
193
- export function usePendingOrders(businessId) {
194
- return useCorePendingOrders(businessId);
76
+ export function useSellerOrderQuery(orderId, enabled = true) {
77
+ const bid = useBusinessId();
78
+ return useQuery({
79
+ queryKey: SELLER_KEYS.order(orderId, bid),
80
+ queryFn: async () => {
81
+ if (!bid)
82
+ throw new Error("No business");
83
+ const order = await getLocalOrder(bid, orderId);
84
+ if (!order)
85
+ throw new Error("Order not found");
86
+ return { data: order };
87
+ },
88
+ enabled: enabled && !!orderId && !!bid,
89
+ staleTime: 5 * 1000,
90
+ });
195
91
  }
196
- // ─── Create POS Order (device-lease aware, via SyncEngine) ────────────────
197
92
  export function useCreatePOSOrderMutation() {
198
- const businessId = useBusinessId();
199
- const queryClient = useQueryClient();
93
+ const bid = useBusinessId();
94
+ const qc = useQueryClient();
200
95
  return useMutation({
201
96
  mutationFn: async (data) => {
202
- const { isOnline, isCloudReachable } = getConnectivityState();
203
- if (!businessId)
204
- throw new Error("Missing business — select a business before creating orders");
205
- try {
206
- const m = await import("../sync/device-lease.service");
207
- const dn = m.getDeviceNumber?.(businessId) ?? null;
208
- if (!dn) {
209
- const leased = await m.ensureDeviceLease?.(businessId);
210
- if (!leased && (!isOnline || isCloudReachable === "unreachable")) {
211
- throw new Error("No device number leased (99 devices max) — cannot create offline orders");
212
- }
213
- }
97
+ if (!bid)
98
+ throw new Error("No business");
99
+ const dn = getDeviceNumber(bid);
100
+ if (!dn) {
101
+ const leased = await ensureLease(bid);
102
+ if (!leased)
103
+ throw new Error("No device number leased — connect online once");
214
104
  }
215
- catch (e) {
216
- if (e?.message?.includes("device number") || e?.message?.includes("No device lease"))
217
- throw e;
218
- }
219
- return posRepository.createOrder(businessId, data);
220
- },
221
- onSuccess: (result) => {
222
- const r = result;
223
- const orderNumber = r?.order_number ?? "";
224
- const queued = orderNumber ? isLocalOrderNumber(orderNumber) && r?.status === "pending" : false;
225
- queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
226
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.orders(businessId) });
227
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
228
- // Toast is host concern — core just invalidates; host can show queued vs created.
229
- void queued;
105
+ // Engine route: cloud → LAN node → queue. Never createLocalOrder
106
+ // directly — it bypasses sync and peers never see the order.
107
+ return orderRepository.createOrder({ businessId: bid, data, entityId: generateUUID() });
230
108
  },
109
+ onSuccess: () => { qc.invalidateQueries({ queryKey: POS_KEYS.orders(bid) }); qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); },
231
110
  });
232
111
  }
233
- // ─── Preview (offline via tax-rules reference) ────────────────────────────
234
- function cartHash(items) {
235
- if (items.length === 0)
236
- return "";
237
- return items
238
- .map((i) => `${i.menu_item.id}:${i.quantity}:${i.selected_variant?.id || ""}:${(i.selected_addons || []).map((a) => a.addon_id).sort().join(",")}`)
239
- .join("|");
240
- }
112
+ function cartHash(items) { return items.map((i) => `${i.menu_item.id}:${i.quantity}:${i.selected_variant?.id || ""}:${(i.selected_addons || []).map((a) => a.addon_id).sort().join(",")}`).join("|"); }
241
113
  export function usePOSOrderPreviewQuery(items, orderType, customerName) {
242
- const businessId = useBusinessId();
243
- const hash = useMemo(() => cartHash(items), [items]);
114
+ const bid = useBusinessId();
115
+ const hash = cartHash(items);
244
116
  return useQuery({
245
- queryKey: [...POS_KEYS.preview(businessId), hash, orderType, customerName],
117
+ queryKey: [...POS_KEYS.preview(bid), hash, orderType, customerName],
246
118
  queryFn: async () => {
247
- const orderItems = items.map((i) => ({
248
- menu_item_id: i.menu_item.id,
249
- quantity: i.quantity,
250
- variant_id: i.selected_variant?.id,
251
- addon_ids: i.selected_addons.length > 0 ? i.selected_addons.map((a) => a.addon_id) : undefined,
252
- }));
119
+ const orderItems = items.map((i) => ({ menu_item_id: i.menu_item.id, quantity: i.quantity, variant_id: i.selected_variant?.id, addon_ids: i.selected_addons.length > 0 ? i.selected_addons.map((a) => a.addon_id) : undefined }));
253
120
  try {
254
- return await posRepository.fetchPreview({ businessId: businessId, items: orderItems, orderType, customerName: customerName || undefined });
121
+ return await posRepository.fetchPreview({ businessId: bid, items: orderItems, orderType, customerName: customerName || undefined });
255
122
  }
256
- catch (e) {
257
- const s = getConnectivityState();
258
- const isOffline = !s.isOnline || s.isCloudReachable === "unreachable";
259
- if (!isOffline)
260
- throw e;
261
- const { openSyncDatabase } = await import("../db/connection");
262
- const { getReferenceData } = await import("../reference/operations");
263
- const { calculatePricing } = await import("../pricing/engine");
123
+ catch {
264
124
  const db = await openSyncDatabase();
265
- const taxRec = await getReferenceData(db, businessId, "tax-rules");
125
+ const taxRec = await getReferenceData(db, bid, "tax-rules");
266
126
  const taxRules = taxRec?.payload?.data ?? [];
267
127
  const pricing = calculatePricing({ items: items, taxRules: taxRules });
268
128
  return { data: { subtotal_minor: pricing.subtotal_minor, discount_minor: pricing.discount_minor, delivery_fee_minor: pricing.delivery_fee_minor, tax_minor: pricing.tax_minor, tip_minor: pricing.tip_minor, total_minor: pricing.total_minor, tax_lines: pricing.tax_lines, currency: pricing.currency_code } };
269
129
  }
270
130
  },
271
- enabled: !!businessId && items.length > 0,
131
+ enabled: !!bid && items.length > 0,
272
132
  staleTime: 30 * 1000,
273
133
  });
274
134
  }
275
- // ─── Seller order detail (offline via snapshot fallback) ───────────────────
276
- export function useSellerOrderQuery(orderId, enabled = true) {
277
- const businessId = useBusinessId();
278
- const connectivity = useConnectivity();
279
- const offline = isOfflineState(connectivity);
280
- const pendingOrders = useCorePendingOrders(businessId);
281
- return useQuery({
282
- queryKey: SELLER_KEYS.order(orderId, businessId),
283
- queryFn: async () => {
284
- const pendingHit = pendingOrders.find((o) => o.id === orderId || o.order_number === orderId);
285
- if (pendingHit)
286
- return { data: pendingHit };
287
- if (offline) {
288
- const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
289
- const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
290
- if (found)
291
- return { data: found };
292
- }
293
- try {
294
- const res = await sellerService.fetchSellerOrder(orderId);
295
- return res;
296
- }
297
- catch (e) {
298
- const list = businessId ? await orderSnapshotsService.getSellerList(businessId, businessId) : null;
299
- const found = list?.find((o) => o.id === orderId || o.order_number === orderId);
300
- if (found)
301
- return { data: found };
302
- const pendingAgain = getPendingOrdersSnapshot().orders.find((o) => o.id === orderId || o.order_number === orderId);
303
- if (pendingAgain)
304
- return { data: pendingAgain };
305
- throw e;
306
- }
307
- },
308
- enabled: enabled && !!orderId && !!businessId,
309
- staleTime: 30 * 1000,
310
- retry: false,
311
- networkMode: "offlineFirst",
312
- });
313
- }
314
- // ─── Helpers for queued UI ───────────────────────────────────────────────
315
- export function isQueuedTransition(result) {
316
- const r = result;
317
- if (!r)
318
- return false;
319
- return r.status === "pending" && !!r.command_id && !r.id;
320
- }
321
- // ─── Order status mutations via SyncEngine (all go through outbox) ────────
322
135
  function makeOrderActionMutation(build) {
323
136
  return function useOrderActionMutation() {
324
- const businessId = useBusinessId();
325
- const queryClient = useQueryClient();
137
+ const bid = useBusinessId();
138
+ const qc = useQueryClient();
326
139
  return useMutation({
327
140
  mutationFn: (orderId) => {
328
- if (!businessId)
141
+ if (!bid)
329
142
  throw new Error("No active business");
330
- return build({ orderId, businessId });
331
- },
332
- onSettled: (_d, _e, orderId) => {
333
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.orders() });
334
- queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
335
- if (orderId)
336
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) });
143
+ return build({ orderId, businessId: bid });
337
144
  },
145
+ onSuccess: (_d, _e, orderId) => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); if (orderId)
146
+ qc.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) }); },
338
147
  });
339
148
  };
340
149
  }
@@ -345,73 +154,45 @@ export const useMarkOrderReadyMutation = makeOrderActionMutation(({ orderId, bus
345
154
  export const useCompletePickupMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.completePickup({ orderId, businessId }));
346
155
  export const useCancelOrderMutation = makeOrderActionMutation(({ orderId, businessId }) => orderRepository.cancelOrder({ orderId, businessId }));
347
156
  export function useConfirmHandoverMutation() {
348
- const businessId = useBusinessId();
349
- const queryClient = useQueryClient();
350
- return useMutation({
351
- mutationFn: (deliveryId) => {
352
- if (!businessId)
353
- throw new Error("No active business");
354
- return orderRepository.confirmHandover({ deliveryId, businessId });
355
- },
356
- onSettled: (_d, _e, deliveryId) => {
357
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
358
- queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
359
- if (deliveryId)
360
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(deliveryId) });
361
- },
362
- });
157
+ const bid = useBusinessId();
158
+ const qc = useQueryClient();
159
+ return useMutation({ mutationFn: (deliveryId) => { if (!bid)
160
+ throw new Error("No business"); assertOnlineFor("order:confirm_handover"); return orderRepository.confirmHandover({ deliveryId, businessId: bid }); }, onSuccess: () => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); } });
363
161
  }
364
162
  export function useAssignRiderMutation() {
365
- const businessId = useBusinessId();
366
- const queryClient = useQueryClient();
367
- return useMutation({
368
- mutationFn: ({ orderId, riderId }) => {
369
- if (!businessId)
370
- throw new Error("No active business");
371
- return orderRepository.assignRider({ orderId, riderId, businessId });
372
- },
373
- onSettled: () => {
374
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
375
- queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
376
- },
377
- });
163
+ const bid = useBusinessId();
164
+ const qc = useQueryClient();
165
+ return useMutation({ mutationFn: ({ orderId, riderId }) => { if (!bid)
166
+ throw new Error("No business"); assertOnlineFor("order:assign_rider"); return orderRepository.assignRider({ orderId, riderId, businessId: bid }); }, onSuccess: () => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); } });
378
167
  }
379
168
  export function useConfirmCashPaymentMutation() {
380
- const businessId = useBusinessId();
381
- const queryClient = useQueryClient();
382
- return useMutation({
383
- mutationFn: (orderId) => {
384
- if (!businessId)
385
- throw new Error("No active business");
386
- return orderRepository.confirmCashPayment({ orderId, businessId });
387
- },
388
- onSettled: (_d, _e, orderId) => {
389
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
390
- queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
391
- if (orderId)
392
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) });
393
- },
394
- });
395
- }
396
- export function useUpdateOrderStatusMutation() {
397
- const accept = useAcceptOrderMutation();
398
- const reject = useRejectOrderMutation();
399
- const preparing = useStartPreparingOrderMutation();
400
- const ready = useMarkOrderReadyMutation();
401
- const complete = useCompletePickupMutation();
402
- const cancel = useCancelOrderMutation();
403
- return {
404
- mutate: (orderId, status) => {
405
- switch (status) {
406
- case "accepted": return accept.mutateAsync(orderId);
407
- case "preparing": return preparing.mutateAsync(orderId);
408
- case "ready": return ready.mutateAsync(orderId);
409
- case "delivered": return complete.mutateAsync(orderId);
410
- case "cancelled": return cancel.mutateAsync(orderId);
411
- case "rejected": return reject.mutateAsync(orderId);
412
- default: throw new Error(`Unknown status ${status}`);
413
- }
414
- },
415
- isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending,
416
- };
169
+ const bid = useBusinessId();
170
+ const qc = useQueryClient();
171
+ return useMutation({ mutationFn: (orderId) => { if (!bid)
172
+ throw new Error("No business"); return orderRepository.confirmCashPayment({ orderId, businessId: bid }); }, onSuccess: (_d, _e, orderId) => { qc.invalidateQueries({ queryKey: SELLER_KEYS.all }); qc.invalidateQueries({ queryKey: POS_KEYS.all }); qc.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all }); if (orderId)
173
+ qc.invalidateQueries({ queryKey: SELLER_KEYS.order(orderId) }); } });
174
+ }
175
+ export function useUpdateOrderStatusMutation() { const accept = useAcceptOrderMutation(); const reject = useRejectOrderMutation(); const preparing = useStartPreparingOrderMutation(); const ready = useMarkOrderReadyMutation(); const complete = useCompletePickupMutation(); const cancel = useCancelOrderMutation(); return { mutate: (orderId, status) => { const s = String(status); if (!["accepted", "preparing", "ready", "delivered", "cancelled", "rejected"].includes(s))
176
+ throw new Error(`Unknown status ${s}`); switch (s) {
177
+ case "accepted": return accept.mutateAsync(orderId);
178
+ case "preparing": return preparing.mutateAsync(orderId);
179
+ case "ready": return ready.mutateAsync(orderId);
180
+ case "delivered": return complete.mutateAsync(orderId);
181
+ case "cancelled": return cancel.mutateAsync(orderId);
182
+ case "rejected": return reject.mutateAsync(orderId);
183
+ default: throw new Error(`Unknown status ${s}`);
184
+ } }, isPending: accept.isPending || reject.isPending || preparing.isPending || ready.isPending || complete.isPending || cancel.isPending, }; }
185
+ export function isQueuedTransition(result) { const r = result; return !!r && r.status === "pending" && !!r.command_id && !r.id; }
186
+ /**
187
+ * Reactive view of locally queued (unconfirmed) order creations.
188
+ * Hydrates persisted optimistic events once (survives restart offline).
189
+ */
190
+ export function usePendingOrders() {
191
+ const snap = useSyncExternalStore(subscribePendingOrders, () => getPendingOrdersSnapshot().orders, () => []);
192
+ useEffect(() => {
193
+ if (!getPendingOrdersSnapshot().hydrated) {
194
+ hydratePendingOrders().catch(() => undefined);
195
+ }
196
+ }, []);
197
+ return snap;
417
198
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Places search hooks — single source for both apps (previously verbatim
3
+ * copies). Pure React + injected placesService; storage-backed caching lives
4
+ * in the host service implementations.
5
+ */
6
+ import type { PlaceSuggestion, PlaceDetails } from "../types/places";
7
+ export declare function usePlacesSearch(debounceMs?: number): {
8
+ query: string;
9
+ setQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
10
+ suggestions: PlaceSuggestion[];
11
+ isLoading: boolean;
12
+ clearSearch: () => void;
13
+ clearAll: () => void;
14
+ };
15
+ export declare function usePlaceDetails(placeId: string | null): {
16
+ details: PlaceDetails | null;
17
+ isLoading: boolean;
18
+ };
19
+ export declare function clearPlacesCache(): void;
20
+ /** Generic debounced-value hook for search/filter inputs. */
21
+ export declare function useDebouncedValue<T>(value: T, delayMs?: number): T;