@teincfood/core 0.7.9 → 0.8.1
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 +8 -0
- package/dist/adapters/auth.js +36 -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 +96 -15
- package/dist/adapters/services.js +241 -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 +24 -0
- package/dist/hooks/image.hooks.js +66 -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 +18 -39
- package/dist/hooks/orders.hooks.js +124 -343
- 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 +28 -0
- package/dist/image-cache/service.js +51 -3
- package/dist/index.d.ts +56 -6
- package/dist/index.js +73 -6
- 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/phoenix-socket.js +30 -1
- package/dist/realtime/useEventsConnection.js +11 -2
- 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 +27 -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 +168 -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 +7 -0
- package/dist/repositories/order.repository.js +15 -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 +81 -16
- 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.js +18 -3
- package/dist/sync/pending-orders.service.d.ts +18 -0
- package/dist/sync/pending-orders.service.js +67 -1
- 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 +84 -26
- package/dist/sync/use-sync-reconciliation.js +15 -1
- 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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu cache-record builders — the ONLY place MenuItem/MenuCategory records
|
|
3
|
+
* are constructed outside the backend.
|
|
4
|
+
*
|
|
5
|
+
* The reference cache holds backend-shaped records exactly as the serializer
|
|
6
|
+
* returns them (see TeincFoodBackend menu_serializer.ex). Form/mutation
|
|
7
|
+
* layers own every field at write time, so they build the record here —
|
|
8
|
+
* core never translates wire payloads into cache records.
|
|
9
|
+
*/
|
|
10
|
+
import type { MenuCategory, MenuItem } from "../types/api/menu.api";
|
|
11
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
12
|
+
export interface MenuItemRecordInput {
|
|
13
|
+
id?: string;
|
|
14
|
+
businessId: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string | null;
|
|
17
|
+
priceMinor: number;
|
|
18
|
+
currencyCode?: string;
|
|
19
|
+
categoryId: string;
|
|
20
|
+
categoryName?: string;
|
|
21
|
+
dietaryTags?: string[];
|
|
22
|
+
variants?: Array<{
|
|
23
|
+
id?: string;
|
|
24
|
+
name: string;
|
|
25
|
+
price_adjustment?: number;
|
|
26
|
+
}>;
|
|
27
|
+
extras?: Array<{
|
|
28
|
+
id?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
price_minor?: number;
|
|
31
|
+
}>;
|
|
32
|
+
available?: boolean;
|
|
33
|
+
imageUrl?: string | null;
|
|
34
|
+
now?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function buildMenuItemRecord(input: MenuItemRecordInput): MenuItem;
|
|
37
|
+
export interface MenuCategoryRecordInput {
|
|
38
|
+
id?: string;
|
|
39
|
+
name: string;
|
|
40
|
+
sort_order?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function buildMenuCategoryRecord(input: MenuCategoryRecordInput): MenuCategory;
|
|
43
|
+
/** Every cached MenuItem for a business across cached + network queries. */
|
|
44
|
+
export declare function getCachedMenuItems(qc: QueryClient, businessId: string): MenuItem[];
|
|
45
|
+
export declare function getCachedMenuItem(qc: QueryClient, businessId: string, id: string): MenuItem | undefined;
|
|
46
|
+
export declare function getCachedCategories(qc: QueryClient, businessId: string): MenuCategory[];
|
|
47
|
+
export declare function getCachedCategoryName(qc: QueryClient, businessId: string, categoryId: string): string;
|
|
48
|
+
export interface MenuItemEnvelopeInput extends MenuItemRecordInput {
|
|
49
|
+
/** Extra wire-only keys (e.g. entity id). */
|
|
50
|
+
wireExtra?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the { wire, cacheRecord } pair for a menu item write. Callers pass
|
|
54
|
+
* backend adjustment-shaped variants; absolute form prices are converted by
|
|
55
|
+
* the caller (form layer owns both fields).
|
|
56
|
+
*
|
|
57
|
+
* Variant/extra ids are preserved when they are backend UUIDs. The backend
|
|
58
|
+
* update path deletes existing rows absent from the incoming list, so
|
|
59
|
+
* stripping ids on edit would wipe + recreate every variant/extra on each
|
|
60
|
+
* save (breaking any external references to those ids).
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildMenuItemEnvelope(input: MenuItemEnvelopeInput): {
|
|
63
|
+
wire: Record<string, unknown>;
|
|
64
|
+
cacheRecord: MenuItem;
|
|
65
|
+
};
|
|
66
|
+
export declare function buildMenuCategoryEnvelope(input: MenuCategoryRecordInput): {
|
|
67
|
+
wire: Record<string, unknown>;
|
|
68
|
+
cacheRecord: MenuCategory;
|
|
69
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu cache-record builders — the ONLY place MenuItem/MenuCategory records
|
|
3
|
+
* are constructed outside the backend.
|
|
4
|
+
*
|
|
5
|
+
* The reference cache holds backend-shaped records exactly as the serializer
|
|
6
|
+
* returns them (see TeincFoodBackend menu_serializer.ex). Form/mutation
|
|
7
|
+
* layers own every field at write time, so they build the record here —
|
|
8
|
+
* core never translates wire payloads into cache records.
|
|
9
|
+
*/
|
|
10
|
+
import { generateUUID } from "../utils/uuid";
|
|
11
|
+
import { CATALOG_KEYS } from "../catalog/hooks";
|
|
12
|
+
import { toStorableImageUrl } from "../image-cache/service";
|
|
13
|
+
export function buildMenuItemRecord(input) {
|
|
14
|
+
const currencyCode = input.currencyCode || "GHS";
|
|
15
|
+
const now = input.now ?? new Date().toISOString();
|
|
16
|
+
return {
|
|
17
|
+
id: input.id ?? generateUUID(),
|
|
18
|
+
name: input.name,
|
|
19
|
+
description: input.description ?? null,
|
|
20
|
+
basePrice: { amountMinor: input.priceMinor, currencyCode },
|
|
21
|
+
// Never persist preview URIs (tfimg:/blob:/file:): the backend stores
|
|
22
|
+
// non-http values verbatim and they poison every other device.
|
|
23
|
+
image_url: toStorableImageUrl(input.imageUrl),
|
|
24
|
+
is_available: input.available ?? true,
|
|
25
|
+
dietary_tags: input.dietaryTags ?? [],
|
|
26
|
+
category_id: input.categoryId,
|
|
27
|
+
business_id: input.businessId,
|
|
28
|
+
category: { id: input.categoryId, name: input.categoryName ?? "" },
|
|
29
|
+
variants: (input.variants ?? []).map((v) => ({
|
|
30
|
+
id: v.id ?? generateUUID(),
|
|
31
|
+
name: v.name,
|
|
32
|
+
// Matches backend serializer: variant price = base + adjustment.
|
|
33
|
+
price: { amountMinor: input.priceMinor + (v.price_adjustment ?? 0), currencyCode },
|
|
34
|
+
})),
|
|
35
|
+
extras: (input.extras ?? []).map((e) => ({
|
|
36
|
+
id: e.id ?? generateUUID(),
|
|
37
|
+
name: e.name,
|
|
38
|
+
pricingType: "fixed",
|
|
39
|
+
amountMinor: e.price_minor ?? 0,
|
|
40
|
+
currencyCode,
|
|
41
|
+
})),
|
|
42
|
+
inserted_at: now,
|
|
43
|
+
updated_at: now,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function buildMenuCategoryRecord(input) {
|
|
47
|
+
return {
|
|
48
|
+
id: input.id ?? generateUUID(),
|
|
49
|
+
name: input.name,
|
|
50
|
+
sort_order: input.sort_order ?? 0,
|
|
51
|
+
is_global: false,
|
|
52
|
+
is_system: false,
|
|
53
|
+
pending_global_request: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ─── Cache lookups (mutation layers read current records to build envelopes)
|
|
57
|
+
function collectItems(blob, out) {
|
|
58
|
+
if (!blob || typeof blob !== "object")
|
|
59
|
+
return;
|
|
60
|
+
if (Array.isArray(blob)) {
|
|
61
|
+
for (const i of blob)
|
|
62
|
+
if (i?.id)
|
|
63
|
+
out.push(i);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const rec = blob;
|
|
67
|
+
if (Array.isArray(rec.data))
|
|
68
|
+
collectItems(rec.data, out);
|
|
69
|
+
if (Array.isArray(rec.pages))
|
|
70
|
+
for (const p of rec.pages)
|
|
71
|
+
collectItems(p?.data, out);
|
|
72
|
+
}
|
|
73
|
+
/** Every cached MenuItem for a business across cached + network queries. */
|
|
74
|
+
export function getCachedMenuItems(qc, businessId) {
|
|
75
|
+
const out = [];
|
|
76
|
+
try {
|
|
77
|
+
for (const [, data] of qc.getQueriesData({ queryKey: CATALOG_KEYS.items(businessId) })) {
|
|
78
|
+
collectItems(data, out);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
return out.filter((i) => (seen.has(i.id) ? false : (seen.add(i.id), true)));
|
|
84
|
+
}
|
|
85
|
+
export function getCachedMenuItem(qc, businessId, id) {
|
|
86
|
+
return getCachedMenuItems(qc, businessId).find((i) => i.id === id);
|
|
87
|
+
}
|
|
88
|
+
function collectCategories(blob, out) {
|
|
89
|
+
if (!blob || typeof blob !== "object")
|
|
90
|
+
return;
|
|
91
|
+
if (Array.isArray(blob)) {
|
|
92
|
+
for (const c of blob)
|
|
93
|
+
if (c?.id)
|
|
94
|
+
out.push(c);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const rec = blob;
|
|
98
|
+
if (Array.isArray(rec.data))
|
|
99
|
+
collectCategories(rec.data, out);
|
|
100
|
+
}
|
|
101
|
+
export function getCachedCategories(qc, businessId) {
|
|
102
|
+
const out = [];
|
|
103
|
+
try {
|
|
104
|
+
for (const [, data] of qc.getQueriesData({ queryKey: CATALOG_KEYS.categories(businessId) })) {
|
|
105
|
+
collectCategories(data, out);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
return out.filter((c) => (seen.has(c.id) ? false : (seen.add(c.id), true)));
|
|
111
|
+
}
|
|
112
|
+
export function getCachedCategoryName(qc, businessId, categoryId) {
|
|
113
|
+
return getCachedCategories(qc, businessId).find((c) => c.id === categoryId)?.name ?? "";
|
|
114
|
+
}
|
|
115
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
116
|
+
/**
|
|
117
|
+
* A variant/extra id is only safe to send when it is a real backend UUID.
|
|
118
|
+
* Form-local ids (e.g. `local-<ts>`) must be omitted: the backend treats a
|
|
119
|
+
* missing/empty id as "insert new", but Repo.get/3 raises on a non-UUID.
|
|
120
|
+
*/
|
|
121
|
+
function wireId(id) {
|
|
122
|
+
return id && UUID_RE.test(id) ? id : undefined;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build the { wire, cacheRecord } pair for a menu item write. Callers pass
|
|
126
|
+
* backend adjustment-shaped variants; absolute form prices are converted by
|
|
127
|
+
* the caller (form layer owns both fields).
|
|
128
|
+
*
|
|
129
|
+
* Variant/extra ids are preserved when they are backend UUIDs. The backend
|
|
130
|
+
* update path deletes existing rows absent from the incoming list, so
|
|
131
|
+
* stripping ids on edit would wipe + recreate every variant/extra on each
|
|
132
|
+
* save (breaking any external references to those ids).
|
|
133
|
+
*/
|
|
134
|
+
export function buildMenuItemEnvelope(input) {
|
|
135
|
+
const id = input.id ?? generateUUID();
|
|
136
|
+
const cacheRecord = buildMenuItemRecord({ ...input, id });
|
|
137
|
+
const wire = {
|
|
138
|
+
id,
|
|
139
|
+
name: input.name,
|
|
140
|
+
description: input.description ?? "",
|
|
141
|
+
price_minor: input.priceMinor,
|
|
142
|
+
category_id: input.categoryId,
|
|
143
|
+
currency_code: input.currencyCode ?? "GHS",
|
|
144
|
+
dietary_tags: input.dietaryTags ?? [],
|
|
145
|
+
variants: (input.variants ?? []).map((v) => {
|
|
146
|
+
const vid = wireId(v.id);
|
|
147
|
+
return vid
|
|
148
|
+
? { id: vid, name: v.name, price_adjustment: v.price_adjustment ?? 0 }
|
|
149
|
+
: { name: v.name, price_adjustment: v.price_adjustment ?? 0 };
|
|
150
|
+
}),
|
|
151
|
+
extras: (input.extras ?? []).map((e) => {
|
|
152
|
+
const eid = wireId(e.id);
|
|
153
|
+
return eid
|
|
154
|
+
? { id: eid, name: e.name, price_minor: e.price_minor ?? 0 }
|
|
155
|
+
: { name: e.name, price_minor: e.price_minor ?? 0 };
|
|
156
|
+
}),
|
|
157
|
+
available: input.available ?? true,
|
|
158
|
+
// Same storable rule as the cache record: preview URIs must not reach
|
|
159
|
+
// the backend (it stores them verbatim).
|
|
160
|
+
image_url: toStorableImageUrl(input.imageUrl),
|
|
161
|
+
...(input.wireExtra ?? {}),
|
|
162
|
+
};
|
|
163
|
+
return { wire, cacheRecord };
|
|
164
|
+
}
|
|
165
|
+
export function buildMenuCategoryEnvelope(input) {
|
|
166
|
+
const cacheRecord = buildMenuCategoryRecord(input);
|
|
167
|
+
return { wire: { id: cacheRecord.id, name: input.name, ...(input.sort_order !== undefined ? { sort_order: input.sort_order } : {}) }, cacheRecord };
|
|
168
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { SQLiteDatabase } from "../adapters/sqlite";
|
|
5
5
|
import type { ReferenceDataKey, ReferenceDataRecord } from "./types";
|
|
6
|
+
import type { OperatingHoursEntry } from "../types/api/business.api";
|
|
6
7
|
export declare function getReferenceData<T>(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<ReferenceDataRecord<T> | null>;
|
|
7
8
|
export declare function getReferenceVersion(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<string | null>;
|
|
8
9
|
export declare function setReferenceData<T>(db: SQLiteDatabase, businessId: string, record: ReferenceDataRecord<T>): Promise<void>;
|
|
@@ -10,3 +11,5 @@ export declare function mergeReferenceData<T extends Record<string, unknown>>(db
|
|
|
10
11
|
export declare function deleteReferenceData(db: SQLiteDatabase, businessId: string, key: ReferenceDataKey): Promise<void>;
|
|
11
12
|
export declare function deleteReferenceDataForBusiness(db: SQLiteDatabase, businessId: string): Promise<void>;
|
|
12
13
|
export declare function getAllReferenceDataKeys(db: SQLiteDatabase, businessId: string): Promise<ReferenceDataKey[]>;
|
|
14
|
+
export declare function setOperatingHoursLocal(businessId: string, entries: OperatingHoursEntry[]): Promise<void>;
|
|
15
|
+
export declare function setBusinessProfileLocal(businessId: string, profile: Record<string, unknown>): Promise<void>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQLite operations for the business-scoped reference data cache.
|
|
3
3
|
*/
|
|
4
|
+
import { compareHLCString, hlcToString, tickHLC, hlcFromString } from "../utils/hlc";
|
|
5
|
+
import { generateUUID } from "../utils/uuid";
|
|
6
|
+
import { openSyncDatabase } from "../db/connection";
|
|
7
|
+
import { getAppInstanceId } from "../adapters/devices";
|
|
8
|
+
import { notifyDbChange } from "../db/operations";
|
|
4
9
|
export async function getReferenceData(db, businessId, key) {
|
|
5
10
|
const row = await db.getFirstAsync("SELECT * FROM reference_data WHERE business_id = ? AND key = ?;", businessId, key);
|
|
6
11
|
if (!row)
|
|
@@ -29,9 +34,9 @@ export async function setReferenceData(db, businessId, record) {
|
|
|
29
34
|
last_event_id = excluded.last_event_id,
|
|
30
35
|
last_hlc = excluded.last_hlc;
|
|
31
36
|
`, businessId, record.key, record.version, JSON.stringify(record.payload), record.downloadedAt, record.lastEventId ?? null, record.lastHlc ?? null);
|
|
37
|
+
notifyDbChange(businessId, `reference_data:${record.key}`);
|
|
32
38
|
}
|
|
33
39
|
export async function mergeReferenceData(db, businessId, key, incoming, hlc, eventId) {
|
|
34
|
-
const { compareHLCString } = await import("../utils/hlc");
|
|
35
40
|
const existing = await getReferenceData(db, businessId, key);
|
|
36
41
|
if (!existing) {
|
|
37
42
|
await setReferenceData(db, businessId, {
|
|
@@ -68,3 +73,32 @@ export async function getAllReferenceDataKeys(db, businessId) {
|
|
|
68
73
|
const rows = await db.getAllAsync("SELECT key FROM reference_data WHERE business_id = ? ORDER BY key;", businessId);
|
|
69
74
|
return rows.map((r) => r.key);
|
|
70
75
|
}
|
|
76
|
+
async function enqueueReferenceSyncJob(businessId, key, hlc) {
|
|
77
|
+
try {
|
|
78
|
+
const db = await openSyncDatabase();
|
|
79
|
+
const id = generateUUID();
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
await db.runAsync(`INSERT OR IGNORE INTO sync_jobs (id, business_id, command_type, entity_id, payload, status, created_at, updated_at, attempts, last_error) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, 0, NULL);`, id, businessId, "reference:sync", key, JSON.stringify({ key, hlc }), now, now);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
export async function setOperatingHoursLocal(businessId, entries) {
|
|
86
|
+
const db = await openSyncDatabase();
|
|
87
|
+
const payload = { data: entries };
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
// HLC ensures local edit wins over stale server pull via LWW
|
|
90
|
+
const existing = await getReferenceData(db, businessId, "operating-hours");
|
|
91
|
+
const lastHlc = existing?.lastHlc ? hlcFromString(existing.lastHlc) : null;
|
|
92
|
+
const hlcStr = hlcToString(tickHLC(Date.now(), lastHlc, getAppInstanceId()));
|
|
93
|
+
await mergeReferenceData(db, businessId, "operating-hours", payload, hlcStr, `evt_${hlcStr}`);
|
|
94
|
+
await enqueueReferenceSyncJob(businessId, "operating-hours", hlcStr);
|
|
95
|
+
}
|
|
96
|
+
export async function setBusinessProfileLocal(businessId, profile) {
|
|
97
|
+
const db = await openSyncDatabase();
|
|
98
|
+
const payload = { data: profile };
|
|
99
|
+
const existing = await getReferenceData(db, businessId, "business:profile");
|
|
100
|
+
const lastHlc = existing?.lastHlc ? hlcFromString(existing.lastHlc) : null;
|
|
101
|
+
const hlcStr = hlcToString(tickHLC(Date.now(), lastHlc, getAppInstanceId()));
|
|
102
|
+
await mergeReferenceData(db, businessId, "business:profile", payload, hlcStr, `evt_${hlcStr}`);
|
|
103
|
+
await enqueueReferenceSyncJob(businessId, "business:profile", hlcStr);
|
|
104
|
+
}
|
|
@@ -12,7 +12,8 @@ import { syncLogger } from "../utils/sync-logger";
|
|
|
12
12
|
import { generateUUID } from "../utils/uuid";
|
|
13
13
|
import { menuService, businessService, rolesService } from "../adapters/services";
|
|
14
14
|
import { cacheImageBatch, pruneImageCache } from "../adapters/image-cache";
|
|
15
|
-
import {
|
|
15
|
+
import { getConnectivityState } from "../adapters/connectivity";
|
|
16
|
+
import { getReferenceData, getReferenceVersion, mergeReferenceData, } from "./operations";
|
|
16
17
|
import { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, } from "./api";
|
|
17
18
|
const DEFAULT_SYNC_KEYS = [
|
|
18
19
|
"menu:items",
|
|
@@ -22,6 +23,8 @@ const DEFAULT_SYNC_KEYS = [
|
|
|
22
23
|
"tax-rules",
|
|
23
24
|
"business:profile",
|
|
24
25
|
"operating-hours",
|
|
26
|
+
"delivery-settings",
|
|
27
|
+
"payment-settings",
|
|
25
28
|
"subscription:entitlements",
|
|
26
29
|
"capabilities",
|
|
27
30
|
];
|
|
@@ -38,6 +41,20 @@ class ReferenceSyncService {
|
|
|
38
41
|
errors: {},
|
|
39
42
|
};
|
|
40
43
|
}
|
|
44
|
+
const { isOnline, isCloudReachable } = getConnectivityState();
|
|
45
|
+
if (!isOnline || isCloudReachable === "unreachable") {
|
|
46
|
+
syncLogger.debug("ReferenceSync", "Skipping sync — cloud not reachable", {
|
|
47
|
+
businessId: options.businessId,
|
|
48
|
+
isOnline,
|
|
49
|
+
isCloudReachable,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
synced: [],
|
|
53
|
+
skipped: options.keys ?? DEFAULT_SYNC_KEYS,
|
|
54
|
+
failed: [],
|
|
55
|
+
errors: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
41
58
|
this.isSyncing = true;
|
|
42
59
|
syncLogger.info("ReferenceSync", "Starting reference data sync", {
|
|
43
60
|
businessId: options.businessId,
|
|
@@ -143,14 +160,9 @@ class ReferenceSyncService {
|
|
|
143
160
|
payload = fallback.payload;
|
|
144
161
|
version = fallback.version;
|
|
145
162
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
payload,
|
|
150
|
-
downloadedAt: new Date().toISOString(),
|
|
151
|
-
lastHlc: version ?? generateUUID(),
|
|
152
|
-
lastEventId: `evt_${version ?? generateUUID()}`,
|
|
153
|
-
});
|
|
163
|
+
// LWW: newer HLC wins — local edits that are newer than server are preserved.
|
|
164
|
+
const hlc = version ?? generateUUID();
|
|
165
|
+
await mergeReferenceData(db, businessId, key, payload, hlc, `evt_${hlc}`);
|
|
154
166
|
return true;
|
|
155
167
|
}
|
|
156
168
|
async fetchFallback(key, businessId) {
|
|
@@ -244,6 +256,21 @@ class ReferenceSyncService {
|
|
|
244
256
|
version: this.hashPayload(response),
|
|
245
257
|
};
|
|
246
258
|
}
|
|
259
|
+
case "delivery-settings": {
|
|
260
|
+
const response = await businessService.fetchDeliverySettings?.(businessId) ?? (await (await import("../adapters/http")).getHttpClient().get(`/businesses/${businessId}/delivery-settings`).then((r) => r.data));
|
|
261
|
+
return {
|
|
262
|
+
payload: response,
|
|
263
|
+
version: this.hashPayload(response),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case "payment-settings": {
|
|
267
|
+
const response = await businessService.fetchBusinessById(businessId);
|
|
268
|
+
const payload = response?.data?.paymentSettings ?? response;
|
|
269
|
+
return {
|
|
270
|
+
payload,
|
|
271
|
+
version: this.hashPayload(payload),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
247
274
|
default:
|
|
248
275
|
throw new Error(`No fallback fetch available for ${key}`);
|
|
249
276
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* It is synchronized separately from transactional data through the
|
|
9
9
|
* ReferenceSyncService.
|
|
10
10
|
*/
|
|
11
|
-
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "subscription:entitlements" | "capabilities";
|
|
11
|
+
export type ReferenceDataKey = "menu:items" | "menu:categories" | "roles" | "members" | "tax-rules" | "business:profile" | "operating-hours" | "delivery-settings" | "payment-settings" | "subscription:entitlements" | "capabilities";
|
|
12
12
|
export interface ReferenceDataRecord<T = unknown> {
|
|
13
13
|
key: ReferenceDataKey;
|
|
14
14
|
version: string;
|
|
@@ -24,6 +24,13 @@ export interface OrderActionInput {
|
|
|
24
24
|
businessId: string;
|
|
25
25
|
}
|
|
26
26
|
export declare const orderRepository: {
|
|
27
|
+
/**
|
|
28
|
+
* Single create path: build an `order:create` command and route it through
|
|
29
|
+
* the SyncEngine (cloud → LAN node → queue). Every path returns an
|
|
30
|
+
* order-shaped payload: cloud/LAN execute the real create, queue returns
|
|
31
|
+
* the optimistic order (same UUID + DD-RRR-CCC, replayed idempotently).
|
|
32
|
+
* Direct `createLocalOrder` bypasses sync — never call it from UI flows.
|
|
33
|
+
*/
|
|
27
34
|
createOrder(input: CreateOrderInput): Promise<SellerOrder>;
|
|
28
35
|
acceptOrder(input: OrderActionInput): Promise<SellerOrder>;
|
|
29
36
|
rejectOrder(input: OrderActionInput): Promise<SellerOrder>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { syncEngine } from "../sync/engine";
|
|
11
11
|
import { buildCommand } from "../sync/command-builder";
|
|
12
|
+
import { generateUUID } from "../utils/uuid";
|
|
12
13
|
async function executeCommand(command) {
|
|
13
14
|
console.debug("[OrderRepository] Dispatching command", JSON.stringify({
|
|
14
15
|
command_id: command.command_id,
|
|
@@ -35,12 +36,24 @@ async function executeCommand(command) {
|
|
|
35
36
|
return event.payload;
|
|
36
37
|
}
|
|
37
38
|
export const orderRepository = {
|
|
39
|
+
/**
|
|
40
|
+
* Single create path: build an `order:create` command and route it through
|
|
41
|
+
* the SyncEngine (cloud → LAN node → queue). Every path returns an
|
|
42
|
+
* order-shaped payload: cloud/LAN execute the real create, queue returns
|
|
43
|
+
* the optimistic order (same UUID + DD-RRR-CCC, replayed idempotently).
|
|
44
|
+
* Direct `createLocalOrder` bypasses sync — never call it from UI flows.
|
|
45
|
+
*/
|
|
38
46
|
async createOrder(input) {
|
|
47
|
+
const entityId = input.entityId ?? generateUUID();
|
|
39
48
|
const command = buildCommand({
|
|
40
49
|
commandType: "order:create",
|
|
41
50
|
businessId: input.businessId,
|
|
42
|
-
entityId
|
|
43
|
-
payload:
|
|
51
|
+
entityId,
|
|
52
|
+
payload: {
|
|
53
|
+
...input.data,
|
|
54
|
+
id: entityId,
|
|
55
|
+
command_id: entityId,
|
|
56
|
+
},
|
|
44
57
|
});
|
|
45
58
|
return executeCommand(command);
|
|
46
59
|
},
|
|
@@ -14,6 +14,10 @@ export interface POSOrderPreviewInput {
|
|
|
14
14
|
customerName?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const posRepository: {
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Single create path is `createLocalOrder` (domain/orders/service).
|
|
19
|
+
* Forwards via `orderRepository.createOrder`, which delegates there directly.
|
|
20
|
+
*/
|
|
17
21
|
createOrder(businessId: string, data: POSOrderCreateRequest, entityId?: string): Promise<SellerOrder>;
|
|
18
22
|
fetchPreview(input: POSOrderPreviewInput): Promise<POSOrderPreviewResponse>;
|
|
19
23
|
fetchOrders(businessId: string, params?: Record<string, string>): Promise<SellerOrdersResponse>;
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import { posService } from "../adapters/services";
|
|
8
8
|
import { orderRepository } from "./order.repository";
|
|
9
9
|
export const posRepository = {
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated Single create path is `createLocalOrder` (domain/orders/service).
|
|
12
|
+
* Forwards via `orderRepository.createOrder`, which delegates there directly.
|
|
13
|
+
*/
|
|
10
14
|
async createOrder(businessId, data, entityId) {
|
|
11
15
|
return orderRepository.createOrder({ businessId, data, entityId });
|
|
12
16
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry backoff — one curve everywhere (sync retries, probes stay fixed).
|
|
3
|
+
*
|
|
4
|
+
* delay(attempts) = min(MAX, BASE * 2^attempts) + jitter(±20%).
|
|
5
|
+
* Attempts reset on any success or connectivity regain, so one bad night
|
|
6
|
+
* never permanently slows the device, and a fleet doesn't thunder-herd a
|
|
7
|
+
* recovering backend at the same millisecond.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BACKOFF_BASE_MS = 2000;
|
|
10
|
+
export declare const BACKOFF_MAX_MS: number;
|
|
11
|
+
export declare function backoffDelayMs(attempts: number): number;
|
|
12
|
+
/** Deterministic center of the curve (no jitter) — for tests and logging. */
|
|
13
|
+
export declare function backoffCenterMs(attempts: number): number;
|
|
14
|
+
/** Attempt counters keyed by scope (e.g. business id). Reset on success/regain. */
|
|
15
|
+
export declare class BackoffTracker {
|
|
16
|
+
private attempts;
|
|
17
|
+
next(scope: string): number;
|
|
18
|
+
reset(scope: string): void;
|
|
19
|
+
resetAll(): void;
|
|
20
|
+
count(scope: string): number;
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry backoff — one curve everywhere (sync retries, probes stay fixed).
|
|
3
|
+
*
|
|
4
|
+
* delay(attempts) = min(MAX, BASE * 2^attempts) + jitter(±20%).
|
|
5
|
+
* Attempts reset on any success or connectivity regain, so one bad night
|
|
6
|
+
* never permanently slows the device, and a fleet doesn't thunder-herd a
|
|
7
|
+
* recovering backend at the same millisecond.
|
|
8
|
+
*/
|
|
9
|
+
export const BACKOFF_BASE_MS = 2000;
|
|
10
|
+
export const BACKOFF_MAX_MS = 5 * 60000;
|
|
11
|
+
const JITTER_RATIO = 0.2;
|
|
12
|
+
export function backoffDelayMs(attempts) {
|
|
13
|
+
const safe = Math.max(0, Math.floor(attempts));
|
|
14
|
+
const capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, safe));
|
|
15
|
+
const jitter = capped * JITTER_RATIO * (Math.random() * 2 - 1);
|
|
16
|
+
return Math.max(0, Math.round(capped + jitter));
|
|
17
|
+
}
|
|
18
|
+
/** Deterministic center of the curve (no jitter) — for tests and logging. */
|
|
19
|
+
export function backoffCenterMs(attempts) {
|
|
20
|
+
const safe = Math.max(0, Math.floor(attempts));
|
|
21
|
+
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * Math.pow(2, safe));
|
|
22
|
+
}
|
|
23
|
+
/** Attempt counters keyed by scope (e.g. business id). Reset on success/regain. */
|
|
24
|
+
export class BackoffTracker {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.attempts = new Map();
|
|
27
|
+
}
|
|
28
|
+
next(scope) {
|
|
29
|
+
const n = this.attempts.get(scope) ?? 0;
|
|
30
|
+
this.attempts.set(scope, n + 1);
|
|
31
|
+
return backoffDelayMs(n);
|
|
32
|
+
}
|
|
33
|
+
reset(scope) {
|
|
34
|
+
this.attempts.delete(scope);
|
|
35
|
+
}
|
|
36
|
+
resetAll() {
|
|
37
|
+
this.attempts.clear();
|
|
38
|
+
}
|
|
39
|
+
count(scope) {
|
|
40
|
+
return this.attempts.get(scope) ?? 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -11,6 +11,14 @@ export function buildCommand(options) {
|
|
|
11
11
|
const user = useAuthStore.getState().user;
|
|
12
12
|
const userId = user?.id ?? "anonymous";
|
|
13
13
|
const deviceId = getAppInstanceId();
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
// Inject inserted_at / updated_at into payload so the backend preserves the
|
|
16
|
+
// original client creation time for offline-first sync.
|
|
17
|
+
const payload = {
|
|
18
|
+
...options.payload,
|
|
19
|
+
inserted_at: timestamp,
|
|
20
|
+
updated_at: timestamp,
|
|
21
|
+
};
|
|
14
22
|
return {
|
|
15
23
|
command_id: generateCommandId(),
|
|
16
24
|
command_type: options.commandType,
|
|
@@ -19,8 +27,8 @@ export function buildCommand(options) {
|
|
|
19
27
|
entity_id: options.entityId,
|
|
20
28
|
user_id: userId,
|
|
21
29
|
device_id: deviceId,
|
|
22
|
-
timestamp
|
|
23
|
-
payload
|
|
30
|
+
timestamp,
|
|
31
|
+
payload,
|
|
24
32
|
};
|
|
25
33
|
}
|
|
26
34
|
export { getAppInstanceId };
|
|
@@ -37,6 +37,18 @@ declare class CommandQueueService {
|
|
|
37
37
|
* Return the number of commands currently in the outbox.
|
|
38
38
|
*/
|
|
39
39
|
getPendingCount(): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* Debug dump of the engine outbox for Diagnostics screens (all statuses,
|
|
42
|
+
* newest last). Mirrors debugDumpSyncJobs.
|
|
43
|
+
*/
|
|
44
|
+
debugDumpCommands(businessId: string): Promise<Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
command_type: string;
|
|
47
|
+
status: string;
|
|
48
|
+
retry_count: number;
|
|
49
|
+
error: string | null;
|
|
50
|
+
created_at: string;
|
|
51
|
+
}>>;
|
|
40
52
|
}
|
|
41
53
|
export declare const commandQueueService: CommandQueueService;
|
|
42
54
|
export {};
|