@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
@@ -0,0 +1,185 @@
1
+ export const ORDER_STATUSES = [
2
+ "pending",
3
+ "awaiting_payment",
4
+ "accepted",
5
+ "preparing",
6
+ "ready",
7
+ "waiting_pickup",
8
+ "on_the_way",
9
+ "arrived",
10
+ "delivered",
11
+ "cancelled",
12
+ ];
13
+ // 5 includes ready (buyer no-show). Locked per ans 1.
14
+ export const CANCELLABLE_STATUSES = [
15
+ "pending",
16
+ "awaiting_payment",
17
+ "accepted",
18
+ "preparing",
19
+ "ready",
20
+ ];
21
+ export const PIPELINE = ["pending", "accepted", "preparing", "ready", "delivered"];
22
+ // ── Helpers ───────────────────────────────────────────────────────────────
23
+ export function isUnpaid(order) {
24
+ const o = order;
25
+ return !o.is_paid && !o.paid_at && !o.payment?.is_paid;
26
+ }
27
+ export function isCancellable(status) {
28
+ return CANCELLABLE_STATUSES.includes(status);
29
+ }
30
+ export function cancelLabelFor(status) {
31
+ if (status === "pending")
32
+ return "Reject";
33
+ return "Cancel";
34
+ }
35
+ export function getCancelAction(status) {
36
+ return status === "pending" ? "reject" : "cancel";
37
+ }
38
+ export function isQueuedResult(result) {
39
+ const r = result;
40
+ if (!r)
41
+ return false;
42
+ return r.status === "pending" && !!r.command_id && !r.id;
43
+ }
44
+ export function isDeliveryOrder(order) {
45
+ const o = order;
46
+ if (o.order_type === "delivery")
47
+ return true;
48
+ return !!o.delivery_id;
49
+ }
50
+ export function isWalkInPickup(order) {
51
+ return !isDeliveryOrder(order);
52
+ }
53
+ // ── State machine ───────────────────────────────────────────────────────
54
+ const STATUS_TO_ACTION = {
55
+ pending: "accept",
56
+ accepted: "start_preparing",
57
+ preparing: "mark_ready",
58
+ };
59
+ export function nextActionFor(order) {
60
+ const status = order.status ?? "";
61
+ // awaiting_payment must pay first — no pipeline progression
62
+ if (status === "awaiting_payment")
63
+ return null;
64
+ if (status === "pending") {
65
+ const isPos = !!order.is_pos;
66
+ if (isPos)
67
+ return { label: "Start preparing", key: "start_preparing" };
68
+ return { label: "Accept", key: "accept" };
69
+ }
70
+ if (status === "accepted")
71
+ return { label: "Start preparing", key: "start_preparing" };
72
+ if (status === "preparing")
73
+ return { label: "Mark ready", key: "mark_ready" };
74
+ if (status === "ready") {
75
+ const orderType = order.order_type ?? null;
76
+ // dine_in walk-in → Complete, else Pickup (takeout or delivery handover gate handled via getAllowedActions)
77
+ const label = orderType === "dine_in" ? "Complete" : "Picked up";
78
+ return { label, key: "complete_pickup" };
79
+ }
80
+ return null;
81
+ }
82
+ export function canTransition(order, action) {
83
+ const status = order.status ?? "";
84
+ if (status === "cancelled" || status === "delivered")
85
+ return false;
86
+ // awaiting_payment: only pay/confirm_cash/cancel/reject
87
+ if (status === "awaiting_payment") {
88
+ return action === "pay" || action === "confirm_cash" || action === "cancel" || action === "reject";
89
+ }
90
+ switch (action) {
91
+ case "accept":
92
+ return status === "pending" && !order.is_pos;
93
+ case "reject":
94
+ return status === "pending";
95
+ case "start_preparing":
96
+ return status === "accepted" || (status === "pending" && !!order.is_pos);
97
+ case "mark_ready":
98
+ return status === "preparing";
99
+ case "complete_pickup":
100
+ // walk-in only — delivery uses handover gates
101
+ return status === "ready" && isWalkInPickup(order);
102
+ case "cancel":
103
+ return isCancellable(status);
104
+ case "assign_rider":
105
+ return status === "ready" && isDeliveryOrder(order) && !order.delivery_id;
106
+ case "confirm_handover":
107
+ return (status === "ready" || status === "waiting_pickup") && isDeliveryOrder(order) && !!order.delivery_id;
108
+ case "confirm_cash":
109
+ case "pay":
110
+ return isUnpaid(order) && status !== "cancelled" && status !== "delivered";
111
+ default:
112
+ return false;
113
+ }
114
+ }
115
+ export function getAllowedActions(order) {
116
+ const status = order.status ?? "";
117
+ if (status === "cancelled" || status === "delivered" || status === "arrived")
118
+ return [];
119
+ if (status === "awaiting_payment") {
120
+ const actions = ["pay", "confirm_cash"];
121
+ if (isCancellable(status))
122
+ actions.push("cancel");
123
+ return actions;
124
+ }
125
+ if (status === "pending") {
126
+ const isPos = !!order.is_pos;
127
+ const actions = isPos ? [] : ["accept", "reject"];
128
+ if (isPos)
129
+ actions.push("start_preparing");
130
+ if (isCancellable(status)) {
131
+ // reject already covers pending; cancel alias not needed
132
+ }
133
+ // Pending can also start preparing directly on some flows — expose via canTransition but primary is accept/reject
134
+ return actions;
135
+ }
136
+ if (status === "accepted") {
137
+ const actions = ["start_preparing"];
138
+ if (isCancellable(status))
139
+ actions.push("cancel");
140
+ return actions;
141
+ }
142
+ if (status === "preparing") {
143
+ const actions = ["mark_ready"];
144
+ if (isCancellable(status))
145
+ actions.push("cancel");
146
+ return actions;
147
+ }
148
+ if (status === "ready") {
149
+ if (isDeliveryOrder(order)) {
150
+ const hasDelivery = !!order.delivery_id;
151
+ if (!hasDelivery)
152
+ return ["assign_rider", ...(isCancellable(status) ? ["cancel"] : [])];
153
+ return ["confirm_handover", ...(isCancellable(status) ? ["cancel"] : [])];
154
+ }
155
+ // walk-in
156
+ const actions = ["complete_pickup"];
157
+ if (isCancellable(status))
158
+ actions.push("cancel");
159
+ if (isUnpaid(order)) {
160
+ // cash collect also allowed on ready walk-in
161
+ actions.push("confirm_cash");
162
+ }
163
+ return actions;
164
+ }
165
+ if (status === "waiting_pickup" || status === "on_the_way") {
166
+ if (isDeliveryOrder(order))
167
+ return ["confirm_handover"];
168
+ return [];
169
+ }
170
+ return [];
171
+ }
172
+ // Re-export queued status map for callers that need it without importing sync internals
173
+ export const QUEUED_STATUS_BY_COMMAND = {
174
+ "order:accept": "accepted",
175
+ "order:reject": "cancelled",
176
+ "order:start_preparing": "preparing",
177
+ "order:mark_ready": "ready",
178
+ "order:complete_pickup": "delivered",
179
+ "order:cancel": "cancelled",
180
+ "delivery:assign": "assigned",
181
+ "delivery:confirm_handover": "delivered",
182
+ };
183
+ export function getQueuedStatus(commandType) {
184
+ return QUEUED_STATUS_BY_COMMAND[commandType] ?? null;
185
+ }
@@ -7,18 +7,19 @@
7
7
  function emit(entry) {
8
8
  const { level, namespace, message, data } = entry;
9
9
  const tag = `[${namespace}] ${message}`;
10
+ const hasData = data !== undefined && data !== null && Object.keys(data).length > 0;
10
11
  switch (level) {
11
12
  case "debug":
12
- console.debug(tag, data);
13
+ hasData ? console.debug(tag, data) : console.debug(tag);
13
14
  break;
14
15
  case "info":
15
- console.info(tag, data);
16
+ hasData ? console.info(tag, data) : console.info(tag);
16
17
  break;
17
18
  case "warn":
18
- console.warn(tag, data);
19
+ hasData ? console.warn(tag, data) : console.warn(tag);
19
20
  break;
20
21
  case "error":
21
- console.error(tag, data);
22
+ hasData ? console.error(tag, data) : console.error(tag);
22
23
  break;
23
24
  }
24
25
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Offline validation mirror — frontend accepts exactly what the backend
3
+ * accepts, so a locally-committed write is never rejected on sync for a
4
+ * reason we could have caught. Each rule cites its backend changeset.
5
+ *
6
+ * Throws ValidationError (with `details` map for per-field form errors) —
7
+ * never returns. Call before every local commit.
8
+ */
9
+ export declare class ValidationError extends Error {
10
+ details: Record<string, string>;
11
+ constructor(message: string, details?: Record<string, string>);
12
+ }
13
+ /** Mirrors MenuItem.changeset: required name/base_price_minor/category_id, price ≥ 0, category UUID. */
14
+ export declare function validateMenuItemInput(input: {
15
+ name?: unknown;
16
+ price_minor?: unknown;
17
+ priceMinor?: unknown;
18
+ category_id?: unknown;
19
+ categoryId?: unknown;
20
+ variants?: Array<{
21
+ name?: unknown;
22
+ }>;
23
+ extras?: Array<{
24
+ name?: unknown;
25
+ }>;
26
+ }): void;
27
+ /** Mirrors MenuCategory.changeset: required name. */
28
+ export declare function validateMenuCategoryInput(input: {
29
+ name?: unknown;
30
+ }): void;
31
+ /** Mirrors Order.changeset: required payment_mode/total/currency, amounts ≥ 0. */
32
+ export declare function validateOrderCreateInput(input: {
33
+ payment_mode?: unknown;
34
+ pos_payment_method?: unknown;
35
+ total_minor?: unknown;
36
+ currency_code?: unknown;
37
+ currency?: unknown;
38
+ items?: unknown;
39
+ order_type?: unknown;
40
+ delivery_address?: unknown;
41
+ }): void;
42
+ /** Mirrors BusinessMemberInvitation changeset: email format. */
43
+ export declare function validateInviteInput(input: {
44
+ email?: unknown;
45
+ invited_email?: unknown;
46
+ }): void;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Offline validation mirror — frontend accepts exactly what the backend
3
+ * accepts, so a locally-committed write is never rejected on sync for a
4
+ * reason we could have caught. Each rule cites its backend changeset.
5
+ *
6
+ * Throws ValidationError (with `details` map for per-field form errors) —
7
+ * never returns. Call before every local commit.
8
+ */
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const EMAIL_RE = /^[^\s]+@[^\s]+\.[^\s]+$/;
11
+ export class ValidationError extends Error {
12
+ constructor(message, details = {}) {
13
+ super(message);
14
+ this.name = "ValidationError";
15
+ this.details = details;
16
+ }
17
+ }
18
+ function fail(field, message, details) {
19
+ details[field] = message;
20
+ throw new ValidationError(message, details);
21
+ }
22
+ /** Mirrors MenuItem.changeset: required name/base_price_minor/category_id, price ≥ 0, category UUID. */
23
+ export function validateMenuItemInput(input) {
24
+ const details = {};
25
+ const name = input.name;
26
+ if (typeof name !== "string" || !name.trim())
27
+ fail("name", "Name is required", details);
28
+ const minor = input.price_minor ?? input.priceMinor;
29
+ if (typeof minor !== "number" || !Number.isFinite(minor))
30
+ fail("price_minor", "Price is required", details);
31
+ if (minor < 0)
32
+ fail("price_minor", "Price must be 0 or more", details);
33
+ const categoryId = input.category_id ?? input.categoryId;
34
+ if (typeof categoryId !== "string" || !categoryId)
35
+ fail("category_id", "Category is required", details);
36
+ if (!UUID_RE.test(categoryId)) {
37
+ // Backend enforces UUID format on category_id — client UUIDs from the
38
+ // envelope builder always pass; temp form ids must never reach here.
39
+ fail("category_id", "Category must be a valid UUID", details);
40
+ }
41
+ for (const v of input.variants ?? []) {
42
+ if (!v || typeof v.name !== "string" || !v.name.trim())
43
+ fail("variants", "Variant name is required", details);
44
+ }
45
+ for (const e of input.extras ?? []) {
46
+ if (!e || typeof e.name !== "string" || !e.name.trim())
47
+ fail("extras", "Extra name is required", details);
48
+ }
49
+ }
50
+ /** Mirrors MenuCategory.changeset: required name. */
51
+ export function validateMenuCategoryInput(input) {
52
+ const details = {};
53
+ if (typeof input.name !== "string" || !input.name.trim())
54
+ fail("name", "Name is required", details);
55
+ }
56
+ /** Mirrors Order.changeset: required payment_mode/total/currency, amounts ≥ 0. */
57
+ export function validateOrderCreateInput(input) {
58
+ const details = {};
59
+ const items = input.item_snapshots ?? input.items;
60
+ if (!Array.isArray(items) || items.length === 0)
61
+ fail("items", "Order must have at least one item", details);
62
+ for (const k of ["total_minor", "subtotal_minor", "delivery_fee_minor", "tax_minor"]) {
63
+ const v = input[k];
64
+ if (v !== undefined && (typeof v !== "number" || v < 0))
65
+ fail(k, `${k} must be 0 or more`, details);
66
+ }
67
+ if (input.order_type === "delivery" && !input.delivery_address) {
68
+ fail("delivery_address", "Delivery address is required for delivery orders", details);
69
+ }
70
+ }
71
+ /** Mirrors BusinessMemberInvitation changeset: email format. */
72
+ export function validateInviteInput(input) {
73
+ const details = {};
74
+ const email = input.invited_email ?? input.email;
75
+ if (typeof email !== "string" || !email.trim())
76
+ fail("email", "Email is required", details);
77
+ if (!EMAIL_RE.test(email))
78
+ fail("email", "Invalid email format", details);
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.7.9",
3
+ "version": "0.8.1",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -53,12 +53,17 @@
53
53
  "@tanstack/react-query": "^5.102.2",
54
54
  "@types/node": "^26.2.0",
55
55
  "@types/react": "^19.2.18",
56
+ "bonjour-service": "^1.4.4",
56
57
  "react": "^19.2.8",
58
+ "tsx": "^4.23.13",
57
59
  "typescript": "^7.0.2",
58
60
  "vitest": "^3.1.0"
59
61
  },
60
62
  "license": "MIT",
61
63
  "publishConfig": {
62
64
  "access": "public"
65
+ },
66
+ "dependencies": {
67
+ "@tanstack/query-async-storage-persister": "^5.102.8"
63
68
  }
64
69
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,142 +0,0 @@
1
- /**
2
- * Server-parity tests for the pricing engine.
3
- *
4
- * Mirrors the backend ExUnit vectors in
5
- * pos_controller_test.exs (inclusive / exclusive / mixed) and the
6
- * rounding formulas in Orders.calculate_tax_amount. Golden values below come
7
- * directly from the Elixir implementation so the two runtimes never diverge.
8
- */
9
- import { describe, it, expect } from "vitest";
10
- import { calculatePricing, calculateTaxLines, calculateExclusiveTax, calculateInclusiveTaxShare, } from "./engine";
11
- function cart(unitPriceMinor, quantity = 1, addonsMinor = []) {
12
- return [
13
- {
14
- id: "cart-1",
15
- menu_item: {
16
- id: "item-1",
17
- name: "Test Item",
18
- description: null,
19
- basePrice: { amountMinor: unitPriceMinor, currencyCode: "GHS" },
20
- image_url: null,
21
- is_available: true,
22
- dietary_tags: [],
23
- category_id: "cat-1",
24
- business_id: "biz-1",
25
- category: null,
26
- variants: [],
27
- extras: [],
28
- inserted_at: "",
29
- updated_at: "",
30
- },
31
- quantity,
32
- selected_variant: null,
33
- selected_addons: addonsMinor.map((price_minor, i) => ({
34
- addon_id: `addon-${i}`,
35
- name: `Addon ${i}`,
36
- price_minor,
37
- })),
38
- notes: "",
39
- // unit_price_minor already includes variant + addons (matches pos.store)
40
- unit_price_minor: unitPriceMinor,
41
- },
42
- ];
43
- }
44
- function taxRule(overrides) {
45
- return {
46
- id: "tax-1",
47
- business_id: "biz-1",
48
- country_code: "GH",
49
- applies_to: "subtotal",
50
- is_inclusive: false,
51
- is_active: true,
52
- display_order: 0,
53
- inserted_at: "",
54
- updated_at: "",
55
- ...overrides,
56
- };
57
- }
58
- describe("calculateExclusiveTax / calculateInclusiveTaxShare (backend rounding)", () => {
59
- it("computes exclusive tax = floor(amount*rate/10000)", () => {
60
- expect(calculateExclusiveTax(2400, 1000)).toBe(240); // 10%
61
- expect(calculateExclusiveTax(1200, 1000)).toBe(120);
62
- expect(calculateExclusiveTax(2400, 0)).toBe(0);
63
- expect(calculateExclusiveTax(0, 1000)).toBe(0);
64
- });
65
- it("computes inclusive share = floor(amount*rate/(10000+rate))", () => {
66
- // Backend: div(2400 * 7500 + 5000, 107500) = 1028
67
- expect(calculateInclusiveTaxShare(2400, 7500)).toBe(1028);
68
- expect(calculateInclusiveTaxShare(0, 7500)).toBe(0);
69
- expect(calculateInclusiveTaxShare(2400, 0)).toBe(0);
70
- });
71
- });
72
- describe("calculateTaxLines", () => {
73
- it("returns exclusive-only additions for exclusive rules", () => {
74
- const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
75
- const { tax_lines, tax_minor, exclusive_tax_minor } = calculateTaxLines(2400, rules);
76
- expect(tax_minor).toBe(240);
77
- expect(exclusive_tax_minor).toBe(240);
78
- expect(tax_lines[0].is_inclusive).toBe(false);
79
- expect(tax_lines[0].tax_amount_minor).toBe(240);
80
- });
81
- it("returns embedded amounts for inclusive rules (not added to total)", () => {
82
- const rules = [taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true })];
83
- const { tax_lines, tax_minor, exclusive_tax_minor } = calculateTaxLines(2400, rules);
84
- expect(tax_minor).toBe(1028); // display share
85
- expect(exclusive_tax_minor).toBe(0); // nothing added to total
86
- expect(tax_lines[0].is_inclusive).toBe(true);
87
- expect(tax_lines[0].tax_amount_minor).toBe(1028);
88
- });
89
- it("applies tax to discounted subtotal first", () => {
90
- const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
91
- const { exclusive_tax_minor } = calculateTaxLines(2400, rules, 400, 0); // 2000 taxable
92
- expect(exclusive_tax_minor).toBe(200);
93
- });
94
- it("honors applies_to = subtotal_plus_delivery", () => {
95
- const rules = [
96
- taxRule({ name: "Service", code: "service", rate_basis_points: 1000, applies_to: "subtotal_plus_delivery" }),
97
- ];
98
- const { exclusive_tax_minor } = calculateTaxLines(2400, rules, 0, 600); // 3000 taxable
99
- expect(exclusive_tax_minor).toBe(300);
100
- });
101
- });
102
- describe("calculatePricing (POS parity with backend)", () => {
103
- it("no tax → total == subtotal", () => {
104
- const pricing = calculatePricing({ items: cart(1200, 2), taxRules: [], currency_code: "GHS" });
105
- expect(pricing.subtotal_minor).toBe(2400);
106
- expect(pricing.total_minor).toBe(2400);
107
- expect(pricing.exclusive_tax_minor).toBe(0);
108
- });
109
- it("inclusive-only tax does NOT inflate total (backend vector: 2400 → 2400)", () => {
110
- const rules = [taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true })];
111
- const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
112
- expect(pricing.subtotal_minor).toBe(2400);
113
- expect(pricing.total_minor).toBe(2400); // NOT 2400 + 168
114
- expect(pricing.exclusive_tax_minor).toBe(0);
115
- expect(pricing.tax_minor).toBe(1028);
116
- expect(pricing.tax_lines).toHaveLength(1);
117
- expect(pricing.tax_lines[0].is_inclusive).toBe(true);
118
- });
119
- it("exclusive tax is added on top (backend vector: 2400 → 2640)", () => {
120
- const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
121
- const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
122
- expect(pricing.subtotal_minor).toBe(2400);
123
- expect(pricing.exclusive_tax_minor).toBe(240);
124
- expect(pricing.total_minor).toBe(2640);
125
- });
126
- it("mixed inclusive + exclusive adds only the exclusive portion (backend vector: 2640)", () => {
127
- const rules = [
128
- taxRule({ name: "Incl VAT", code: "incl_vat", rate_basis_points: 7500, is_inclusive: true }),
129
- taxRule({ name: "Service", code: "service", rate_basis_points: 1000 }),
130
- ];
131
- const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, currency_code: "GHS" });
132
- expect(pricing.total_minor).toBe(2640);
133
- expect(pricing.exclusive_tax_minor).toBe(240);
134
- expect(pricing.tax_minor).toBe(1028 + 240); // display breakdown
135
- expect(pricing.tax_lines).toHaveLength(2);
136
- });
137
- it("applies tips when provided (POS total = subtotal + excl tax + tip)", () => {
138
- const rules = [taxRule({ name: "Service", code: "service", rate_basis_points: 1000 })];
139
- const pricing = calculatePricing({ items: cart(1200, 2), taxRules: rules, tip_minor: 500, currency_code: "GHS" });
140
- expect(pricing.total_minor).toBe(2400 + 240 + 500);
141
- });
142
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,23 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { lwwMerge, toLWWMap, fromLWWMap } from "./lww";
3
- describe("LWW merge", () => {
4
- it("divergent fields both merge", () => {
5
- const a = toLWWMap({ price: 500 }, "1000-000000-01", "01");
6
- const b = toLWWMap({ name: "Spicy" }, "1001-000000-02", "02");
7
- const merged = lwwMerge(lwwMerge(null, a), b);
8
- expect(fromLWWMap(merged)).toEqual({ price: 500, name: "Spicy" });
9
- });
10
- it("same field last HLC wins", () => {
11
- const a = toLWWMap({ price: 500 }, "1000-000000-01", "01");
12
- const b = toLWWMap({ price: 600 }, "1001-000000-02", "02");
13
- const merged = lwwMerge(a, b);
14
- expect(merged.price.value).toBe(600);
15
- const merged2 = lwwMerge(b, a);
16
- expect(merged2.price.value).toBe(600);
17
- });
18
- it("idempotent", () => {
19
- const a = toLWWMap({ x: 1 }, "1000-000000-01", "01");
20
- const m1 = lwwMerge(a, a);
21
- expect(m1).toEqual(a);
22
- });
23
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,66 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { getReferenceData, setReferenceData, mergeReferenceData } from "./operations";
3
- // simple in-memory sqlite mock for operations tests
4
- class MemDB {
5
- constructor() {
6
- this.tables = new Map();
7
- }
8
- async execAsync(_sql) { }
9
- async runAsync(sql, ...params) {
10
- if (sql.includes("INSERT INTO reference_data")) {
11
- const [business_id, key, version, payload, downloaded_at, last_event_id, last_hlc] = params;
12
- let m = this.tables.get("reference_data");
13
- if (!m) {
14
- m = new Map();
15
- this.tables.set("reference_data", m);
16
- }
17
- const existing = m.get(`${business_id}:${key}`);
18
- // mimic merge behavior via setReferenceData logic in test calling mergeReferenceData
19
- m.set(`${business_id}:${key}`, JSON.stringify({ business_id, key, version, payload, downloaded_at, last_event_id, last_hlc }));
20
- return { lastInsertRowId: 1, changes: 1 };
21
- }
22
- if (sql.includes("INSERT INTO reference_data") && sql.includes("ON CONFLICT")) { }
23
- return { lastInsertRowId: 1, changes: 1 };
24
- }
25
- async getFirstAsync(sql, ...params) {
26
- if (sql.includes("SELECT * FROM reference_data")) {
27
- const [business_id, key] = params;
28
- const m = this.tables.get("reference_data");
29
- if (!m)
30
- return null;
31
- const raw = m.get(`${business_id}:${key}`);
32
- if (!raw)
33
- return null;
34
- const j = JSON.parse(raw);
35
- return { key: j.key, version: j.version, payload: j.payload, downloaded_at: j.downloaded_at, last_event_id: j.last_event_id, last_hlc: j.last_hlc };
36
- }
37
- if (sql.includes("SELECT version")) {
38
- const [business_id, key] = params;
39
- const m = this.tables.get("reference_data");
40
- const raw = m?.get(`${business_id}:${key}`);
41
- if (!raw)
42
- return null;
43
- const j = JSON.parse(raw);
44
- return { version: j.version };
45
- }
46
- return null;
47
- }
48
- async getAllAsync() { return []; }
49
- }
50
- describe("reference operations LWW", () => {
51
- it("merge keeps newer HLC", async () => {
52
- const db = new MemDB();
53
- await setReferenceData(db, "biz1", { key: "business:profile", version: "1000-000000-01", payload: { name: "A" }, downloadedAt: new Date().toISOString(), lastHlc: "1000-000000-01", lastEventId: "evt_1" });
54
- let rec = await getReferenceData(db, "biz1", "business:profile");
55
- expect(rec?.lastHlc).toBe("1000-000000-01");
56
- // older HLC should not overwrite
57
- await mergeReferenceData(db, "biz1", "business:profile", { name: "B" }, "0999-000000-01", "evt_0");
58
- rec = await getReferenceData(db, "biz1", "business:profile");
59
- // still A because incoming older
60
- expect(rec?.payload).toEqual({ name: "A" });
61
- // newer wins
62
- await mergeReferenceData(db, "biz1", "business:profile", { name: "C" }, "1001-000000-02", "evt_2");
63
- rec = await getReferenceData(db, "biz1", "business:profile");
64
- expect(rec?.payload).toEqual({ name: "C" });
65
- });
66
- });
@@ -1 +0,0 @@
1
- export {};