@teincfood/core 0.7.7 → 0.7.9

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 (48) hide show
  1. package/dist/adapters/auth.d.ts +2 -0
  2. package/dist/adapters/auth.js +7 -0
  3. package/dist/adapters/filesystem.d.ts +22 -0
  4. package/dist/adapters/filesystem.js +6 -0
  5. package/dist/adapters/print.d.ts +29 -0
  6. package/dist/adapters/print.js +12 -0
  7. package/dist/adapters/services.d.ts +3 -0
  8. package/dist/adapters/services.js +3 -0
  9. package/dist/cart/hooks.d.ts +36 -0
  10. package/dist/cart/hooks.js +45 -0
  11. package/dist/cart/store.d.ts +33 -0
  12. package/dist/cart/store.js +122 -0
  13. package/dist/catalog/hooks.d.ts +25 -0
  14. package/dist/catalog/hooks.js +160 -0
  15. package/dist/hooks/orders.hooks.d.ts +17 -0
  16. package/dist/hooks/orders.hooks.js +117 -0
  17. package/dist/image-cache/service.d.ts +29 -0
  18. package/dist/image-cache/service.js +125 -0
  19. package/dist/index.d.ts +24 -2
  20. package/dist/index.js +33 -2
  21. package/dist/printer/hooks.d.ts +32 -0
  22. package/dist/printer/hooks.js +53 -0
  23. package/dist/printer/receipt.d.ts +12 -0
  24. package/dist/printer/receipt.js +315 -0
  25. package/dist/printer/service.d.ts +31 -0
  26. package/dist/printer/service.js +119 -0
  27. package/dist/printer/store.d.ts +24 -0
  28. package/dist/printer/store.js +94 -0
  29. package/dist/printer/types.d.ts +12 -0
  30. package/dist/printer/types.js +1 -0
  31. package/dist/realtime/events.service.d.ts +18 -0
  32. package/dist/realtime/events.service.js +76 -0
  33. package/dist/realtime/order-events.service.d.ts +6 -0
  34. package/dist/realtime/order-events.service.js +17 -0
  35. package/dist/realtime/phoenix-socket.d.ts +55 -0
  36. package/dist/realtime/phoenix-socket.js +240 -0
  37. package/dist/realtime/useEventsConnection.d.ts +1 -0
  38. package/dist/realtime/useEventsConnection.js +41 -0
  39. package/dist/reference/capabilities.js +1 -0
  40. package/dist/repositories/order.repository.d.ts +1 -0
  41. package/dist/repositories/order.repository.js +9 -0
  42. package/dist/sync/command-queue.service.js +34 -2
  43. package/dist/sync/order-snapshots.service.d.ts +1 -0
  44. package/dist/sync/order-snapshots.service.js +30 -0
  45. package/dist/sync/pending-orders.service.js +81 -2
  46. package/dist/sync/transport-rest.js +12 -5
  47. package/dist/sync/use-sync-reconciliation.js +43 -15
  48. package/package.json +1 -1
@@ -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
@@ -99,8 +100,12 @@ export async function hydratePendingOrders() {
99
100
  export async function applyOrderStateChangeEvent(event, origin = "local") {
100
101
  try {
101
102
  const isQueued = QUEUED_STATUS_EVENT_TYPES.has(event.event_type);
102
- if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued)
103
- return;
103
+ const isPaymentQueued = PAYMENT_QUEUED_TYPES.has(event.event_type);
104
+ if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued && !isPaymentQueued) {
105
+ // Also handle cloud payment.completed for is_paid patch
106
+ if (event.event_type !== "payment.completed")
107
+ return;
108
+ }
104
109
  syncLogger.debug("OrderState", "Order state changed", {
105
110
  origin,
106
111
  event_type: event.event_type,
@@ -111,6 +116,19 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
111
116
  // Queued transitions carry to_status/order_id so they patch instantly offline.
112
117
  if (isQueued) {
113
118
  await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
119
+ patchPendingStatus(event);
120
+ invalidator?.();
121
+ return;
122
+ }
123
+ if (isPaymentQueued) {
124
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
125
+ patchPendingPaid(event);
126
+ invalidator?.();
127
+ return;
128
+ }
129
+ if (event.event_type === "payment.completed") {
130
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
131
+ patchPendingPaid(event);
114
132
  invalidator?.();
115
133
  return;
116
134
  }
@@ -124,6 +142,46 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
124
142
  });
125
143
  }
126
144
  }
145
+ function patchPendingStatus(event) {
146
+ try {
147
+ const payload = event.payload;
148
+ const toStatus = payload.to_status || payload.status;
149
+ const orderId = payload.order_id || event.entity_id;
150
+ if (!toStatus || !orderId)
151
+ return;
152
+ const store = usePendingOrdersStore.getState();
153
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
154
+ if (!target)
155
+ return;
156
+ const patched = { ...target, status: toStatus };
157
+ store.addOrder(patched);
158
+ }
159
+ catch { }
160
+ }
161
+ function patchPendingPaid(event) {
162
+ try {
163
+ const payload = event.payload;
164
+ const orderId = payload.order_id || event.entity_id;
165
+ if (!orderId)
166
+ return;
167
+ const now = new Date().toISOString();
168
+ const store = usePendingOrdersStore.getState();
169
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === payload.order_number || o.id === event.entity_id);
170
+ const altTarget = !target ? store.orders.find((o) => o.id === orderId) : null;
171
+ const t = target ?? altTarget;
172
+ if (!t)
173
+ return;
174
+ const paidAt = payload.paid_at || now;
175
+ const patched = {
176
+ ...t,
177
+ is_paid: true,
178
+ paid_at: paidAt,
179
+ payment: { ...t.payment, is_paid: true, paid_at: paidAt },
180
+ };
181
+ store.addOrder(patched);
182
+ }
183
+ catch { }
184
+ }
127
185
  export async function applyPendingOrderEvent(event, origin = "local") {
128
186
  try {
129
187
  if (event.event_type === PENDING_EVENT_TYPE) {
@@ -144,6 +202,27 @@ export async function applyPendingOrderEvent(event, origin = "local") {
144
202
  if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
145
203
  // Queued transitions: patch the read model so listing reflects new status offline.
146
204
  await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
205
+ // Also patch pending store so isPending overlay shows new status immediately.
206
+ try {
207
+ const payload = event.payload;
208
+ const toStatus = payload.to_status || payload.status;
209
+ const orderId = payload.order_id || event.entity_id;
210
+ if (toStatus && orderId) {
211
+ const store = usePendingOrdersStore.getState();
212
+ const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
213
+ if (target)
214
+ store.addOrder({ ...target, status: toStatus });
215
+ }
216
+ }
217
+ catch { }
218
+ const db = await openSyncDatabase();
219
+ await insertEvent(db, { event, origin });
220
+ invalidator?.();
221
+ return;
222
+ }
223
+ if (PAYMENT_QUEUED_TYPES.has(event.event_type) || event.event_type === "payment.completed") {
224
+ await orderSnapshotsService.applyPaymentEvent(event, origin);
225
+ patchPendingPaid(event);
147
226
  const db = await openSyncDatabase();
148
227
  await insertEvent(db, { event, origin });
149
228
  invalidator?.();
@@ -50,11 +50,12 @@ export class RestTransport {
50
50
  case "order:create": {
51
51
  const posPayload = {
52
52
  ...payload,
53
+ id: command.command_id,
53
54
  command_id: command.command_id,
54
55
  local_order_number: payload.order_number,
55
56
  };
56
57
  const response = await posService.createPOSOrder(business_id, posPayload);
57
- return this.wrapResponse("order.created", response.data, entity_id, timestamp, command);
58
+ return this.wrapResponse("order.created", response.data, entity_id ?? command.command_id, timestamp, command);
58
59
  }
59
60
  case "order:accept": {
60
61
  const response = await sellerService.acceptOrder(entity_id);
@@ -89,8 +90,9 @@ export class RestTransport {
89
90
  return this.wrapResponse("delivery.status_changed", response, entity_id, timestamp, command);
90
91
  }
91
92
  case "menu:create_item": {
92
- const response = await menuService.createMenuItem(business_id, payload);
93
- return this.wrapResponse("menu.item_created", response, entity_id, timestamp, command);
93
+ const createPayload = { ...payload, id: payload.id ?? command.command_id, command_id: command.command_id };
94
+ const response = await menuService.createMenuItem(business_id, createPayload);
95
+ return this.wrapResponse("menu.item_created", response, command.command_id, timestamp, command);
94
96
  }
95
97
  case "menu:update_item": {
96
98
  const response = await menuService.updateMenuItem(entity_id, payload);
@@ -101,8 +103,13 @@ export class RestTransport {
101
103
  return this.wrapResponse("menu.item_deleted", response, entity_id, timestamp, command);
102
104
  }
103
105
  case "menu:create_category": {
104
- const response = await menuService.createCategory(business_id, payload);
105
- return this.wrapResponse("menu.category_created", response, entity_id, timestamp, command);
106
+ const createPayload = { ...payload, id: payload.id ?? command.command_id, command_id: command.command_id };
107
+ const response = await menuService.createCategory(business_id, createPayload);
108
+ return this.wrapResponse("menu.category_created", response, command.command_id, timestamp, command);
109
+ }
110
+ case "payment:confirm_cash": {
111
+ const response = await sellerService.confirmCashPayment(entity_id);
112
+ return this.wrapResponse("payment.completed", response, entity_id, timestamp, command);
106
113
  }
107
114
  default:
108
115
  throw new Error(`RestTransport: unsupported command type ${command_type}`);
@@ -10,6 +10,7 @@
10
10
  import { useEffect } from "react";
11
11
  import { useQueryClient } from "@tanstack/react-query";
12
12
  import { syncEngine } from "./engine";
13
+ import { getActiveBusiness } from "../adapters/business";
13
14
  import { applyPendingOrderEvent, applyOrderStateChangeEvent, registerPendingOrdersInvalidator, } from "./pending-orders.service";
14
15
  import { localNodeService } from "../adapters/local-node";
15
16
  import { getSyncDatabase } from "../db/connection";
@@ -24,16 +25,42 @@ function isLocalOrderEvent(event) {
24
25
  const localOrderNumber = payload.local_order_number;
25
26
  return typeof localOrderNumber === "string" && /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(localOrderNumber);
26
27
  }
28
+ const RECONCILE_DEBOUNCE_MS = 400;
27
29
  export function useSyncReconciliation() {
28
30
  const queryClient = useQueryClient();
29
31
  useEffect(() => {
32
+ let timer;
33
+ const pending = new Set();
34
+ const flush = () => {
35
+ timer = undefined;
36
+ if (pending.has("reference")) {
37
+ queryClient.invalidateQueries({ queryKey: MENU_KEYS.all });
38
+ queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
39
+ queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
40
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
41
+ }
42
+ if (pending.has("pending")) {
43
+ queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
44
+ queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
45
+ queryClient.invalidateQueries({ queryKey: KIOSK_KEYS.all });
46
+ queryClient.invalidateQueries({ queryKey: SELLER_ORDERS_KEYS.all });
47
+ queryClient.invalidateQueries({ queryKey: BUSINESS_DELIVERIES_KEYS.all });
48
+ }
49
+ if (pending.has("order")) {
50
+ // order.created with local number already covered by pending, but keep fine-grained
51
+ // POS.orders(businessId) is also covered by pending's broad POS.all, no extra needed.
52
+ }
53
+ pending.clear();
54
+ };
55
+ const schedule = (kind) => {
56
+ pending.add(kind);
57
+ if (!timer)
58
+ timer = setTimeout(flush, RECONCILE_DEBOUNCE_MS);
59
+ };
30
60
  // Reference changes re-sync offline data and refresh caches on this device
31
61
  // and (via the LAN relay) on every connected terminal.
32
62
  const unregisterReferenceInvalidator = registerReferenceInvalidator(() => {
33
- queryClient.invalidateQueries({ queryKey: MENU_KEYS.all });
34
- queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
35
- queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
36
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
63
+ schedule("reference");
37
64
  });
38
65
  // Pending-order reconciliation invalidates POS + seller order lists so the
39
66
  // server-confirmed order replaces the optimistic one. Invalidate from the
@@ -42,17 +69,21 @@ export function useSyncReconciliation() {
42
69
  // keys. Also cover kiosk/seller-orders/business-deliveries so boards
43
70
  // refresh even when there is no Local Node (offline queue + Phoenix).
44
71
  const unregisterInvalidator = registerPendingOrdersInvalidator(() => {
45
- queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
46
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
47
- queryClient.invalidateQueries({ queryKey: KIOSK_KEYS.all });
48
- queryClient.invalidateQueries({ queryKey: SELLER_ORDERS_KEYS.all });
49
- queryClient.invalidateQueries({ queryKey: BUSINESS_DELIVERIES_KEYS.all });
72
+ schedule("pending");
50
73
  });
51
74
  // Reference-change events are published through the Sync Engine but are
52
75
  // not backed by a command, so the engine has no transport dependency on
53
76
  // the reference module. Register the singleton once.
54
77
  registerReferenceEventEngine(syncEngine);
55
78
  const unsubscribe = syncEngine.subscribe(async (event) => {
79
+ // LAN strict: ignore events for other business on same subnet
80
+ try {
81
+ const activeId = getActiveBusiness()?.id;
82
+ const payloadBid = event.payload?.business_id;
83
+ if (activeId && payloadBid && payloadBid !== activeId)
84
+ return;
85
+ }
86
+ catch { }
56
87
  syncLogger.event(event, "received");
57
88
  // The node relays engine events (e.g. order.created when a queued
58
89
  // command is replayed) to LAN clients so they reconcile their pending
@@ -90,15 +121,12 @@ export function useSyncReconciliation() {
90
121
  error: String(error),
91
122
  });
92
123
  }
93
- const payload = event.payload;
94
- const businessId = payload.business_id;
95
- if (businessId) {
96
- queryClient.invalidateQueries({ queryKey: POS_KEYS.orders(businessId) });
97
- queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
98
- }
124
+ schedule("pending");
99
125
  }
100
126
  });
101
127
  return () => {
128
+ if (timer)
129
+ clearTimeout(timer);
102
130
  unsubscribe();
103
131
  unregisterInvalidator();
104
132
  unregisterReferenceInvalidator();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
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",