@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.
Files changed (198) hide show
  1. package/README.md +26 -21
  2. package/dist/adapters/auth.d.ts +10 -0
  3. package/dist/adapters/auth.js +43 -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 +99 -15
  16. package/dist/adapters/services.js +244 -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 +20 -0
  47. package/dist/hooks/image.hooks.js +62 -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 +19 -39
  55. package/dist/hooks/orders.hooks.js +126 -321
  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 +8 -0
  67. package/dist/image-cache/service.js +15 -3
  68. package/dist/index.d.ts +60 -5
  69. package/dist/index.js +78 -5
  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/events.service.d.ts +18 -0
  88. package/dist/realtime/events.service.js +76 -0
  89. package/dist/realtime/order-events.service.d.ts +6 -0
  90. package/dist/realtime/order-events.service.js +17 -0
  91. package/dist/realtime/phoenix-socket.d.ts +55 -0
  92. package/dist/realtime/phoenix-socket.js +269 -0
  93. package/dist/realtime/useEventsConnection.d.ts +1 -0
  94. package/dist/realtime/useEventsConnection.js +50 -0
  95. package/dist/reference/api.d.ts +2 -0
  96. package/dist/reference/api.js +2 -0
  97. package/dist/reference/capabilities.d.ts +12 -7
  98. package/dist/reference/capabilities.js +28 -13
  99. package/dist/reference/hooks.d.ts +34 -0
  100. package/dist/reference/hooks.js +124 -0
  101. package/dist/reference/menu-apply.d.ts +29 -0
  102. package/dist/reference/menu-apply.js +289 -0
  103. package/dist/reference/menu-record.d.ts +69 -0
  104. package/dist/reference/menu-record.js +163 -0
  105. package/dist/reference/operations.d.ts +3 -0
  106. package/dist/reference/operations.js +35 -1
  107. package/dist/reference/service.js +36 -9
  108. package/dist/reference/types.d.ts +1 -1
  109. package/dist/repositories/order.repository.d.ts +8 -0
  110. package/dist/repositories/order.repository.js +24 -2
  111. package/dist/repositories/pos.repository.d.ts +4 -0
  112. package/dist/repositories/pos.repository.js +4 -0
  113. package/dist/sync/backoff.d.ts +21 -0
  114. package/dist/sync/backoff.js +42 -0
  115. package/dist/sync/command-builder.js +10 -2
  116. package/dist/sync/command-queue.service.d.ts +12 -0
  117. package/dist/sync/command-queue.service.js +115 -18
  118. package/dist/sync/device-lease.service.d.ts +3 -3
  119. package/dist/sync/device-lease.service.js +80 -20
  120. package/dist/sync/engine.d.ts +3 -1
  121. package/dist/sync/engine.js +44 -4
  122. package/dist/sync/full-sync.d.ts +6 -0
  123. package/dist/sync/full-sync.js +72 -0
  124. package/dist/sync/health-probe.d.ts +24 -0
  125. package/dist/sync/health-probe.js +138 -0
  126. package/dist/sync/offline-capable.js +25 -0
  127. package/dist/sync/online-gates.d.ts +17 -0
  128. package/dist/sync/online-gates.js +55 -0
  129. package/dist/sync/order-number.d.ts +4 -4
  130. package/dist/sync/order-number.js +33 -13
  131. package/dist/sync/order-snapshots.service.d.ts +1 -0
  132. package/dist/sync/order-snapshots.service.js +47 -2
  133. package/dist/sync/pending-orders.service.d.ts +18 -0
  134. package/dist/sync/pending-orders.service.js +148 -3
  135. package/dist/sync/reconciliation/index.d.ts +5 -0
  136. package/dist/sync/reconciliation/index.js +5 -0
  137. package/dist/sync/snapshots/entity.d.ts +6 -0
  138. package/dist/sync/snapshots/entity.js +6 -0
  139. package/dist/sync/sync-jobs.service.d.ts +18 -0
  140. package/dist/sync/sync-jobs.service.js +539 -0
  141. package/dist/sync/transition-endpoints.d.ts +11 -0
  142. package/dist/sync/transition-endpoints.js +30 -0
  143. package/dist/sync/transport-local-node.js +3 -2
  144. package/dist/sync/transport-rest.d.ts +7 -0
  145. package/dist/sync/transport-rest.js +90 -25
  146. package/dist/sync/use-sync-reconciliation.js +58 -16
  147. package/dist/test-helpers/memory-driver.js +182 -4
  148. package/dist/test-helpers/seed-hours.d.ts +1 -0
  149. package/dist/test-helpers/seed-hours.js +23 -0
  150. package/dist/types/api/business.api.d.ts +42 -0
  151. package/dist/types/api/menu.api.d.ts +15 -0
  152. package/dist/types/api/seller.api.d.ts +20 -0
  153. package/dist/types/places.d.ts +19 -0
  154. package/dist/types/places.js +2 -0
  155. package/dist/types/rider.d.ts +67 -0
  156. package/dist/types/rider.js +2 -0
  157. package/dist/utils/attempt-stats.d.ts +23 -0
  158. package/dist/utils/attempt-stats.js +61 -0
  159. package/dist/utils/business-hours.d.ts +37 -0
  160. package/dist/utils/business-hours.js +96 -0
  161. package/dist/utils/debug-log.d.ts +23 -0
  162. package/dist/utils/debug-log.js +153 -0
  163. package/dist/utils/order-filter.d.ts +1 -1
  164. package/dist/utils/order-filter.js +1 -1
  165. package/dist/utils/order-transitions.d.ts +30 -0
  166. package/dist/utils/order-transitions.js +185 -0
  167. package/dist/utils/sync-logger.js +5 -4
  168. package/dist/validation/offline.d.ts +46 -0
  169. package/dist/validation/offline.js +79 -0
  170. package/package.json +6 -1
  171. package/dist/pricing/engine.test.d.ts +0 -1
  172. package/dist/pricing/engine.test.js +0 -142
  173. package/dist/reference/lww.test.d.ts +0 -1
  174. package/dist/reference/lww.test.js +0 -23
  175. package/dist/reference/operations.test.d.ts +0 -1
  176. package/dist/reference/operations.test.js +0 -66
  177. package/dist/standalone.integration.test.d.ts +0 -1
  178. package/dist/standalone.integration.test.js +0 -235
  179. package/dist/sync/device-lease.test.d.ts +0 -1
  180. package/dist/sync/device-lease.test.js +0 -38
  181. package/dist/sync/offline-integration.test.d.ts +0 -1
  182. package/dist/sync/offline-integration.test.js +0 -52
  183. package/dist/sync/order-number.test.d.ts +0 -1
  184. package/dist/sync/order-number.test.js +0 -51
  185. package/dist/sync/perms.test.d.ts +0 -1
  186. package/dist/sync/perms.test.js +0 -15
  187. package/dist/utils/currency.test.d.ts +0 -1
  188. package/dist/utils/currency.test.js +0 -17
  189. package/dist/utils/hlc.test.d.ts +0 -1
  190. package/dist/utils/hlc.test.js +0 -29
  191. package/dist/utils/phone.test.d.ts +0 -1
  192. package/dist/utils/phone.test.js +0 -19
  193. package/dist/utils/responsive.d.ts +0 -13
  194. package/dist/utils/responsive.js +0 -18
  195. package/dist/utils/uuid.test.d.ts +0 -1
  196. package/dist/utils/uuid.test.js +0 -13
  197. package/dist/utils/validation.test.d.ts +0 -1
  198. package/dist/utils/validation.test.js +0 -23
@@ -108,6 +108,7 @@ export interface SellerOrder {
108
108
  user: OrderUser | null;
109
109
  items: OrderItem[];
110
110
  inserted_at: string;
111
+ updated_at?: string;
111
112
  rider?: {
112
113
  id: string;
113
114
  name: string | null;
@@ -170,3 +171,22 @@ export interface BusinessDelivery {
170
171
  rider: DeliveryRider | null;
171
172
  }
172
173
  export type BusinessDeliveriesResponse = PaginatedResponse<BusinessDelivery>;
174
+ export interface SellerDashboardMetrics {
175
+ today_orders: number;
176
+ today_revenue_minor: number;
177
+ pending_orders: number;
178
+ average_rating: number;
179
+ active_orders: number;
180
+ }
181
+ export interface SellerDashboardResponse {
182
+ data: {
183
+ metrics: SellerDashboardMetrics;
184
+ recent_orders: Array<{
185
+ id: string;
186
+ order_number: string;
187
+ status: string;
188
+ total_minor: number;
189
+ item_count: number;
190
+ }>;
191
+ };
192
+ }
@@ -0,0 +1,19 @@
1
+ /** Place autocomplete/detail types — backend places payloads. */
2
+ export interface PlaceSuggestion {
3
+ placeId: string;
4
+ description: string;
5
+ mainText: string;
6
+ secondaryText: string;
7
+ }
8
+ export interface PlaceDetails {
9
+ placeId: string;
10
+ formattedAddress: string;
11
+ street: string;
12
+ city: string;
13
+ region: string;
14
+ postalCode: string;
15
+ country: string;
16
+ countryCode?: string;
17
+ latitude: number;
18
+ longitude: number;
19
+ }
@@ -0,0 +1,2 @@
1
+ /** Place autocomplete/detail types — backend places payloads. */
2
+ export {};
@@ -0,0 +1,67 @@
1
+ export interface RiderMetrics {
2
+ today_earnings_minor: number;
3
+ today_deliveries: number;
4
+ available_deliveries: number;
5
+ total_deliveries: number;
6
+ status: string;
7
+ }
8
+ export interface RiderActiveDelivery {
9
+ id: string;
10
+ orderId: string;
11
+ pickup: string;
12
+ dropoff: string;
13
+ status: 'assigned' | 'picked_up' | 'delivering' | 'delivered';
14
+ estimatedTime: string;
15
+ }
16
+ export interface RiderEarnings {
17
+ today: number;
18
+ thisWeek: number;
19
+ thisMonth: number;
20
+ total: number;
21
+ pendingPayout: number;
22
+ currency: string;
23
+ }
24
+ export interface EarningsBreakdown {
25
+ baseDeliveryFees: number;
26
+ tipsReceived: number;
27
+ bonuses: number;
28
+ totalThisWeek: number;
29
+ currency: string;
30
+ }
31
+ export interface RiderTransaction {
32
+ id: string;
33
+ orderId: string;
34
+ amount: number;
35
+ date: string;
36
+ type: 'delivery' | 'bonus' | 'tip';
37
+ currency: string;
38
+ }
39
+ export interface RiderStatistics {
40
+ totalDeliveries: number;
41
+ acceptanceRate: number;
42
+ averageRating: number;
43
+ totalEarnings: number;
44
+ currency: string;
45
+ }
46
+ export interface DeliveryZone {
47
+ id: string;
48
+ name: string;
49
+ enabled: boolean;
50
+ radius: string;
51
+ deliveries: number;
52
+ latitude: number;
53
+ longitude: number;
54
+ }
55
+ export interface NavigationSettings {
56
+ selectedApp: 'google_maps' | 'apple_maps' | 'waze';
57
+ autoStart: boolean;
58
+ avoidTolls: boolean;
59
+ avoidHighways: boolean;
60
+ }
61
+ export interface NavigationApp {
62
+ id: string;
63
+ name: string;
64
+ description: string;
65
+ icon: string;
66
+ installed: boolean;
67
+ }
@@ -0,0 +1,2 @@
1
+ // Rider-related models
2
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Attempt counters — monitor-first instrumentation for overload diagnosis.
3
+ *
4
+ * Every retry/poll/reconnect path records one counter tick with its caller
5
+ * tag. Diagnostics screens render per-subsystem totals + per-minute rates so
6
+ * a hot loop (e.g. discovery sweep, WS reconnect, drain storm) is visible
7
+ * without guessing. Pure in-memory; no timers, no network, no behavior.
8
+ */
9
+ export type AttemptSubsystem = "ws_connect" | "ws_reconnect" | "ws_heartbeat" | "lan_discover" | "lan_dial" | "sweep_tick" | "ttl_reap" | "sync_drain" | "rest_attempt" | "probe" | "refetch";
10
+ export interface SubsystemStat {
11
+ subsystem: AttemptSubsystem;
12
+ total: number;
13
+ /** Attempts in the trailing 60s window. */
14
+ perMin: number;
15
+ lastAt: string | null;
16
+ lastDetail: string | null;
17
+ }
18
+ /** Record one attempt. Detail is a short caller/param tag, e.g. "delay=4000" or "GET /orders". */
19
+ export declare function recordAttempt(subsystem: AttemptSubsystem, detail?: string): void;
20
+ /** Snapshot for Diagnostics screens. Never throws. */
21
+ export declare function getAttemptStats(): SubsystemStat[];
22
+ /** Test-only reset. */
23
+ export declare function resetAttemptStats(): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Attempt counters — monitor-first instrumentation for overload diagnosis.
3
+ *
4
+ * Every retry/poll/reconnect path records one counter tick with its caller
5
+ * tag. Diagnostics screens render per-subsystem totals + per-minute rates so
6
+ * a hot loop (e.g. discovery sweep, WS reconnect, drain storm) is visible
7
+ * without guessing. Pure in-memory; no timers, no network, no behavior.
8
+ */
9
+ const WINDOW_MS = 60000;
10
+ const counters = new Map();
11
+ function counterFor(s) {
12
+ let c = counters.get(s);
13
+ if (!c) {
14
+ c = { total: 0, stamps: [], lastAt: null, lastDetail: null };
15
+ counters.set(s, c);
16
+ }
17
+ return c;
18
+ }
19
+ /** Record one attempt. Detail is a short caller/param tag, e.g. "delay=4000" or "GET /orders". */
20
+ export function recordAttempt(subsystem, detail) {
21
+ try {
22
+ const c = counterFor(subsystem);
23
+ const now = Date.now();
24
+ c.total += 1;
25
+ c.stamps.push(now);
26
+ // Prune outside the trailing window (amortized — stamps only grow via ticks).
27
+ while (c.stamps.length > 0 && now - c.stamps[0] > WINDOW_MS)
28
+ c.stamps.shift();
29
+ c.lastAt = new Date(now).toISOString();
30
+ if (detail !== undefined)
31
+ c.lastDetail = detail;
32
+ }
33
+ catch { }
34
+ }
35
+ /** Snapshot for Diagnostics screens. Never throws. */
36
+ export function getAttemptStats() {
37
+ try {
38
+ const now = Date.now();
39
+ const out = [];
40
+ counters.forEach((c, subsystem) => {
41
+ while (c.stamps.length > 0 && now - c.stamps[0] > WINDOW_MS)
42
+ c.stamps.shift();
43
+ out.push({
44
+ subsystem,
45
+ total: c.total,
46
+ perMin: c.stamps.length,
47
+ lastAt: c.lastAt,
48
+ lastDetail: c.lastDetail,
49
+ });
50
+ });
51
+ out.sort((a, b) => b.perMin - a.perMin || b.total - a.total);
52
+ return out;
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ /** Test-only reset. */
59
+ export function resetAttemptStats() {
60
+ counters.clear();
61
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Business open/closed evaluation — single source for the lib.
3
+ *
4
+ * Mirrors backend `TeincFoodBackend.Businesses.is_open?/1` exactly:
5
+ * - `manually_closed` → closed
6
+ * - else UTC clock: day = Elixir `Date.day_of_week - 1` (Monday=0 … Sunday=6),
7
+ * a row matches when `day_of_week` equals today, `is_closed` is false and
8
+ * `open_time <= now < close_time` (open inclusive, close exclusive).
9
+ * - no matching row → closed.
10
+ *
11
+ * Reads from SQLite `reference_data` (`operating-hours` + `business:profile`),
12
+ * so it works fully offline. Known parity limitation (copied from backend, not
13
+ * fixed here): overnight ranges (close <= open) never match.
14
+ */
15
+ export type BusinessClosedReason = "manually_closed" | "outside_hours" | "no_hours_configured";
16
+ export interface BusinessOpenState {
17
+ open: boolean;
18
+ reason?: BusinessClosedReason;
19
+ /** 0=Monday … 6=Sunday (backend day_of_week convention) */
20
+ dayOfWeek: number;
21
+ /** HH:MM:SS UTC used for the evaluation */
22
+ timeUtc: string;
23
+ evaluatedAt: string;
24
+ }
25
+ export declare class BusinessHoursError extends Error {
26
+ code: "BUSINESS_CLOSED" | "HOURS_NOT_SYNCED";
27
+ reason?: BusinessClosedReason;
28
+ constructor(code: "BUSINESS_CLOSED" | "HOURS_NOT_SYNCED", message: string, reason?: BusinessClosedReason);
29
+ }
30
+ export declare function isBusinessOpenNow(businessId: string, now?: Date): Promise<BusinessOpenState>;
31
+ /**
32
+ * Guard for order creation — fail-closed. Throws `BusinessHoursError` with
33
+ * code `HOURS_NOT_SYNCED` when no hours are cached (configure in Settings,
34
+ * works offline) or `BUSINESS_CLOSED` when the business is closed.
35
+ * Backend remains the final gate for clock-skew edge cases.
36
+ */
37
+ export declare function assertBusinessOpenForCreate(businessId: string, now?: Date): Promise<BusinessOpenState>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Business open/closed evaluation — single source for the lib.
3
+ *
4
+ * Mirrors backend `TeincFoodBackend.Businesses.is_open?/1` exactly:
5
+ * - `manually_closed` → closed
6
+ * - else UTC clock: day = Elixir `Date.day_of_week - 1` (Monday=0 … Sunday=6),
7
+ * a row matches when `day_of_week` equals today, `is_closed` is false and
8
+ * `open_time <= now < close_time` (open inclusive, close exclusive).
9
+ * - no matching row → closed.
10
+ *
11
+ * Reads from SQLite `reference_data` (`operating-hours` + `business:profile`),
12
+ * so it works fully offline. Known parity limitation (copied from backend, not
13
+ * fixed here): overnight ranges (close <= open) never match.
14
+ */
15
+ import { openSyncDatabase } from "../db/connection";
16
+ import { getReferenceData } from "../reference/operations";
17
+ export class BusinessHoursError extends Error {
18
+ constructor(code, message, reason) {
19
+ super(message);
20
+ this.name = "BusinessHoursError";
21
+ this.code = code;
22
+ this.reason = reason;
23
+ }
24
+ }
25
+ function pad2(n) {
26
+ return n < 10 ? `0${n}` : String(n);
27
+ }
28
+ function toTimeString(v) {
29
+ if (!v || typeof v !== "string")
30
+ return null;
31
+ // Backend Time serializes "HH:MM:SS"; settings screen writes "HH:MM:00".
32
+ return v.slice(0, 8);
33
+ }
34
+ function normalizeEntries(payload) {
35
+ if (!payload)
36
+ return [];
37
+ if (Array.isArray(payload))
38
+ return payload;
39
+ const data = payload.data;
40
+ if (Array.isArray(data))
41
+ return data;
42
+ return [];
43
+ }
44
+ export async function isBusinessOpenNow(businessId, now = new Date()) {
45
+ const evaluatedAt = new Date().toISOString();
46
+ // Backend: Date.day_of_week (Mon=1..Sun=7) minus 1 → Mon=0..Sun=6 (UTC).
47
+ const dayOfWeek = (now.getUTCDay() + 6) % 7;
48
+ const timeUtc = `${pad2(now.getUTCHours())}:${pad2(now.getUTCMinutes())}:${pad2(now.getUTCSeconds())}`;
49
+ const db = await openSyncDatabase();
50
+ const [hoursRec, profileRec] = await Promise.all([
51
+ getReferenceData(db, businessId, "operating-hours"),
52
+ getReferenceData(db, businessId, "business:profile"),
53
+ ]);
54
+ const profile = (profileRec?.payload ??
55
+ null);
56
+ const profileData = profile && typeof profile === "object" && "data" in profile && profile.data !== undefined
57
+ ? profile.data
58
+ : (profile ?? {});
59
+ if (profileData.manually_closed === true) {
60
+ return { open: false, reason: "manually_closed", dayOfWeek, timeUtc, evaluatedAt };
61
+ }
62
+ const entries = normalizeEntries(hoursRec?.payload);
63
+ if (entries.length === 0) {
64
+ return { open: false, reason: "no_hours_configured", dayOfWeek, timeUtc, evaluatedAt };
65
+ }
66
+ const match = entries.some((e) => {
67
+ if (e.day_of_week !== dayOfWeek || e.is_closed)
68
+ return false;
69
+ const open = toTimeString(e.open_time);
70
+ const close = toTimeString(e.close_time);
71
+ if (!open || !close)
72
+ return false;
73
+ return open <= timeUtc && timeUtc < close;
74
+ });
75
+ return match
76
+ ? { open: true, dayOfWeek, timeUtc, evaluatedAt }
77
+ : { open: false, reason: "outside_hours", dayOfWeek, timeUtc, evaluatedAt };
78
+ }
79
+ /**
80
+ * Guard for order creation — fail-closed. Throws `BusinessHoursError` with
81
+ * code `HOURS_NOT_SYNCED` when no hours are cached (configure in Settings,
82
+ * works offline) or `BUSINESS_CLOSED` when the business is closed.
83
+ * Backend remains the final gate for clock-skew edge cases.
84
+ */
85
+ export async function assertBusinessOpenForCreate(businessId, now = new Date()) {
86
+ const state = await isBusinessOpenNow(businessId, now);
87
+ if (state.reason === "no_hours_configured") {
88
+ throw new BusinessHoursError("HOURS_NOT_SYNCED", "Operating hours are not available offline — configure them in Settings (works offline) or sync once before taking orders", state.reason);
89
+ }
90
+ if (!state.open) {
91
+ throw new BusinessHoursError("BUSINESS_CLOSED", state.reason === "manually_closed"
92
+ ? "Business is manually closed — order creation is blocked"
93
+ : "Business is currently closed — order creation is blocked", state.reason);
94
+ }
95
+ return state;
96
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared debug-log ring buffer — single implementation for Business (Expo)
3
+ * and Desktop (Tauri), replacing the per-app copies.
4
+ *
5
+ * Captures (a) console.* output and (b) core syncLogger output into one
6
+ * observable store that Diagnostics screens render with level tabs, search,
7
+ * copy/share and clear. Install once at app bootstrap via
8
+ * initDebugLogCapture(); safe to call repeatedly.
9
+ */
10
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "log";
11
+ export interface LogEntry {
12
+ id: number;
13
+ ts: string;
14
+ level: LogLevel;
15
+ args: unknown[];
16
+ text: string;
17
+ }
18
+ /** Newest-first snapshot for list rendering. */
19
+ export declare function getDebugLogs(): LogEntry[];
20
+ export declare function clearDebugLogs(): void;
21
+ export declare function subscribeDebugLogs(fn: () => void): () => void;
22
+ export declare function pushDebugLog(level: LogLevel, text: string, data?: unknown): void;
23
+ export declare function initDebugLogCapture(): void;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared debug-log ring buffer — single implementation for Business (Expo)
3
+ * and Desktop (Tauri), replacing the per-app copies.
4
+ *
5
+ * Captures (a) console.* output and (b) core syncLogger output into one
6
+ * observable store that Diagnostics screens render with level tabs, search,
7
+ * copy/share and clear. Install once at app bootstrap via
8
+ * initDebugLogCapture(); safe to call repeatedly.
9
+ */
10
+ import { syncLogger } from "./sync-logger";
11
+ const MAX_ENTRIES = 600;
12
+ let entries = [];
13
+ let seq = 0;
14
+ const listeners = new Set();
15
+ let installed = false;
16
+ function fmt(args) {
17
+ return args
18
+ .map((a) => {
19
+ if (typeof a === "string")
20
+ return a;
21
+ try {
22
+ return JSON.stringify(a, null, 2);
23
+ }
24
+ catch {
25
+ return String(a);
26
+ }
27
+ })
28
+ .join(" ");
29
+ }
30
+ let notifyQueued = false;
31
+ function notifyListeners() {
32
+ // Deferred: console.* fires inside render bodies too (and React forbids
33
+ // sync setState of another component mid-render). Microtask fan-out keeps
34
+ // log screens live without ever triggering setState-in-render.
35
+ if (notifyQueued)
36
+ return;
37
+ notifyQueued = true;
38
+ queueMicrotask(() => {
39
+ notifyQueued = false;
40
+ listeners.forEach((fn) => {
41
+ try {
42
+ fn();
43
+ }
44
+ catch { }
45
+ });
46
+ });
47
+ }
48
+ function push(level, args) {
49
+ const e = { id: ++seq, ts: new Date().toISOString(), level, args, text: fmt(args) };
50
+ entries.push(e);
51
+ if (entries.length > MAX_ENTRIES)
52
+ entries = entries.slice(-MAX_ENTRIES);
53
+ notifyListeners();
54
+ }
55
+ /** Newest-first snapshot for list rendering. */
56
+ export function getDebugLogs() {
57
+ return entries.slice().reverse();
58
+ }
59
+ export function clearDebugLogs() {
60
+ entries = [];
61
+ notifyListeners();
62
+ }
63
+ export function subscribeDebugLogs(fn) {
64
+ listeners.add(fn);
65
+ return () => {
66
+ listeners.delete(fn);
67
+ };
68
+ }
69
+ export function pushDebugLog(level, text, data) {
70
+ push(level, data !== undefined ? [text, data] : [text]);
71
+ }
72
+ const SYNC_WRAPPED = "__teincDebugLogWrapped";
73
+ export function initDebugLogCapture() {
74
+ if (installed)
75
+ return;
76
+ installed = true;
77
+ // Force core SyncEngine logger on even in production builds — otherwise
78
+ // strategy/timeout/queue decisions stay silent in the field.
79
+ try {
80
+ syncLogger.setEnabled?.(true);
81
+ }
82
+ catch { }
83
+ // Mirror syncLogger output into the buffer (wrap once).
84
+ try {
85
+ const rec = syncLogger;
86
+ if (!rec[SYNC_WRAPPED]) {
87
+ rec[SYNC_WRAPPED] = true;
88
+ ["debug", "info", "warn", "error"].forEach((m) => {
89
+ const orig = rec[m]?.bind(syncLogger);
90
+ if (!orig)
91
+ return;
92
+ rec[m] = (...a) => {
93
+ try {
94
+ push(m, a);
95
+ }
96
+ catch { }
97
+ orig(...a);
98
+ };
99
+ });
100
+ }
101
+ }
102
+ catch { }
103
+ // Mirror console.* output (guarded: React Native has no window).
104
+ try {
105
+ const c = console;
106
+ const orig = {
107
+ debug: console.debug.bind(console),
108
+ info: console.info.bind(console),
109
+ warn: console.warn.bind(console),
110
+ error: console.error.bind(console),
111
+ log: console.log.bind(console),
112
+ };
113
+ console.debug = (...a) => {
114
+ push("debug", a);
115
+ orig.debug(...a);
116
+ };
117
+ console.info = (...a) => {
118
+ push("info", a);
119
+ orig.info(...a);
120
+ };
121
+ console.warn = (...a) => {
122
+ push("warn", a);
123
+ orig.warn(...a);
124
+ };
125
+ console.error = (...a) => {
126
+ push("error", a);
127
+ orig.error(...a);
128
+ };
129
+ console.log = (...a) => {
130
+ push("log", a);
131
+ orig.log(...a);
132
+ };
133
+ void c;
134
+ }
135
+ catch { }
136
+ try {
137
+ const g = globalThis;
138
+ if (typeof g.addEventListener === "function") {
139
+ g.addEventListener("error", (e) => {
140
+ const err = e;
141
+ push("error", [err?.message, err?.error]);
142
+ });
143
+ g.addEventListener("unhandledrejection", (e) => {
144
+ push("error", ["unhandledrejection", e?.reason]);
145
+ });
146
+ }
147
+ }
148
+ catch { }
149
+ try {
150
+ console.debug("[debug-log] capture installed");
151
+ }
152
+ catch { }
153
+ }
@@ -22,6 +22,6 @@ export declare function paginateOffline<T>(orders: T[], page: number, pageSize?:
22
22
  page: number;
23
23
  page_size: number;
24
24
  total_pages: number;
25
- total_count: number;
25
+ total_entries: number;
26
26
  };
27
27
  };
@@ -45,5 +45,5 @@ export function paginateOffline(orders, page, pageSize = 20) {
45
45
  const start = (page - 1) * pageSize;
46
46
  const slice = orders.slice(start, start + pageSize);
47
47
  const totalPages = Math.max(1, Math.ceil(orders.length / pageSize));
48
- return { data: slice, pagination: { page, page_size: pageSize, total_pages: totalPages, total_count: orders.length } };
48
+ return { data: slice, pagination: { page, page_size: pageSize, total_pages: totalPages, total_entries: orders.length } };
49
49
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared order transition business logic — single source.
3
+ *
4
+ * All TeincFood business apps (Business + Desktop) must use this module for
5
+ * deciding valid transitions, next action, cancellability, payment display and
6
+ * walk-in vs delivery branching. No per-app fork.
7
+ *
8
+ * UI markup stays per-app; this module is pure (no React/Tamagui/sonner).
9
+ */
10
+ import type { SellerOrder } from "../types/api/seller.api";
11
+ export declare const ORDER_STATUSES: readonly ["pending", "awaiting_payment", "accepted", "preparing", "ready", "waiting_pickup", "on_the_way", "arrived", "delivered", "cancelled"];
12
+ export type OrderStatus = (typeof ORDER_STATUSES)[number];
13
+ export declare const CANCELLABLE_STATUSES: readonly ["pending", "awaiting_payment", "accepted", "preparing", "ready"];
14
+ export declare const PIPELINE: readonly ["pending", "accepted", "preparing", "ready", "delivered"];
15
+ export type OrderAction = "accept" | "reject" | "start_preparing" | "mark_ready" | "complete_pickup" | "cancel" | "assign_rider" | "confirm_handover" | "confirm_cash" | "pay";
16
+ export declare function isUnpaid(order: SellerOrder): boolean;
17
+ export declare function isCancellable(status: string): boolean;
18
+ export declare function cancelLabelFor(status: string): string;
19
+ export declare function getCancelAction(status: string): "reject" | "cancel";
20
+ export declare function isQueuedResult(result: unknown): boolean;
21
+ export declare function isDeliveryOrder(order: Pick<SellerOrder, "order_type" | "delivery_id" | "is_pos">): boolean;
22
+ export declare function isWalkInPickup(order: Pick<SellerOrder, "order_type" | "is_pos">): boolean;
23
+ export declare function nextActionFor(order: SellerOrder): {
24
+ label: string;
25
+ key: OrderAction;
26
+ } | null;
27
+ export declare function canTransition(order: SellerOrder, action: OrderAction): boolean;
28
+ export declare function getAllowedActions(order: SellerOrder): OrderAction[];
29
+ export declare const QUEUED_STATUS_BY_COMMAND: Record<string, string>;
30
+ export declare function getQueuedStatus(commandType: string): string | null;