@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.
- package/README.md +26 -21
- package/dist/adapters/auth.d.ts +10 -0
- package/dist/adapters/auth.js +43 -0
- package/dist/adapters/devices.d.ts +3 -1
- package/dist/adapters/devices.js +187 -3
- package/dist/adapters/filesystem.d.ts +4 -0
- package/dist/adapters/kv.d.ts +10 -0
- package/dist/adapters/kv.js +23 -7
- package/dist/adapters/local-node.d.ts +4 -6
- package/dist/adapters/local-node.js +9 -32
- package/dist/adapters/notify.d.ts +12 -0
- package/dist/adapters/notify.js +33 -0
- package/dist/adapters/query-keys.d.ts +4 -0
- package/dist/adapters/query-keys.js +4 -0
- package/dist/adapters/services.d.ts +99 -15
- package/dist/adapters/services.js +244 -4
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +172 -0
- package/dist/catalog/hooks.d.ts +10 -0
- package/dist/catalog/hooks.js +82 -11
- package/dist/db/migrations.js +274 -0
- package/dist/db/operations.d.ts +53 -2
- package/dist/db/operations.js +177 -8
- package/dist/domain/business/index.d.ts +5 -0
- package/dist/domain/business/index.js +5 -0
- package/dist/domain/orders/lifecycle.d.ts +6 -0
- package/dist/domain/orders/lifecycle.js +6 -0
- package/dist/domain/orders/service.d.ts +24 -0
- package/dist/domain/orders/service.js +242 -0
- package/dist/domain/payments/index.d.ts +6 -0
- package/dist/domain/payments/index.js +5 -0
- package/dist/domain/reference/index.d.ts +8 -0
- package/dist/domain/reference/index.js +8 -0
- package/dist/e2e/harness.d.ts +1 -0
- package/dist/e2e/harness.js +61 -0
- package/dist/hooks/analytics.hooks.d.ts +25 -0
- package/dist/hooks/analytics.hooks.js +214 -0
- package/dist/hooks/business-hours.hooks.d.ts +8 -0
- package/dist/hooks/business-hours.hooks.js +45 -0
- package/dist/hooks/dashboard.hooks.d.ts +30 -0
- package/dist/hooks/dashboard.hooks.js +132 -0
- package/dist/hooks/delivery.hooks.d.ts +225 -0
- package/dist/hooks/delivery.hooks.js +80 -0
- package/dist/hooks/fleet.hooks.d.ts +37 -0
- package/dist/hooks/fleet.hooks.js +229 -0
- package/dist/hooks/image.hooks.d.ts +20 -0
- package/dist/hooks/image.hooks.js +62 -0
- package/dist/hooks/kiosk.hooks.d.ts +22 -5
- package/dist/hooks/kiosk.hooks.js +74 -5
- package/dist/hooks/members.hooks.d.ts +48 -0
- package/dist/hooks/members.hooks.js +302 -0
- package/dist/hooks/offline-orders.hooks.d.ts +9 -0
- package/dist/hooks/offline-orders.hooks.js +51 -0
- package/dist/hooks/orders.hooks.d.ts +19 -39
- package/dist/hooks/orders.hooks.js +126 -321
- package/dist/hooks/places.hooks.d.ts +21 -0
- package/dist/hooks/places.hooks.js +112 -0
- package/dist/hooks/realtime-stubs.d.ts +28 -0
- package/dist/hooks/realtime-stubs.js +85 -0
- package/dist/hooks/rider.hooks.d.ts +470 -0
- package/dist/hooks/rider.hooks.js +373 -0
- package/dist/hooks/use-infinite-list.d.ts +243 -0
- package/dist/hooks/use-infinite-list.js +58 -0
- package/dist/hooks/wallet.hooks.d.ts +219 -0
- package/dist/hooks/wallet.hooks.js +56 -0
- package/dist/image-cache/service.d.ts +8 -0
- package/dist/image-cache/service.js +15 -3
- package/dist/index.d.ts +60 -5
- package/dist/index.js +78 -5
- package/dist/local-node/adapters.d.ts +87 -0
- package/dist/local-node/adapters.js +4 -0
- package/dist/local-node/index.d.ts +5 -0
- package/dist/local-node/index.js +5 -0
- package/dist/local-node/security.d.ts +25 -0
- package/dist/local-node/security.js +149 -0
- package/dist/local-node/service.d.ts +83 -0
- package/dist/local-node/service.js +706 -0
- package/dist/local-node/store.d.ts +50 -0
- package/dist/local-node/store.js +284 -0
- package/dist/local-node/types.d.ts +92 -0
- package/dist/local-node/types.js +4 -0
- package/dist/printer/receipt.d.ts +4 -0
- package/dist/printer/receipt.js +75 -0
- package/dist/printer/service.js +1 -1
- package/dist/providers/query-client.d.ts +13 -0
- package/dist/providers/query-client.js +58 -0
- package/dist/realtime/events.service.d.ts +18 -0
- package/dist/realtime/events.service.js +76 -0
- package/dist/realtime/order-events.service.d.ts +6 -0
- package/dist/realtime/order-events.service.js +17 -0
- package/dist/realtime/phoenix-socket.d.ts +55 -0
- package/dist/realtime/phoenix-socket.js +269 -0
- package/dist/realtime/useEventsConnection.d.ts +1 -0
- package/dist/realtime/useEventsConnection.js +50 -0
- package/dist/reference/api.d.ts +2 -0
- package/dist/reference/api.js +2 -0
- package/dist/reference/capabilities.d.ts +12 -7
- package/dist/reference/capabilities.js +28 -13
- package/dist/reference/hooks.d.ts +34 -0
- package/dist/reference/hooks.js +124 -0
- package/dist/reference/menu-apply.d.ts +29 -0
- package/dist/reference/menu-apply.js +289 -0
- package/dist/reference/menu-record.d.ts +69 -0
- package/dist/reference/menu-record.js +163 -0
- package/dist/reference/operations.d.ts +3 -0
- package/dist/reference/operations.js +35 -1
- package/dist/reference/service.js +36 -9
- package/dist/reference/types.d.ts +1 -1
- package/dist/repositories/order.repository.d.ts +8 -0
- package/dist/repositories/order.repository.js +24 -2
- package/dist/repositories/pos.repository.d.ts +4 -0
- package/dist/repositories/pos.repository.js +4 -0
- package/dist/sync/backoff.d.ts +21 -0
- package/dist/sync/backoff.js +42 -0
- package/dist/sync/command-builder.js +10 -2
- package/dist/sync/command-queue.service.d.ts +12 -0
- package/dist/sync/command-queue.service.js +115 -18
- package/dist/sync/device-lease.service.d.ts +3 -3
- package/dist/sync/device-lease.service.js +80 -20
- package/dist/sync/engine.d.ts +3 -1
- package/dist/sync/engine.js +44 -4
- package/dist/sync/full-sync.d.ts +6 -0
- package/dist/sync/full-sync.js +72 -0
- package/dist/sync/health-probe.d.ts +24 -0
- package/dist/sync/health-probe.js +138 -0
- package/dist/sync/offline-capable.js +25 -0
- package/dist/sync/online-gates.d.ts +17 -0
- package/dist/sync/online-gates.js +55 -0
- package/dist/sync/order-number.d.ts +4 -4
- package/dist/sync/order-number.js +33 -13
- package/dist/sync/order-snapshots.service.d.ts +1 -0
- package/dist/sync/order-snapshots.service.js +47 -2
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +148 -3
- package/dist/sync/reconciliation/index.d.ts +5 -0
- package/dist/sync/reconciliation/index.js +5 -0
- package/dist/sync/snapshots/entity.d.ts +6 -0
- package/dist/sync/snapshots/entity.js +6 -0
- package/dist/sync/sync-jobs.service.d.ts +18 -0
- package/dist/sync/sync-jobs.service.js +539 -0
- package/dist/sync/transition-endpoints.d.ts +11 -0
- package/dist/sync/transition-endpoints.js +30 -0
- package/dist/sync/transport-local-node.js +3 -2
- package/dist/sync/transport-rest.d.ts +7 -0
- package/dist/sync/transport-rest.js +90 -25
- package/dist/sync/use-sync-reconciliation.js +58 -16
- package/dist/test-helpers/memory-driver.js +182 -4
- package/dist/test-helpers/seed-hours.d.ts +1 -0
- package/dist/test-helpers/seed-hours.js +23 -0
- package/dist/types/api/business.api.d.ts +42 -0
- package/dist/types/api/menu.api.d.ts +15 -0
- package/dist/types/api/seller.api.d.ts +20 -0
- package/dist/types/places.d.ts +19 -0
- package/dist/types/places.js +2 -0
- package/dist/types/rider.d.ts +67 -0
- package/dist/types/rider.js +2 -0
- package/dist/utils/attempt-stats.d.ts +23 -0
- package/dist/utils/attempt-stats.js +61 -0
- package/dist/utils/business-hours.d.ts +37 -0
- package/dist/utils/business-hours.js +96 -0
- package/dist/utils/debug-log.d.ts +23 -0
- package/dist/utils/debug-log.js +153 -0
- package/dist/utils/order-filter.d.ts +1 -1
- package/dist/utils/order-filter.js +1 -1
- package/dist/utils/order-transitions.d.ts +30 -0
- package/dist/utils/order-transitions.js +185 -0
- package/dist/utils/sync-logger.js +5 -4
- package/dist/validation/offline.d.ts +46 -0
- package/dist/validation/offline.js +79 -0
- package/package.json +6 -1
- package/dist/pricing/engine.test.d.ts +0 -1
- package/dist/pricing/engine.test.js +0 -142
- package/dist/reference/lww.test.d.ts +0 -1
- package/dist/reference/lww.test.js +0 -23
- package/dist/reference/operations.test.d.ts +0 -1
- package/dist/reference/operations.test.js +0 -66
- package/dist/standalone.integration.test.d.ts +0 -1
- package/dist/standalone.integration.test.js +0 -235
- package/dist/sync/device-lease.test.d.ts +0 -1
- package/dist/sync/device-lease.test.js +0 -38
- package/dist/sync/offline-integration.test.d.ts +0 -1
- package/dist/sync/offline-integration.test.js +0 -52
- package/dist/sync/order-number.test.d.ts +0 -1
- package/dist/sync/order-number.test.js +0 -51
- package/dist/sync/perms.test.d.ts +0 -1
- package/dist/sync/perms.test.js +0 -15
- package/dist/utils/currency.test.d.ts +0 -1
- package/dist/utils/currency.test.js +0 -17
- package/dist/utils/hlc.test.d.ts +0 -1
- package/dist/utils/hlc.test.js +0 -29
- package/dist/utils/phone.test.d.ts +0 -1
- package/dist/utils/phone.test.js +0 -19
- package/dist/utils/responsive.d.ts +0 -13
- package/dist/utils/responsive.js +0 -18
- package/dist/utils/uuid.test.d.ts +0 -1
- package/dist/utils/uuid.test.js +0 -13
- package/dist/utils/validation.test.d.ts +0 -1
- 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 {
|
|
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,40 +48,38 @@ export class RestTransport {
|
|
|
45
48
|
}
|
|
46
49
|
async dispatchCommand(command) {
|
|
47
50
|
const { command_type, business_id, entity_id, payload } = command;
|
|
48
|
-
|
|
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
|
|
52
|
-
...payload,
|
|
59
|
+
const order = await createLocalOrder(business_id, {
|
|
60
|
+
...(payload ?? {}),
|
|
53
61
|
command_id: command.command_id,
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return this.wrapResponse("order.created", response.data, entity_id, timestamp, command);
|
|
62
|
+
id: entity_id ?? command.command_id,
|
|
63
|
+
});
|
|
64
|
+
return this.wrapResponse("order.created", order, order.id, timestamp, command);
|
|
58
65
|
}
|
|
59
66
|
case "order:accept": {
|
|
60
|
-
|
|
61
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
67
|
+
return this.executeOrderTransition(command, "accept", timestamp, () => sellerService.acceptOrder(entity_id));
|
|
62
68
|
}
|
|
63
69
|
case "order:reject": {
|
|
64
|
-
|
|
65
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
70
|
+
return this.executeOrderTransition(command, "reject", timestamp, () => sellerService.rejectOrder(entity_id));
|
|
66
71
|
}
|
|
67
72
|
case "order:start_preparing": {
|
|
68
|
-
|
|
69
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
73
|
+
return this.executeOrderTransition(command, "start_preparing", timestamp, () => sellerService.startPreparingOrder(entity_id));
|
|
70
74
|
}
|
|
71
75
|
case "order:mark_ready": {
|
|
72
|
-
|
|
73
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
76
|
+
return this.executeOrderTransition(command, "mark_ready", timestamp, () => sellerService.markOrderReady(entity_id));
|
|
74
77
|
}
|
|
75
78
|
case "order:complete_pickup": {
|
|
76
|
-
|
|
77
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
79
|
+
return this.executeOrderTransition(command, "complete_pickup", timestamp, () => sellerService.completeOrderPickup(entity_id));
|
|
78
80
|
}
|
|
79
81
|
case "order:cancel": {
|
|
80
|
-
|
|
81
|
-
return this.wrapResponse("order.status_changed", response.data, entity_id, timestamp, command);
|
|
82
|
+
return this.executeOrderTransition(command, "cancel", timestamp, () => sellerService.cancelOrder(entity_id));
|
|
82
83
|
}
|
|
83
84
|
case "delivery:assign": {
|
|
84
85
|
const response = await kioskService.assignKioskOrderToRider(entity_id, payload.rider_id);
|
|
@@ -89,11 +90,18 @@ 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
|
-
|
|
93
|
-
|
|
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 };
|
|
98
|
+
const response = await menuService.createMenuItem(business_id, createPayload);
|
|
99
|
+
return this.wrapResponse("menu.item_created", response, command.command_id, timestamp, command);
|
|
94
100
|
}
|
|
95
101
|
case "menu:update_item": {
|
|
96
|
-
const
|
|
102
|
+
const { cacheRecord: _omitUpdate, ...wireUpdate } = payload;
|
|
103
|
+
void _omitUpdate;
|
|
104
|
+
const response = await menuService.updateMenuItem(entity_id, wireUpdate);
|
|
97
105
|
return this.wrapResponse("menu.item_updated", response, entity_id, timestamp, command);
|
|
98
106
|
}
|
|
99
107
|
case "menu:delete_item": {
|
|
@@ -101,13 +109,70 @@ export class RestTransport {
|
|
|
101
109
|
return this.wrapResponse("menu.item_deleted", response, entity_id, timestamp, command);
|
|
102
110
|
}
|
|
103
111
|
case "menu:create_category": {
|
|
104
|
-
const
|
|
105
|
-
|
|
112
|
+
const { cacheRecord: _omitCatCreate, ...wireCatCreate } = payload;
|
|
113
|
+
void _omitCatCreate;
|
|
114
|
+
const createPayload = { ...wireCatCreate, id: wireCatCreate.id ?? command.command_id, command_id: command.command_id };
|
|
115
|
+
const response = await menuService.createCategory(business_id, createPayload);
|
|
116
|
+
return this.wrapResponse("menu.category_created", response, command.command_id, timestamp, command);
|
|
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
|
+
}
|
|
128
|
+
case "payment:confirm_cash": {
|
|
129
|
+
return this.executeOrderTransition(command, "confirm_cash", timestamp, () => sellerService.confirmCashPayment(entity_id), "payment.completed");
|
|
106
130
|
}
|
|
107
131
|
default:
|
|
108
132
|
throw new Error(`RestTransport: unsupported command type ${command_type}`);
|
|
109
133
|
}
|
|
110
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
|
+
}
|
|
111
176
|
wrapResponse(eventType, data, entityId, timestamp, command) {
|
|
112
177
|
return {
|
|
113
178
|
event_id: generateEventId(),
|
|
@@ -10,12 +10,14 @@
|
|
|
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";
|
|
16
17
|
import { markEventsSynced } from "../db/operations";
|
|
17
18
|
import { syncLogger } from "../utils/sync-logger";
|
|
18
|
-
import {
|
|
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";
|
|
19
21
|
import { applyReferenceChangeEvent, registerReferenceInvalidator, registerReferenceEventEngine, REFERENCE_CHANGED_EVENT_TYPE, } from "../reference/events";
|
|
20
22
|
function isLocalOrderEvent(event) {
|
|
21
23
|
const payload = event.payload;
|
|
@@ -24,16 +26,44 @@ function isLocalOrderEvent(event) {
|
|
|
24
26
|
const localOrderNumber = payload.local_order_number;
|
|
25
27
|
return typeof localOrderNumber === "string" && /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(localOrderNumber);
|
|
26
28
|
}
|
|
29
|
+
const RECONCILE_DEBOUNCE_MS = 400;
|
|
27
30
|
export function useSyncReconciliation() {
|
|
28
31
|
const queryClient = useQueryClient();
|
|
29
32
|
useEffect(() => {
|
|
33
|
+
let timer;
|
|
34
|
+
const pending = new Set();
|
|
35
|
+
const flush = () => {
|
|
36
|
+
timer = undefined;
|
|
37
|
+
if (pending.has("reference")) {
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: MENU_KEYS.all });
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
|
|
40
|
+
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
41
|
+
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
42
|
+
queryClient.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all });
|
|
43
|
+
}
|
|
44
|
+
if (pending.has("pending")) {
|
|
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 });
|
|
50
|
+
queryClient.invalidateQueries({ queryKey: SELLER_DASHBOARD_KEYS.all });
|
|
51
|
+
}
|
|
52
|
+
if (pending.has("order")) {
|
|
53
|
+
// order.created with local number already covered by pending, but keep fine-grained
|
|
54
|
+
// POS.orders(businessId) is also covered by pending's broad POS.all, no extra needed.
|
|
55
|
+
}
|
|
56
|
+
pending.clear();
|
|
57
|
+
};
|
|
58
|
+
const schedule = (kind) => {
|
|
59
|
+
pending.add(kind);
|
|
60
|
+
if (!timer)
|
|
61
|
+
timer = setTimeout(flush, RECONCILE_DEBOUNCE_MS);
|
|
62
|
+
};
|
|
30
63
|
// Reference changes re-sync offline data and refresh caches on this device
|
|
31
64
|
// and (via the LAN relay) on every connected terminal.
|
|
32
65
|
const unregisterReferenceInvalidator = registerReferenceInvalidator(() => {
|
|
33
|
-
|
|
34
|
-
queryClient.invalidateQueries({ queryKey: REFERENCE_KEYS.all });
|
|
35
|
-
queryClient.invalidateQueries({ queryKey: POS_KEYS.all });
|
|
36
|
-
queryClient.invalidateQueries({ queryKey: SELLER_KEYS.all });
|
|
66
|
+
schedule("reference");
|
|
37
67
|
});
|
|
38
68
|
// Pending-order reconciliation invalidates POS + seller order lists so the
|
|
39
69
|
// server-confirmed order replaces the optimistic one. Invalidate from the
|
|
@@ -42,17 +72,21 @@ export function useSyncReconciliation() {
|
|
|
42
72
|
// keys. Also cover kiosk/seller-orders/business-deliveries so boards
|
|
43
73
|
// refresh even when there is no Local Node (offline queue + Phoenix).
|
|
44
74
|
const unregisterInvalidator = registerPendingOrdersInvalidator(() => {
|
|
45
|
-
|
|
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 });
|
|
75
|
+
schedule("pending");
|
|
50
76
|
});
|
|
51
77
|
// Reference-change events are published through the Sync Engine but are
|
|
52
78
|
// not backed by a command, so the engine has no transport dependency on
|
|
53
79
|
// the reference module. Register the singleton once.
|
|
54
80
|
registerReferenceEventEngine(syncEngine);
|
|
55
81
|
const unsubscribe = syncEngine.subscribe(async (event) => {
|
|
82
|
+
// LAN strict: ignore events for other business on same subnet
|
|
83
|
+
try {
|
|
84
|
+
const activeId = getActiveBusiness()?.id;
|
|
85
|
+
const payloadBid = event.payload?.business_id;
|
|
86
|
+
if (activeId && payloadBid && payloadBid !== activeId)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
56
90
|
syncLogger.event(event, "received");
|
|
57
91
|
// The node relays engine events (e.g. order.created when a queued
|
|
58
92
|
// command is replayed) to LAN clients so they reconcile their pending
|
|
@@ -74,6 +108,10 @@ export function useSyncReconciliation() {
|
|
|
74
108
|
// Order/delivery state changed: refresh order caches so this terminal
|
|
75
109
|
// shows the same order states as the device that made the change.
|
|
76
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
|
+
}
|
|
77
115
|
// Route both queued (optimistic) and reconciled (created) order events
|
|
78
116
|
// into the pending-orders store.
|
|
79
117
|
if (event.event_type === "order.create_queued" ||
|
|
@@ -90,18 +128,22 @@ export function useSyncReconciliation() {
|
|
|
90
128
|
error: String(error),
|
|
91
129
|
});
|
|
92
130
|
}
|
|
93
|
-
|
|
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
|
-
}
|
|
131
|
+
schedule("pending");
|
|
99
132
|
}
|
|
100
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
|
+
});
|
|
101
140
|
return () => {
|
|
141
|
+
if (timer)
|
|
142
|
+
clearTimeout(timer);
|
|
102
143
|
unsubscribe();
|
|
103
144
|
unregisterInvalidator();
|
|
104
145
|
unregisterReferenceInvalidator();
|
|
146
|
+
dbUnsub();
|
|
105
147
|
};
|
|
106
148
|
}, [queryClient]);
|
|
107
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
|
|
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
|
-
|
|
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
|
+
}
|