@teincfood/core 0.10.8 → 0.10.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.
- package/dist/adapters/pending-orders.d.ts +7 -0
- package/dist/adapters/pending-orders.js +19 -0
- package/dist/db/operations.d.ts +9 -0
- package/dist/db/operations.js +13 -0
- package/dist/hooks/orders.hooks.d.ts +1 -1
- package/dist/hooks/orders.hooks.js +7 -1
- package/dist/local-node/service.d.ts +12 -0
- package/dist/local-node/service.js +77 -1
- package/dist/sync/command-queue.service.js +27 -11
- package/dist/sync/pending-orders.service.js +3 -3
- package/package.json +1 -1
|
@@ -14,6 +14,13 @@ export declare const usePendingOrdersStore: {
|
|
|
14
14
|
hydrated: boolean;
|
|
15
15
|
orders: SellerOrder[];
|
|
16
16
|
addOrder: (order: SellerOrder) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Replace an existing pending order in place. Used for queued
|
|
19
|
+
* transitions/payments so the optimistic overlay reflects the new state
|
|
20
|
+
* immediately. Unknown orders are ignored (a transition must never
|
|
21
|
+
* resurrect an order that was already reconciled).
|
|
22
|
+
*/
|
|
23
|
+
updateOrder: (order: SellerOrder) => void;
|
|
17
24
|
removeByOrderNumber: (orderNumber: string) => void;
|
|
18
25
|
removeByOrderId: (orderId: string) => void;
|
|
19
26
|
setHydrated: (v: boolean) => void;
|
|
@@ -30,6 +30,25 @@ export const usePendingOrdersStore = {
|
|
|
30
30
|
notify();
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
+
/**
|
|
34
|
+
* Replace an existing pending order in place. Used for queued
|
|
35
|
+
* transitions/payments so the optimistic overlay reflects the new state
|
|
36
|
+
* immediately. Unknown orders are ignored (a transition must never
|
|
37
|
+
* resurrect an order that was already reconciled).
|
|
38
|
+
*/
|
|
39
|
+
updateOrder: (order) => {
|
|
40
|
+
const index = orders.findIndex((o) => o.id === order.id || o.order_number === order.order_number);
|
|
41
|
+
if (index === -1)
|
|
42
|
+
return;
|
|
43
|
+
const current = orders[index];
|
|
44
|
+
if (current.status === order.status &&
|
|
45
|
+
current.is_paid === order.is_paid &&
|
|
46
|
+
current.paid_at === order.paid_at) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
orders = orders.map((o, i) => (i === index ? order : o));
|
|
50
|
+
notify();
|
|
51
|
+
},
|
|
33
52
|
removeByOrderNumber: (orderNumber) => {
|
|
34
53
|
const next = orders.filter((o) => o.order_number !== orderNumber);
|
|
35
54
|
if (next.length !== orders.length) {
|
package/dist/db/operations.d.ts
CHANGED
|
@@ -112,6 +112,15 @@ export declare function upsertServerOrder(db: SQLiteDatabase, order: import("../
|
|
|
112
112
|
preserveStatus?: boolean;
|
|
113
113
|
preservePayment?: boolean;
|
|
114
114
|
}): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Recent orders plus their local HLC, for LAN catch-up.
|
|
117
|
+
*
|
|
118
|
+
* The HLC must travel with the row: `insertOrder` is HLC-guarded, so a peer
|
|
119
|
+
* replaying an older snapshot can never overwrite a newer local status.
|
|
120
|
+
*/
|
|
121
|
+
export declare function listOrdersWithHlc(db: SQLiteDatabase, businessId: string, limit?: number): Promise<Array<import("../types/api/seller.api").SellerOrder & {
|
|
122
|
+
hlc?: string;
|
|
123
|
+
}>>;
|
|
115
124
|
export declare function listOrders(db: SQLiteDatabase, businessId: string, filters?: {
|
|
116
125
|
status?: string;
|
|
117
126
|
q?: string;
|
package/dist/db/operations.js
CHANGED
|
@@ -376,6 +376,19 @@ export async function upsertServerOrder(db, order, opts = {}) {
|
|
|
376
376
|
ON CONFLICT(id) DO UPDATE SET business_id=excluded.business_id, order_number=excluded.order_number, callout_number=excluded.callout_number, status=excluded.status, order_type=excluded.order_type, is_pos=excluded.is_pos, items=excluded.items, subtotal_minor=excluded.subtotal_minor, total_minor=excluded.total_minor, tax_minor=excluded.tax_minor, tax_lines=excluded.tax_lines, payment=excluded.payment, paid_at=COALESCE(excluded.paid_at, orders.paid_at), customer_name=excluded.customer_name, customer_phone=excluded.customer_phone, pos_payment_method=excluded.pos_payment_method, delivery_id=excluded.delivery_id, business=excluded.business, user=excluded.user, updated_at=excluded.updated_at, hlc=excluded.hlc, sync_failed_at=NULL, sync_error=NULL;`, order.id, order.business_id, order.order_number, order.callout_number, status, order.order_type, order.is_pos ? 1 : 0, JSON.stringify(order.items ?? []), order.subtotal_minor ?? 0, order.total_minor ?? 0, order.tax_minor ?? 0, JSON.stringify(order.tax_lines ?? []), JSON.stringify(payment), paidAt, order.customer_name, order.customer_phone, order.pos_payment_method, order.delivery_id, order.business ? JSON.stringify(order.business) : null, order.user ? JSON.stringify(order.user) : null, order.inserted_at, updatedAt, hlc);
|
|
377
377
|
notifyDbChange(order.business_id, "orders");
|
|
378
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Recent orders plus their local HLC, for LAN catch-up.
|
|
381
|
+
*
|
|
382
|
+
* The HLC must travel with the row: `insertOrder` is HLC-guarded, so a peer
|
|
383
|
+
* replaying an older snapshot can never overwrite a newer local status.
|
|
384
|
+
*/
|
|
385
|
+
export async function listOrdersWithHlc(db, businessId, limit = 200) {
|
|
386
|
+
const rows = await db.getAllAsync(`SELECT * FROM orders WHERE business_id = ? AND deleted_at IS NULL ORDER BY inserted_at DESC LIMIT ?;`, businessId, limit);
|
|
387
|
+
return rows.map((row) => ({
|
|
388
|
+
...orderRowToSellerOrder(row),
|
|
389
|
+
hlc: row.hlc ?? row.updated_at ?? row.inserted_at,
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
379
392
|
export async function listOrders(db, businessId, filters, page = 1, pageSize = 20) {
|
|
380
393
|
const where = ["business_id = ?"];
|
|
381
394
|
const args = [businessId];
|
|
@@ -22,7 +22,7 @@ export interface SellerOrderFilters {
|
|
|
22
22
|
date_to?: string;
|
|
23
23
|
}
|
|
24
24
|
export declare function useSellerOrdersQuery(enabled?: boolean, filters?: SellerOrderFilters): import("@tanstack/react-query").UseInfiniteQueryResult<SellerOrder[], Error>;
|
|
25
|
-
export declare function useSellerOrderQuery(orderId: string, enabled?: boolean): import("@tanstack/react-query").UseQueryResult<{
|
|
25
|
+
export declare function useSellerOrderQuery(orderId: string, enabled?: boolean, placeholder?: SellerOrder | null): import("@tanstack/react-query").UseQueryResult<{
|
|
26
26
|
data: SellerOrder;
|
|
27
27
|
}, Error>;
|
|
28
28
|
export declare function useCreatePOSOrderMutation(): import("@tanstack/react-query").UseMutationResult<SellerOrder, Error, POSOrderCreateRequest, unknown>;
|
|
@@ -81,7 +81,7 @@ export function useSellerOrdersQuery(enabled = true, filters) {
|
|
|
81
81
|
select: (data) => data.pages.flatMap((p) => p.data),
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
-
export function useSellerOrderQuery(orderId, enabled = true) {
|
|
84
|
+
export function useSellerOrderQuery(orderId, enabled = true, placeholder) {
|
|
85
85
|
const bid = useBusinessId();
|
|
86
86
|
return useQuery({
|
|
87
87
|
queryKey: SELLER_KEYS.order(orderId, bid),
|
|
@@ -94,6 +94,12 @@ export function useSellerOrderQuery(orderId, enabled = true) {
|
|
|
94
94
|
return { data: order };
|
|
95
95
|
},
|
|
96
96
|
enabled: enabled && !!orderId && !!bid,
|
|
97
|
+
// The order list already has the full row. Seed the dialog with it so
|
|
98
|
+
// opening details is instant; the DB read still runs and replaces it.
|
|
99
|
+
placeholderData: placeholder ? { data: placeholder } : undefined,
|
|
100
|
+
// Local SQLite reads cannot recover from a transient network error; a
|
|
101
|
+
// missing row should surface immediately instead of retrying for seconds.
|
|
102
|
+
retry: false,
|
|
97
103
|
staleTime: 5 * 1000,
|
|
98
104
|
});
|
|
99
105
|
}
|
|
@@ -24,6 +24,18 @@ declare class LocalNodeServiceCore {
|
|
|
24
24
|
init(deps: LocalNodeDeps): void;
|
|
25
25
|
private requestReferenceFromPeers;
|
|
26
26
|
private requestAssetFromPeers;
|
|
27
|
+
/**
|
|
28
|
+
* Catch a newly connected peer up on orders created while the mesh was not
|
|
29
|
+
* connected (e.g. a sale rung up before Local Node was enabled).
|
|
30
|
+
*
|
|
31
|
+
* Live order events only cover changes that happen *after* the socket is
|
|
32
|
+
* up. Without this handshake a pre-existing offline sale stayed invisible
|
|
33
|
+
* on the other terminal forever. Both sides call this on connect; the
|
|
34
|
+
* cooldown dedupes the inbound/outbound callbacks for the same peer.
|
|
35
|
+
*/
|
|
36
|
+
private orderCatchupAt;
|
|
37
|
+
private static readonly ORDER_CATCHUP_COOLDOWN_MS;
|
|
38
|
+
private publishOrderCatchup;
|
|
27
39
|
private getDeps;
|
|
28
40
|
private getDeviceId;
|
|
29
41
|
private runLifecycle;
|
|
@@ -12,7 +12,9 @@ import { REFERENCE_CHANGED_EVENT_TYPE } from "../reference/events";
|
|
|
12
12
|
import { applyReferenceChangeEvent } from "../reference/events";
|
|
13
13
|
import { referenceSyncService } from "../reference/service";
|
|
14
14
|
import { openSyncDatabase } from "../db/connection";
|
|
15
|
-
import { insertOrder } from "../db/operations";
|
|
15
|
+
import { insertOrder, listOrdersWithHlc } from "../db/operations";
|
|
16
|
+
import { getPendingOrdersSnapshot } from "../adapters/pending-orders";
|
|
17
|
+
import { generateEventId } from "../utils/uuid";
|
|
16
18
|
import { createLocalOrder, transitionLocalOrder } from "../domain/orders/service";
|
|
17
19
|
import { getConnectivityState } from "../adapters/connectivity";
|
|
18
20
|
import { deriveLanSecret as deriveSecretCore } from "./security";
|
|
@@ -46,6 +48,16 @@ class LocalNodeServiceCore {
|
|
|
46
48
|
this.published = false;
|
|
47
49
|
/** Whether discovery scanning was successfully started. */
|
|
48
50
|
this.discovering = false;
|
|
51
|
+
/**
|
|
52
|
+
* Catch a newly connected peer up on orders created while the mesh was not
|
|
53
|
+
* connected (e.g. a sale rung up before Local Node was enabled).
|
|
54
|
+
*
|
|
55
|
+
* Live order events only cover changes that happen *after* the socket is
|
|
56
|
+
* up. Without this handshake a pre-existing offline sale stayed invisible
|
|
57
|
+
* on the other terminal forever. Both sides call this on connect; the
|
|
58
|
+
* cooldown dedupes the inbound/outbound callbacks for the same peer.
|
|
59
|
+
*/
|
|
60
|
+
this.orderCatchupAt = new Map();
|
|
49
61
|
this.storeUnsub = null;
|
|
50
62
|
this.dialInFlight = new Set();
|
|
51
63
|
this.dialTimers = new Map();
|
|
@@ -89,6 +101,63 @@ class LocalNodeServiceCore {
|
|
|
89
101
|
}
|
|
90
102
|
return null;
|
|
91
103
|
}
|
|
104
|
+
async publishOrderCatchup(peerDeviceId) {
|
|
105
|
+
if (!this.businessId)
|
|
106
|
+
return;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const last = this.orderCatchupAt.get(peerDeviceId) ?? 0;
|
|
109
|
+
if (now - last < LocalNodeServiceCore.ORDER_CATCHUP_COOLDOWN_MS)
|
|
110
|
+
return;
|
|
111
|
+
this.orderCatchupAt.set(peerDeviceId, now);
|
|
112
|
+
try {
|
|
113
|
+
const db = await openSyncDatabase();
|
|
114
|
+
const rows = await listOrdersWithHlc(db, this.businessId, 200);
|
|
115
|
+
const byId = new Map();
|
|
116
|
+
for (const order of rows)
|
|
117
|
+
byId.set(order.id, order);
|
|
118
|
+
// Queued offline creations have no `orders` row yet; they live in the
|
|
119
|
+
// pending overlay. Include them so a pre-linkup sale is shared too.
|
|
120
|
+
for (const order of getPendingOrdersSnapshot().orders) {
|
|
121
|
+
if (order.business_id === this.businessId)
|
|
122
|
+
byId.set(order.id, order);
|
|
123
|
+
}
|
|
124
|
+
const peer = this.peers.get(peerDeviceId);
|
|
125
|
+
const canTarget = !!peer?.isConnected() && typeof peer.publishEvent === "function";
|
|
126
|
+
for (const order of byId.values()) {
|
|
127
|
+
if (!order?.id || !order.order_number)
|
|
128
|
+
continue;
|
|
129
|
+
const event = {
|
|
130
|
+
event_id: generateEventId(),
|
|
131
|
+
event_type: "order:sync",
|
|
132
|
+
event_version: "1",
|
|
133
|
+
entity_id: order.id,
|
|
134
|
+
user_id: "",
|
|
135
|
+
device_id: this.cachedDeviceId ?? "",
|
|
136
|
+
payload: order,
|
|
137
|
+
timestamp: order.updated_at ?? order.inserted_at ?? new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
if (canTarget) {
|
|
140
|
+
await peer.publishEvent(event);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.server?.broadcast(event);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
syncLogger.info("LocalNodeService", "Order catch-up published", {
|
|
147
|
+
peer: peerDeviceId,
|
|
148
|
+
orders: byId.size,
|
|
149
|
+
targeted: canTarget,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
syncLogger.warn("LocalNodeService", "Order catch-up failed", {
|
|
154
|
+
peer: peerDeviceId,
|
|
155
|
+
error: String(error),
|
|
156
|
+
});
|
|
157
|
+
// Allow a later reconnect to retry after a transient DB/socket failure.
|
|
158
|
+
this.orderCatchupAt.delete(peerDeviceId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
92
161
|
getDeps() {
|
|
93
162
|
if (!this.deps)
|
|
94
163
|
throw new Error("LocalNodeService not initialised: call initLocalNodeService(deps) first");
|
|
@@ -200,6 +269,7 @@ class LocalNodeServiceCore {
|
|
|
200
269
|
catch { }
|
|
201
270
|
this.peers.clear();
|
|
202
271
|
this.peerInfos.clear();
|
|
272
|
+
this.orderCatchupAt.clear();
|
|
203
273
|
if (this.server) {
|
|
204
274
|
await withTimeout(this.server.stop(), LIFECYCLE_TIMEOUT_MS).catch((e) => syncLogger.warn("LocalNodeService", "Server stop timed out", { error: String(e) }));
|
|
205
275
|
this.server = null;
|
|
@@ -359,6 +429,9 @@ class LocalNodeServiceCore {
|
|
|
359
429
|
onClientConnected: (info) => {
|
|
360
430
|
localNodeStore.getState().addConnectedClient({ deviceId: info.deviceId, deviceName: info.deviceName, host: info.host, connectedAt: new Date().toISOString() });
|
|
361
431
|
syncLogger.info("LocalNodeService", "Client connected", { deviceId: info.deviceId, connected_clients: localNodeStore.getState().connectedClients.length });
|
|
432
|
+
// Inbound connect: push this node's pre-existing orders to the new
|
|
433
|
+
// peer (covers mDNS asymmetry where only one side dials).
|
|
434
|
+
void this.publishOrderCatchup(info.deviceId);
|
|
362
435
|
},
|
|
363
436
|
onClientDisconnected: (deviceId) => {
|
|
364
437
|
localNodeStore.getState().removeConnectedClient(deviceId);
|
|
@@ -602,6 +675,8 @@ class LocalNodeServiceCore {
|
|
|
602
675
|
syncLogger.info("LocalNodeService", "Connected to peer", { deviceId: node.deviceId });
|
|
603
676
|
this.peerInfos.set(node.deviceId, { deviceId: node.deviceId, host: node.host, port: node.port });
|
|
604
677
|
localNodeStore.getState().addConnectedClient({ deviceId: node.deviceId, deviceName: node.deviceName, host: node.host, connectedAt: new Date().toISOString() });
|
|
678
|
+
// Outbound connect: push orders created before this socket existed.
|
|
679
|
+
void this.publishOrderCatchup(node.deviceId);
|
|
605
680
|
// A terminal may have missed `reference_data.changed` while it was
|
|
606
681
|
// disconnected. Compare versions with the peer on reconnect so menu,
|
|
607
682
|
// members, hours and tax changes converge even on a dark LAN.
|
|
@@ -817,6 +892,7 @@ class LocalNodeServiceCore {
|
|
|
817
892
|
}
|
|
818
893
|
getState() { return { isLocalNode: this.isLocalNode(), isConnectedToNode: this.isConnectedToNode(), electedNode: this.getElectedNode(), client: this.getClient() }; }
|
|
819
894
|
}
|
|
895
|
+
LocalNodeServiceCore.ORDER_CATCHUP_COOLDOWN_MS = 10000;
|
|
820
896
|
LocalNodeServiceCore.MISMATCH_BACKOFF_MS = 60000;
|
|
821
897
|
export const localNodeService = new LocalNodeServiceCore();
|
|
822
898
|
export function initLocalNodeService(deps) { localNodeService.init(deps); return localNodeService; }
|
|
@@ -18,6 +18,7 @@ import { assertBusinessOpenForCreate } from "../utils/business-hours";
|
|
|
18
18
|
import { extractApiErrorCode } from "../utils/error";
|
|
19
19
|
import { generateEventId } from "../utils/uuid";
|
|
20
20
|
import { buildItemSnapshots, snapshotsToOrderLines } from "../domain/orders/snapshots";
|
|
21
|
+
import { DEFAULT_ORDER_FLOW_MODE, getBusinessOrderFlowMode, initialPosOrderStatus, } from "../domain/orders/order-flow";
|
|
21
22
|
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrderNumber, } from "./order-number";
|
|
22
23
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
23
24
|
import { canBusinessSync } from "./sync-guard";
|
|
@@ -53,7 +54,7 @@ const PAYMENT_QUEUED_PAYLOAD = {
|
|
|
53
54
|
export function getQueuedStatus(commandType) {
|
|
54
55
|
return QUEUED_STATUS_BY_COMMAND[commandType] ?? null;
|
|
55
56
|
}
|
|
56
|
-
function buildOptimisticEvent(command) {
|
|
57
|
+
function buildOptimisticEvent(command, flowMode = DEFAULT_ORDER_FLOW_MODE) {
|
|
57
58
|
const payload = command.payload;
|
|
58
59
|
if (command.command_type === "order:create") {
|
|
59
60
|
return {
|
|
@@ -63,7 +64,7 @@ function buildOptimisticEvent(command) {
|
|
|
63
64
|
entity_id: command.entity_id ?? command.command_id,
|
|
64
65
|
user_id: command.user_id,
|
|
65
66
|
device_id: command.device_id,
|
|
66
|
-
payload: buildOptimisticOrder(command, payload),
|
|
67
|
+
payload: buildOptimisticOrder(command, payload, flowMode),
|
|
67
68
|
timestamp: new Date().toISOString(),
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -77,6 +78,7 @@ function buildOptimisticEvent(command) {
|
|
|
77
78
|
device_id: command.device_id,
|
|
78
79
|
payload: {
|
|
79
80
|
...payload,
|
|
81
|
+
business_id: command.business_id,
|
|
80
82
|
command_id: command.command_id,
|
|
81
83
|
is_paid: true,
|
|
82
84
|
paid_at: new Date().toISOString(),
|
|
@@ -95,6 +97,9 @@ function buildOptimisticEvent(command) {
|
|
|
95
97
|
device_id: command.device_id,
|
|
96
98
|
payload: {
|
|
97
99
|
...payload,
|
|
100
|
+
// Required by the offline read-model patch so queued transitions update
|
|
101
|
+
// the canonical orders table (not just seller_orders snapshots).
|
|
102
|
+
business_id: command.business_id,
|
|
98
103
|
command_id: command.command_id,
|
|
99
104
|
status: "pending",
|
|
100
105
|
// Used by offline read-model patching so the list reflects the queued transition.
|
|
@@ -106,7 +111,7 @@ function buildOptimisticEvent(command) {
|
|
|
106
111
|
timestamp: new Date().toISOString(),
|
|
107
112
|
};
|
|
108
113
|
}
|
|
109
|
-
function buildOptimisticOrder(command, payload) {
|
|
114
|
+
function buildOptimisticOrder(command, payload, flowMode) {
|
|
110
115
|
const snapshots = buildItemSnapshots((payload.item_snapshots ?? payload.items ?? []));
|
|
111
116
|
const totalMinor = payload.total_minor ?? 0;
|
|
112
117
|
const subtotalMinor = payload.subtotal_minor ?? totalMinor;
|
|
@@ -114,6 +119,8 @@ function buildOptimisticOrder(command, payload) {
|
|
|
114
119
|
const taxLines = payload.tax_lines ?? [];
|
|
115
120
|
const currencyCode = payload.currency_code ?? "GHS";
|
|
116
121
|
const now = new Date().toISOString();
|
|
122
|
+
const method = payload.pos_payment_method ?? "cash";
|
|
123
|
+
const isCash = method === "cash";
|
|
117
124
|
const orderNumber = payload.order_number ?? generateOrderNumberForBusiness(command.business_id);
|
|
118
125
|
return {
|
|
119
126
|
id: command.entity_id ?? command.command_id,
|
|
@@ -123,20 +130,23 @@ function buildOptimisticOrder(command, payload) {
|
|
|
123
130
|
is_pos: true,
|
|
124
131
|
customer_name: payload.customer_name ?? null,
|
|
125
132
|
customer_phone: payload.customer_phone ?? null,
|
|
126
|
-
pos_payment_method:
|
|
127
|
-
paid_at: null,
|
|
128
|
-
|
|
133
|
+
pos_payment_method: method,
|
|
134
|
+
paid_at: isCash ? now : null,
|
|
135
|
+
// Must match createLocalOrder: direct-flow cash sales are born delivered,
|
|
136
|
+
// even when the command is parked in the offline outbox. Hardcoding
|
|
137
|
+
// "accepted" made a completed sale show up on the preparing board.
|
|
138
|
+
status: initialPosOrderStatus(method, flowMode),
|
|
129
139
|
delivery_id: null,
|
|
130
140
|
delivery_status: null,
|
|
131
|
-
is_paid:
|
|
141
|
+
is_paid: isCash,
|
|
132
142
|
payment: {
|
|
133
|
-
is_paid:
|
|
143
|
+
is_paid: isCash,
|
|
134
144
|
reference: null,
|
|
135
145
|
amount_minor: totalMinor,
|
|
136
146
|
currency: currencyCode,
|
|
137
|
-
paid_at:
|
|
147
|
+
paid_at: isCash ? now : null,
|
|
138
148
|
},
|
|
139
|
-
payment_mode: "pos_cash",
|
|
149
|
+
payment_mode: isCash ? "pos_cash" : "prepaid",
|
|
140
150
|
total_minor: totalMinor,
|
|
141
151
|
subtotal_minor: subtotalMinor,
|
|
142
152
|
delivery_fee_minor: payload.delivery_fee_minor ?? 0,
|
|
@@ -268,7 +278,13 @@ class CommandQueueService {
|
|
|
268
278
|
order_number: preparedCommand.payload.order_number ?? null,
|
|
269
279
|
outbox_size: pending.length,
|
|
270
280
|
});
|
|
271
|
-
|
|
281
|
+
// Direct-flow cash sales are complete on creation. The optimistic order
|
|
282
|
+
// must use the same cached business setting as createLocalOrder, otherwise
|
|
283
|
+
// an offline direct sale appears as "accepted" and offers kitchen steps.
|
|
284
|
+
const flowMode = preparedCommand.command_type === "order:create"
|
|
285
|
+
? await getBusinessOrderFlowMode(preparedCommand.business_id)
|
|
286
|
+
: DEFAULT_ORDER_FLOW_MODE;
|
|
287
|
+
const event = buildOptimisticEvent(preparedCommand, flowMode);
|
|
272
288
|
syncLogger.info("CommandQueueService", "Optimistic event built", {
|
|
273
289
|
event_type: event.event_type,
|
|
274
290
|
entity_id: event.entity_id,
|
|
@@ -220,7 +220,7 @@ function patchPendingStatus(event) {
|
|
|
220
220
|
if (!target)
|
|
221
221
|
return;
|
|
222
222
|
const patched = { ...target, status: toStatus };
|
|
223
|
-
store.
|
|
223
|
+
store.updateOrder(patched);
|
|
224
224
|
}
|
|
225
225
|
catch { }
|
|
226
226
|
}
|
|
@@ -244,7 +244,7 @@ function patchPendingPaid(event) {
|
|
|
244
244
|
paid_at: paidAt,
|
|
245
245
|
payment: { ...t.payment, is_paid: true, paid_at: paidAt },
|
|
246
246
|
};
|
|
247
|
-
store.
|
|
247
|
+
store.updateOrder(patched);
|
|
248
248
|
}
|
|
249
249
|
catch { }
|
|
250
250
|
}
|
|
@@ -277,7 +277,7 @@ export async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
277
277
|
const store = usePendingOrdersStore.getState();
|
|
278
278
|
const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
|
|
279
279
|
if (target)
|
|
280
|
-
store.
|
|
280
|
+
store.updateOrder({ ...target, status: toStatus });
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
catch { }
|
package/package.json
CHANGED