@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.
Files changed (191) hide show
  1. package/README.md +26 -21
  2. package/dist/adapters/auth.d.ts +8 -0
  3. package/dist/adapters/auth.js +36 -0
  4. package/dist/adapters/devices.d.ts +3 -1
  5. package/dist/adapters/devices.js +187 -3
  6. package/dist/adapters/filesystem.d.ts +4 -0
  7. package/dist/adapters/kv.d.ts +10 -0
  8. package/dist/adapters/kv.js +23 -7
  9. package/dist/adapters/local-node.d.ts +4 -6
  10. package/dist/adapters/local-node.js +9 -32
  11. package/dist/adapters/notify.d.ts +12 -0
  12. package/dist/adapters/notify.js +33 -0
  13. package/dist/adapters/query-keys.d.ts +4 -0
  14. package/dist/adapters/query-keys.js +4 -0
  15. package/dist/adapters/services.d.ts +96 -15
  16. package/dist/adapters/services.js +241 -4
  17. package/dist/auth/session.d.ts +66 -0
  18. package/dist/auth/session.js +172 -0
  19. package/dist/catalog/hooks.d.ts +10 -0
  20. package/dist/catalog/hooks.js +82 -11
  21. package/dist/db/migrations.js +274 -0
  22. package/dist/db/operations.d.ts +53 -2
  23. package/dist/db/operations.js +177 -8
  24. package/dist/domain/business/index.d.ts +5 -0
  25. package/dist/domain/business/index.js +5 -0
  26. package/dist/domain/orders/lifecycle.d.ts +6 -0
  27. package/dist/domain/orders/lifecycle.js +6 -0
  28. package/dist/domain/orders/service.d.ts +24 -0
  29. package/dist/domain/orders/service.js +242 -0
  30. package/dist/domain/payments/index.d.ts +6 -0
  31. package/dist/domain/payments/index.js +5 -0
  32. package/dist/domain/reference/index.d.ts +8 -0
  33. package/dist/domain/reference/index.js +8 -0
  34. package/dist/e2e/harness.d.ts +1 -0
  35. package/dist/e2e/harness.js +61 -0
  36. package/dist/hooks/analytics.hooks.d.ts +25 -0
  37. package/dist/hooks/analytics.hooks.js +214 -0
  38. package/dist/hooks/business-hours.hooks.d.ts +8 -0
  39. package/dist/hooks/business-hours.hooks.js +45 -0
  40. package/dist/hooks/dashboard.hooks.d.ts +30 -0
  41. package/dist/hooks/dashboard.hooks.js +132 -0
  42. package/dist/hooks/delivery.hooks.d.ts +225 -0
  43. package/dist/hooks/delivery.hooks.js +80 -0
  44. package/dist/hooks/fleet.hooks.d.ts +37 -0
  45. package/dist/hooks/fleet.hooks.js +229 -0
  46. package/dist/hooks/image.hooks.d.ts +24 -0
  47. package/dist/hooks/image.hooks.js +66 -0
  48. package/dist/hooks/kiosk.hooks.d.ts +22 -5
  49. package/dist/hooks/kiosk.hooks.js +74 -5
  50. package/dist/hooks/members.hooks.d.ts +48 -0
  51. package/dist/hooks/members.hooks.js +302 -0
  52. package/dist/hooks/offline-orders.hooks.d.ts +9 -0
  53. package/dist/hooks/offline-orders.hooks.js +51 -0
  54. package/dist/hooks/orders.hooks.d.ts +18 -39
  55. package/dist/hooks/orders.hooks.js +124 -343
  56. package/dist/hooks/places.hooks.d.ts +21 -0
  57. package/dist/hooks/places.hooks.js +112 -0
  58. package/dist/hooks/realtime-stubs.d.ts +28 -0
  59. package/dist/hooks/realtime-stubs.js +85 -0
  60. package/dist/hooks/rider.hooks.d.ts +470 -0
  61. package/dist/hooks/rider.hooks.js +373 -0
  62. package/dist/hooks/use-infinite-list.d.ts +243 -0
  63. package/dist/hooks/use-infinite-list.js +58 -0
  64. package/dist/hooks/wallet.hooks.d.ts +219 -0
  65. package/dist/hooks/wallet.hooks.js +56 -0
  66. package/dist/image-cache/service.d.ts +28 -0
  67. package/dist/image-cache/service.js +51 -3
  68. package/dist/index.d.ts +56 -6
  69. package/dist/index.js +73 -6
  70. package/dist/local-node/adapters.d.ts +87 -0
  71. package/dist/local-node/adapters.js +4 -0
  72. package/dist/local-node/index.d.ts +5 -0
  73. package/dist/local-node/index.js +5 -0
  74. package/dist/local-node/security.d.ts +25 -0
  75. package/dist/local-node/security.js +149 -0
  76. package/dist/local-node/service.d.ts +83 -0
  77. package/dist/local-node/service.js +706 -0
  78. package/dist/local-node/store.d.ts +50 -0
  79. package/dist/local-node/store.js +284 -0
  80. package/dist/local-node/types.d.ts +92 -0
  81. package/dist/local-node/types.js +4 -0
  82. package/dist/printer/receipt.d.ts +4 -0
  83. package/dist/printer/receipt.js +75 -0
  84. package/dist/printer/service.js +1 -1
  85. package/dist/providers/query-client.d.ts +13 -0
  86. package/dist/providers/query-client.js +58 -0
  87. package/dist/realtime/phoenix-socket.js +30 -1
  88. package/dist/realtime/useEventsConnection.js +11 -2
  89. package/dist/reference/api.d.ts +2 -0
  90. package/dist/reference/api.js +2 -0
  91. package/dist/reference/capabilities.d.ts +12 -7
  92. package/dist/reference/capabilities.js +27 -13
  93. package/dist/reference/hooks.d.ts +34 -0
  94. package/dist/reference/hooks.js +124 -0
  95. package/dist/reference/menu-apply.d.ts +29 -0
  96. package/dist/reference/menu-apply.js +289 -0
  97. package/dist/reference/menu-record.d.ts +69 -0
  98. package/dist/reference/menu-record.js +168 -0
  99. package/dist/reference/operations.d.ts +3 -0
  100. package/dist/reference/operations.js +35 -1
  101. package/dist/reference/service.js +36 -9
  102. package/dist/reference/types.d.ts +1 -1
  103. package/dist/repositories/order.repository.d.ts +7 -0
  104. package/dist/repositories/order.repository.js +15 -2
  105. package/dist/repositories/pos.repository.d.ts +4 -0
  106. package/dist/repositories/pos.repository.js +4 -0
  107. package/dist/sync/backoff.d.ts +21 -0
  108. package/dist/sync/backoff.js +42 -0
  109. package/dist/sync/command-builder.js +10 -2
  110. package/dist/sync/command-queue.service.d.ts +12 -0
  111. package/dist/sync/command-queue.service.js +81 -16
  112. package/dist/sync/device-lease.service.d.ts +3 -3
  113. package/dist/sync/device-lease.service.js +80 -20
  114. package/dist/sync/engine.d.ts +3 -1
  115. package/dist/sync/engine.js +44 -4
  116. package/dist/sync/full-sync.d.ts +6 -0
  117. package/dist/sync/full-sync.js +72 -0
  118. package/dist/sync/health-probe.d.ts +24 -0
  119. package/dist/sync/health-probe.js +138 -0
  120. package/dist/sync/offline-capable.js +25 -0
  121. package/dist/sync/online-gates.d.ts +17 -0
  122. package/dist/sync/online-gates.js +55 -0
  123. package/dist/sync/order-number.d.ts +4 -4
  124. package/dist/sync/order-number.js +33 -13
  125. package/dist/sync/order-snapshots.service.js +18 -3
  126. package/dist/sync/pending-orders.service.d.ts +18 -0
  127. package/dist/sync/pending-orders.service.js +67 -1
  128. package/dist/sync/reconciliation/index.d.ts +5 -0
  129. package/dist/sync/reconciliation/index.js +5 -0
  130. package/dist/sync/snapshots/entity.d.ts +6 -0
  131. package/dist/sync/snapshots/entity.js +6 -0
  132. package/dist/sync/sync-jobs.service.d.ts +18 -0
  133. package/dist/sync/sync-jobs.service.js +539 -0
  134. package/dist/sync/transition-endpoints.d.ts +11 -0
  135. package/dist/sync/transition-endpoints.js +30 -0
  136. package/dist/sync/transport-local-node.js +3 -2
  137. package/dist/sync/transport-rest.d.ts +7 -0
  138. package/dist/sync/transport-rest.js +84 -26
  139. package/dist/sync/use-sync-reconciliation.js +15 -1
  140. package/dist/test-helpers/memory-driver.js +182 -4
  141. package/dist/test-helpers/seed-hours.d.ts +1 -0
  142. package/dist/test-helpers/seed-hours.js +23 -0
  143. package/dist/types/api/business.api.d.ts +42 -0
  144. package/dist/types/api/menu.api.d.ts +15 -0
  145. package/dist/types/api/seller.api.d.ts +20 -0
  146. package/dist/types/places.d.ts +19 -0
  147. package/dist/types/places.js +2 -0
  148. package/dist/types/rider.d.ts +67 -0
  149. package/dist/types/rider.js +2 -0
  150. package/dist/utils/attempt-stats.d.ts +23 -0
  151. package/dist/utils/attempt-stats.js +61 -0
  152. package/dist/utils/business-hours.d.ts +37 -0
  153. package/dist/utils/business-hours.js +96 -0
  154. package/dist/utils/debug-log.d.ts +23 -0
  155. package/dist/utils/debug-log.js +153 -0
  156. package/dist/utils/order-filter.d.ts +1 -1
  157. package/dist/utils/order-filter.js +1 -1
  158. package/dist/utils/order-transitions.d.ts +30 -0
  159. package/dist/utils/order-transitions.js +185 -0
  160. package/dist/utils/sync-logger.js +5 -4
  161. package/dist/validation/offline.d.ts +46 -0
  162. package/dist/validation/offline.js +79 -0
  163. package/package.json +6 -1
  164. package/dist/pricing/engine.test.d.ts +0 -1
  165. package/dist/pricing/engine.test.js +0 -142
  166. package/dist/reference/lww.test.d.ts +0 -1
  167. package/dist/reference/lww.test.js +0 -23
  168. package/dist/reference/operations.test.d.ts +0 -1
  169. package/dist/reference/operations.test.js +0 -66
  170. package/dist/standalone.integration.test.d.ts +0 -1
  171. package/dist/standalone.integration.test.js +0 -235
  172. package/dist/sync/device-lease.test.d.ts +0 -1
  173. package/dist/sync/device-lease.test.js +0 -38
  174. package/dist/sync/offline-integration.test.d.ts +0 -1
  175. package/dist/sync/offline-integration.test.js +0 -52
  176. package/dist/sync/order-number.test.d.ts +0 -1
  177. package/dist/sync/order-number.test.js +0 -51
  178. package/dist/sync/perms.test.d.ts +0 -1
  179. package/dist/sync/perms.test.js +0 -15
  180. package/dist/utils/currency.test.d.ts +0 -1
  181. package/dist/utils/currency.test.js +0 -17
  182. package/dist/utils/hlc.test.d.ts +0 -1
  183. package/dist/utils/hlc.test.js +0 -29
  184. package/dist/utils/phone.test.d.ts +0 -1
  185. package/dist/utils/phone.test.js +0 -19
  186. package/dist/utils/responsive.d.ts +0 -13
  187. package/dist/utils/responsive.js +0 -18
  188. package/dist/utils/uuid.test.d.ts +0 -1
  189. package/dist/utils/uuid.test.js +0 -13
  190. package/dist/utils/validation.test.d.ts +0 -1
  191. package/dist/utils/validation.test.js +0 -23
@@ -1,235 +0,0 @@
1
- /**
2
- * Standalone integration tests for 0.7.0
3
- * Covers: menu, categories, taxes, operating-hours, business:profile, orders
4
- * Modes: single node, multi-node (LAN), backend sync
5
- * No network, no fallback, pure memory driver.
6
- */
7
- import { describe, it, expect, beforeEach } from "vitest";
8
- import { createMemoryDriver } from "./test-helpers/memory-driver";
9
- import { setSqliteDriver } from "./adapters/sqlite";
10
- import { openSyncDatabase, closeSyncDatabase } from "./db/connection";
11
- import { setReferenceData, getReferenceData, mergeReferenceData, getReferenceVersion } from "./reference/operations";
12
- import { generateOrderNumber, resetCalloutForTests } from "./sync/order-number";
13
- import { setItemSync, removeItemSync } from "./adapters/kv";
14
- import { insertEvent, getUnsyncedEvents, markEventsSynced, upsertEntitySnapshot, getEntitySnapshot } from "./db/operations";
15
- import { generateEventId } from "./utils/uuid";
16
- import { tickHLC, hlcToString, compareHLCString } from "./utils/hlc";
17
- // helper to build reference payloads per key
18
- function payloadForKey(key, suffix = "") {
19
- switch (key) {
20
- case "menu:items":
21
- return { data: [{ id: "item1", name: `Jollof${suffix}`, price: 5000 }] };
22
- case "menu:categories":
23
- return { data: [{ id: "cat1", name: `Rice${suffix}` }] };
24
- case "tax-rules":
25
- return { data: [{ id: "tax1", rate_basis_points: 500 }] };
26
- case "operating-hours":
27
- return { data: [{ day_of_week: 1, open_time: "08:00", is_closed: false }] };
28
- case "business:profile":
29
- return { data: { name: `Biz${suffix}`, business_number: "AB12C" } };
30
- case "roles":
31
- return { data: [{ name: "owner" }] };
32
- case "members":
33
- return { data: [{ user_id: "u1", role: "owner" }] };
34
- case "capabilities":
35
- return { data: { capabilities: ["pos:create_order", "menu:manage"] } };
36
- default:
37
- return { data: {} };
38
- }
39
- }
40
- const ALL_KEYS = [
41
- "menu:items",
42
- "menu:categories",
43
- "tax-rules",
44
- "operating-hours",
45
- "business:profile",
46
- "roles",
47
- "members",
48
- "capabilities",
49
- ];
50
- describe("standalone integration — single node", () => {
51
- beforeEach(async () => {
52
- const driver = createMemoryDriver();
53
- setSqliteDriver(driver);
54
- // ensure fresh connection resets initPromise
55
- // @ts-ignore
56
- // close and reopen to run migrations
57
- try {
58
- await closeSyncDatabase();
59
- }
60
- catch { }
61
- removeItemSync("teinc_callout_seq");
62
- resetCalloutForTests();
63
- });
64
- it("single node can store and retrieve all reference keys", async () => {
65
- const db = await openSyncDatabase();
66
- const biz = "biz-single";
67
- for (const key of ALL_KEYS) {
68
- const payload = payloadForKey(key, "-v1");
69
- const hlc = hlcToString(tickHLC(Date.now(), null, "01"));
70
- await setReferenceData(db, biz, { key, version: hlc, payload, downloadedAt: new Date().toISOString(), lastHlc: hlc, lastEventId: generateEventId() });
71
- }
72
- for (const key of ALL_KEYS) {
73
- const rec = await getReferenceData(db, biz, key);
74
- expect(rec).not.toBeNull();
75
- expect(rec.key).toBe(key);
76
- }
77
- // verify getReferenceVersion works for HLC
78
- const v = await getReferenceVersion(db, biz, "menu:items");
79
- expect(v).toMatch(/^[0-9]+-[0-9]+-01$/);
80
- });
81
- it("single device order DD-RRR-CCC generation and offline storage", async () => {
82
- setItemSync("teinc_device_number:biz-single", "07");
83
- const { orderNumber, callout } = generateOrderNumber("07");
84
- expect(orderNumber).toMatch(/^07-[0-9]{3}-[0-9]{3}$/);
85
- expect(callout).toBeGreaterThan(0);
86
- // persist as event (offline order)
87
- const db = await openSyncDatabase();
88
- const evt = {
89
- event_id: generateEventId(),
90
- event_type: "order.create_queued",
91
- event_version: "1.0",
92
- entity_id: "cmd-1",
93
- user_id: "u1",
94
- device_id: "dev-07",
95
- payload: { order_number: orderNumber, callout_number: callout },
96
- timestamp: new Date().toISOString(),
97
- };
98
- await insertEvent(db, { event: evt, origin: "local" });
99
- const unsynced = await getUnsyncedEvents(db, "local");
100
- expect(unsynced.length).toBe(1);
101
- expect(unsynced[0].payload.order_number).toBe(orderNumber);
102
- });
103
- it("callout wraps 999->001", async () => {
104
- setItemSync("teinc_callout_seq", 998);
105
- const a = generateOrderNumber("03");
106
- expect(a.callout).toBe(999);
107
- const b = generateOrderNumber("03");
108
- expect(b.callout).toBe(1);
109
- expect(b.orderNumber).not.toBe(a.orderNumber);
110
- });
111
- });
112
- describe("standalone integration — multi-node LAN", () => {
113
- beforeEach(async () => {
114
- const driver = createMemoryDriver();
115
- setSqliteDriver(driver);
116
- try {
117
- await closeSyncDatabase();
118
- }
119
- catch { }
120
- removeItemSync("teinc_callout_seq");
121
- resetCalloutForTests();
122
- });
123
- it("two nodes editing same menu:items converge via HLC per-key", async () => {
124
- const db = await openSyncDatabase();
125
- const biz = "biz-multi";
126
- // Node A (device 01) writes price 500 at HLC wall 1000
127
- const hlcA = hlcToString(tickHLC(1000, null, "01"));
128
- await setReferenceData(db, biz, { key: "menu:items", version: hlcA, payload: payloadForKey("menu:items", "-A"), downloadedAt: new Date().toISOString(), lastHlc: hlcA, lastEventId: "evt_A" });
129
- // Node B (device 02) concurrently writes different payload at wall 1001 (later) — should win
130
- const hlcB = hlcToString(tickHLC(1001, null, "02"));
131
- await mergeReferenceData(db, biz, "menu:items", payloadForKey("menu:items", "-B"), hlcB, "evt_B");
132
- let rec = await getReferenceData(db, biz, "menu:items");
133
- expect(rec.payload.data[0].name).toContain("-B");
134
- // Node A tries to replay older HLC (1000) — should be rejected (LWW)
135
- await mergeReferenceData(db, biz, "menu:items", payloadForKey("menu:items", "-A-old"), hlcA, "evt_A2");
136
- rec = await getReferenceData(db, biz, "menu:items");
137
- expect(rec.payload.data[0].name).toContain("-B");
138
- });
139
- it("divergent fields via LWW-Map style: operating-hours + tax-rules both persist across nodes", async () => {
140
- const db = await openSyncDatabase();
141
- const biz = "biz-multi2";
142
- // Node A updates operating-hours, Node B updates tax-rules — no conflict, both keys coexist
143
- const hlcA = hlcToString(tickHLC(2000, null, "01"));
144
- const hlcB = hlcToString(tickHLC(2000, null, "02"));
145
- await setReferenceData(db, biz, { key: "operating-hours", version: hlcA, payload: payloadForKey("operating-hours"), downloadedAt: new Date().toISOString(), lastHlc: hlcA, lastEventId: "evt_oh" });
146
- await setReferenceData(db, biz, { key: "tax-rules", version: hlcB, payload: payloadForKey("tax-rules"), downloadedAt: new Date().toISOString(), lastHlc: hlcB, lastEventId: "evt_tax" });
147
- const oh = await getReferenceData(db, biz, "operating-hours");
148
- const tax = await getReferenceData(db, biz, "tax-rules");
149
- expect(oh).not.toBeNull();
150
- expect(tax).not.toBeNull();
151
- });
152
- it("orders: two LAN clients see each other's queued orders via events", async () => {
153
- const db = await openSyncDatabase();
154
- // Device 07 creates order
155
- setItemSync("teinc_device_number:biz-multi", "07");
156
- const o07 = generateOrderNumber("07");
157
- const evt07 = { event_id: generateEventId(), event_type: "order.create_queued", event_version: "1.0", entity_id: "cmd-07", user_id: "u07", device_id: "07", payload: { order_number: o07.orderNumber, callout_number: o07.callout }, timestamp: new Date().toISOString() };
158
- await insertEvent(db, { event: evt07, origin: "local" });
159
- // Device 08 creates order (LAN peer) — inserted as lan origin on first device's DB (simulating broadcast)
160
- const o08 = generateOrderNumber("08");
161
- const evt08 = { event_id: generateEventId(), event_type: "order.create_queued", event_version: "1.0", entity_id: "cmd-08", user_id: "u08", device_id: "08", payload: { order_number: o08.orderNumber, callout_number: o08.callout }, timestamp: new Date().toISOString() };
162
- await insertEvent(db, { event: evt08, origin: "lan" });
163
- const localUnsynced = await getUnsyncedEvents(db, "local");
164
- const lanUnsynced = await getUnsyncedEvents(db, "lan");
165
- expect(localUnsynced.length).toBe(1);
166
- expect(lanUnsynced.length).toBe(1);
167
- expect(localUnsynced[0].payload.order_number).toBe(o07.orderNumber);
168
- expect(lanUnsynced[0].payload.order_number).toBe(o08.orderNumber);
169
- // Also test entity_snapshots for order status via HLC
170
- const hlcStatus = hlcToString(tickHLC(Date.now(), null, "07"));
171
- await upsertEntitySnapshot(db, { entityType: "seller_orders", entityId: "list-main", businessId: "biz-multi", lastEventId: evt07.event_id, lastEventTimestamp: evt07.timestamp, payload: { data: [evt07.payload], savedAt: new Date().toISOString() } });
172
- const snap = await getEntitySnapshot(db, "seller_orders", "list-main");
173
- expect(snap).not.toBeNull();
174
- expect(snap.payload.data[0].order_number).toBe(o07.orderNumber);
175
- // LAN peer updates same list with later timestamp — should win (LWW last_event_timestamp)
176
- const later = new Date(Date.now() + 1000).toISOString();
177
- await upsertEntitySnapshot(db, { entityType: "seller_orders", entityId: "list-main", businessId: "biz-multi", lastEventId: evt08.event_id, lastEventTimestamp: later, payload: { data: [evt07.payload, evt08.payload], savedAt: later } });
178
- const snap2 = await getEntitySnapshot(db, "seller_orders", "list-main");
179
- expect(snap2.payload.data.length).toBe(2);
180
- });
181
- });
182
- describe("standalone integration — backend sync simulation", () => {
183
- beforeEach(async () => {
184
- const driver = createMemoryDriver();
185
- setSqliteDriver(driver);
186
- try {
187
- await closeSyncDatabase();
188
- }
189
- catch { }
190
- removeItemSync("teinc_callout_seq");
191
- resetCalloutForTests();
192
- });
193
- it("backend adopt offline DD-RRR-CCC and mark synced (no re-number)", async () => {
194
- const db = await openSyncDatabase();
195
- setItemSync("teinc_device_number:biz-backend", "05");
196
- const { orderNumber } = generateOrderNumber("05");
197
- const evt = { event_id: generateEventId(), event_type: "order.create_queued", event_version: "1.0", entity_id: "cmd-05", user_id: "u05", device_id: "05", payload: { order_number: orderNumber, callout_number: parseInt(orderNumber.split("-")[2], 10) }, timestamp: new Date().toISOString() };
198
- await insertEvent(db, { event: evt, origin: "local" });
199
- // Simulate backend adopting same order_number (no change) and returning order.created with local_order_number echo
200
- const cloudEvt = { event_id: generateEventId(), event_type: "order.created", event_version: "1.0", entity_id: "cmd-05", user_id: "u05", device_id: "05", payload: { order_number: orderNumber, local_order_number: orderNumber, callout_number: parseInt(orderNumber.split("-")[2], 10) }, timestamp: new Date().toISOString() };
201
- // Mark local unsynced as synced after cloud ack
202
- let unsynced = await getUnsyncedEvents(db, "local");
203
- expect(unsynced.length).toBe(1);
204
- await markEventsSynced(db, [unsynced[0].event_id]);
205
- unsynced = await getUnsyncedEvents(db, "local");
206
- expect(unsynced.length).toBe(0);
207
- // Cloud event could be inserted as cloud origin
208
- await insertEvent(db, { event: cloudEvt, origin: "cloud", syncedToCloud: true });
209
- const cloudUnsynced = await getUnsyncedEvents(db, "cloud");
210
- expect(cloudUnsynced.length).toBe(0);
211
- });
212
- it("reference backend HLC bump: version grows monotonic", async () => {
213
- const db = await openSyncDatabase();
214
- const biz = "biz-backend";
215
- const h1 = hlcToString(tickHLC(3000, null, "backend"));
216
- await setReferenceData(db, biz, { key: "business:profile", version: h1, payload: payloadForKey("business:profile", "-v1"), downloadedAt: new Date().toISOString(), lastHlc: h1, lastEventId: "evt1" });
217
- const h2 = hlcToString(tickHLC(3001, { wallMs: 3000, counter: 0, deviceId: "backend" }, "backend"));
218
- await mergeReferenceData(db, biz, "business:profile", payloadForKey("business:profile", "-v2"), h2, "evt2");
219
- const rec = await getReferenceData(db, biz, "business:profile");
220
- expect(compareHLCString(rec.lastHlc, h1)).toBeGreaterThan(0);
221
- expect(rec.payload.data.name).toContain("-v2");
222
- });
223
- it("categories + taxes sync independently via backend versions", async () => {
224
- const db = await openSyncDatabase();
225
- const biz = "biz-backend2";
226
- for (const key of ["menu:categories", "tax-rules", "operating-hours"]) {
227
- const hlc = hlcToString(tickHLC(Date.now(), null, "backend"));
228
- await setReferenceData(db, biz, { key, version: hlc, payload: payloadForKey(key), downloadedAt: new Date().toISOString(), lastHlc: hlc, lastEventId: generateEventId() });
229
- }
230
- for (const key of ["menu:categories", "tax-rules", "operating-hours"]) {
231
- const rec = await getReferenceData(db, biz, key);
232
- expect(rec).not.toBeNull();
233
- }
234
- });
235
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,38 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { getDeviceNumber, isOnlineOnly } from "./device-lease.service";
3
- import { removeItemSync, setItemSync } from "../adapters/kv";
4
- import { setHttpClient } from "../adapters/http";
5
- describe("device-lease", () => {
6
- beforeEach(() => {
7
- removeItemSync("teinc_device_number:biz1");
8
- removeItemSync("teinc_device_number:biz1:leased_at");
9
- removeItemSync("teinc_app_instance_id");
10
- });
11
- it("returns null when no lease", () => {
12
- expect(getDeviceNumber("biz1")).toBeNull();
13
- expect(isOnlineOnly("biz1")).toBe(true);
14
- });
15
- it("returns leased number after set", () => {
16
- setItemSync("teinc_device_number:biz1", "05");
17
- expect(getDeviceNumber("biz1")).toBe("05");
18
- expect(isOnlineOnly("biz1")).toBe(false);
19
- });
20
- it("rejects invalid DD 00", () => {
21
- setItemSync("teinc_device_number:biz1", "00");
22
- expect(getDeviceNumber("biz1")).toBeNull();
23
- });
24
- it("ensureLease handles 429 limit", async () => {
25
- // mock http client that returns 429
26
- setHttpClient({
27
- get: async () => ({ data: null }),
28
- post: async () => { throw Object.assign(new Error("DEVICE_LIMIT_REACHED"), { status: 429, name: "ApiError" }); },
29
- put: async () => ({ data: null }),
30
- patch: async () => ({ data: null }),
31
- delete: async () => ({ data: null }),
32
- });
33
- const { ensureLease } = await import("./device-lease.service");
34
- const res = await ensureLease("biz1");
35
- expect(res).toBeNull();
36
- expect(isOnlineOnly("biz1")).toBe(true);
37
- });
38
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,52 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { generateOrderNumber, resetCalloutForTests } from "./order-number";
3
- import { removeItemSync, setItemSync } from "../adapters/kv";
4
- // minimal in-memory DB for outbox
5
- class MemDB {
6
- constructor() {
7
- this.commands = [];
8
- this.events = [];
9
- }
10
- async execAsync() { }
11
- async runAsync(sql, ...params) {
12
- if (sql.includes("INSERT INTO commands")) {
13
- this.commands.push({ command_id: params[0], status: params[8] });
14
- return { lastInsertRowId: this.commands.length };
15
- }
16
- if (sql.includes("INSERT INTO events")) {
17
- this.events.push({ event_id: params[0] });
18
- return { lastInsertRowId: 1 };
19
- }
20
- if (sql.includes("SELECT * FROM commands WHERE id")) {
21
- return null;
22
- }
23
- return { lastInsertRowId: 1 };
24
- }
25
- async getFirstAsync() { return null; }
26
- async getAllAsync() { return []; }
27
- }
28
- describe("offline integration DD-RRR-CCC", () => {
29
- beforeEach(() => {
30
- removeItemSync("teinc_callout_seq");
31
- removeItemSync("teinc_last_random");
32
- resetCalloutForTests();
33
- });
34
- it("queue uses DD-RRR-CCC and survives wrap", () => {
35
- setItemSync("teinc_device_number:biz1", "01");
36
- // generate two orders
37
- const a = generateOrderNumber("01");
38
- const b = generateOrderNumber("01");
39
- expect(a.orderNumber).toMatch(/^[0-9]{2}-[0-9]{3}-[0-9]{3}$/);
40
- expect(b.callout).toBe(a.callout + 1);
41
- // ensure per-order random different 100-999
42
- expect(a.random).toBeGreaterThanOrEqual(100);
43
- expect(b.random).toBeGreaterThanOrEqual(100);
44
- });
45
- it("offline vs online no difference - order_number adopted", async () => {
46
- setItemSync("teinc_callout_seq", 0);
47
- const { orderNumber } = generateOrderNumber("05");
48
- // backend would adopt this; ensure format
49
- expect(orderNumber).toBe("05-" + orderNumber.split("-")[1] + "-" + orderNumber.split("-")[2]);
50
- expect(orderNumber.length).toBe(10);
51
- });
52
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { generateOrderNumber, isLocalOrderNumber, isOfflineOrderNumber, parseOrderNumber, resetCalloutForTests, } from "./order-number";
3
- import { removeItemSync, setItemSync } from "../adapters/kv";
4
- describe("order-number DD-RRR-CCC", () => {
5
- beforeEach(() => {
6
- removeItemSync("teinc_callout_seq");
7
- removeItemSync("teinc_last_random");
8
- resetCalloutForTests();
9
- });
10
- it("generates 10-char DD-RRR-CCC", () => {
11
- const { orderNumber, callout } = generateOrderNumber("10");
12
- expect(orderNumber).toMatch(/^[0-9]{2}-[0-9]{3}-[0-9]{3}$/);
13
- expect(orderNumber.length).toBe(10);
14
- expect(callout).toBe(1);
15
- expect(isLocalOrderNumber(orderNumber)).toBe(true);
16
- expect(isOfflineOrderNumber(orderNumber)).toBe(true);
17
- expect(parseOrderNumber(orderNumber)?.deviceNumber).toBe("10");
18
- });
19
- it("rejects invalid DD 00 and 0a", () => {
20
- expect(() => generateOrderNumber("00")).toThrow();
21
- expect(() => generateOrderNumber("0a")).toThrow();
22
- expect(() => generateOrderNumber("100")).toThrow();
23
- });
24
- it("RRR 100-999 and per-order random", () => {
25
- const a = generateOrderNumber("01");
26
- const b = generateOrderNumber("01");
27
- const ra = parseOrderNumber(a.orderNumber).random;
28
- const rb = parseOrderNumber(b.orderNumber).random;
29
- expect(Number(ra)).toBeGreaterThanOrEqual(100);
30
- expect(Number(ra)).toBeLessThanOrEqual(999);
31
- // random may collide once, but generally different
32
- // at least not all equal across 5
33
- const rs = Array.from({ length: 5 }, () => generateOrderNumber("01").random);
34
- // all in range
35
- for (const r of [a.random, b.random, ...rs])
36
- expect(r).toBeGreaterThanOrEqual(100);
37
- });
38
- it("CCC wraps 999->001 and new RRR", () => {
39
- setItemSync("teinc_callout_seq", 998);
40
- const a = generateOrderNumber("10");
41
- expect(a.callout).toBe(999);
42
- const b = generateOrderNumber("10");
43
- expect(b.callout).toBe(1);
44
- // both valid format
45
- expect(isLocalOrderNumber(b.orderNumber)).toBe(true);
46
- });
47
- it("isLocalOrderNumber false for legacy", () => {
48
- expect(isLocalOrderNumber("TFL-20260824-0001")).toBe(false);
49
- expect(isLocalOrderNumber("KF-260701-0042")).toBe(false);
50
- });
51
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,15 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { CAPABILITY_BY_COMMAND } from "../reference/capabilities";
3
- describe("perms guard", () => {
4
- it("maps order:create to orders_create (backend snake_case)", () => {
5
- expect(CAPABILITY_BY_COMMAND["order:create"]).toBe("orders_create");
6
- expect(CAPABILITY_BY_COMMAND["menu:update_item"]).toBe("menu_manage");
7
- });
8
- it("hasCapabilitySyncSync fail open when no cache", async () => {
9
- const { hasCapabilitySync } = await import("../reference/capabilities");
10
- // without DB, should return true (fail open)
11
- // we can't easily test without DB, just ensure it resolves
12
- const res = await hasCapabilitySync("biz-no-cache", "order:create");
13
- expect(res).toBe(true);
14
- });
15
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,17 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { formatPrice, formatPriceFromMinor, getCurrencySymbol, DEFAULT_CURRENCY } from "./currency";
3
- describe("currency", () => {
4
- it("returns symbol for known currencies", () => {
5
- expect(getCurrencySymbol("GHS")).toBe("GH₵");
6
- expect(getCurrencySymbol("USD")).toBe("$");
7
- expect(getCurrencySymbol("UNKNOWN")).toBe("UNKNOWN");
8
- });
9
- it("formats price with default currency", () => {
10
- expect(formatPrice(10.5)).toBe(`${getCurrencySymbol(DEFAULT_CURRENCY)}10.50`);
11
- });
12
- it("formats price from minor units", () => {
13
- expect(formatPriceFromMinor(1050, "GHS")).toBe("GH₵10.50");
14
- expect(formatPriceFromMinor(100, "USD")).toBe("$1.00");
15
- expect(formatPriceFromMinor(0)).toBe(`${getCurrencySymbol(DEFAULT_CURRENCY)}0.00`);
16
- });
17
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,29 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { tickHLC, compareHLCString, hlcToString, hlcFromString } from "./hlc";
3
- describe("HLC", () => {
4
- it("monotonic wall", () => {
5
- const a = tickHLC(1000, null, "01");
6
- const b = tickHLC(1001, a, "01");
7
- expect(compareHLCString(hlcToString(b), hlcToString(a))).toBeGreaterThan(0);
8
- });
9
- it("counter on same wall", () => {
10
- const a = tickHLC(1000, null, "01");
11
- const b = tickHLC(1000, a, "01");
12
- expect(b.counter).toBe(1);
13
- });
14
- it("skew bump", () => {
15
- const a = tickHLC(1000, null, "01");
16
- const b = tickHLC(999, a, "02");
17
- expect(b.wallMs).toBe(1000);
18
- expect(b.counter).toBe(1);
19
- });
20
- it("deviceId tie-break", () => {
21
- const a = { wallMs: 1000, counter: 0, deviceId: "01" };
22
- const b = { wallMs: 1000, counter: 0, deviceId: "02" };
23
- expect(compareHLCString(hlcToString(a), hlcToString(b))).toBeLessThan(0);
24
- });
25
- it("roundtrip", () => {
26
- const h = { wallMs: 1714560000000, counter: 2, deviceId: "10" };
27
- expect(hlcFromString(hlcToString(h))).toEqual(h);
28
- });
29
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,19 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { normalizePhoneForCountry, isLikelyE164, getDialCodeFromCountry } from "./phone";
3
- describe("phone", () => {
4
- it("normalizes GH numbers", () => {
5
- expect(normalizePhoneForCountry("0501234567", "GH")).toBe("+233501234567");
6
- expect(normalizePhoneForCountry("+233501234567", "GH")).toBe("+233501234567");
7
- expect(normalizePhoneForCountry(" ", "GH")).toBeNull();
8
- });
9
- it("validates E.164", () => {
10
- expect(isLikelyE164("+233501234567")).toBe(true);
11
- expect(isLikelyE164("0501234567")).toBe(false);
12
- expect(isLikelyE164("+1abc")).toBe(false);
13
- });
14
- it("resolves dial codes", () => {
15
- expect(getDialCodeFromCountry("GH")).toBe("+233");
16
- expect(getDialCodeFromCountry("US")).toBe("+1");
17
- expect(getDialCodeFromCountry("XX")).toBeNull();
18
- });
19
- });
@@ -1,13 +0,0 @@
1
- /**
2
- * Responsive helper — plain, no react-native.
3
- * Host apps with RN can override via adapter; core provides window-based fallback.
4
- */
5
- export interface ResponsiveState {
6
- width: number;
7
- height: number;
8
- isTablet: boolean;
9
- isLargeTablet: boolean;
10
- isLandscape: boolean;
11
- columns: (minItemWidth: number) => number;
12
- }
13
- export declare function useResponsive(): ResponsiveState;
@@ -1,18 +0,0 @@
1
- /**
2
- * Responsive helper — plain, no react-native.
3
- * Host apps with RN can override via adapter; core provides window-based fallback.
4
- */
5
- function getWindowSize() {
6
- if (typeof window !== "undefined") {
7
- return { width: window.innerWidth, height: window.innerHeight };
8
- }
9
- return { width: 1024, height: 768 };
10
- }
11
- export function useResponsive() {
12
- const { width, height } = getWindowSize();
13
- const isTablet = width >= 768;
14
- const isLandscape = width > height;
15
- const isLargeTablet = width >= 1024;
16
- const columns = (minItemWidth) => Math.max(1, Math.floor(width / minItemWidth));
17
- return { width, height, isTablet, isLargeTablet, isLandscape, columns };
18
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { generateUUID, generateCommandId, generateEventId } from "./uuid";
3
- describe("uuid", () => {
4
- it("generates valid v4 UUIDs", () => {
5
- const id = generateUUID();
6
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
7
- });
8
- it("generates prefixed ids", () => {
9
- expect(generateCommandId()).toMatch(/^cmd_/);
10
- expect(generateEventId()).toMatch(/^evt_/);
11
- expect(generateCommandId()).not.toBe(generateEventId());
12
- });
13
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,23 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { validateEmail, validatePhone, validatePassword, validatePromoCode } from "./validation";
3
- describe("validation", () => {
4
- it("validates email", () => {
5
- expect(validateEmail("a@b.com").valid).toBe(true);
6
- expect(validateEmail("bad").valid).toBe(false);
7
- expect(validateEmail("").errors).toContain("Email is required");
8
- });
9
- it("validates phone", () => {
10
- expect(validatePhone("+233501234567").valid).toBe(true);
11
- expect(validatePhone("123").valid).toBe(true); // minimal valid per regex
12
- expect(validatePhone("").valid).toBe(false);
13
- });
14
- it("validates password strength", () => {
15
- expect(validatePassword("StrongPass1").valid).toBe(true);
16
- expect(validatePassword("short").valid).toBe(false);
17
- expect(validatePassword("nouppercase1").valid).toBe(false);
18
- });
19
- it("validates promo code", () => {
20
- expect(validatePromoCode("SAVE10").valid).toBe(true);
21
- expect(validatePromoCode("ab").valid).toBe(false);
22
- });
23
- });