@teincfood/core 0.9.24 → 0.9.26
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/catalog/hooks.js +11 -2
- package/dist/db/migrations.js +25 -0
- package/dist/db/operations.d.ts +7 -0
- package/dist/db/operations.js +95 -17
- package/dist/domain/orders/order-flow.d.ts +41 -0
- package/dist/domain/orders/order-flow.js +66 -0
- package/dist/domain/orders/service.d.ts +2 -1
- package/dist/domain/orders/service.js +9 -3
- package/dist/hooks/delivery.hooks.d.ts +24 -24
- package/dist/hooks/rider.hooks.d.ts +48 -48
- package/dist/hooks/startup-readiness.hooks.js +20 -2
- package/dist/hooks/use-infinite-list.d.ts +24 -24
- package/dist/hooks/wallet.hooks.d.ts +24 -24
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sync/device-lease.service.js +3 -1
- package/dist/sync/order-merge.js +7 -2
- package/dist/sync/order-snapshots.service.js +17 -1
- package/dist/sync/startup-readiness.js +29 -16
- package/dist/test-helpers/memory-driver.js +17 -10
- package/dist/types/api/business.api.d.ts +7 -0
- package/package.json +2 -2
package/dist/catalog/hooks.js
CHANGED
|
@@ -73,9 +73,14 @@ export function useMenuItemsQuery(filters, enabled = true) {
|
|
|
73
73
|
? networkQuery.data.pages.flatMap((p) => p.data)
|
|
74
74
|
: undefined;
|
|
75
75
|
const data = isOnline && networkData !== undefined ? networkData : enabled ? filtered : undefined;
|
|
76
|
+
// A local snapshot (even an empty list) means the POS has something to
|
|
77
|
+
// render: never gate the whole screen on the in-flight network refresh.
|
|
78
|
+
const hasLocalSnapshot = cachedQuery.data != null;
|
|
79
|
+
const pending = cachedQuery.isPending || (!hasLocalSnapshot && networkQuery.isFetching);
|
|
76
80
|
return {
|
|
77
81
|
data,
|
|
78
|
-
isPending:
|
|
82
|
+
isPending: pending,
|
|
83
|
+
isLoading: pending,
|
|
79
84
|
isFetching: networkQuery.isFetching,
|
|
80
85
|
isError: cachedQuery.isError || networkQuery.isError,
|
|
81
86
|
error: (cachedQuery.error ?? networkQuery.error),
|
|
@@ -117,10 +122,14 @@ export function useMenuCategoriesQuery() {
|
|
|
117
122
|
const usingNetwork = isOnline && networkQuery.data;
|
|
118
123
|
const data = usingNetwork ? networkQuery.data : cachedQuery.data;
|
|
119
124
|
const hasCache = cachedQuery.data != null;
|
|
125
|
+
// Only report "loading" when there is nothing local to show; the network
|
|
126
|
+
// refresh must never hide an already-cached category list.
|
|
127
|
+
const pending = cachedQuery.isPending || (!hasCache && networkQuery.isFetching);
|
|
120
128
|
return {
|
|
121
129
|
...networkQuery,
|
|
122
130
|
data,
|
|
123
|
-
isPending:
|
|
131
|
+
isPending: pending,
|
|
132
|
+
isLoading: pending,
|
|
124
133
|
isError: hasCache ? false : networkQuery.isError,
|
|
125
134
|
error: hasCache ? null : networkQuery.error,
|
|
126
135
|
};
|
package/dist/db/migrations.js
CHANGED
|
@@ -423,4 +423,29 @@ export const MIGRATIONS = [
|
|
|
423
423
|
`);
|
|
424
424
|
},
|
|
425
425
|
},
|
|
426
|
+
{
|
|
427
|
+
version: 12,
|
|
428
|
+
up: async (db) => {
|
|
429
|
+
// Payment timestamp at the top level of the order.
|
|
430
|
+
//
|
|
431
|
+
// The server returns `paid_at` on the order and (for cash sales taken at
|
|
432
|
+
// the till) no payment transaction at all. Keeping the timestamp only
|
|
433
|
+
// inside the `payment` JSON blob meant every pull overwrote a locally
|
|
434
|
+
// paid cash order with the server's `{is_paid:false, paid_at:null}`,
|
|
435
|
+
// so the row read back as UNPAID. One column, backfilled from the blob,
|
|
436
|
+
// is the single local source of truth.
|
|
437
|
+
await db.execAsync(`
|
|
438
|
+
ALTER TABLE orders ADD COLUMN paid_at TEXT;
|
|
439
|
+
`);
|
|
440
|
+
try {
|
|
441
|
+
await db.execAsync(`
|
|
442
|
+
UPDATE orders
|
|
443
|
+
SET paid_at = json_extract(payment, '$.paid_at')
|
|
444
|
+
WHERE paid_at IS NULL
|
|
445
|
+
AND json_extract(payment, '$.paid_at') IS NOT NULL;
|
|
446
|
+
`);
|
|
447
|
+
}
|
|
448
|
+
catch { }
|
|
449
|
+
},
|
|
450
|
+
},
|
|
426
451
|
];
|
package/dist/db/operations.d.ts
CHANGED
|
@@ -66,10 +66,17 @@ export interface OrderRow {
|
|
|
66
66
|
inserted_at: string;
|
|
67
67
|
updated_at: string | null;
|
|
68
68
|
hlc: string | null;
|
|
69
|
+
paid_at?: string | null;
|
|
69
70
|
deleted_at: string | null;
|
|
70
71
|
sync_failed_at?: string | null;
|
|
71
72
|
sync_error?: string | null;
|
|
72
73
|
}
|
|
74
|
+
/** True when the order has any recorded payment. */
|
|
75
|
+
export declare function isOrderPaid(order: {
|
|
76
|
+
is_paid?: boolean;
|
|
77
|
+
paid_at?: string | null;
|
|
78
|
+
payment?: unknown;
|
|
79
|
+
} | null | undefined): boolean;
|
|
73
80
|
export declare function insertOrder(db: SQLiteDatabase, order: import("../types/api/seller.api").SellerOrder & {
|
|
74
81
|
hlc?: string;
|
|
75
82
|
}): Promise<void>;
|
package/dist/db/operations.js
CHANGED
|
@@ -198,7 +198,47 @@ function rowToDomainEvent(row) {
|
|
|
198
198
|
timestamp: row.timestamp,
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
|
+
const EMPTY_PAYMENT = {
|
|
202
|
+
is_paid: false,
|
|
203
|
+
reference: null,
|
|
204
|
+
amount_minor: null,
|
|
205
|
+
currency: null,
|
|
206
|
+
paid_at: null,
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Effective payment of a stored order.
|
|
210
|
+
*
|
|
211
|
+
* `payment` (server payment summary) is the primary shape, but the order's
|
|
212
|
+
* own `paid_at` column wins when the server did not report a payment. Cash
|
|
213
|
+
* sales taken at a POS terminal only carry `paid_at` — no payment
|
|
214
|
+
* transaction — so reading the blob alone made them look unpaid.
|
|
215
|
+
*/
|
|
216
|
+
function paymentStateOf(row) {
|
|
217
|
+
let parsed = {};
|
|
218
|
+
if (row.payment) {
|
|
219
|
+
try {
|
|
220
|
+
const value = JSON.parse(row.payment);
|
|
221
|
+
if (value && typeof value === "object")
|
|
222
|
+
parsed = value;
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
}
|
|
226
|
+
const payment = { ...EMPTY_PAYMENT, ...parsed };
|
|
227
|
+
const paidAt = payment.paid_at ?? row.paid_at ?? null;
|
|
228
|
+
const isPaid = payment.is_paid === true || !!paidAt;
|
|
229
|
+
return { payment: { ...payment, paid_at: paidAt, is_paid: isPaid }, isPaid, paidAt };
|
|
230
|
+
}
|
|
231
|
+
/** True when the order has any recorded payment. */
|
|
232
|
+
export function isOrderPaid(order) {
|
|
233
|
+
if (!order)
|
|
234
|
+
return false;
|
|
235
|
+
if (order.is_paid === true || order.paid_at)
|
|
236
|
+
return true;
|
|
237
|
+
const payment = order.payment;
|
|
238
|
+
return payment?.is_paid === true || !!payment?.paid_at;
|
|
239
|
+
}
|
|
201
240
|
function orderRowToSellerOrder(row) {
|
|
241
|
+
const { payment, isPaid, paidAt } = paymentStateOf(row);
|
|
202
242
|
return {
|
|
203
243
|
id: row.id,
|
|
204
244
|
business_id: row.business_id,
|
|
@@ -212,7 +252,7 @@ function orderRowToSellerOrder(row) {
|
|
|
212
252
|
total_minor: row.total_minor,
|
|
213
253
|
tax_minor: row.tax_minor,
|
|
214
254
|
tax_lines: row.tax_lines ? JSON.parse(row.tax_lines) : [],
|
|
215
|
-
payment
|
|
255
|
+
payment,
|
|
216
256
|
customer_name: row.customer_name,
|
|
217
257
|
customer_phone: row.customer_phone,
|
|
218
258
|
pos_payment_method: row.pos_payment_method,
|
|
@@ -222,15 +262,15 @@ function orderRowToSellerOrder(row) {
|
|
|
222
262
|
delivery_lat: null,
|
|
223
263
|
delivery_lng: null,
|
|
224
264
|
user: row.user ? JSON.parse(row.user) : null,
|
|
225
|
-
currency:
|
|
226
|
-
currency_code:
|
|
265
|
+
currency: payment.currency ?? "GHS",
|
|
266
|
+
currency_code: payment.currency ?? "GHS",
|
|
227
267
|
discount_minor: 0,
|
|
228
268
|
promo_code: null,
|
|
229
269
|
tip_minor: null,
|
|
230
270
|
delivery_fee_minor: 0,
|
|
231
271
|
delivery_status: null,
|
|
232
|
-
is_paid:
|
|
233
|
-
paid_at:
|
|
272
|
+
is_paid: isPaid,
|
|
273
|
+
paid_at: paidAt,
|
|
234
274
|
inserted_at: row.inserted_at,
|
|
235
275
|
updated_at: row.updated_at ?? row.inserted_at,
|
|
236
276
|
sync_failed_at: row.sync_failed_at ?? null,
|
|
@@ -240,10 +280,13 @@ function orderRowToSellerOrder(row) {
|
|
|
240
280
|
export async function insertOrder(db, order) {
|
|
241
281
|
const hlc = order.hlc ?? new Date().toISOString();
|
|
242
282
|
const updatedAt = order.updated_at ?? order.inserted_at;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
283
|
+
const paidAt = order.paid_at ??
|
|
284
|
+
order.payment?.paid_at ??
|
|
285
|
+
(isOrderPaid(order) ? updatedAt : null);
|
|
286
|
+
await db.runAsync(`INSERT INTO orders (id, business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, paid_at, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, updated_at, hlc)
|
|
287
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
288
|
+
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
|
|
289
|
+
WHERE excluded.hlc >= orders.hlc OR orders.hlc IS NULL;`, order.id, order.business_id, order.order_number, order.callout_number, order.status, order.order_type, order.is_pos ? 1 : 0, JSON.stringify(order.items), order.subtotal_minor, order.total_minor, order.tax_minor, JSON.stringify(order.tax_lines), JSON.stringify(order.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);
|
|
247
290
|
notifyDbChange(order.business_id, "orders");
|
|
248
291
|
}
|
|
249
292
|
export async function getOrderById(db, businessId, idOrNumber) {
|
|
@@ -253,7 +296,7 @@ export async function getOrderById(db, businessId, idOrNumber) {
|
|
|
253
296
|
export async function getPendingOrderIntent(db, businessId, orderId) {
|
|
254
297
|
const empty = { status: null, isPaid: false, hasPending: false };
|
|
255
298
|
try {
|
|
256
|
-
const rows = await db.getAllAsync(`SELECT command_type FROM sync_jobs WHERE business_id = ? AND entity_id = ? AND status = 'pending' ORDER BY created_at ASC, rowid ASC;`, businessId, orderId);
|
|
299
|
+
const rows = await db.getAllAsync(`SELECT command_type, payload FROM sync_jobs WHERE business_id = ? AND entity_id = ? AND status = 'pending' ORDER BY created_at ASC, rowid ASC;`, businessId, orderId);
|
|
257
300
|
if (!rows.length)
|
|
258
301
|
return empty;
|
|
259
302
|
let status = null;
|
|
@@ -266,6 +309,19 @@ export async function getPendingOrderIntent(db, businessId, orderId) {
|
|
|
266
309
|
if (commandType === "payment:confirm_cash" || commandType === "order:confirm_cash") {
|
|
267
310
|
isPaid = true;
|
|
268
311
|
}
|
|
312
|
+
// A cash sale is paid at the moment it is rung up — the queued create
|
|
313
|
+
// itself is the record of that intent. Without this, a pull landing
|
|
314
|
+
// before the create reaches the backend replaces the paid local row
|
|
315
|
+
// with the server's (older, unpaid) copy.
|
|
316
|
+
if (commandType === "order:create") {
|
|
317
|
+
try {
|
|
318
|
+
const payload = row.payload ? JSON.parse(row.payload) : null;
|
|
319
|
+
const method = payload?.pos_payment_method ?? "cash";
|
|
320
|
+
if (method === "cash")
|
|
321
|
+
isPaid = true;
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
}
|
|
269
325
|
}
|
|
270
326
|
return { status, isPaid, hasPending: true };
|
|
271
327
|
}
|
|
@@ -289,15 +345,35 @@ export async function upsertServerOrder(db, order, opts = {}) {
|
|
|
289
345
|
return;
|
|
290
346
|
const existing = await getOrderById(db, order.business_id, order.id);
|
|
291
347
|
const status = opts.preserveStatus && existing ? existing.status : order.status;
|
|
292
|
-
const
|
|
348
|
+
const serverPayment = (order.payment ?? {});
|
|
349
|
+
const serverPaidAt = order.paid_at ?? serverPayment.paid_at ?? null;
|
|
350
|
+
const serverIsPaid = order.is_paid === true || !!serverPaidAt || serverPayment.is_paid === true;
|
|
351
|
+
let payment;
|
|
352
|
+
if (existing &&
|
|
353
|
+
(opts.preservePayment || (!serverIsPaid && isOrderPaid(existing)))) {
|
|
354
|
+
// Keep the device's payment. A pull must never un-pay an order: the
|
|
355
|
+
// server copy of a cash sale rung up moments ago can lag behind, and it
|
|
356
|
+
// used to reset the row to UNPAID until the create finished syncing.
|
|
357
|
+
payment = { ...existing.payment };
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
payment = {
|
|
361
|
+
...serverPayment,
|
|
362
|
+
is_paid: serverIsPaid,
|
|
363
|
+
paid_at: serverPaidAt,
|
|
364
|
+
amount_minor: serverPayment.amount_minor ?? order.total_minor ?? null,
|
|
365
|
+
currency: serverPayment.currency ?? order.currency_code ?? null,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const paidAt = payment.paid_at ?? null;
|
|
293
369
|
const hlc = order.hlc ??
|
|
294
370
|
order.updated_at ??
|
|
295
371
|
order.inserted_at ??
|
|
296
372
|
new Date().toISOString();
|
|
297
373
|
const updatedAt = order.updated_at ?? order.inserted_at;
|
|
298
|
-
await db.runAsync(`INSERT INTO orders (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, updated_at, hlc)
|
|
299
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
300
|
-
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, 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), 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);
|
|
374
|
+
await db.runAsync(`INSERT INTO orders (id, business_id, order_number, callout_number, status, order_type, is_pos, items, subtotal_minor, total_minor, tax_minor, tax_lines, payment, paid_at, customer_name, customer_phone, pos_payment_method, delivery_id, business, user, inserted_at, updated_at, hlc)
|
|
375
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
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);
|
|
301
377
|
notifyDbChange(order.business_id, "orders");
|
|
302
378
|
}
|
|
303
379
|
export async function listOrders(db, businessId, filters, page = 1, pageSize = 20) {
|
|
@@ -318,11 +394,13 @@ export async function listOrders(db, businessId, filters, page = 1, pageSize = 2
|
|
|
318
394
|
where.push("order_type = ?");
|
|
319
395
|
args.push(filters.order_type);
|
|
320
396
|
}
|
|
397
|
+
// Paid means "the order has a payment timestamp" (cash at the till) or the
|
|
398
|
+
// payment blob says so (gateway payment) — mirroring the backend filter.
|
|
321
399
|
if (filters?.is_paid === "paid") {
|
|
322
|
-
where.push("json_extract(payment, '$.is_paid') = 1");
|
|
400
|
+
where.push("(json_extract(payment, '$.is_paid') = 1 OR paid_at IS NOT NULL)");
|
|
323
401
|
}
|
|
324
402
|
else if (filters?.is_paid === "unpaid") {
|
|
325
|
-
where.push("(json_extract(payment, '$.is_paid')
|
|
403
|
+
where.push("(COALESCE(json_extract(payment, '$.is_paid'), 0) = 0 AND paid_at IS NULL)");
|
|
326
404
|
}
|
|
327
405
|
if (filters?.q) {
|
|
328
406
|
where.push("(order_number LIKE ? OR customer_name LIKE ? OR customer_phone LIKE ?)");
|
|
@@ -366,7 +444,7 @@ export async function updateOrderPayment(db, businessId, orderId, patch) {
|
|
|
366
444
|
if (!row)
|
|
367
445
|
return;
|
|
368
446
|
const payment = { ...row.payment, is_paid: patch.is_paid, paid_at: patch.paid_at };
|
|
369
|
-
await db.runAsync(`UPDATE orders SET payment = ?, hlc = ? WHERE business_id = ? AND (id = ? OR order_number = ?);`, JSON.stringify(payment), new Date().toISOString(), businessId, orderId, orderId);
|
|
447
|
+
await db.runAsync(`UPDATE orders SET payment = ?, paid_at = COALESCE(?, paid_at), hlc = ? WHERE business_id = ? AND (id = ? OR order_number = ?);`, JSON.stringify(payment), patch.is_paid ? patch.paid_at : null, new Date().toISOString(), businessId, orderId, orderId);
|
|
370
448
|
notifyDbChange(businessId, "orders");
|
|
371
449
|
}
|
|
372
450
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business-configurable fulfilment flow for terminal / business-app orders.
|
|
3
|
+
*
|
|
4
|
+
* Buyer-placed online orders always use the full pipeline
|
|
5
|
+
* (accept → preparing → ready → completed). Orders rung up on a POS terminal
|
|
6
|
+
* or in the business app follow the business setting:
|
|
7
|
+
*
|
|
8
|
+
* "staged" (default) — same pipeline as online orders.
|
|
9
|
+
* "direct" — the sale is completed the moment it is taken, so the
|
|
10
|
+
* order is born `delivered` and no pipeline buttons are
|
|
11
|
+
* offered.
|
|
12
|
+
*
|
|
13
|
+
* The setting lives on the business (`businesses.pos_order_flow`) and is
|
|
14
|
+
* cached in the `business:profile` reference record, so it is readable while
|
|
15
|
+
* offline.
|
|
16
|
+
*/
|
|
17
|
+
import type { SQLiteDatabase } from "../../adapters/sqlite";
|
|
18
|
+
export type OrderFlowMode = "staged" | "direct";
|
|
19
|
+
export declare const DEFAULT_ORDER_FLOW_MODE: OrderFlowMode;
|
|
20
|
+
/** Coerce any server/local value into a known mode. */
|
|
21
|
+
export declare function normalizeOrderFlowMode(value: unknown): OrderFlowMode;
|
|
22
|
+
export declare function isDirectOrderFlow(value: unknown): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Read `pos_order_flow` out of a `business:profile` payload. Accepts both the
|
|
25
|
+
* reference-data envelope (`{ data: {...} }`) and a bare profile object.
|
|
26
|
+
*/
|
|
27
|
+
export declare function orderFlowModeFromProfile(payload: unknown): OrderFlowMode;
|
|
28
|
+
/**
|
|
29
|
+
* Cached business flow mode (works fully offline). Falls back to "staged" when
|
|
30
|
+
* the profile has never been synced — the safe, full-pipeline default.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getBusinessOrderFlowMode(businessId: string, db?: SQLiteDatabase): Promise<OrderFlowMode>;
|
|
33
|
+
/**
|
|
34
|
+
* Status a POS/business-app order starts in.
|
|
35
|
+
*
|
|
36
|
+
* Cash sales in direct flow are complete on creation. Anything that still
|
|
37
|
+
* needs money (mobile money / card) must stay `awaiting_payment` first — the
|
|
38
|
+
* backend moves it to `accepted` (staged) or `delivered` (direct) once the
|
|
39
|
+
* payment lands.
|
|
40
|
+
*/
|
|
41
|
+
export declare function initialPosOrderStatus(paymentMethod: string | null | undefined, mode: OrderFlowMode): "accepted" | "awaiting_payment" | "delivered";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business-configurable fulfilment flow for terminal / business-app orders.
|
|
3
|
+
*
|
|
4
|
+
* Buyer-placed online orders always use the full pipeline
|
|
5
|
+
* (accept → preparing → ready → completed). Orders rung up on a POS terminal
|
|
6
|
+
* or in the business app follow the business setting:
|
|
7
|
+
*
|
|
8
|
+
* "staged" (default) — same pipeline as online orders.
|
|
9
|
+
* "direct" — the sale is completed the moment it is taken, so the
|
|
10
|
+
* order is born `delivered` and no pipeline buttons are
|
|
11
|
+
* offered.
|
|
12
|
+
*
|
|
13
|
+
* The setting lives on the business (`businesses.pos_order_flow`) and is
|
|
14
|
+
* cached in the `business:profile` reference record, so it is readable while
|
|
15
|
+
* offline.
|
|
16
|
+
*/
|
|
17
|
+
import { openSyncDatabase } from "../../db/connection";
|
|
18
|
+
import { getReferenceData } from "../../reference/operations";
|
|
19
|
+
export const DEFAULT_ORDER_FLOW_MODE = "staged";
|
|
20
|
+
/** Coerce any server/local value into a known mode. */
|
|
21
|
+
export function normalizeOrderFlowMode(value) {
|
|
22
|
+
return value === "direct" ? "direct" : DEFAULT_ORDER_FLOW_MODE;
|
|
23
|
+
}
|
|
24
|
+
export function isDirectOrderFlow(value) {
|
|
25
|
+
return normalizeOrderFlowMode(value) === "direct";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read `pos_order_flow` out of a `business:profile` payload. Accepts both the
|
|
29
|
+
* reference-data envelope (`{ data: {...} }`) and a bare profile object.
|
|
30
|
+
*/
|
|
31
|
+
export function orderFlowModeFromProfile(payload) {
|
|
32
|
+
if (!payload || typeof payload !== "object")
|
|
33
|
+
return DEFAULT_ORDER_FLOW_MODE;
|
|
34
|
+
const record = payload;
|
|
35
|
+
return normalizeOrderFlowMode(record.data?.pos_order_flow ?? record.pos_order_flow);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Cached business flow mode (works fully offline). Falls back to "staged" when
|
|
39
|
+
* the profile has never been synced — the safe, full-pipeline default.
|
|
40
|
+
*/
|
|
41
|
+
export async function getBusinessOrderFlowMode(businessId, db) {
|
|
42
|
+
if (!businessId)
|
|
43
|
+
return DEFAULT_ORDER_FLOW_MODE;
|
|
44
|
+
try {
|
|
45
|
+
const connection = db ?? (await openSyncDatabase());
|
|
46
|
+
const record = await getReferenceData(connection, businessId, "business:profile");
|
|
47
|
+
return orderFlowModeFromProfile(record?.payload);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return DEFAULT_ORDER_FLOW_MODE;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Status a POS/business-app order starts in.
|
|
55
|
+
*
|
|
56
|
+
* Cash sales in direct flow are complete on creation. Anything that still
|
|
57
|
+
* needs money (mobile money / card) must stay `awaiting_payment` first — the
|
|
58
|
+
* backend moves it to `accepted` (staged) or `delivered` (direct) once the
|
|
59
|
+
* payment lands.
|
|
60
|
+
*/
|
|
61
|
+
export function initialPosOrderStatus(paymentMethod, mode) {
|
|
62
|
+
const method = paymentMethod ?? "cash";
|
|
63
|
+
if (method !== "cash")
|
|
64
|
+
return "awaiting_payment";
|
|
65
|
+
return mode === "direct" ? "delivered" : "accepted";
|
|
66
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local Orders Service — SQLite is real DB (like backend Orders).
|
|
3
3
|
* Device-partitioned by UUID PK + DD-RRR-CCC per business.
|
|
4
|
-
*
|
|
4
|
+
* POS orders start in the business's configured flow (staged → accepted,
|
|
5
|
+
* direct → delivered); online payment requires connectivity.
|
|
5
6
|
*/
|
|
6
7
|
import type { SellerOrder } from "../../types/api/seller.api";
|
|
7
8
|
import type { POSOrderCreateRequest } from "../../types/pos.types";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local Orders Service — SQLite is real DB (like backend Orders).
|
|
3
3
|
* Device-partitioned by UUID PK + DD-RRR-CCC per business.
|
|
4
|
-
*
|
|
4
|
+
* POS orders start in the business's configured flow (staged → accepted,
|
|
5
|
+
* direct → delivered); online payment requires connectivity.
|
|
5
6
|
*/
|
|
6
7
|
import { openSyncDatabase } from "../../db/connection";
|
|
7
8
|
import { insertOrder, getOrderById, listOrders, updateOrderStatus, updateOrderPayment, getEntitySnapshotsByType } from "../../db/operations";
|
|
@@ -15,6 +16,7 @@ import { canTransition } from "../../utils/order-transitions";
|
|
|
15
16
|
import { TRANSITION_ERROR_CODES, TransitionError } from "../../sync/transition-errors";
|
|
16
17
|
import { validateOrderCreateInput } from "../../validation/offline";
|
|
17
18
|
import { buildItemSnapshots, snapshotsToOrderLines } from "./snapshots";
|
|
19
|
+
import { getBusinessOrderFlowMode, initialPosOrderStatus } from "./order-flow";
|
|
18
20
|
import { drainOne, pullRemoteOrders } from "../../sync/sync-jobs.service";
|
|
19
21
|
import { getAuthToken } from "../../adapters/auth";
|
|
20
22
|
import { canBusinessSync } from "../../sync/sync-guard";
|
|
@@ -78,6 +80,10 @@ export async function createLocalOrder(businessId, input) {
|
|
|
78
80
|
// Offline validation mirror — backend Order.changeset rules, pre-commit.
|
|
79
81
|
validateOrderCreateInput(input);
|
|
80
82
|
const method = input.pos_payment_method ?? "cash";
|
|
83
|
+
// Business-configurable fulfilment flow (cached business profile, offline).
|
|
84
|
+
// "direct" completes walk-in sales the moment they are rung up; "staged"
|
|
85
|
+
// keeps accept → preparing → ready → completed.
|
|
86
|
+
const flowMode = await getBusinessOrderFlowMode(businessId);
|
|
81
87
|
// Operating-hours gate (fail-closed, offline-first): matches backend
|
|
82
88
|
// validate_business_open. Throws BUSINESS_CLOSED / HOURS_NOT_SYNCED before
|
|
83
89
|
// anything is written, so a doomed order never enters SQLite/sync_jobs.
|
|
@@ -128,7 +134,7 @@ export async function createLocalOrder(businessId, input) {
|
|
|
128
134
|
id,
|
|
129
135
|
order_number: orderNumber,
|
|
130
136
|
callout_number: callout,
|
|
131
|
-
status: isPos ?
|
|
137
|
+
status: isPos ? initialPosOrderStatus(method, flowMode) : "pending",
|
|
132
138
|
order_type: input.order_type ?? "takeout",
|
|
133
139
|
is_pos: isPos,
|
|
134
140
|
customer_name: input.customer_name ?? null,
|
|
@@ -161,7 +167,7 @@ export async function createLocalOrder(businessId, input) {
|
|
|
161
167
|
};
|
|
162
168
|
const db = await openSyncDatabase();
|
|
163
169
|
await insertOrder(db, order);
|
|
164
|
-
syncLogger.debug("DomainOrders", "createLocalOrder inserted", { businessId, id, orderNumber, isPos, connectivity: getConnectivityState(), hasToken: !!getAuthToken(), deviceNumber: dd });
|
|
170
|
+
syncLogger.debug("DomainOrders", "createLocalOrder inserted", { businessId, id, orderNumber, isPos, flowMode, status: order.status, connectivity: getConnectivityState(), hasToken: !!getAuthToken(), deviceNumber: dd });
|
|
165
171
|
try {
|
|
166
172
|
await db.runAsync(`INSERT OR IGNORE INTO sync_jobs (id, business_id, command_type, entity_id, payload, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?);`, id, businessId, "order:create", id, JSON.stringify({ ...input, order_number: orderNumber, id }), now, now);
|
|
167
173
|
syncLogger.debug("DomainOrders", "sync_jobs enqueued", { businessId, id, command_type: "order:create" });
|
|
@@ -36,10 +36,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
36
36
|
isRefetching: boolean;
|
|
37
37
|
isStale: boolean;
|
|
38
38
|
isEnabled: boolean;
|
|
39
|
-
refetch: (options?: import("@tanstack/query
|
|
40
|
-
fetchStatus: import("@tanstack/query
|
|
41
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
42
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
39
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
40
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
41
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
42
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
43
43
|
hasNextPage: boolean;
|
|
44
44
|
hasPreviousPage: boolean;
|
|
45
45
|
isFetchingNextPage: boolean;
|
|
@@ -70,10 +70,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
70
70
|
isRefetching: boolean;
|
|
71
71
|
isStale: boolean;
|
|
72
72
|
isEnabled: boolean;
|
|
73
|
-
refetch: (options?: import("@tanstack/query
|
|
74
|
-
fetchStatus: import("@tanstack/query
|
|
75
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
76
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
73
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
74
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
75
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
76
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
77
77
|
hasNextPage: boolean;
|
|
78
78
|
hasPreviousPage: boolean;
|
|
79
79
|
isFetchingNextPage: boolean;
|
|
@@ -105,10 +105,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
105
105
|
isRefetching: boolean;
|
|
106
106
|
isStale: boolean;
|
|
107
107
|
isEnabled: boolean;
|
|
108
|
-
refetch: (options?: import("@tanstack/query
|
|
109
|
-
fetchStatus: import("@tanstack/query
|
|
110
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
111
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
108
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
109
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
110
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
111
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
112
112
|
hasNextPage: boolean;
|
|
113
113
|
hasPreviousPage: boolean;
|
|
114
114
|
isFetchingNextPage: boolean;
|
|
@@ -138,10 +138,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
138
138
|
isRefetching: boolean;
|
|
139
139
|
isStale: boolean;
|
|
140
140
|
isEnabled: boolean;
|
|
141
|
-
refetch: (options?: import("@tanstack/query
|
|
142
|
-
fetchStatus: import("@tanstack/query
|
|
143
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
144
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
141
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
142
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
143
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
144
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
145
145
|
hasNextPage: boolean;
|
|
146
146
|
hasPreviousPage: boolean;
|
|
147
147
|
isFetchingNextPage: boolean;
|
|
@@ -172,10 +172,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
172
172
|
isRefetching: boolean;
|
|
173
173
|
isStale: boolean;
|
|
174
174
|
isEnabled: boolean;
|
|
175
|
-
refetch: (options?: import("@tanstack/query
|
|
176
|
-
fetchStatus: import("@tanstack/query
|
|
177
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
178
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
175
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
176
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
177
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
178
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
179
179
|
hasNextPage: boolean;
|
|
180
180
|
hasPreviousPage: boolean;
|
|
181
181
|
isFetchNextPageError: boolean;
|
|
@@ -206,10 +206,10 @@ export declare function useBusinessDeliveriesQuery(enabled?: boolean, filters?:
|
|
|
206
206
|
isRefetching: boolean;
|
|
207
207
|
isStale: boolean;
|
|
208
208
|
isEnabled: boolean;
|
|
209
|
-
refetch: (options?: import("@tanstack/query
|
|
210
|
-
fetchStatus: import("@tanstack/query
|
|
211
|
-
fetchNextPage: (options?: import("@tanstack/query
|
|
212
|
-
fetchPreviousPage: (options?: import("@tanstack/query
|
|
209
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
210
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
211
|
+
fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
212
|
+
fetchPreviousPage: (options?: import("@tanstack/react-query").FetchPreviousPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<ListPage<BusinessDelivery>, unknown>, Error>>;
|
|
213
213
|
hasNextPage: boolean;
|
|
214
214
|
hasPreviousPage: boolean;
|
|
215
215
|
isFetchingNextPage: boolean;
|