@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
|
@@ -16,23 +16,37 @@ function cryptoRandom100to999() {
|
|
|
16
16
|
}
|
|
17
17
|
return 100 + Math.floor(Math.random() * 900);
|
|
18
18
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
19
|
+
function calloutKeyForBusiness(businessId) {
|
|
20
|
+
return businessId ? `${CALLOUT_SEQ_KEY}:${businessId}` : CALLOUT_SEQ_KEY;
|
|
21
|
+
}
|
|
22
|
+
function getNextCallout(businessId) {
|
|
23
|
+
const key = calloutKeyForBusiness(businessId);
|
|
24
|
+
const stored = getItem(key);
|
|
21
25
|
let next = (stored ?? 0) + 1;
|
|
22
26
|
if (next > 999)
|
|
23
27
|
next = 1;
|
|
24
|
-
setItem(
|
|
28
|
+
setItem(key, next);
|
|
29
|
+
// migrate legacy global once: keep global in sync for backward compat
|
|
30
|
+
if (businessId) {
|
|
31
|
+
const legacy = getItem(CALLOUT_SEQ_KEY) ?? 0;
|
|
32
|
+
if (legacy < next)
|
|
33
|
+
setItem(CALLOUT_SEQ_KEY, next);
|
|
34
|
+
}
|
|
25
35
|
return next;
|
|
26
36
|
}
|
|
27
|
-
export function peekCallout() {
|
|
28
|
-
const
|
|
29
|
-
|
|
37
|
+
export function peekCallout(businessId) {
|
|
38
|
+
const key = calloutKeyForBusiness(businessId);
|
|
39
|
+
const v = getItem(key);
|
|
40
|
+
if (v != null)
|
|
41
|
+
return v;
|
|
42
|
+
const legacy = getItem(CALLOUT_SEQ_KEY);
|
|
43
|
+
return legacy ?? 0;
|
|
30
44
|
}
|
|
31
|
-
export function generateOrderNumber(deviceNumber) {
|
|
45
|
+
export function generateOrderNumber(deviceNumber, businessId) {
|
|
32
46
|
if (!/^(0[1-9]|[1-9][0-9])$/.test(deviceNumber)) {
|
|
33
47
|
throw new Error(`Invalid deviceNumber ${deviceNumber} — must be 01-99`);
|
|
34
48
|
}
|
|
35
|
-
const callout = getNextCallout();
|
|
49
|
+
const callout = getNextCallout(businessId);
|
|
36
50
|
let random = cryptoRandom100to999();
|
|
37
51
|
// On wrap to 001, ensure new random (already new per order, but keep hook)
|
|
38
52
|
if (callout === 1) {
|
|
@@ -53,11 +67,11 @@ export function generateOrderNumberForBusiness(businessId) {
|
|
|
53
67
|
if (!v || !/^(0[1-9]|[1-9][0-9])$/.test(v)) {
|
|
54
68
|
throw new Error(`No device lease for business ${businessId} — call ensureLease(businessId) first (online-only until leased)`);
|
|
55
69
|
}
|
|
56
|
-
return generateOrderNumber(v).orderNumber;
|
|
70
|
+
return generateOrderNumber(v, businessId).orderNumber;
|
|
57
71
|
}
|
|
58
|
-
export function generateLocalCalloutNumber() {
|
|
59
|
-
// kept for SellerOrder.callout_number compatibility — returns CCC
|
|
60
|
-
const next = peekCallout() + 1;
|
|
72
|
+
export function generateLocalCalloutNumber(businessId) {
|
|
73
|
+
// kept for SellerOrder.callout_number compatibility — returns next CCC
|
|
74
|
+
const next = peekCallout(businessId) + 1;
|
|
61
75
|
return next > 999 ? 1 : next;
|
|
62
76
|
}
|
|
63
77
|
// New canonical check — unified for DD-RRR-CCC
|
|
@@ -74,6 +88,12 @@ export function parseOrderNumber(orderNumber) {
|
|
|
74
88
|
return { deviceNumber: m[1], random: m[2], callout: m[3] };
|
|
75
89
|
}
|
|
76
90
|
// Test helpers
|
|
77
|
-
export function resetCalloutForTests() {
|
|
91
|
+
export function resetCalloutForTests(businessId) {
|
|
92
|
+
if (businessId) {
|
|
93
|
+
setItem(calloutKeyForBusiness(businessId), 0);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
78
96
|
setItem(CALLOUT_SEQ_KEY, 0);
|
|
97
|
+
// also clear per-business keys that tests may have touched
|
|
98
|
+
// caller should pass businessId if they used business-scoped sequences
|
|
79
99
|
}
|
|
@@ -29,6 +29,7 @@ declare class OrderSnapshotsService {
|
|
|
29
29
|
*/
|
|
30
30
|
applyQueuedStatusEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
|
|
31
31
|
private applyOrderStatusToReadModelInternal;
|
|
32
|
+
applyPaymentEvent(event: DomainEvent, origin?: "local" | "lan" | "cloud"): Promise<void>;
|
|
32
33
|
/**
|
|
33
34
|
* Patch the latest status of a known order inside every list snapshot for the
|
|
34
35
|
* business, so offline devices see live status changes without a refetch.
|
|
@@ -74,7 +74,19 @@ class OrderSnapshotsService {
|
|
|
74
74
|
}
|
|
75
75
|
async applyOrderStatusToReadModelInternal(db, orderId, toStatus, event) {
|
|
76
76
|
try {
|
|
77
|
-
const
|
|
77
|
+
const payload = event.payload;
|
|
78
|
+
const businessId = payload?.business_id ?? payload?.businessId ?? null;
|
|
79
|
+
// Dual-write: keep canonical orders table in sync so POS/KDS reads converge instantly.
|
|
80
|
+
if (businessId) {
|
|
81
|
+
try {
|
|
82
|
+
const { updateOrderStatus } = await import("../db/operations");
|
|
83
|
+
await updateOrderStatus(db, businessId, orderId, toStatus, event.timestamp);
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
}
|
|
87
|
+
const lists = businessId
|
|
88
|
+
? await getEntitySnapshotsByType(db, "seller_orders", businessId)
|
|
89
|
+
: await getEntitySnapshotsByType(db, "seller_orders");
|
|
78
90
|
for (const list of lists) {
|
|
79
91
|
const data = list.payload.data ?? [];
|
|
80
92
|
const idx = data.findIndex((o) => o.id === orderId);
|
|
@@ -97,6 +109,39 @@ class OrderSnapshotsService {
|
|
|
97
109
|
});
|
|
98
110
|
}
|
|
99
111
|
}
|
|
112
|
+
async applyPaymentEvent(event, origin = "local") {
|
|
113
|
+
const payload = event.payload;
|
|
114
|
+
const orderId = payload?.order_id ?? event.entity_id;
|
|
115
|
+
if (!orderId)
|
|
116
|
+
return;
|
|
117
|
+
const paidAt = payload?.paid_at || new Date().toISOString();
|
|
118
|
+
try {
|
|
119
|
+
const db = await openSyncDatabase();
|
|
120
|
+
await insertEvent(db, { event, origin });
|
|
121
|
+
const businessId = payload?.business_id ?? payload?.businessId ?? null;
|
|
122
|
+
const lists = businessId
|
|
123
|
+
? await getEntitySnapshotsByType(db, "seller_orders", businessId)
|
|
124
|
+
: await getEntitySnapshotsByType(db, "seller_orders");
|
|
125
|
+
for (const list of lists) {
|
|
126
|
+
const data = list.payload.data ?? [];
|
|
127
|
+
const idx = data.findIndex((o) => o.id === orderId || o.order_number === payload?.order_number);
|
|
128
|
+
if (idx === -1)
|
|
129
|
+
continue;
|
|
130
|
+
const updated = data.map((o, i) => i === idx ? { ...o, is_paid: true, paid_at: paidAt, payment: { ...o.payment, is_paid: true, paid_at: paidAt } } : o);
|
|
131
|
+
await upsertEntitySnapshot(db, {
|
|
132
|
+
entityType: "seller_orders",
|
|
133
|
+
entityId: list.entity_id,
|
|
134
|
+
businessId: list.business_id,
|
|
135
|
+
lastEventId: event.event_id,
|
|
136
|
+
lastEventTimestamp: event.timestamp,
|
|
137
|
+
payload: { ...list.payload, data: updated },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
syncLogger.error("OrderSnapshots", "Failed to apply payment", { error: String(error) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
100
145
|
/**
|
|
101
146
|
* Patch the latest status of a known order inside every list snapshot for the
|
|
102
147
|
* business, so offline devices see live status changes without a refetch.
|
|
@@ -143,7 +188,7 @@ class OrderSnapshotsService {
|
|
|
143
188
|
async getSellerList(businessId, scope) {
|
|
144
189
|
try {
|
|
145
190
|
const db = await openSyncDatabase();
|
|
146
|
-
const snap = await getEntitySnapshot(db, "seller_orders", scope);
|
|
191
|
+
const snap = await getEntitySnapshot(db, "seller_orders", scope, businessId);
|
|
147
192
|
if (!snap || snap.business_id !== businessId)
|
|
148
193
|
return null;
|
|
149
194
|
return snap.payload.data ?? null;
|
|
@@ -15,11 +15,29 @@
|
|
|
15
15
|
* order's `order_number`.
|
|
16
16
|
*/
|
|
17
17
|
import type { DomainEvent } from "./types";
|
|
18
|
+
import type { SellerOrder } from "../types/api/seller.api";
|
|
18
19
|
/**
|
|
19
20
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
20
21
|
* order is reconciled. Returns an unregister function.
|
|
21
22
|
*/
|
|
22
23
|
export declare function registerPendingOrdersInvalidator(fn: () => void): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Fire the registered query invalidator (lists refetch from SQLite).
|
|
26
|
+
* Used after LAN-applied writes (order:sync) that bypass the engine event
|
|
27
|
+
* pipeline, so peers' UIs converge without waiting for a cloud echo.
|
|
28
|
+
*/
|
|
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
|
+
export declare function mergePendingIntoList(base: SellerOrder[], businessId: string, page: number): Promise<SellerOrder[]>;
|
|
23
41
|
/**
|
|
24
42
|
* Load persisted unsynced order:create_queued events into the store.
|
|
25
43
|
* Idempotent; safe to call after app restart while still offline.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* order's `order_number`.
|
|
16
16
|
*/
|
|
17
17
|
import { openSyncDatabase } from "../db/connection";
|
|
18
|
-
import { insertEvent, getUnsyncedEvents, markEventsSynced, } from "../db/operations";
|
|
18
|
+
import { insertEvent, getUnsyncedEvents, markEventsSynced, getEntitySnapshotsByType, } from "../db/operations";
|
|
19
19
|
import { usePendingOrdersStore } from "../adapters/pending-orders";
|
|
20
20
|
import { orderSnapshotsService } from "./order-snapshots.service";
|
|
21
21
|
import { syncLogger } from "../utils/sync-logger";
|
|
@@ -36,6 +36,7 @@ const QUEUED_STATUS_EVENT_TYPES = new Set([
|
|
|
36
36
|
"delivery.assign_queued",
|
|
37
37
|
"delivery.confirm_handover_queued",
|
|
38
38
|
]);
|
|
39
|
+
const PAYMENT_QUEUED_TYPES = new Set(["payment.completed_queued"]);
|
|
39
40
|
let invalidator = null;
|
|
40
41
|
/**
|
|
41
42
|
* Register a callback used to invalidate TanStack Query caches when a pending
|
|
@@ -48,6 +49,72 @@ export function registerPendingOrdersInvalidator(fn) {
|
|
|
48
49
|
invalidator = null;
|
|
49
50
|
};
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Fire the registered query invalidator (lists refetch from SQLite).
|
|
54
|
+
* Used after LAN-applied writes (order:sync) that bypass the engine event
|
|
55
|
+
* pipeline, so peers' UIs converge without waiting for a cloud echo.
|
|
56
|
+
*/
|
|
57
|
+
export function notifyPendingOrdersChanged() {
|
|
58
|
+
try {
|
|
59
|
+
invalidator?.();
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge optimistic (queued, unconfirmed) state into a page of listed orders.
|
|
65
|
+
* Called by list queryFns (page 1 only) so offline work is visible:
|
|
66
|
+
* 1. Pending creations (order.create_queued, local or LAN) missing from the
|
|
67
|
+
* SQLite page are prepended — deduped by id/order_number.
|
|
68
|
+
* 2. Queued transitions only patch the snapshot read model, never the
|
|
69
|
+
* orders table — overlay snapshot statuses onto base rows so the list
|
|
70
|
+
* reflects the queued status immediately (converges on replay).
|
|
71
|
+
* Reactivity comes from the invalidator: every apply path above refetches.
|
|
72
|
+
*/
|
|
73
|
+
export async function mergePendingIntoList(base, businessId, page) {
|
|
74
|
+
if (page !== 1)
|
|
75
|
+
return base;
|
|
76
|
+
let merged = base;
|
|
77
|
+
try {
|
|
78
|
+
const pending = usePendingOrdersStore.getState().orders;
|
|
79
|
+
if (pending.length) {
|
|
80
|
+
const seen = new Set(merged.map((o) => String(o.id ?? o.order_number)));
|
|
81
|
+
const fresh = pending.filter((p) => !seen.has(String(p.id ?? p.order_number)));
|
|
82
|
+
if (fresh.length)
|
|
83
|
+
merged = [...fresh, ...merged];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
try {
|
|
88
|
+
const db = await openSyncDatabase();
|
|
89
|
+
const snaps = await getEntitySnapshotsByType(db, "seller_orders", businessId);
|
|
90
|
+
if (!snaps.length)
|
|
91
|
+
return merged;
|
|
92
|
+
const statusById = new Map();
|
|
93
|
+
for (const s of snaps) {
|
|
94
|
+
const arr = s.payload?.data ?? [];
|
|
95
|
+
for (const o of arr) {
|
|
96
|
+
if (o?.id && o.status)
|
|
97
|
+
statusById.set(String(o.id), String(o.status));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!statusById.size)
|
|
101
|
+
return merged;
|
|
102
|
+
let patched = false;
|
|
103
|
+
merged = merged.map((o) => {
|
|
104
|
+
const want = statusById.get(String(o.id));
|
|
105
|
+
if (want && want !== o.status) {
|
|
106
|
+
patched = true;
|
|
107
|
+
return { ...o, status: want };
|
|
108
|
+
}
|
|
109
|
+
return o;
|
|
110
|
+
});
|
|
111
|
+
if (patched) {
|
|
112
|
+
syncLogger.debug("PendingOrders", "overlaid queued statuses onto list", { businessId, count: merged.length });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
51
118
|
/**
|
|
52
119
|
* Load persisted unsynced order:create_queued events into the store.
|
|
53
120
|
* Idempotent; safe to call after app restart while still offline.
|
|
@@ -99,8 +166,12 @@ export async function hydratePendingOrders() {
|
|
|
99
166
|
export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
100
167
|
try {
|
|
101
168
|
const isQueued = QUEUED_STATUS_EVENT_TYPES.has(event.event_type);
|
|
102
|
-
|
|
103
|
-
|
|
169
|
+
const isPaymentQueued = PAYMENT_QUEUED_TYPES.has(event.event_type);
|
|
170
|
+
if (!ORDER_STATE_EVENT_TYPES.has(event.event_type) && !isQueued && !isPaymentQueued) {
|
|
171
|
+
// Also handle cloud payment.completed for is_paid patch
|
|
172
|
+
if (event.event_type !== "payment.completed")
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
104
175
|
syncLogger.debug("OrderState", "Order state changed", {
|
|
105
176
|
origin,
|
|
106
177
|
event_type: event.event_type,
|
|
@@ -111,6 +182,19 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
|
111
182
|
// Queued transitions carry to_status/order_id so they patch instantly offline.
|
|
112
183
|
if (isQueued) {
|
|
113
184
|
await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
|
|
185
|
+
patchPendingStatus(event);
|
|
186
|
+
invalidator?.();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (isPaymentQueued) {
|
|
190
|
+
await orderSnapshotsService.applyPaymentEvent(event, origin);
|
|
191
|
+
patchPendingPaid(event);
|
|
192
|
+
invalidator?.();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (event.event_type === "payment.completed") {
|
|
196
|
+
await orderSnapshotsService.applyPaymentEvent(event, origin);
|
|
197
|
+
patchPendingPaid(event);
|
|
114
198
|
invalidator?.();
|
|
115
199
|
return;
|
|
116
200
|
}
|
|
@@ -124,6 +208,46 @@ export async function applyOrderStateChangeEvent(event, origin = "local") {
|
|
|
124
208
|
});
|
|
125
209
|
}
|
|
126
210
|
}
|
|
211
|
+
function patchPendingStatus(event) {
|
|
212
|
+
try {
|
|
213
|
+
const payload = event.payload;
|
|
214
|
+
const toStatus = payload.to_status || payload.status;
|
|
215
|
+
const orderId = payload.order_id || event.entity_id;
|
|
216
|
+
if (!toStatus || !orderId)
|
|
217
|
+
return;
|
|
218
|
+
const store = usePendingOrdersStore.getState();
|
|
219
|
+
const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
|
|
220
|
+
if (!target)
|
|
221
|
+
return;
|
|
222
|
+
const patched = { ...target, status: toStatus };
|
|
223
|
+
store.addOrder(patched);
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
}
|
|
227
|
+
function patchPendingPaid(event) {
|
|
228
|
+
try {
|
|
229
|
+
const payload = event.payload;
|
|
230
|
+
const orderId = payload.order_id || event.entity_id;
|
|
231
|
+
if (!orderId)
|
|
232
|
+
return;
|
|
233
|
+
const now = new Date().toISOString();
|
|
234
|
+
const store = usePendingOrdersStore.getState();
|
|
235
|
+
const target = store.orders.find((o) => o.id === orderId || o.order_number === payload.order_number || o.id === event.entity_id);
|
|
236
|
+
const altTarget = !target ? store.orders.find((o) => o.id === orderId) : null;
|
|
237
|
+
const t = target ?? altTarget;
|
|
238
|
+
if (!t)
|
|
239
|
+
return;
|
|
240
|
+
const paidAt = payload.paid_at || now;
|
|
241
|
+
const patched = {
|
|
242
|
+
...t,
|
|
243
|
+
is_paid: true,
|
|
244
|
+
paid_at: paidAt,
|
|
245
|
+
payment: { ...t.payment, is_paid: true, paid_at: paidAt },
|
|
246
|
+
};
|
|
247
|
+
store.addOrder(patched);
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
}
|
|
127
251
|
export async function applyPendingOrderEvent(event, origin = "local") {
|
|
128
252
|
try {
|
|
129
253
|
if (event.event_type === PENDING_EVENT_TYPE) {
|
|
@@ -144,6 +268,27 @@ export async function applyPendingOrderEvent(event, origin = "local") {
|
|
|
144
268
|
if (QUEUED_STATUS_EVENT_TYPES.has(event.event_type)) {
|
|
145
269
|
// Queued transitions: patch the read model so listing reflects new status offline.
|
|
146
270
|
await orderSnapshotsService.applyQueuedStatusEvent(event, origin);
|
|
271
|
+
// Also patch pending store so isPending overlay shows new status immediately.
|
|
272
|
+
try {
|
|
273
|
+
const payload = event.payload;
|
|
274
|
+
const toStatus = payload.to_status || payload.status;
|
|
275
|
+
const orderId = payload.order_id || event.entity_id;
|
|
276
|
+
if (toStatus && orderId) {
|
|
277
|
+
const store = usePendingOrdersStore.getState();
|
|
278
|
+
const target = store.orders.find((o) => o.id === orderId || o.order_number === orderId);
|
|
279
|
+
if (target)
|
|
280
|
+
store.addOrder({ ...target, status: toStatus });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
const db = await openSyncDatabase();
|
|
285
|
+
await insertEvent(db, { event, origin });
|
|
286
|
+
invalidator?.();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (PAYMENT_QUEUED_TYPES.has(event.event_type) || event.event_type === "payment.completed") {
|
|
290
|
+
await orderSnapshotsService.applyPaymentEvent(event, origin);
|
|
291
|
+
patchPendingPaid(event);
|
|
147
292
|
const db = await openSyncDatabase();
|
|
148
293
|
await insertEvent(db, { event, origin });
|
|
149
294
|
invalidator?.();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity snapshots — local read model (§9 SQLite Model).
|
|
3
|
+
* Canonical in src/db/operations + src/sync/order-snapshots.service.
|
|
4
|
+
*/
|
|
5
|
+
export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
|
|
6
|
+
export { orderSnapshotsService } from "../order-snapshots.service";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity snapshots — local read model (§9 SQLite Model).
|
|
3
|
+
* Canonical in src/db/operations + src/sync/order-snapshots.service.
|
|
4
|
+
*/
|
|
5
|
+
export { upsertEntitySnapshot, getEntitySnapshot, getEntitySnapshotsByType, pruneEntitySnapshots } from "../../db/operations";
|
|
6
|
+
export { orderSnapshotsService } from "../order-snapshots.service";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Jobs FIFO — near real-time drain after local commit.
|
|
3
|
+
* Tries PHX business:{id} push first, then REST, queues on fail.
|
|
4
|
+
* Core-only, single in_flight per business, coalesced via isOnline gate.
|
|
5
|
+
*/
|
|
6
|
+
export declare function drainOne(businessId: string, entityId: string): Promise<void>;
|
|
7
|
+
export declare function pullRemoteOrders(businessId: string): Promise<void>;
|
|
8
|
+
export declare function drainPending(businessId: string): Promise<void>;
|
|
9
|
+
export declare function setupSyncJobsDrain(): void;
|
|
10
|
+
export declare function getSyncJobsPendingCount(businessId: string): Promise<number>;
|
|
11
|
+
export declare function debugDumpSyncJobs(businessId: string): Promise<Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
command_type: string;
|
|
14
|
+
status: string;
|
|
15
|
+
created_at: string;
|
|
16
|
+
attempts: number;
|
|
17
|
+
last_error: string | null;
|
|
18
|
+
}>>;
|