@teincfood/core 0.10.21 → 0.10.22
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/db/migrations.js +24 -0
- package/dist/db/operations.js +2 -2
- package/dist/domain/orders/order-flow.d.ts +5 -4
- package/dist/domain/orders/order-flow.js +7 -4
- package/dist/domain/orders/service.js +9 -3
- package/dist/printer/service.js +1 -1
- package/dist/sync/command-queue.service.js +10 -5
- package/dist/sync/order-number.d.ts +13 -6
- package/dist/sync/order-number.js +55 -42
- package/dist/sync/pending-orders.service.d.ts +0 -10
- package/dist/sync/pending-orders.service.js +6 -3
- package/dist/sync/sync-jobs.service.js +1 -1
- package/dist/sync/use-sync-reconciliation.js +1 -1
- package/package.json +1 -1
package/dist/db/migrations.js
CHANGED
|
@@ -473,4 +473,28 @@ export const MIGRATIONS = [
|
|
|
473
473
|
catch { }
|
|
474
474
|
},
|
|
475
475
|
},
|
|
476
|
+
{
|
|
477
|
+
version: 14,
|
|
478
|
+
up: async (db) => {
|
|
479
|
+
// Fresh local data for the DD-NNNN-MMM order-number cutover.
|
|
480
|
+
//
|
|
481
|
+
// Old local rows, queued commands and cached reference data can carry
|
|
482
|
+
// stale order numbers/statuses and would otherwise be replayed under
|
|
483
|
+
// the new sequence. Accounts will sign in again after this update, so a
|
|
484
|
+
// clean local slate is intentional: everything is re-hydrated from the
|
|
485
|
+
// server on the next login/sync.
|
|
486
|
+
//
|
|
487
|
+
// The per-business callout sequence lives in KV, not SQLite, and is
|
|
488
|
+
// deliberately left untouched so a reset can never reuse an existing
|
|
489
|
+
// server-side order number.
|
|
490
|
+
await db.execAsync(`
|
|
491
|
+
DELETE FROM sync_jobs;
|
|
492
|
+
DELETE FROM commands;
|
|
493
|
+
DELETE FROM events;
|
|
494
|
+
DELETE FROM entity_snapshots;
|
|
495
|
+
DELETE FROM orders;
|
|
496
|
+
DELETE FROM reference_data;
|
|
497
|
+
`);
|
|
498
|
+
},
|
|
499
|
+
},
|
|
476
500
|
];
|
package/dist/db/operations.js
CHANGED
|
@@ -384,7 +384,7 @@ export async function upsertServerOrder(db, order, opts = {}) {
|
|
|
384
384
|
* replaying an older snapshot can never overwrite a newer local status.
|
|
385
385
|
*/
|
|
386
386
|
export async function listOrdersWithHlc(db, businessId, limit = 200) {
|
|
387
|
-
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
|
+
const rows = await db.getAllAsync(`SELECT * FROM orders WHERE business_id = ? AND deleted_at IS NULL ORDER BY inserted_at DESC, rowid DESC LIMIT ?;`, businessId, limit);
|
|
388
388
|
return rows.map((row) => ({
|
|
389
389
|
...orderRowToSellerOrder(row),
|
|
390
390
|
hlc: row.hlc ?? row.updated_at ?? row.inserted_at,
|
|
@@ -433,7 +433,7 @@ export async function listOrders(db, businessId, filters, page = 1, pageSize = 2
|
|
|
433
433
|
const countRow = await db.getFirstAsync(`SELECT COUNT(*) as c FROM orders WHERE ${whereSql};`, ...args);
|
|
434
434
|
const total = countRow?.c ?? 0;
|
|
435
435
|
const offset = (page - 1) * pageSize;
|
|
436
|
-
const rows = await db.getAllAsync(`SELECT * FROM orders WHERE ${whereSql} ORDER BY inserted_at DESC LIMIT ? OFFSET ?;`, ...args, pageSize, offset);
|
|
436
|
+
const rows = await db.getAllAsync(`SELECT * FROM orders WHERE ${whereSql} ORDER BY inserted_at DESC, rowid DESC LIMIT ? OFFSET ?;`, ...args, pageSize, offset);
|
|
437
437
|
return { data: rows.map(orderRowToSellerOrder), total };
|
|
438
438
|
}
|
|
439
439
|
export async function updateOrderStatus(db, businessId, orderId, toStatus, hlc) {
|
|
@@ -33,9 +33,10 @@ export declare function getBusinessOrderFlowMode(businessId: string, db?: SQLite
|
|
|
33
33
|
/**
|
|
34
34
|
* Status a POS/business-app order starts in.
|
|
35
35
|
*
|
|
36
|
-
* Cash sales in direct flow are complete on creation
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Cash sales in direct flow are complete on creation, except delivery orders:
|
|
37
|
+
* those must always enter the rider pipeline. Anything that still needs money
|
|
38
|
+
* (mobile money / card) must stay `awaiting_payment` first — the backend moves
|
|
39
|
+
* it to `accepted` (staged/delivery) or `delivered` (direct walk-in) once the
|
|
39
40
|
* payment lands.
|
|
40
41
|
*/
|
|
41
|
-
export declare function initialPosOrderStatus(paymentMethod: string | null | undefined, mode: OrderFlowMode): "accepted" | "awaiting_payment" | "delivered";
|
|
42
|
+
export declare function initialPosOrderStatus(paymentMethod: string | null | undefined, mode: OrderFlowMode, orderType?: string | null | undefined): "accepted" | "awaiting_payment" | "delivered";
|
|
@@ -53,14 +53,17 @@ export async function getBusinessOrderFlowMode(businessId, db) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Status a POS/business-app order starts in.
|
|
55
55
|
*
|
|
56
|
-
* Cash sales in direct flow are complete on creation
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Cash sales in direct flow are complete on creation, except delivery orders:
|
|
57
|
+
* those must always enter the rider pipeline. Anything that still needs money
|
|
58
|
+
* (mobile money / card) must stay `awaiting_payment` first — the backend moves
|
|
59
|
+
* it to `accepted` (staged/delivery) or `delivered` (direct walk-in) once the
|
|
59
60
|
* payment lands.
|
|
60
61
|
*/
|
|
61
|
-
export function initialPosOrderStatus(paymentMethod, mode) {
|
|
62
|
+
export function initialPosOrderStatus(paymentMethod, mode, orderType = "takeout") {
|
|
62
63
|
const method = paymentMethod ?? "cash";
|
|
63
64
|
if (method !== "cash")
|
|
64
65
|
return "awaiting_payment";
|
|
66
|
+
if (orderType === "delivery")
|
|
67
|
+
return "accepted";
|
|
65
68
|
return mode === "direct" ? "delivered" : "accepted";
|
|
66
69
|
}
|
|
@@ -80,6 +80,7 @@ export async function createLocalOrder(businessId, input) {
|
|
|
80
80
|
// Offline validation mirror — backend Order.changeset rules, pre-commit.
|
|
81
81
|
validateOrderCreateInput(input);
|
|
82
82
|
const method = input.pos_payment_method ?? "cash";
|
|
83
|
+
const orderType = input.order_type ?? "takeout";
|
|
83
84
|
// Business-configurable fulfilment flow (cached business profile, offline).
|
|
84
85
|
// "direct" completes walk-in sales the moment they are rung up; "staged"
|
|
85
86
|
// keeps accept → preparing → ready → completed.
|
|
@@ -144,8 +145,10 @@ export async function createLocalOrder(businessId, input) {
|
|
|
144
145
|
id,
|
|
145
146
|
order_number: orderNumber,
|
|
146
147
|
callout_number: callout,
|
|
147
|
-
status: isPos
|
|
148
|
-
|
|
148
|
+
status: isPos
|
|
149
|
+
? initialPosOrderStatus(method, flowMode, orderType)
|
|
150
|
+
: "pending",
|
|
151
|
+
order_type: orderType,
|
|
149
152
|
is_pos: isPos,
|
|
150
153
|
customer_name: input.customer_name ?? null,
|
|
151
154
|
customer_phone: input.customer_phone ?? null,
|
|
@@ -222,7 +225,10 @@ export async function transitionLocalOrder(businessId, orderId, action) {
|
|
|
222
225
|
}
|
|
223
226
|
else {
|
|
224
227
|
const flowMode = await getBusinessOrderFlowMode(businessId, db);
|
|
225
|
-
toStatus =
|
|
228
|
+
toStatus =
|
|
229
|
+
flowMode === "direct" && order.order_type !== "delivery"
|
|
230
|
+
? "delivered"
|
|
231
|
+
: "accepted";
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
await updateOrderPayment(db, businessId, orderId, { is_paid: true, paid_at: new Date().toISOString() });
|
package/dist/printer/service.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function printOrderReceipt(order, opts) {
|
|
|
63
63
|
if (isTcp) {
|
|
64
64
|
try {
|
|
65
65
|
// Prefer backend escpos when we have a real UUID (not DD-RRR-CCC)
|
|
66
|
-
const isLocal = /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(order.order_number) || /^[0-9A-F-]{36}$/.test(order.id) === false && order.id === order.order_number;
|
|
66
|
+
const isLocal = /^[0-9]{2}-[0-9]{3,4}-[0-9]{3}$/.test(order.order_number) || /^[0-9A-F-]{36}$/.test(order.id) === false && order.id === order.order_number;
|
|
67
67
|
if (!isLocal || order.id) {
|
|
68
68
|
try {
|
|
69
69
|
escpos = await fetchReceipt(order.id, "escpos");
|
|
@@ -19,7 +19,7 @@ import { extractApiErrorCode } from "../utils/error";
|
|
|
19
19
|
import { generateEventId } from "../utils/uuid";
|
|
20
20
|
import { buildItemSnapshots, snapshotsToOrderLines } from "../domain/orders/snapshots";
|
|
21
21
|
import { DEFAULT_ORDER_FLOW_MODE, getBusinessOrderFlowMode, initialPosOrderStatus, } from "../domain/orders/order-flow";
|
|
22
|
-
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrderNumber, } from "./order-number";
|
|
22
|
+
import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrderNumber, parseOrderNumber, } from "./order-number";
|
|
23
23
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
24
24
|
import { useAuthStore } from "../adapters/auth";
|
|
25
25
|
import { canBusinessSync } from "./sync-guard";
|
|
@@ -121,6 +121,7 @@ function buildOptimisticOrder(command, payload, flowMode) {
|
|
|
121
121
|
const currencyCode = payload.currency_code ?? "GHS";
|
|
122
122
|
const now = new Date().toISOString();
|
|
123
123
|
const method = payload.pos_payment_method ?? "cash";
|
|
124
|
+
const orderType = payload.order_type ?? "takeout";
|
|
124
125
|
const isCash = method === "cash";
|
|
125
126
|
const authUser = useAuthStore.getState().user;
|
|
126
127
|
const teller = authUser
|
|
@@ -133,11 +134,15 @@ function buildOptimisticOrder(command, payload, flowMode) {
|
|
|
133
134
|
}
|
|
134
135
|
: null;
|
|
135
136
|
const orderNumber = payload.order_number ?? generateOrderNumberForBusiness(command.business_id);
|
|
137
|
+
// Keep the optimistic callout in lock-step with the order number that was
|
|
138
|
+
// already generated by createLocalOrder. Calling the sequence peek here
|
|
139
|
+
// would point one order ahead.
|
|
140
|
+
const parsedCallout = parseOrderNumber(orderNumber)?.callout;
|
|
136
141
|
return {
|
|
137
142
|
id: command.entity_id ?? command.command_id,
|
|
138
143
|
order_number: orderNumber,
|
|
139
|
-
callout_number: generateLocalCalloutNumber(command.business_id),
|
|
140
|
-
order_type:
|
|
144
|
+
callout_number: parsedCallout ? Number(parsedCallout) : generateLocalCalloutNumber(command.business_id),
|
|
145
|
+
order_type: orderType,
|
|
141
146
|
is_pos: true,
|
|
142
147
|
customer_name: payload.customer_name ?? null,
|
|
143
148
|
customer_phone: payload.customer_phone ?? null,
|
|
@@ -146,7 +151,7 @@ function buildOptimisticOrder(command, payload, flowMode) {
|
|
|
146
151
|
// Must match createLocalOrder: direct-flow cash sales are born delivered,
|
|
147
152
|
// even when the command is parked in the offline outbox. Hardcoding
|
|
148
153
|
// "accepted" made a completed sale show up on the preparing board.
|
|
149
|
-
status: initialPosOrderStatus(method, flowMode),
|
|
154
|
+
status: initialPosOrderStatus(method, flowMode, orderType),
|
|
150
155
|
delivery_id: null,
|
|
151
156
|
delivery_status: null,
|
|
152
157
|
is_paid: isCash,
|
|
@@ -437,7 +442,7 @@ class CommandQueueService {
|
|
|
437
442
|
try {
|
|
438
443
|
const payload = command.payload;
|
|
439
444
|
const on = payload.order_number;
|
|
440
|
-
const parsed = on ? on.match(/^([0-9]{2})-([0-9]{3})-([0-9]{3})$/) : null;
|
|
445
|
+
const parsed = on ? on.match(/^([0-9]{2})-([0-9]{3,4})-([0-9]{3})$/) : null;
|
|
441
446
|
if (parsed) {
|
|
442
447
|
const deviceNumber = parsed[1];
|
|
443
448
|
const { orderNumber: newOn } = generateOrderNumber(deviceNumber, command.business_id);
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Universal order number: DD-
|
|
2
|
+
* Universal order number: DD-NNNN-MMM (11 chars)
|
|
3
3
|
* DD = device number 01-99 (leased per business)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* NNNN = block number 0001-9999, increments after MMM reaches 999
|
|
5
|
+
* MMM = order sequence 001-999 inside the current block
|
|
6
|
+
*
|
|
7
|
+
* Examples: "10-0001-001" ... "10-0001-999", "10-0002-001".
|
|
8
|
+
*
|
|
9
|
+
* The sequence is persisted per business (per device install), so it survives
|
|
10
|
+
* restarts. Legacy DD-RRR-CCC numbers (7-digit order part) are still accepted
|
|
11
|
+
* when reading old rows; only new numbers use the block format.
|
|
7
12
|
*/
|
|
13
|
+
/** Current (last generated) callout number, for receipt/display fallbacks. */
|
|
8
14
|
export declare function peekCallout(businessId?: string | null): number;
|
|
9
15
|
export declare function generateOrderNumber(deviceNumber: string, businessId?: string | null): {
|
|
10
16
|
orderNumber: string;
|
|
11
17
|
callout: number;
|
|
12
|
-
|
|
18
|
+
block: number;
|
|
19
|
+
sequence: number;
|
|
13
20
|
};
|
|
14
21
|
export declare function generateLocalOrderNumber(): string;
|
|
15
22
|
export declare function generateOrderNumberForBusiness(businessId: string): string;
|
|
@@ -18,7 +25,7 @@ export declare function isLocalOrderNumber(orderNumber: string): boolean;
|
|
|
18
25
|
export declare function isOfflineOrderNumber(orderNumber: string): boolean;
|
|
19
26
|
export declare function parseOrderNumber(orderNumber: string): {
|
|
20
27
|
deviceNumber: string;
|
|
21
|
-
|
|
28
|
+
block: string;
|
|
22
29
|
callout: string;
|
|
23
30
|
} | null;
|
|
24
31
|
export declare function resetCalloutForTests(businessId?: string | null): void;
|
|
@@ -1,60 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Universal order number: DD-
|
|
2
|
+
* Universal order number: DD-NNNN-MMM (11 chars)
|
|
3
3
|
* DD = device number 01-99 (leased per business)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* NNNN = block number 0001-9999, increments after MMM reaches 999
|
|
5
|
+
* MMM = order sequence 001-999 inside the current block
|
|
6
|
+
*
|
|
7
|
+
* Examples: "10-0001-001" ... "10-0001-999", "10-0002-001".
|
|
8
|
+
*
|
|
9
|
+
* The sequence is persisted per business (per device install), so it survives
|
|
10
|
+
* restarts. Legacy DD-RRR-CCC numbers (7-digit order part) are still accepted
|
|
11
|
+
* when reading old rows; only new numbers use the block format.
|
|
7
12
|
*/
|
|
8
13
|
import { getItemSync as getItem, setItemSync as setItem } from "../adapters/kv";
|
|
9
14
|
const CALLOUT_SEQ_KEY = "teinc_callout_seq";
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const arr = new Uint32Array(1);
|
|
14
|
-
crypto.getRandomValues(arr);
|
|
15
|
-
return 100 + (arr[0] % 900);
|
|
16
|
-
}
|
|
17
|
-
return 100 + Math.floor(Math.random() * 900);
|
|
18
|
-
}
|
|
15
|
+
const ORDERS_PER_BLOCK = 999;
|
|
16
|
+
const MAX_BLOCKS = 9999;
|
|
17
|
+
const MAX_SEQUENCE = ORDERS_PER_BLOCK * MAX_BLOCKS;
|
|
19
18
|
function calloutKeyForBusiness(businessId) {
|
|
20
19
|
return businessId ? `${CALLOUT_SEQ_KEY}:${businessId}` : CALLOUT_SEQ_KEY;
|
|
21
20
|
}
|
|
22
|
-
|
|
21
|
+
/** Last consumed sequence number for this business (0 when brand new). */
|
|
22
|
+
function getStoredSequence(businessId) {
|
|
23
|
+
const key = calloutKeyForBusiness(businessId);
|
|
24
|
+
// Per-business isolation matters: the legacy global key is a mirror only,
|
|
25
|
+
// never the starting point for another business (that would leak a
|
|
26
|
+
// sequence across tenants on the same install).
|
|
27
|
+
return Number(getItem(key) ?? 0) || 0;
|
|
28
|
+
}
|
|
29
|
+
function sequenceParts(sequence) {
|
|
30
|
+
const zero = Math.max(0, sequence - 1);
|
|
31
|
+
return {
|
|
32
|
+
block: Math.floor(zero / ORDERS_PER_BLOCK) + 1,
|
|
33
|
+
callout: (zero % ORDERS_PER_BLOCK) + 1,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function getNextSequence(businessId) {
|
|
23
37
|
const key = calloutKeyForBusiness(businessId);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const next = getStoredSequence(businessId) + 1;
|
|
39
|
+
if (next > MAX_SEQUENCE) {
|
|
40
|
+
throw new Error(`Order number sequence exhausted for business ${businessId ?? "global"}`);
|
|
41
|
+
}
|
|
28
42
|
setItem(key, next);
|
|
29
|
-
//
|
|
43
|
+
// Keep the legacy global key in sync for older readers, but never let a
|
|
44
|
+
// different business reset it downwards.
|
|
30
45
|
if (businessId) {
|
|
31
|
-
const legacy = getItem(CALLOUT_SEQ_KEY) ?? 0;
|
|
46
|
+
const legacy = Number(getItem(CALLOUT_SEQ_KEY) ?? 0) || 0;
|
|
32
47
|
if (legacy < next)
|
|
33
48
|
setItem(CALLOUT_SEQ_KEY, next);
|
|
34
49
|
}
|
|
35
50
|
return next;
|
|
36
51
|
}
|
|
52
|
+
/** Current (last generated) callout number, for receipt/display fallbacks. */
|
|
37
53
|
export function peekCallout(businessId) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (v != null)
|
|
41
|
-
return v;
|
|
42
|
-
const legacy = getItem(CALLOUT_SEQ_KEY);
|
|
43
|
-
return legacy ?? 0;
|
|
54
|
+
const stored = getStoredSequence(businessId);
|
|
55
|
+
return stored > 0 ? sequenceParts(stored).callout : 0;
|
|
44
56
|
}
|
|
45
57
|
export function generateOrderNumber(deviceNumber, businessId) {
|
|
46
58
|
if (!/^(0[1-9]|[1-9][0-9])$/.test(deviceNumber)) {
|
|
47
59
|
throw new Error(`Invalid deviceNumber ${deviceNumber} — must be 01-99`);
|
|
48
60
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
random = cryptoRandom100to999();
|
|
54
|
-
}
|
|
55
|
-
setItem(ORDER_RANDOM_KEY, random);
|
|
56
|
-
const orderNumber = `${deviceNumber}-${String(random).padStart(3, "0")}-${String(callout).padStart(3, "0")}`;
|
|
57
|
-
return { orderNumber, callout, random };
|
|
61
|
+
const sequence = getNextSequence(businessId);
|
|
62
|
+
const { block, callout } = sequenceParts(sequence);
|
|
63
|
+
const orderNumber = `${deviceNumber}-${String(block).padStart(4, "0")}-${String(callout).padStart(3, "0")}`;
|
|
64
|
+
return { orderNumber, callout, block, sequence };
|
|
58
65
|
}
|
|
59
66
|
// No legacy fallback — 0.7.0 requires business-scoped device lease via device-lease.service.
|
|
60
67
|
// Kept for internal compat only; callers must supply businessId via generateOrderNumberForBusiness.
|
|
@@ -70,22 +77,28 @@ export function generateOrderNumberForBusiness(businessId) {
|
|
|
70
77
|
return generateOrderNumber(v, businessId).orderNumber;
|
|
71
78
|
}
|
|
72
79
|
export function generateLocalCalloutNumber(businessId) {
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
// Kept for SellerOrder.callout_number compatibility — returns the callout
|
|
81
|
+
// that the next generated order will use, without consuming it.
|
|
82
|
+
const sequence = getStoredSequence(businessId) + 1;
|
|
83
|
+
return sequenceParts(sequence).callout;
|
|
76
84
|
}
|
|
77
|
-
//
|
|
85
|
+
// Accept both the new DD-NNNN-MMM format and legacy DD-RRR-CCC rows.
|
|
78
86
|
export function isLocalOrderNumber(orderNumber) {
|
|
79
|
-
return /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(orderNumber);
|
|
87
|
+
return /^[0-9]{2}-[0-9]{3,4}-[0-9]{3}$/.test(orderNumber);
|
|
80
88
|
}
|
|
81
89
|
export function isOfflineOrderNumber(orderNumber) {
|
|
82
90
|
return isLocalOrderNumber(orderNumber);
|
|
83
91
|
}
|
|
84
92
|
export function parseOrderNumber(orderNumber) {
|
|
85
|
-
const m = orderNumber.match(/^([0-9]{2})-([0-9]{3})-([0-9]{3})$/);
|
|
93
|
+
const m = orderNumber.match(/^([0-9]{2})-([0-9]{3,4})-([0-9]{3})$/);
|
|
86
94
|
if (!m)
|
|
87
95
|
return null;
|
|
88
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
deviceNumber: m[1],
|
|
98
|
+
// Normalise legacy 3-digit middles to the new 4-digit block shape.
|
|
99
|
+
block: m[2].padStart(4, "0"),
|
|
100
|
+
callout: m[3],
|
|
101
|
+
};
|
|
89
102
|
}
|
|
90
103
|
// Test helpers
|
|
91
104
|
export function resetCalloutForTests(businessId) {
|
|
@@ -27,16 +27,6 @@ export declare function registerPendingOrdersInvalidator(fn: () => void): () =>
|
|
|
27
27
|
* pipeline, so peers' UIs converge without waiting for a cloud echo.
|
|
28
28
|
*/
|
|
29
29
|
export declare function notifyPendingOrdersChanged(): void;
|
|
30
|
-
/**
|
|
31
|
-
* Merge optimistic (queued, unconfirmed) state into a page of listed orders.
|
|
32
|
-
* Called by list queryFns (page 1 only) so offline work is visible:
|
|
33
|
-
* 1. Pending creations (order.create_queued, local or LAN) missing from the
|
|
34
|
-
* SQLite page are prepended — deduped by id/order_number.
|
|
35
|
-
* 2. Queued transitions only patch the snapshot read model, never the
|
|
36
|
-
* orders table — overlay snapshot statuses onto base rows so the list
|
|
37
|
-
* reflects the queued status immediately (converges on replay).
|
|
38
|
-
* Reactivity comes from the invalidator: every apply path above refetches.
|
|
39
|
-
*/
|
|
40
30
|
export declare function mergePendingIntoList(base: SellerOrder[], businessId: string, page: number): Promise<SellerOrder[]>;
|
|
41
31
|
/**
|
|
42
32
|
* Load persisted unsynced order:create_queued events into the store.
|
|
@@ -70,6 +70,9 @@ export function notifyPendingOrdersChanged() {
|
|
|
70
70
|
* reflects the queued status immediately (converges on replay).
|
|
71
71
|
* Reactivity comes from the invalidator: every apply path above refetches.
|
|
72
72
|
*/
|
|
73
|
+
function sortByCreatedDesc(orders) {
|
|
74
|
+
return [...orders].sort((a, b) => String(b.inserted_at ?? "").localeCompare(String(a.inserted_at ?? "")));
|
|
75
|
+
}
|
|
73
76
|
export async function mergePendingIntoList(base, businessId, page) {
|
|
74
77
|
if (page !== 1)
|
|
75
78
|
return base;
|
|
@@ -88,7 +91,7 @@ export async function mergePendingIntoList(base, businessId, page) {
|
|
|
88
91
|
const db = await openSyncDatabase();
|
|
89
92
|
const snaps = await getEntitySnapshotsByType(db, "seller_orders", businessId);
|
|
90
93
|
if (!snaps.length)
|
|
91
|
-
return merged;
|
|
94
|
+
return sortByCreatedDesc(merged);
|
|
92
95
|
const statusById = new Map();
|
|
93
96
|
for (const s of snaps) {
|
|
94
97
|
const arr = s.payload?.data ?? [];
|
|
@@ -98,7 +101,7 @@ export async function mergePendingIntoList(base, businessId, page) {
|
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
if (!statusById.size)
|
|
101
|
-
return merged;
|
|
104
|
+
return sortByCreatedDesc(merged);
|
|
102
105
|
let patched = false;
|
|
103
106
|
merged = merged.map((o) => {
|
|
104
107
|
const want = statusById.get(String(o.id));
|
|
@@ -113,7 +116,7 @@ export async function mergePendingIntoList(base, businessId, page) {
|
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
catch { }
|
|
116
|
-
return merged;
|
|
119
|
+
return sortByCreatedDesc(merged);
|
|
117
120
|
}
|
|
118
121
|
/**
|
|
119
122
|
* Load persisted unsynced order:create_queued events into the store.
|
|
@@ -481,7 +481,7 @@ export async function drainOne(businessId, entityId) {
|
|
|
481
481
|
const is409 = rest.status === 409 && rest.code === "ORDER_NUMBER_TAKEN";
|
|
482
482
|
if (is409) {
|
|
483
483
|
const on = (payload.order_number ?? payload.local_order_number);
|
|
484
|
-
const parsed = on ? String(on).match(/^([0-9]{2})-([0-9]{3})-([0-9]{3})$/) : null;
|
|
484
|
+
const parsed = on ? String(on).match(/^([0-9]{2})-([0-9]{3,4})-([0-9]{3})$/) : null;
|
|
485
485
|
if (parsed) {
|
|
486
486
|
const newOn = generateOrderNumber(parsed[1], businessId).orderNumber;
|
|
487
487
|
const newPayload = { ...payload, order_number: newOn, local_order_number: newOn };
|
|
@@ -25,7 +25,7 @@ function isLocalOrderEvent(event) {
|
|
|
25
25
|
if (!payload)
|
|
26
26
|
return false;
|
|
27
27
|
const localOrderNumber = payload.local_order_number;
|
|
28
|
-
return typeof localOrderNumber === "string" && /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(localOrderNumber);
|
|
28
|
+
return typeof localOrderNumber === "string" && /^[0-9]{2}-[0-9]{3,4}-[0-9]{3}$/.test(localOrderNumber);
|
|
29
29
|
}
|
|
30
30
|
const RECONCILE_DEBOUNCE_MS = 400;
|
|
31
31
|
export function useSyncReconciliation() {
|
package/package.json
CHANGED