@teincfood/core 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +26 -21
  2. package/dist/adapters/auth.d.ts +10 -0
  3. package/dist/adapters/auth.js +43 -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 +99 -15
  16. package/dist/adapters/services.js +244 -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 +19 -39
  55. package/dist/hooks/orders.hooks.js +126 -321
  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 +60 -5
  69. package/dist/index.js +78 -5
  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/events.service.d.ts +18 -0
  88. package/dist/realtime/events.service.js +76 -0
  89. package/dist/realtime/order-events.service.d.ts +6 -0
  90. package/dist/realtime/order-events.service.js +17 -0
  91. package/dist/realtime/phoenix-socket.d.ts +55 -0
  92. package/dist/realtime/phoenix-socket.js +269 -0
  93. package/dist/realtime/useEventsConnection.d.ts +1 -0
  94. package/dist/realtime/useEventsConnection.js +50 -0
  95. package/dist/reference/api.d.ts +2 -0
  96. package/dist/reference/api.js +2 -0
  97. package/dist/reference/capabilities.d.ts +12 -7
  98. package/dist/reference/capabilities.js +28 -13
  99. package/dist/reference/hooks.d.ts +34 -0
  100. package/dist/reference/hooks.js +124 -0
  101. package/dist/reference/menu-apply.d.ts +29 -0
  102. package/dist/reference/menu-apply.js +289 -0
  103. package/dist/reference/menu-record.d.ts +69 -0
  104. package/dist/reference/menu-record.js +163 -0
  105. package/dist/reference/operations.d.ts +3 -0
  106. package/dist/reference/operations.js +35 -1
  107. package/dist/reference/service.js +36 -9
  108. package/dist/reference/types.d.ts +1 -1
  109. package/dist/repositories/order.repository.d.ts +8 -0
  110. package/dist/repositories/order.repository.js +24 -2
  111. package/dist/repositories/pos.repository.d.ts +4 -0
  112. package/dist/repositories/pos.repository.js +4 -0
  113. package/dist/sync/backoff.d.ts +21 -0
  114. package/dist/sync/backoff.js +42 -0
  115. package/dist/sync/command-builder.js +10 -2
  116. package/dist/sync/command-queue.service.d.ts +12 -0
  117. package/dist/sync/command-queue.service.js +115 -18
  118. package/dist/sync/device-lease.service.d.ts +3 -3
  119. package/dist/sync/device-lease.service.js +80 -20
  120. package/dist/sync/engine.d.ts +3 -1
  121. package/dist/sync/engine.js +44 -4
  122. package/dist/sync/full-sync.d.ts +6 -0
  123. package/dist/sync/full-sync.js +72 -0
  124. package/dist/sync/health-probe.d.ts +24 -0
  125. package/dist/sync/health-probe.js +138 -0
  126. package/dist/sync/offline-capable.js +25 -0
  127. package/dist/sync/online-gates.d.ts +17 -0
  128. package/dist/sync/online-gates.js +55 -0
  129. package/dist/sync/order-number.d.ts +4 -4
  130. package/dist/sync/order-number.js +33 -13
  131. package/dist/sync/order-snapshots.service.d.ts +1 -0
  132. package/dist/sync/order-snapshots.service.js +47 -2
  133. package/dist/sync/pending-orders.service.d.ts +18 -0
  134. package/dist/sync/pending-orders.service.js +148 -3
  135. package/dist/sync/reconciliation/index.d.ts +5 -0
  136. package/dist/sync/reconciliation/index.js +5 -0
  137. package/dist/sync/snapshots/entity.d.ts +6 -0
  138. package/dist/sync/snapshots/entity.js +6 -0
  139. package/dist/sync/sync-jobs.service.d.ts +18 -0
  140. package/dist/sync/sync-jobs.service.js +539 -0
  141. package/dist/sync/transition-endpoints.d.ts +11 -0
  142. package/dist/sync/transition-endpoints.js +30 -0
  143. package/dist/sync/transport-local-node.js +3 -2
  144. package/dist/sync/transport-rest.d.ts +7 -0
  145. package/dist/sync/transport-rest.js +90 -25
  146. package/dist/sync/use-sync-reconciliation.js +58 -16
  147. package/dist/test-helpers/memory-driver.js +182 -4
  148. package/dist/test-helpers/seed-hours.d.ts +1 -0
  149. package/dist/test-helpers/seed-hours.js +23 -0
  150. package/dist/types/api/business.api.d.ts +42 -0
  151. package/dist/types/api/menu.api.d.ts +15 -0
  152. package/dist/types/api/seller.api.d.ts +20 -0
  153. package/dist/types/places.d.ts +19 -0
  154. package/dist/types/places.js +2 -0
  155. package/dist/types/rider.d.ts +67 -0
  156. package/dist/types/rider.js +2 -0
  157. package/dist/utils/attempt-stats.d.ts +23 -0
  158. package/dist/utils/attempt-stats.js +61 -0
  159. package/dist/utils/business-hours.d.ts +37 -0
  160. package/dist/utils/business-hours.js +96 -0
  161. package/dist/utils/debug-log.d.ts +23 -0
  162. package/dist/utils/debug-log.js +153 -0
  163. package/dist/utils/order-filter.d.ts +1 -1
  164. package/dist/utils/order-filter.js +1 -1
  165. package/dist/utils/order-transitions.d.ts +30 -0
  166. package/dist/utils/order-transitions.js +185 -0
  167. package/dist/utils/sync-logger.js +5 -4
  168. package/dist/validation/offline.d.ts +46 -0
  169. package/dist/validation/offline.js +79 -0
  170. package/package.json +6 -1
  171. package/dist/pricing/engine.test.d.ts +0 -1
  172. package/dist/pricing/engine.test.js +0 -142
  173. package/dist/reference/lww.test.d.ts +0 -1
  174. package/dist/reference/lww.test.js +0 -23
  175. package/dist/reference/operations.test.d.ts +0 -1
  176. package/dist/reference/operations.test.js +0 -66
  177. package/dist/standalone.integration.test.d.ts +0 -1
  178. package/dist/standalone.integration.test.js +0 -235
  179. package/dist/sync/device-lease.test.d.ts +0 -1
  180. package/dist/sync/device-lease.test.js +0 -38
  181. package/dist/sync/offline-integration.test.d.ts +0 -1
  182. package/dist/sync/offline-integration.test.js +0 -52
  183. package/dist/sync/order-number.test.d.ts +0 -1
  184. package/dist/sync/order-number.test.js +0 -51
  185. package/dist/sync/perms.test.d.ts +0 -1
  186. package/dist/sync/perms.test.js +0 -15
  187. package/dist/utils/currency.test.d.ts +0 -1
  188. package/dist/utils/currency.test.js +0 -17
  189. package/dist/utils/hlc.test.d.ts +0 -1
  190. package/dist/utils/hlc.test.js +0 -29
  191. package/dist/utils/phone.test.d.ts +0 -1
  192. package/dist/utils/phone.test.js +0 -19
  193. package/dist/utils/responsive.d.ts +0 -13
  194. package/dist/utils/responsive.js +0 -18
  195. package/dist/utils/uuid.test.d.ts +0 -1
  196. package/dist/utils/uuid.test.js +0 -13
  197. package/dist/utils/validation.test.d.ts +0 -1
  198. package/dist/utils/validation.test.js +0 -23
@@ -16,23 +16,37 @@ function cryptoRandom100to999() {
16
16
  }
17
17
  return 100 + Math.floor(Math.random() * 900);
18
18
  }
19
- function getNextCallout() {
20
- const stored = getItem(CALLOUT_SEQ_KEY);
19
+ function calloutKeyForBusiness(businessId) {
20
+ return businessId ? `${CALLOUT_SEQ_KEY}:${businessId}` : CALLOUT_SEQ_KEY;
21
+ }
22
+ function getNextCallout(businessId) {
23
+ const key = calloutKeyForBusiness(businessId);
24
+ const stored = getItem(key);
21
25
  let next = (stored ?? 0) + 1;
22
26
  if (next > 999)
23
27
  next = 1;
24
- setItem(CALLOUT_SEQ_KEY, next);
28
+ setItem(key, next);
29
+ // migrate legacy global once: keep global in sync for backward compat
30
+ if (businessId) {
31
+ const legacy = getItem(CALLOUT_SEQ_KEY) ?? 0;
32
+ if (legacy < next)
33
+ setItem(CALLOUT_SEQ_KEY, next);
34
+ }
25
35
  return next;
26
36
  }
27
- export function peekCallout() {
28
- const stored = getItem(CALLOUT_SEQ_KEY);
29
- return stored ?? 0;
37
+ export function peekCallout(businessId) {
38
+ const key = calloutKeyForBusiness(businessId);
39
+ const v = getItem(key);
40
+ if (v != null)
41
+ return v;
42
+ const legacy = getItem(CALLOUT_SEQ_KEY);
43
+ return legacy ?? 0;
30
44
  }
31
- export function generateOrderNumber(deviceNumber) {
45
+ export function generateOrderNumber(deviceNumber, businessId) {
32
46
  if (!/^(0[1-9]|[1-9][0-9])$/.test(deviceNumber)) {
33
47
  throw new Error(`Invalid deviceNumber ${deviceNumber} — must be 01-99`);
34
48
  }
35
- const callout = getNextCallout();
49
+ const callout = getNextCallout(businessId);
36
50
  let random = cryptoRandom100to999();
37
51
  // On wrap to 001, ensure new random (already new per order, but keep hook)
38
52
  if (callout === 1) {
@@ -53,11 +67,11 @@ export function generateOrderNumberForBusiness(businessId) {
53
67
  if (!v || !/^(0[1-9]|[1-9][0-9])$/.test(v)) {
54
68
  throw new Error(`No device lease for business ${businessId} — call ensureLease(businessId) first (online-only until leased)`);
55
69
  }
56
- return generateOrderNumber(v).orderNumber;
70
+ return generateOrderNumber(v, businessId).orderNumber;
57
71
  }
58
- export function generateLocalCalloutNumber() {
59
- // kept for SellerOrder.callout_number compatibility — returns CCC
60
- const next = peekCallout() + 1;
72
+ export function generateLocalCalloutNumber(businessId) {
73
+ // kept for SellerOrder.callout_number compatibility — returns next CCC
74
+ const next = peekCallout(businessId) + 1;
61
75
  return next > 999 ? 1 : next;
62
76
  }
63
77
  // New canonical check — unified for DD-RRR-CCC
@@ -74,6 +88,12 @@ export function parseOrderNumber(orderNumber) {
74
88
  return { deviceNumber: m[1], random: m[2], callout: m[3] };
75
89
  }
76
90
  // Test helpers
77
- export function resetCalloutForTests() {
91
+ export function resetCalloutForTests(businessId) {
92
+ if (businessId) {
93
+ setItem(calloutKeyForBusiness(businessId), 0);
94
+ return;
95
+ }
78
96
  setItem(CALLOUT_SEQ_KEY, 0);
97
+ // also clear per-business keys that tests may have touched
98
+ // caller should pass businessId if they used business-scoped sequences
79
99
  }
@@ -29,6 +29,7 @@ declare class OrderSnapshotsService {
29
29
  */
30
30
  applyQueuedStatusEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
31
31
  private applyOrderStatusToReadModelInternal;
32
+ applyPaymentEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
32
33
  /**
33
34
  * Patch the latest status of a known order inside every list snapshot for the
34
35
  * business, so offline devices see live status changes without a refetch.
@@ -74,7 +74,19 @@ class OrderSnapshotsService {
74
74
  }
75
75
  async applyOrderStatusToReadModelInternal(db, orderId, toStatus, event) {
76
76
  try {
77
- const lists = await getEntitySnapshotsByType(db, "seller_orders");
77
+ const payload = event.payload;
78
+ const businessId = payload?.business_id ?? payload?.businessId ?? null;
79
+ // Dual-write: keep canonical orders table in sync so POS/KDS reads converge instantly.
80
+ if (businessId) {
81
+ try {
82
+ const { updateOrderStatus } = await import("../db/operations");
83
+ await updateOrderStatus(db, businessId, orderId, toStatus, event.timestamp);
84
+ }
85
+ catch { }
86
+ }
87
+ const lists = businessId
88
+ ? await getEntitySnapshotsByType(db, "seller_orders", businessId)
89
+ : await getEntitySnapshotsByType(db, "seller_orders");
78
90
  for (const list of lists) {
79
91
  const data = list.payload.data ?? [];
80
92
  const idx = data.findIndex((o) => o.id === orderId);
@@ -97,6 +109,39 @@ class OrderSnapshotsService {
97
109
  });
98
110
  }
99
111
  }
112
+ async applyPaymentEvent(event, origin = "local") {
113
+ const payload = event.payload;
114
+ const orderId = payload?.order_id ?? event.entity_id;
115
+ if (!orderId)
116
+ return;
117
+ const paidAt = payload?.paid_at || new Date().toISOString();
118
+ try {
119
+ const db = await openSyncDatabase();
120
+ await insertEvent(db, { event, origin });
121
+ const businessId = payload?.business_id ?? payload?.businessId ?? null;
122
+ const lists = businessId
123
+ ? await getEntitySnapshotsByType(db, "seller_orders", businessId)
124
+ : await getEntitySnapshotsByType(db, "seller_orders");
125
+ for (const list of lists) {
126
+ const data = list.payload.data ?? [];
127
+ const idx = data.findIndex((o) => o.id === orderId || o.order_number === payload?.order_number);
128
+ if (idx === -1)
129
+ continue;
130
+ const updated = data.map((o, i) => i === idx ? { ...o, is_paid: true, paid_at: paidAt, payment: { ...o.payment, is_paid: true, paid_at: paidAt } } : o);
131
+ await upsertEntitySnapshot(db, {
132
+ entityType: "seller_orders",
133
+ entityId: list.entity_id,
134
+ businessId: list.business_id,
135
+ lastEventId: event.event_id,
136
+ lastEventTimestamp: event.timestamp,
137
+ payload: { ...list.payload, data: updated },
138
+ });
139
+ }
140
+ }
141
+ catch (error) {
142
+ syncLogger.error("OrderSnapshots", "Failed to apply payment", { error: String(error) });
143
+ }
144
+ }
100
145
  /**
101
146
  * Patch the latest status of a known order inside every list snapshot for the
102
147
  * business, so offline devices see live status changes without a refetch.
@@ -143,7 +188,7 @@ class OrderSnapshotsService {
143
188
  async getSellerList(businessId, scope) {
144
189
  try {
145
190
  const db = await openSyncDatabase();
146
- const snap = await getEntitySnapshot(db, "seller_orders", scope);
191
+ const snap = await getEntitySnapshot(db, "seller_orders", scope, businessId);
147
192
  if (!snap || snap.business_id !== businessId)
148
193
  return null;
149
194
  return snap.payload.data ?? null;
@@ -15,11 +15,29 @@
15
15
  * order's `order_number`.
16
16
  */
17
17
  import type { DomainEvent } from "./types";
18
+ import type { SellerOrder } from "../types/api/seller.api";
18
19
  /**
19
20
  * Register a callback used to invalidate TanStack Query caches when a pending
20
21
  * order is reconciled. Returns an unregister function.
21
22
  */
22
23
  export declare function registerPendingOrdersInvalidator(fn: () => void): () => void;
24
+ /**
25
+ * Fire the registered query invalidator (lists refetch from SQLite).
26
+ * Used after LAN-applied writes (order:sync) that bypass the engine event
27
+ * pipeline, so peers' UIs converge without waiting for a cloud echo.
28
+ */
29
+ export declare function notifyPendingOrdersChanged(): void;
30
+ /**
31
+ * Merge optimistic (queued, unconfirmed) state into a page of listed orders.
32
+ * Called by list queryFns (page 1 only) so offline work is visible:
33
+ * 1. Pending creations (order.create_queued, local or LAN) missing from the
34
+ * SQLite page are prepended — deduped by id/order_number.
35
+ * 2. Queued transitions only patch the snapshot read model, never the
36
+ * orders table — overlay snapshot statuses onto base rows so the list
37
+ * reflects the queued status immediately (converges on replay).
38
+ * Reactivity comes from the invalidator: every apply path above refetches.
39
+ */
40
+ export declare function mergePendingIntoList(base: SellerOrder[], businessId: string, page: number): Promise<SellerOrder[]>;
23
41
  /**
24
42
  * Load persisted unsynced order:create_queued events into the store.
25
43
  * Idempotent; safe to call after app restart while still offline.
@@ -15,7 +15,7 @@
15
15
  * order's `order_number`.
16
16
  */
17
17
  import { openSyncDatabase } from "../db/connection";
18
- import { insertEvent, getUnsyncedEvents, markEventsSynced, } from "../db/operations";
18
+ import { insertEvent, getUnsyncedEvents, markEventsSynced, getEntitySnapshotsByType, } from "../db/operations";
19
19
  import { usePendingOrdersStore } from "../adapters/pending-orders";
20
20
  import { orderSnapshotsService } from "./order-snapshots.service";
21
21
  import { syncLogger } from "../utils/sync-logger";
@@ -36,6 +36,7 @@ const QUEUED_STATUS_EVENT_TYPES = new Set([
36
36
  "delivery.assign_queued",
37
37
  "delivery.confirm_handover_queued",
38
38
  ]);
39
+ const PAYMENT_QUEUED_TYPES = new Set(["payment.completed_queued"]);
39
40
  let invalidator = null;
40
41
  /**
41
42
  * Register a callback used to invalidate TanStack Query caches when a pending
@@ -48,6 +49,72 @@ export function registerPendingOrdersInvalidator(fn) {
48
49
  invalidator = null;
49
50
  };
50
51
  }
52
+ /**
53
+ * Fire the registered query invalidator (lists refetch from SQLite).
54
+ * Used after LAN-applied writes (order:sync) that bypass the engine event
55
+ * pipeline, so peers' UIs converge without waiting for a cloud echo.
56
+ */
57
+ export function notifyPendingOrdersChanged() {
58
+ try {
59
+ invalidator?.();
60
+ }
61
+ catch { }
62
+ }
63
+ /**
64
+ * Merge optimistic (queued, unconfirmed) state into a page of listed orders.
65
+ * Called by list queryFns (page 1 only) so offline work is visible:
66
+ * 1. Pending creations (order.create_queued, local or LAN) missing from the
67
+ * SQLite page are prepended — deduped by id/order_number.
68
+ * 2. Queued transitions only patch the snapshot read model, never the
69
+ * orders table — overlay snapshot statuses onto base rows so the list
70
+ * reflects the queued status immediately (converges on replay).
71
+ * Reactivity comes from the invalidator: every apply path above refetches.
72
+ */
73
+ export async function mergePendingIntoList(base, businessId, page) {
74
+ if (page !== 1)
75
+ return base;
76
+ let merged = base;
77
+ try {
78
+ const pending = usePendingOrdersStore.getState().orders;
79
+ if (pending.length) {
80
+ const seen = new Set(merged.map((o) => String(o.id ?? o.order_number)));
81
+ const fresh = pending.filter((p) => !seen.has(String(p.id ?? p.order_number)));
82
+ if (fresh.length)
83
+ merged = [...fresh, ...merged];
84
+ }
85
+ }
86
+ catch { }
87
+ try {
88
+ const db = await openSyncDatabase();
89
+ const snaps = await getEntitySnapshotsByType(db, "seller_orders", businessId);
90
+ if (!snaps.length)
91
+ return merged;
92
+ const statusById = new Map();
93
+ for (const s of snaps) {
94
+ const arr = s.payload?.data ?? [];
95
+ for (const o of arr) {
96
+ if (o?.id && o.status)
97
+ statusById.set(String(o.id), String(o.status));
98
+ }
99
+ }
100
+ if (!statusById.size)
101
+ return merged;
102
+ let patched = false;
103
+ merged = merged.map((o) => {
104
+ const want = statusById.get(String(o.id));
105
+ if (want && want !== o.status) {
106
+ patched = true;
107
+ return { ...o, status: want };
108
+ }
109
+ return o;
110
+ });
111
+ if (patched) {
112
+ syncLogger.debug("PendingOrders", "overlaid queued statuses onto list", { businessId, count: merged.length });
113
+ }
114
+ }
115
+ catch { }
116
+ return merged;
117
+ }
51
118
  /**
52
119
  * Load persisted unsynced order:create_queued events into the store.
53
120
  * Idempotent; safe to call after app restart while still offline.
@@ -99,8 +166,12 @@ export async function hydratePendingOrders() {
99
166
  export async function applyOrderStateChangeEvent(event, origin = "local") {
100
167
  try {
101
168
  const isQueued = QUEUED_STATUS_EVENT_TYPES.has(event.event_type);
102
- if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued)
103
- return;
169
+ const isPaymentQueued = PAYMENT_QUEUED_TYPES.has(event.event_type);
170
+ if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued && !isPaymentQueued) {
171
+ // Also handle cloud payment.completed for is_paid patch
172
+ if (event.event_type !== "payment.completed")
173
+ return;
174
+ }
104
175
  syncLogger.debug("OrderState", "Order state changed", {
105
176
  origin,
106
177
  event_type: event.event_type,
@@ -111,6 +182,19 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
111
182
  // Queued transitions carry to_status/order_id so they patch instantly offline.
112
183
  if (isQueued) {
113
184
  await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
185
+ patchPendingStatus(event);
186
+ invalidator?.();
187
+ return;
188
+ }
189
+ if (isPaymentQueued) {
190
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
191
+ patchPendingPaid(event);
192
+ invalidator?.();
193
+ return;
194
+ }
195
+ if (event.event_type === "payment.completed") {
196
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
197
+ patchPendingPaid(event);
114
198
  invalidator?.();
115
199
  return;
116
200
  }
@@ -124,6 +208,46 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
124
208
  });
125
209
  }
126
210
  }
211
+ function patchPendingStatus(event) {
212
+ try {
213
+ const payload = event.payload;
214
+ const toStatus = payload.to_status || payload.status;
215
+ const orderId = payload.order_id || event.entity_id;
216
+ if (!toStatus || !orderId)
217
+ return;
218
+ const store = usePendingOrdersStore.getState();
219
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
220
+ if (!target)
221
+ return;
222
+ const patched = { ...target, status: toStatus };
223
+ store.addOrder(patched);
224
+ }
225
+ catch { }
226
+ }
227
+ function patchPendingPaid(event) {
228
+ try {
229
+ const payload = event.payload;
230
+ const orderId = payload.order_id || event.entity_id;
231
+ if (!orderId)
232
+ return;
233
+ const now = new Date().toISOString();
234
+ const store = usePendingOrdersStore.getState();
235
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === payload.order_number || o.id === event.entity_id);
236
+ const altTarget = !target ? store.orders.find((o) => o.id === orderId) : null;
237
+ const t = target ?? altTarget;
238
+ if (!t)
239
+ return;
240
+ const paidAt = payload.paid_at || now;
241
+ const patched = {
242
+ ...t,
243
+ is_paid: true,
244
+ paid_at: paidAt,
245
+ payment: { ...t.payment, is_paid: true, paid_at: paidAt },
246
+ };
247
+ store.addOrder(patched);
248
+ }
249
+ catch { }
250
+ }
127
251
  export async function applyPendingOrderEvent(event, origin = "local") {
128
252
  try {
129
253
  if (event.event_type === PENDING_EVENT_TYPE) {
@@ -144,6 +268,27 @@ export async function applyPendingOrderEvent(event, origin = "local") {
144
268
  if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
145
269
  // Queued transitions: patch the read model so listing reflects new status offline.
146
270
  await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
271
+ // Also patch pending store so isPending overlay shows new status immediately.
272
+ try {
273
+ const payload = event.payload;
274
+ const toStatus = payload.to_status || payload.status;
275
+ const orderId = payload.order_id || event.entity_id;
276
+ if (toStatus && orderId) {
277
+ const store = usePendingOrdersStore.getState();
278
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
279
+ if (target)
280
+ store.addOrder({ ...target, status: toStatus });
281
+ }
282
+ }
283
+ catch { }
284
+ const db = await openSyncDatabase();
285
+ await insertEvent(db, { event, origin });
286
+ invalidator?.();
287
+ return;
288
+ }
289
+ if (PAYMENT_QUEUED_TYPES.has(event.event_type) || event.event_type === "payment.completed") {
290
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
291
+ patchPendingPaid(event);
147
292
  const db = await openSyncDatabase();
148
293
  await insertEvent(db, { event, origin });
149
294
  invalidator?.();
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Global realtime reconciliation — RECONCILE_DEBOUNCE_MS 400 Set coalesce (§24).
3
+ * Canonical in src/sync/use-sync-reconciliation.
4
+ */
5
+ export * from "../use-sync-reconciliation";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Global realtime reconciliation — RECONCILE_DEBOUNCE_MS 400 Set coalesce (§24).
3
+ * Canonical in src/sync/use-sync-reconciliation.
4
+ */
5
+ export * from "../use-sync-reconciliation";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Entity snapshots — local read model (§9 SQLite Model).
3
+ * Canonical in src/db/operations + src/sync/order-snapshots.service.
4
+ */
5
+ export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
6
+ export { orderSnapshotsService } from "../order-snapshots.service";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Entity snapshots — local read model (§9 SQLite Model).
3
+ * Canonical in src/db/operations + src/sync/order-snapshots.service.
4
+ */
5
+ export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
6
+ export { orderSnapshotsService } from "../order-snapshots.service";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Sync Jobs FIFO — near real-time drain after local commit.
3
+ * Tries PHX business:{id} push first, then REST, queues on fail.
4
+ * Core-only, single in_flight per business, coalesced via isOnline gate.
5
+ */
6
+ export declare function drainOne(businessId: string, entityId: string): Promise<void>;
7
+ export declare function pullRemoteOrders(businessId: string): Promise<void>;
8
+ export declare function drainPending(businessId: string): Promise<void>;
9
+ export declare function setupSyncJobsDrain(): void;
10
+ export declare function getSyncJobsPendingCount(businessId: string): Promise<number>;
11
+ export declare function debugDumpSyncJobs(businessId: string): Promise<Array<{
12
+ id: string;
13
+ command_type: string;
14
+ status: string;
15
+ created_at: string;
16
+ attempts: number;
17
+ last_error: string | null;
18
+ }>>;