@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
@@ -5,9 +5,12 @@
5
5
  * resulting domain events. This transport is used when the cloud API is
6
6
  * reachable.
7
7
  */
8
- import { posService, sellerService, kioskService, menuService } from "../adapters/services";
8
+ import { sellerService, kioskService, menuService } from "../adapters/services";
9
9
  import { syncLogger } from "../utils/sync-logger";
10
10
  import { generateEventId } from "../utils/uuid";
11
+ import { createLocalOrder, transitionLocalOrder } from "../domain/orders/service";
12
+ import { openSyncDatabase } from "../db/connection";
13
+ import { getOrderById, updateOrderStatus } from "../db/operations";
11
14
  export class RestTransport {
12
15
  constructor() {
13
16
  this.name = "rest";
@@ -45,41 +48,38 @@ export class RestTransport {
45
48
  }
46
49
  async dispatchCommand(command) {
47
50
  const { command_type, business_id, entity_id, payload } = command;
48
- const timestamp = new Date().toISOString();
51
+ // Use the command's original timestamp (set at creation time) so the event
52
+ // reflects when the mutation actually happened, not when it was dispatched.
53
+ const timestamp = command.timestamp || new Date().toISOString();
49
54
  switch (command_type) {
55
+ // Single create path — createLocalOrder owns SQLite + sync_jobs + the
56
+ // operating-hours gate. No direct-REST fallback: guard rejections
57
+ // (BUSINESS_CLOSED / HOURS_NOT_SYNCED) must stand, never be bypassed.
50
58
  case "order:create": {
51
- const posPayload = {
52
- ...payload,
53
- id: command.command_id,
59
+ const order = await createLocalOrder(business_id, {
60
+ ...(payload ?? {}),
54
61
  command_id: command.command_id,
55
- local_order_number: payload.order_number,
56
- };
57
- const response = await posService.createPOSOrder(business_id, posPayload);
58
- return this.wrapResponse("order.created", response.data, entity_id ?? command.command_id, timestamp, command);
62
+ id: entity_id ?? command.command_id,
63
+ });
64
+ return this.wrapResponse("order.created", order, order.id, timestamp, command);
59
65
  }
60
66
  case "order:accept": {
61
- const response = await sellerService.acceptOrder(entity_id);
62
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
67
+ return this.executeOrderTransition(command, "accept", timestamp, () => sellerService.acceptOrder(entity_id));
63
68
  }
64
69
  case "order:reject": {
65
- const response = await sellerService.rejectOrder(entity_id);
66
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
70
+ return this.executeOrderTransition(command, "reject", timestamp, () => sellerService.rejectOrder(entity_id));
67
71
  }
68
72
  case "order:start_preparing": {
69
- const response = await sellerService.startPreparingOrder(entity_id);
70
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
73
+ return this.executeOrderTransition(command, "start_preparing", timestamp, () => sellerService.startPreparingOrder(entity_id));
71
74
  }
72
75
  case "order:mark_ready": {
73
- const response = await sellerService.markOrderReady(entity_id);
74
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
76
+ return this.executeOrderTransition(command, "mark_ready", timestamp, () => sellerService.markOrderReady(entity_id));
75
77
  }
76
78
  case "order:complete_pickup": {
77
- const response = await sellerService.completeOrderPickup(entity_id);
78
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
79
+ return this.executeOrderTransition(command, "complete_pickup", timestamp, () => sellerService.completeOrderPickup(entity_id));
79
80
  }
80
81
  case "order:cancel": {
81
- const response = await sellerService.cancelOrder(entity_id);
82
- return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
82
+ return this.executeOrderTransition(command, "cancel", timestamp, () => sellerService.cancelOrder(entity_id));
83
83
  }
84
84
  case "delivery:assign": {
85
85
  const response = await kioskService.assignKioskOrderToRider(entity_id, payload.rider_id);
@@ -90,12 +90,18 @@ export class RestTransport {
90
90
  return this.wrapResponse("delivery.status_changed", response, entity_id, timestamp, command);
91
91
  }
92
92
  case "menu:create_item": {
93
- const createPayload = { ...payload, id: payload.id ?? command.command_id, command_id: command.command_id };
93
+ // cacheRecord is local-cache only — strip it so the backend receives
94
+ // the flat wire payload it accepts (it owns canonical on converge).
95
+ const { cacheRecord: _omitCreate, ...wireCreate } = payload;
96
+ void _omitCreate;
97
+ const createPayload = { ...wireCreate, id: wireCreate.id ?? command.command_id, command_id: command.command_id };
94
98
  const response = await menuService.createMenuItem(business_id, createPayload);
95
99
  return this.wrapResponse("menu.item_created", response, command.command_id, timestamp, command);
96
100
  }
97
101
  case "menu:update_item": {
98
- const response = await menuService.updateMenuItem(entity_id, payload);
102
+ const { cacheRecord: _omitUpdate, ...wireUpdate } = payload;
103
+ void _omitUpdate;
104
+ const response = await menuService.updateMenuItem(entity_id, wireUpdate);
99
105
  return this.wrapResponse("menu.item_updated", response, entity_id, timestamp, command);
100
106
  }
101
107
  case "menu:delete_item": {
@@ -103,18 +109,70 @@ export class RestTransport {
103
109
  return this.wrapResponse("menu.item_deleted", response, entity_id, timestamp, command);
104
110
  }
105
111
  case "menu:create_category": {
106
- const createPayload = { ...payload, id: payload.id ?? command.command_id, command_id: command.command_id };
112
+ const { cacheRecord: _omitCatCreate, ...wireCatCreate } = payload;
113
+ void _omitCatCreate;
114
+ const createPayload = { ...wireCatCreate, id: wireCatCreate.id ?? command.command_id, command_id: command.command_id };
107
115
  const response = await menuService.createCategory(business_id, createPayload);
108
116
  return this.wrapResponse("menu.category_created", response, command.command_id, timestamp, command);
109
117
  }
118
+ case "menu:update_category": {
119
+ const { cacheRecord: _omitCatUpdate, ...wireCatUpdate } = payload;
120
+ void _omitCatUpdate;
121
+ const response = await menuService.updateCategory(entity_id, wireCatUpdate);
122
+ return this.wrapResponse("menu.category_updated", response, entity_id, timestamp, command);
123
+ }
124
+ case "menu:delete_category": {
125
+ const response = await menuService.deleteCategory(entity_id);
126
+ return this.wrapResponse("menu.category_deleted", response, entity_id, timestamp, command);
127
+ }
110
128
  case "payment:confirm_cash": {
111
- const response = await sellerService.confirmCashPayment(entity_id);
112
- return this.wrapResponse("payment.completed", response, entity_id, timestamp, command);
129
+ return this.executeOrderTransition(command, "confirm_cash", timestamp, () => sellerService.confirmCashPayment(entity_id), "payment.completed");
113
130
  }
114
131
  default:
115
132
  throw new Error(`RestTransport: unsupported command type ${command_type}`);
116
133
  }
117
134
  }
135
+ /**
136
+ * Order transitions go through the domain service first (single write path:
137
+ * SQLite + sync_jobs). If this device never saw the order (e.g. a LAN node
138
+ * handling a cloud-created order before its first pull), fall back to the
139
+ * previous direct-REST behavior so availability never regresses.
140
+ */
141
+ async executeOrderTransition(command, action, timestamp, restFallback, eventType = "order.status_changed") {
142
+ const { business_id, entity_id } = command;
143
+ try {
144
+ const order = await transitionLocalOrder(business_id, entity_id, action);
145
+ const data = { ...order, order_id: order.id };
146
+ return this.wrapResponse(eventType, data, order.id, timestamp, command);
147
+ }
148
+ catch (error) {
149
+ syncLogger.warn("RestTransport", "domain transition unavailable, REST fallback", {
150
+ command_type: command.command_type,
151
+ entity_id,
152
+ error: error instanceof Error ? error.message : String(error),
153
+ });
154
+ const response = await restFallback();
155
+ const raw = response.data;
156
+ const data = { ...raw };
157
+ const orderId = data.id ?? entity_id;
158
+ if (!data.order_id)
159
+ data.order_id = orderId;
160
+ if (business_id && orderId) {
161
+ try {
162
+ const db = await openSyncDatabase();
163
+ const existing = await getOrderById(db, business_id, orderId);
164
+ if (existing) {
165
+ const newStatus = data.status ?? existing.status;
166
+ if (newStatus !== existing.status) {
167
+ await updateOrderStatus(db, business_id, orderId, newStatus);
168
+ }
169
+ }
170
+ }
171
+ catch { }
172
+ }
173
+ return this.wrapResponse(eventType, data, orderId, timestamp, command);
174
+ }
175
+ }
118
176
  wrapResponse(eventType, data, entityId, timestamp, command) {
119
177
  return {
120
178
  event_id: generateEventId(),
@@ -16,7 +16,8 @@ import { localNodeService } from "../adapters/local-node";
16
16
  import { getSyncDatabase } from "../db/connection";
17
17
  import { markEventsSynced } from "../db/operations";
18
18
  import { syncLogger } from "../utils/sync-logger";
19
- import { POS_KEYS, SELLER_KEYS, KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS, } from "../adapters/query-keys";
19
+ import { subscribeDbChanges } from "../db/operations";
20
+ import { POS_KEYS, SELLER_KEYS, KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS, SELLER_DASHBOARD_KEYS, } from "../adapters/query-keys";
20
21
  import { applyReferenceChangeEvent, registerReferenceInvalidator, registerReferenceEventEngine, REFERENCE_CHANGED_EVENT_TYPE, } from "../reference/events";
21
22
  function isLocalOrderEvent(event) {
22
23
  const payload = event.payload;
@@ -38,6 +39,7 @@ export function useSyncReconciliation() {
38
39
  queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
39
40
  queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
40
41
  queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
42
+ queryClient.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all });
41
43
  }
42
44
  if (pending.has("pending")) {
43
45
  queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
@@ -45,6 +47,7 @@ export function useSyncReconciliation() {
45
47
  queryClient.invalidateQueries({ queryKey: KIOSK_KEYS.all });
46
48
  queryClient.invalidateQueries({ queryKey: SELLER_ORDERS_KEYS.all });
47
49
  queryClient.invalidateQueries({ queryKey: BUSINESS_DELIVERIES_KEYS.all });
50
+ queryClient.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all });
48
51
  }
49
52
  if (pending.has("order")) {
50
53
  // order.created with local number already covered by pending, but keep fine-grained
@@ -105,6 +108,10 @@ export function useSyncReconciliation() {
105
108
  // Order/delivery state changed: refresh order caches so this terminal
106
109
  // shows the same order states as the device that made the change.
107
110
  await applyOrderStateChangeEvent(event, "local");
111
+ // Any order/delivery event should refresh dashboard recent orders immediately (fresh on transition)
112
+ if (event.event_type.startsWith("order.") || event.event_type.startsWith("delivery.")) {
113
+ schedule("pending");
114
+ }
108
115
  // Route both queued (optimistic) and reconciled (created) order events
109
116
  // into the pending-orders store.
110
117
  if (event.event_type === "order.create_queued" ||
@@ -124,12 +131,19 @@ export function useSyncReconciliation() {
124
131
  schedule("pending");
125
132
  }
126
133
  });
134
+ const dbUnsub = subscribeDbChanges((info) => {
135
+ if (info.table.startsWith("reference_data"))
136
+ schedule("reference");
137
+ else if (info.table === "orders" || info.table.startsWith("entity_snapshots"))
138
+ schedule("pending");
139
+ });
127
140
  return () => {
128
141
  if (timer)
129
142
  clearTimeout(timer);
130
143
  unsubscribe();
131
144
  unregisterInvalidator();
132
145
  unregisterReferenceInvalidator();
146
+ dbUnsub();
133
147
  };
134
148
  }, [queryClient]);
135
149
  }
@@ -5,6 +5,8 @@ export function createMemoryDriver() {
5
5
  entity_snapshots: [],
6
6
  commands: [],
7
7
  events: [],
8
+ orders: [],
9
+ sync_jobs: [],
8
10
  };
9
11
  let autoId = { commands: 1, events: 1, migrations: 1 };
10
12
  const driver = {
@@ -93,9 +95,13 @@ export function createMemoryDriver() {
93
95
  tables.commands.push(row);
94
96
  return { lastInsertRowId: row.id, changes: 1 };
95
97
  }
96
- // commands update status
98
+ // commands update status (4- or 5-placeholder variants — always take the
99
+ // LAST arg as command_id; older code assumed 5).
97
100
  if (s.includes("UPDATE commands SET status = ?")) {
98
- const [status, error, _now, _now2, command_id] = args;
101
+ const argv = args;
102
+ const status = argv[0];
103
+ const error = argv[1];
104
+ const command_id = argv[argv.length - 1];
99
105
  // also handles variants with retry_count increment
100
106
  const row = tables.commands.find((r) => r.command_id === command_id);
101
107
  if (row) {
@@ -126,10 +132,17 @@ export function createMemoryDriver() {
126
132
  return { lastInsertRowId: 0, changes: 1 };
127
133
  }
128
134
  if (s.includes("UPDATE commands SET payload")) {
129
- const [payload, command_id] = args;
135
+ // handles both old (payload, command_id) and new (payload, error, updated_at, command_id)
136
+ const payload = args[0];
137
+ const command_id = args[args.length - 1];
130
138
  const row = tables.commands.find((r) => r.command_id === command_id);
131
- if (row)
139
+ if (row) {
132
140
  row.payload = payload;
141
+ if (args.length === 4) {
142
+ row.error = args[1];
143
+ row.updated_at = args[2];
144
+ }
145
+ }
133
146
  return { lastInsertRowId: 0, changes: 1 };
134
147
  }
135
148
  // events insert
@@ -162,6 +175,101 @@ export function createMemoryDriver() {
162
175
  row.synced_to_cloud = 1;
163
176
  return { lastInsertRowId: 0, changes: ids.length };
164
177
  }
178
+ // orders insert (primary DB)
179
+ if (s.includes("INSERT INTO orders")) {
180
+ const [id, business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, hlc] = args;
181
+ const existing = tables.orders.find((r) => r.id === id);
182
+ // LWW check via hlc (if existing and incoming older, skip)
183
+ if (existing) {
184
+ if (hlc && existing.hlc && hlc < existing.hlc)
185
+ return { lastInsertRowId: 0, changes: 0 };
186
+ Object.assign(existing, { business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, hlc });
187
+ return { lastInsertRowId: 0, changes: 1 };
188
+ }
189
+ tables.orders.push({ id, business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, hlc, deleted_at: null });
190
+ return { lastInsertRowId: 1, changes: 1 };
191
+ }
192
+ // sync_jobs insert
193
+ if (s.includes("INSERT OR IGNORE INTO sync_jobs")) {
194
+ const [id, business_id, command_type, entity_id, payload, created_at, updated_at] = args;
195
+ if (tables.sync_jobs.find((r) => r.id === id))
196
+ return { lastInsertRowId: 0, changes: 0 };
197
+ tables.sync_jobs.push({ id, business_id, command_type, entity_id, payload, status: "pending", attempts: 0, created_at, updated_at });
198
+ return { lastInsertRowId: 1, changes: 1 };
199
+ }
200
+ // sync_jobs update status=completed
201
+ if (s.includes("UPDATE sync_jobs SET status='completed'")) {
202
+ const [updated_at, id] = args;
203
+ const row = tables.sync_jobs.find((r) => r.id === id);
204
+ if (row) {
205
+ row.status = "completed";
206
+ row.updated_at = updated_at;
207
+ }
208
+ return { lastInsertRowId: 0, changes: 1 };
209
+ }
210
+ if (s.includes("UPDATE sync_jobs SET payload=")) {
211
+ const [payload, updated_at, id] = args;
212
+ const row = tables.sync_jobs.find((r) => r.id === id);
213
+ if (row) {
214
+ row.payload = payload;
215
+ row.updated_at = updated_at;
216
+ }
217
+ return { lastInsertRowId: 0, changes: 1 };
218
+ }
219
+ if (s.includes("UPDATE sync_jobs SET attempts = attempts + 1")) {
220
+ const [last_error, updated_at, id] = args;
221
+ const row = tables.sync_jobs.find((r) => r.id === id);
222
+ if (row) {
223
+ row.attempts = (row.attempts ?? 0) + 1;
224
+ row.last_error = last_error;
225
+ row.updated_at = updated_at;
226
+ }
227
+ return { lastInsertRowId: 0, changes: 1 };
228
+ }
229
+ // orders updates
230
+ if (s.includes("UPDATE orders SET status = ?")) {
231
+ const [status, hlc, business_id, id1, id2] = args;
232
+ const row = tables.orders.find((r) => r.business_id === business_id && (r.id === id1 || r.order_number === id2));
233
+ if (row) {
234
+ row.status = status;
235
+ row.hlc = hlc;
236
+ }
237
+ return { lastInsertRowId: 0, changes: 1 };
238
+ }
239
+ if (s.includes("UPDATE orders SET payment = ?")) {
240
+ const [payment, hlc, business_id, id1, id2] = args;
241
+ const row = tables.orders.find((r) => r.business_id === business_id && (r.id === id1 || r.order_number === id2));
242
+ if (row) {
243
+ row.payment = payment;
244
+ row.hlc = hlc;
245
+ }
246
+ return { lastInsertRowId: 0, changes: 1 };
247
+ }
248
+ if (s.includes("UPDATE orders SET order_number=")) {
249
+ const [order_number, hlc, business_id, id] = args;
250
+ const row = tables.orders.find((r) => r.business_id === business_id && r.id === id);
251
+ if (row) {
252
+ row.order_number = order_number;
253
+ row.hlc = hlc;
254
+ }
255
+ return { lastInsertRowId: 0, changes: 1 };
256
+ }
257
+ // cleanup deletes for v7
258
+ if (s.includes("DELETE FROM orders WHERE order_number = '[object Object]'")) {
259
+ const before = tables.orders.length;
260
+ tables.orders = tables.orders.filter((r) => r.order_number !== "[object Object]");
261
+ return { lastInsertRowId: 0, changes: before - tables.orders.length };
262
+ }
263
+ if (s.includes("DELETE FROM sync_jobs WHERE payload LIKE")) {
264
+ const before = tables.sync_jobs.length;
265
+ tables.sync_jobs = tables.sync_jobs.filter((r) => !String(r.payload).includes("[object Object]"));
266
+ return { lastInsertRowId: 0, changes: before - tables.sync_jobs.length };
267
+ }
268
+ if (s.includes("DELETE FROM entity_snapshots WHERE payload LIKE")) {
269
+ const before = tables.entity_snapshots.length;
270
+ tables.entity_snapshots = tables.entity_snapshots.filter((r) => !String(r.payload).includes("[object Object]"));
271
+ return { lastInsertRowId: 0, changes: before - tables.entity_snapshots.length };
272
+ }
165
273
  return { lastInsertRowId: 0, changes: 0 };
166
274
  },
167
275
  async getFirstAsync(sql, ...args) {
@@ -186,6 +294,11 @@ export function createMemoryDriver() {
186
294
  const row = tables.entity_snapshots.find((r) => r.entity_type === entity_type && r.entity_id === entity_id);
187
295
  return (row ? { last_event_timestamp: row.last_event_timestamp } : null);
188
296
  }
297
+ if (s.includes("SELECT * FROM entity_snapshots WHERE entity_type = ? AND entity_id = ? AND business_id =")) {
298
+ const [entity_type, entity_id, business_id] = args;
299
+ const row = tables.entity_snapshots.find((r) => r.entity_type === entity_type && r.entity_id === entity_id && r.business_id === business_id);
300
+ return row ?? null;
301
+ }
189
302
  if (s.includes("SELECT * FROM entity_snapshots WHERE entity_type = ? AND entity_id =")) {
190
303
  const [entity_type, entity_id] = args;
191
304
  const row = tables.entity_snapshots.find((r) => r.entity_type === entity_type && r.entity_id === entity_id);
@@ -196,6 +309,25 @@ export function createMemoryDriver() {
196
309
  const row = tables.commands.find((r) => r.id === id);
197
310
  return row ?? null;
198
311
  }
312
+ // orders count
313
+ if (s.includes("SELECT COUNT(*) as c FROM orders WHERE")) {
314
+ const business_id = args[0];
315
+ const c = tables.orders.filter((r) => r.business_id === business_id).length;
316
+ return { c };
317
+ }
318
+ // orders get by id or order_number
319
+ if (s.includes("SELECT * FROM orders WHERE business_id = ? AND (id = ? OR order_number = ?)")) {
320
+ const [business_id, id1, id2] = args;
321
+ const row = tables.orders.find((r) => r.business_id === business_id && (r.id === id1 || r.order_number === id2));
322
+ return row ?? null;
323
+ }
324
+ // sync_jobs pending single
325
+ if (s.includes("SELECT * FROM sync_jobs WHERE business_id = ? AND entity_id = ? AND status='pending'")) {
326
+ const [business_id, entity_id] = args;
327
+ const row = tables.sync_jobs.find((r) => r.business_id === business_id && r.entity_id === entity_id && r.status === "pending");
328
+ return row ?? null;
329
+ }
330
+ // generic orders exists check for migrations backfill: SELECT * FROM orders WHERE business_id=? (not needed)
199
331
  return null;
200
332
  },
201
333
  async getAllAsync(sql, ...args) {
@@ -204,6 +336,11 @@ export function createMemoryDriver() {
204
336
  const [business_id] = args;
205
337
  return tables.reference_data.filter((r) => r.business_id === business_id).map((r) => ({ key: r.key }));
206
338
  }
339
+ if (s.includes("SELECT * FROM entity_snapshots WHERE entity_type = ? AND business_id = ? ORDER BY last_event_timestamp DESC")) {
340
+ const [entity_type, business_id] = args;
341
+ const rows = tables.entity_snapshots.filter((r) => r.entity_type === entity_type && r.business_id === business_id).sort((a, b) => (a.last_event_timestamp < b.last_event_timestamp ? 1 : -1));
342
+ return rows;
343
+ }
207
344
  if (s.includes("SELECT * FROM entity_snapshots WHERE entity_type = ? ORDER BY last_event_timestamp DESC")) {
208
345
  const [entity_type] = args;
209
346
  const rows = tables.entity_snapshots.filter((r) => r.entity_type === entity_type).sort((a, b) => (a.last_event_timestamp < b.last_event_timestamp ? 1 : -1));
@@ -230,6 +367,47 @@ export function createMemoryDriver() {
230
367
  rows = rows.slice(0, limit);
231
368
  return rows;
232
369
  }
370
+ // orders list with filters (simple business_id filter + pagination)
371
+ if (s.includes("SELECT * FROM orders WHERE")) {
372
+ const business_id = args[0];
373
+ // args includes where-clause params plus LIMIT and OFFSET at end
374
+ // For test, just filter by business_id and apply basic status/q if present in sql string via args slicing?
375
+ // Simpler: return all orders for that business, sorted by inserted_at desc, handle LIMIT/OFFSET manually
376
+ let rows = tables.orders.filter((r) => r.business_id === business_id);
377
+ // handle status filter if sql contains status IN
378
+ if (s.includes("status IN")) {
379
+ // find args that are status values (excluding business_id, limit, offset)
380
+ // Our sql: SELECT * FROM orders WHERE business_id = ? AND status IN (?,?) ... LIMIT ? OFFSET ?
381
+ // So statuses are args[1]..args[?]
382
+ // Count statuses by parsing SQL placeholder count?
383
+ // For test, just extract known statuses from args that look like status strings
384
+ const possibleStatuses = ["pending", "awaiting_payment", "accepted", "preparing", "ready", "waiting_pickup", "on_the_way", "arrived", "delivered", "cancelled", "assigned", "picked_up", "en_route", "failed"];
385
+ const statusArgs = args.filter((v) => possibleStatuses.includes(String(v)));
386
+ if (statusArgs.length)
387
+ rows = rows.filter((r) => statusArgs.includes(r.status));
388
+ }
389
+ rows = rows.sort((a, b) => (a.inserted_at < b.inserted_at ? 1 : -1));
390
+ const limit = args[args.length - 2];
391
+ const offset = args[args.length - 1];
392
+ if (typeof limit === "number" && typeof offset === "number") {
393
+ rows = rows.slice(offset, offset + limit);
394
+ }
395
+ return rows;
396
+ }
397
+ // sync_jobs pending list (generic, handles LIMIT 1/20)
398
+ if (s.includes("SELECT entity_id FROM sync_jobs") && s.includes("status='pending'")) {
399
+ const [business_id] = args;
400
+ const m = s.match(/LIMIT\s+(\d+)/);
401
+ const lim = m ? parseInt(m[1], 10) : 20;
402
+ let rows = tables.sync_jobs.filter((r) => r.business_id === business_id && r.status === "pending").sort((a, b) => (a.created_at < b.created_at ? -1 : 1)).map((r) => ({ entity_id: r.entity_id }));
403
+ rows = rows.slice(0, lim);
404
+ return rows;
405
+ }
406
+ if (s.includes("SELECT * FROM sync_jobs WHERE") || s.includes("SELECT id, command_type, status, created_at FROM sync_jobs")) {
407
+ const [business_id] = args;
408
+ const rows = tables.sync_jobs.filter((r) => r.business_id === business_id);
409
+ return rows;
410
+ }
233
411
  return [];
234
412
  },
235
413
  };
@@ -0,0 +1 @@
1
+ export declare function seedOpenHours(businessId: string): Promise<void>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Test helper — seeds 24/7 operating hours into SQLite reference_data.
3
+ * Creation is fail-closed: without cached hours every create throws
4
+ * HOURS_NOT_SYNCED, so tests that create orders must seed first.
5
+ */
6
+ import { openSyncDatabase } from "../db/connection";
7
+ import { setReferenceData } from "../reference/operations";
8
+ export async function seedOpenHours(businessId) {
9
+ const db = await openSyncDatabase();
10
+ const entries = Array.from({ length: 7 }, (_, dayOfWeek) => ({
11
+ day_of_week: dayOfWeek,
12
+ // "24:00:00" close keeps string-compare open for the whole UTC day.
13
+ open_time: "00:00:00",
14
+ close_time: "24:00:00",
15
+ is_closed: false,
16
+ }));
17
+ await setReferenceData(db, businessId, {
18
+ key: "operating-hours",
19
+ version: "test-open",
20
+ payload: { data: entries },
21
+ downloadedAt: new Date().toISOString(),
22
+ });
23
+ }
@@ -8,6 +8,8 @@ export interface Business {
8
8
  status: string;
9
9
  currency: string;
10
10
  is_open: boolean;
11
+ /** Seller-facing open/close toggle (present when backend serializes it). */
12
+ manually_closed?: boolean;
11
13
  owner_id: string;
12
14
  business_number: string;
13
15
  latitude: number;
@@ -135,3 +137,43 @@ export interface TaxRuleRequest {
135
137
  display_order?: number;
136
138
  };
137
139
  }
140
+ export interface BusinessMemberInvitation {
141
+ id: string;
142
+ business_id: string;
143
+ invited_email: string;
144
+ role: string;
145
+ status: string;
146
+ expires_at: string;
147
+ inserted_at: string;
148
+ invited_by?: {
149
+ id: string;
150
+ full_name: string;
151
+ email: string;
152
+ } | null;
153
+ }
154
+ export interface BusinessMemberInvitationListResponse {
155
+ data: BusinessMemberInvitation[];
156
+ }
157
+ export interface UserInvitation {
158
+ id: string;
159
+ type: 'business_owner' | 'fleet_rider' | 'business_member' | 'rider_linked';
160
+ status: string;
161
+ business_name: string | null;
162
+ business_id: string | null;
163
+ code: string | null;
164
+ role?: string;
165
+ is_fleet_managed: boolean;
166
+ created_at: string | null;
167
+ }
168
+ export interface RoleCapabilityItem {
169
+ key: string;
170
+ label: string;
171
+ description?: string;
172
+ }
173
+ export interface RoleCapability {
174
+ label: string;
175
+ capabilities: RoleCapabilityItem[];
176
+ }
177
+ export interface RolesResponse {
178
+ data: Record<string, RoleCapability>;
179
+ }
@@ -78,13 +78,28 @@ export interface CatalogItemRequest {
78
78
  currency_code: string;
79
79
  dietary_tags?: string[];
80
80
  variants: {
81
+ id?: string;
81
82
  name: string;
82
83
  price_adjustment: number;
83
84
  }[];
84
85
  extras: {
86
+ id?: string;
85
87
  name: string;
86
88
  price_minor: number;
87
89
  }[];
88
90
  available: boolean;
89
91
  image_url?: string | null;
90
92
  }
93
+ /** menu:create_item / menu:update_item command payload. */
94
+ export interface MenuItemCommandPayload extends CatalogItemRequest {
95
+ cacheRecord: MenuItem;
96
+ }
97
+ /** menu:create_category / menu:update_category command payload. */
98
+ export interface MenuCategoryCommandPayload {
99
+ name: string;
100
+ description?: string;
101
+ sort_order?: number;
102
+ categoryId?: string;
103
+ id?: string;
104
+ cacheRecord: MenuCategory;
105
+ }
@@ -108,6 +108,7 @@ export interface SellerOrder {
108
108
  user: OrderUser | null;
109
109
  items: OrderItem[];
110
110
  inserted_at: string;
111
+ updated_at?: string;
111
112
  rider?: {
112
113
  id: string;
113
114
  name: string | null;
@@ -170,3 +171,22 @@ export interface BusinessDelivery {
170
171
  rider: DeliveryRider | null;
171
172
  }
172
173
  export type BusinessDeliveriesResponse = PaginatedResponse<BusinessDelivery>;
174
+ export interface SellerDashboardMetrics {
175
+ today_orders: number;
176
+ today_revenue_minor: number;
177
+ pending_orders: number;
178
+ average_rating: number;
179
+ active_orders: number;
180
+ }
181
+ export interface SellerDashboardResponse {
182
+ data: {
183
+ metrics: SellerDashboardMetrics;
184
+ recent_orders: Array<{
185
+ id: string;
186
+ order_number: string;
187
+ status: string;
188
+ total_minor: number;
189
+ item_count: number;
190
+ }>;
191
+ };
192
+ }
@@ -0,0 +1,19 @@
1
+ /** Place autocomplete/detail types — backend places payloads. */
2
+ export interface PlaceSuggestion {
3
+ placeId: string;
4
+ description: string;
5
+ mainText: string;
6
+ secondaryText: string;
7
+ }
8
+ export interface PlaceDetails {
9
+ placeId: string;
10
+ formattedAddress: string;
11
+ street: string;
12
+ city: string;
13
+ region: string;
14
+ postalCode: string;
15
+ country: string;
16
+ countryCode?: string;
17
+ latitude: number;
18
+ longitude: number;
19
+ }
@@ -0,0 +1,2 @@
1
+ /** Place autocomplete/detail types — backend places payloads. */
2
+ export {};