@aglyn/plugins-commerce 1.0.0-beta.232 → 1.0.0-beta.234

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 (228) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart-extras.d.ts +6 -0
  3. package/src/lib/components/cart-extras.js +4 -1
  4. package/src/lib/components/cart-extras.js.map +1 -1
  5. package/src/lib/components/cart-fulfillment.d.ts +117 -0
  6. package/src/lib/components/cart-fulfillment.js +367 -0
  7. package/src/lib/components/cart-fulfillment.js.map +1 -0
  8. package/src/lib/components/cart.js +52 -10
  9. package/src/lib/components/cart.js.map +1 -1
  10. package/src/lib/components/commerce-console-page.js +42 -1
  11. package/src/lib/components/commerce-console-page.js.map +1 -1
  12. package/src/lib/components/console/host-orders-card.component.js +38 -10
  13. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  14. package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
  15. package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
  16. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
  17. package/src/lib/components/console/locations-card.component.js +177 -3
  18. package/src/lib/components/console/locations-card.component.js.map +1 -1
  19. package/src/lib/components/console/order-detail-dialog.component.js +10 -0
  20. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  21. package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
  22. package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
  23. package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
  24. package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
  25. package/src/lib/components/console/order-offline-notice.component.js +53 -0
  26. package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
  27. package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
  28. package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
  30. package/src/lib/components/console/pos/pos-api.d.ts +24 -1
  31. package/src/lib/components/console/pos/pos-api.js +13 -0
  32. package/src/lib/components/console/pos/pos-api.js.map +1 -1
  33. package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
  34. package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
  35. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  36. package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
  37. package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
  38. package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
  39. package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
  40. package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
  41. package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
  42. package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
  43. package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
  44. package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
  45. package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
  46. package/src/lib/components/console/pos/use-pos-display.js +3 -2
  47. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  48. package/src/lib/components/console/pos-devices-card.component.js +2 -2
  49. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  50. package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
  51. package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
  52. package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
  53. package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
  54. package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
  55. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  56. package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
  57. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
  58. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
  59. package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
  60. package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
  61. package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
  62. package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
  63. package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
  64. package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
  65. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
  66. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
  67. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
  68. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
  69. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
  70. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
  71. package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
  72. package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
  73. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
  74. package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
  75. package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
  76. package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
  77. package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
  78. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
  79. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
  80. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
  81. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
  82. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
  83. package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
  84. package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
  85. package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
  86. package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
  87. package/src/lib/components/console/pos-offline/register-offline.js +24 -0
  88. package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
  89. package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
  90. package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
  91. package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
  92. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
  93. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
  94. package/src/lib/components/console/pos-page.component.js +174 -27
  95. package/src/lib/components/console/pos-page.component.js.map +1 -1
  96. package/src/lib/components/console/products-hub-card.component.js +35 -10
  97. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  98. package/src/lib/components/console/shipping-settings-card.component.js +1 -1
  99. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  100. package/src/lib/components/console/stock-movements-card.component.js +48 -16
  101. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  102. package/src/lib/components/console/store-zones.d.ts +8 -1
  103. package/src/lib/components/console/store-zones.js +11 -1
  104. package/src/lib/components/console/store-zones.js.map +1 -1
  105. package/src/lib/components/order-status.js +66 -0
  106. package/src/lib/components/order-status.js.map +1 -1
  107. package/src/lib/components/product-detail.js +42 -15
  108. package/src/lib/components/product-detail.js.map +1 -1
  109. package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
  110. package/src/lib/constants/local-fulfillment-list-query.js +84 -0
  111. package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
  112. package/src/lib/constants/orders-list-query.d.ts +7 -2
  113. package/src/lib/constants/orders-list-query.js +89 -8
  114. package/src/lib/constants/orders-list-query.js.map +1 -1
  115. package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
  116. package/src/lib/constants/pos-shift-list-query.js +115 -0
  117. package/src/lib/constants/pos-shift-list-query.js.map +1 -0
  118. package/src/lib/constants/product-list-query.d.ts +6 -1
  119. package/src/lib/constants/product-list-query.js +77 -8
  120. package/src/lib/constants/product-list-query.js.map +1 -1
  121. package/src/lib/constants/stock-movement-query.d.ts +3 -2
  122. package/src/lib/constants/stock-movement-query.js +52 -8
  123. package/src/lib/constants/stock-movement-query.js.map +1 -1
  124. package/src/lib/model/buyer-notifications.d.ts +5 -3
  125. package/src/lib/model/buyer-notifications.js +25 -4
  126. package/src/lib/model/buyer-notifications.js.map +1 -1
  127. package/src/lib/model/commerce-orders.d.ts +59 -0
  128. package/src/lib/model/commerce-orders.js.map +1 -1
  129. package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
  130. package/src/lib/model/commerce-pos-kiosk.js +147 -0
  131. package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
  132. package/src/lib/model/commerce-pos-offline.d.ts +234 -0
  133. package/src/lib/model/commerce-pos-offline.js +301 -0
  134. package/src/lib/model/commerce-pos-offline.js.map +1 -0
  135. package/src/lib/model/commerce-pos-ops.d.ts +16 -6
  136. package/src/lib/model/commerce-pos-ops.js +12 -4
  137. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  138. package/src/lib/model/commerce-pos.d.ts +13 -1
  139. package/src/lib/model/commerce-pos.js +5 -4
  140. package/src/lib/model/commerce-pos.js.map +1 -1
  141. package/src/lib/model/commerce-stock-holds.d.ts +24 -3
  142. package/src/lib/model/commerce-stock-holds.js +26 -8
  143. package/src/lib/model/commerce-stock-holds.js.map +1 -1
  144. package/src/lib/model/commerce.d.ts +3 -0
  145. package/src/lib/model/commerce.js.map +1 -1
  146. package/src/lib/model/index.d.ts +4 -0
  147. package/src/lib/model/index.js +4 -0
  148. package/src/lib/model/index.js.map +1 -1
  149. package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
  150. package/src/lib/model/local-fulfillment-settings.js +361 -0
  151. package/src/lib/model/local-fulfillment-settings.js.map +1 -0
  152. package/src/lib/model/order-list-fields.d.ts +10 -1
  153. package/src/lib/model/order-list-fields.js +1 -0
  154. package/src/lib/model/order-list-fields.js.map +1 -1
  155. package/src/lib/model/order-local-fulfillment.d.ts +131 -0
  156. package/src/lib/model/order-local-fulfillment.js +125 -0
  157. package/src/lib/model/order-local-fulfillment.js.map +1 -0
  158. package/src/lib/model/order-shipping-export.js +4 -0
  159. package/src/lib/model/order-shipping-export.js.map +1 -1
  160. package/src/lib/model/order-status-view.d.ts +20 -1
  161. package/src/lib/model/order-status-view.js +60 -6
  162. package/src/lib/model/order-status-view.js.map +1 -1
  163. package/src/lib/plugin-config.js +6 -2
  164. package/src/lib/plugin-config.js.map +1 -1
  165. package/src/lib/plugin.js +8 -0
  166. package/src/lib/plugin.js.map +1 -1
  167. package/src/lib/pos-kiosk-config.d.ts +26 -0
  168. package/src/lib/pos-kiosk-config.js +59 -0
  169. package/src/lib/pos-kiosk-config.js.map +1 -0
  170. package/src/lib/server/api-v1/openapi.js +8 -0
  171. package/src/lib/server/api-v1/openapi.js.map +1 -1
  172. package/src/lib/server/api-v1/order-view.d.ts +19 -0
  173. package/src/lib/server/api-v1/order-view.js +35 -0
  174. package/src/lib/server/api-v1/order-view.js.map +1 -1
  175. package/src/lib/server/billing-webhook.js +238 -87
  176. package/src/lib/server/billing-webhook.js.map +1 -1
  177. package/src/lib/server/cart-checkout.d.ts +2 -0
  178. package/src/lib/server/cart-checkout.js +173 -13
  179. package/src/lib/server/cart-checkout.js.map +1 -1
  180. package/src/lib/server/checkout-extras.js +18 -4
  181. package/src/lib/server/checkout-extras.js.map +1 -1
  182. package/src/lib/server/checkout.js +71 -14
  183. package/src/lib/server/checkout.js.map +1 -1
  184. package/src/lib/server/local-fulfillment-status.d.ts +86 -0
  185. package/src/lib/server/local-fulfillment-status.js +379 -0
  186. package/src/lib/server/local-fulfillment-status.js.map +1 -0
  187. package/src/lib/server/local-fulfillment.d.ts +190 -0
  188. package/src/lib/server/local-fulfillment.js +580 -0
  189. package/src/lib/server/local-fulfillment.js.map +1 -0
  190. package/src/lib/server/membership-register.js +3 -1
  191. package/src/lib/server/membership-register.js.map +1 -1
  192. package/src/lib/server/order-notifications.js +61 -0
  193. package/src/lib/server/order-notifications.js.map +1 -1
  194. package/src/lib/server/pos-display.d.ts +27 -1
  195. package/src/lib/server/pos-display.js +50 -9
  196. package/src/lib/server/pos-display.js.map +1 -1
  197. package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
  198. package/src/lib/server/pos-kiosk-principal.js +42 -0
  199. package/src/lib/server/pos-kiosk-principal.js.map +1 -0
  200. package/src/lib/server/pos-kiosk.d.ts +54 -0
  201. package/src/lib/server/pos-kiosk.js +972 -0
  202. package/src/lib/server/pos-kiosk.js.map +1 -0
  203. package/src/lib/server/pos-offline-sync.d.ts +85 -0
  204. package/src/lib/server/pos-offline-sync.js +765 -0
  205. package/src/lib/server/pos-offline-sync.js.map +1 -0
  206. package/src/lib/server/pos-order.js +22 -4
  207. package/src/lib/server/pos-order.js.map +1 -1
  208. package/src/lib/server/pos-payment.d.ts +6 -0
  209. package/src/lib/server/pos-payment.js +225 -6
  210. package/src/lib/server/pos-payment.js.map +1 -1
  211. package/src/lib/server/pos-print.js +5 -0
  212. package/src/lib/server/pos-print.js.map +1 -1
  213. package/src/lib/server/pos-return.js +27 -0
  214. package/src/lib/server/pos-return.js.map +1 -1
  215. package/src/lib/server/pos-sale.d.ts +6 -0
  216. package/src/lib/server/pos-sale.js +2 -0
  217. package/src/lib/server/pos-sale.js.map +1 -1
  218. package/src/lib/server/pos-shift.js +8 -1
  219. package/src/lib/server/pos-shift.js.map +1 -1
  220. package/src/lib/server/refund.js +3 -0
  221. package/src/lib/server/refund.js.map +1 -1
  222. package/src/lib/server/stock-hold.d.ts +9 -1
  223. package/src/lib/server/stock-hold.js +15 -7
  224. package/src/lib/server/stock-hold.js.map +1 -1
  225. package/src/lib/server.js +16 -0
  226. package/src/lib/server.js.map +1 -1
  227. package/src/lib/tenant-emails.js +161 -0
  228. package/src/lib/tenant-emails.js.map +1 -1
@@ -0,0 +1,972 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createHmac, timingSafeEqual } from "crypto";
18
+ import * as Aglyn from "@aglyn/aglyn/server";
19
+ import { resolveMediaSrc } from "@aglyn/aglyn/app-utils/media-ref";
20
+ import { pluginSmsAvailable } from "@aglyn/aglyn/plugin-manager/plugin-sms-messaging";
21
+ import { consumeRateLimit, firebaseAdmin, getLockdownVerdict, getOrgForHost, getPluginConfig } from "@aglyn/tenant-data-admin";
22
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
23
+ import * as CommerceModel from "../model/index.js";
24
+ import { posRegisterSettings } from "../plugin-config.js";
25
+ import { posKioskSettings } from "../pos-kiosk-config.js";
26
+ import { verifyMemberPassword } from "./membership.js";
27
+ import { authorizePosStaff, posIdempotencyKey, posQueryBody, posRequestBody } from "./pos-auth.js";
28
+ import { displayBranding, displayToken, posDisplayCurrency, resolveDisplayToken } from "./pos-display.js";
29
+ import { withPosKioskPrincipal } from "./pos-kiosk-principal.js";
30
+ import { posOrderHandler } from "./pos-order.js";
31
+ import { recordReceiptChoice, voidPosSale } from "./pos-payment.js";
32
+ import { posPaymentId, posSaleSummary, readPosSale } from "./pos-sale.js";
33
+ import { posStripeTestMode, posTerminalAvailable } from "./pos-stripe.js";
34
+ import { cancelPosCardPayment, createCardPresentIntent, createPosTerminalConnectionToken, refreshPosCardPayment, simulatePosReaderTap, startCardPresentPayment } from "./pos-terminal.js";
35
+ const DEVICE_READS = new Set([
36
+ 'context',
37
+ 'catalog',
38
+ 'sale'
39
+ ]);
40
+ const STAFF_READS = new Set([
41
+ 'queue'
42
+ ]);
43
+ /** The prefix of a token row's id a kiosk's orders are stamped with. */ export function posKioskDeviceId(device) {
44
+ var _ref;
45
+ var _device_ref;
46
+ return String((_ref = (_device_ref = device.ref) == null ? void 0 : _device_ref.id) != null ? _ref : '').slice(0, 16);
47
+ }
48
+ function refuse(status, error) {
49
+ return {
50
+ status,
51
+ body: {
52
+ error
53
+ }
54
+ };
55
+ }
56
+ /** The kiosk's gate, asked again on every call. */ async function authorizeKiosk(req, body, write) {
57
+ var _host_get, _ref, _ref1, _ref2, _register_data;
58
+ var _membership_permissions, _owner_org;
59
+ const device = await resolveDisplayToken(displayToken(req, body));
60
+ if (!device) return refuse(401, 'This kiosk is not paired.');
61
+ if (device.mode !== 'kiosk') return refuse(403, 'This device is a customer display, not a kiosk.');
62
+ const firestore = firebaseAdmin.app().firestore();
63
+ const hostRef = firestore.collection('hosts').doc(device.hostId);
64
+ const [host, register] = await Promise.all([
65
+ hostRef.get(),
66
+ hostRef.collection('registers').doc(device.registerId).get()
67
+ ]);
68
+ if (!host.exists || !register.exists) return refuse(401, 'This kiosk is not paired.');
69
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[device.createdBy];
70
+ const membership = device.createdBy ? await resolveOrgPermissions(device.createdBy, {
71
+ hostId: device.hostId
72
+ }) : null;
73
+ if (role !== 'admin' && role !== 'editor' || !(membership == null ? void 0 : (_membership_permissions = membership.permissions) == null ? void 0 : _membership_permissions.managePos)) {
74
+ return refuse(403, 'This kiosk needs a staff member to pair it again.');
75
+ }
76
+ const owner = await getOrgForHost(device.hostId);
77
+ if (!Aglyn.checkEntitlement(owner == null ? void 0 : owner.org, 'pos')) {
78
+ return refuse(403, 'This kiosk is not available. Please order at the counter.');
79
+ }
80
+ const locked = await getLockdownVerdict({
81
+ uid: device.createdBy,
82
+ org: owner == null ? void 0 : owner.org,
83
+ host: host.data(),
84
+ intent: write ? 'write' : 'read'
85
+ });
86
+ if (locked) return {
87
+ status: 423,
88
+ body: {
89
+ error: 'This kiosk is paused. Please order at the counter.',
90
+ locked: true
91
+ }
92
+ };
93
+ const orgId = String((_ref = (_ref1 = owner == null ? void 0 : (_owner_org = owner.org) == null ? void 0 : _owner_org.id) != null ? _ref1 : owner == null ? void 0 : owner.orgId) != null ? _ref : '');
94
+ const config = await getPluginConfig(orgId || undefined, 'commerce', {
95
+ hostId: device.hostId
96
+ }).catch(()=>({}));
97
+ return {
98
+ device,
99
+ deviceId: posKioskDeviceId(device),
100
+ hostId: device.hostId,
101
+ registerId: device.registerId,
102
+ hostRef,
103
+ uid: device.createdBy,
104
+ orgId,
105
+ org: (_ref2 = owner == null ? void 0 : owner.org) != null ? _ref2 : null,
106
+ register: (_register_data = register.data()) != null ? _register_data : {},
107
+ settings: posRegisterSettings(config),
108
+ kiosk: posKioskSettings(config)
109
+ };
110
+ }
111
+ /** One durable budget per kiosk and action; refused requests do nothing. */ async function withinBudget(gate, action, limit) {
112
+ const rate = await consumeRateLimit(`pos-kiosk:${action}:${gate.deviceId}`, {
113
+ limit,
114
+ windowMs: 10 * 60 * 1000
115
+ });
116
+ return rate.allowed;
117
+ }
118
+ /** The register's card readers, when Terminal is switched on. */ async function registerReaders(gate) {
119
+ if (!posTerminalAvailable()) return [];
120
+ const snapshot = await gate.hostRef.collection('terminalReaders').where('registerId', '==', gate.registerId).limit(10).get();
121
+ return snapshot.docs.map((doc)=>{
122
+ var _doc_get, _doc_get1;
123
+ return {
124
+ id: String(doc.id),
125
+ label: String((_doc_get = doc.get('label')) != null ? _doc_get : 'Card reader'),
126
+ status: String((_doc_get1 = doc.get('status')) != null ? _doc_get1 : 'offline')
127
+ };
128
+ });
129
+ }
130
+ /** The reader this kiosk sends cards to: the one chosen in its settings, else the register's only one. */ async function kioskReaderId(gate) {
131
+ var _gate_device_data_kioskReaderId;
132
+ const readers = await registerReaders(gate);
133
+ const chosen = String((_gate_device_data_kioskReaderId = gate.device.data['kioskReaderId']) != null ? _gate_device_data_kioskReaderId : '');
134
+ if (chosen && readers.some((reader)=>reader.id === chosen)) return chosen;
135
+ return readers.length === 1 ? readers[0].id : null;
136
+ }
137
+ /** The receipt choices this kiosk can honor. */ async function receiptChannels(gate) {
138
+ const channels = [
139
+ 'email'
140
+ ];
141
+ if (pluginSmsAvailable()) channels.push('sms');
142
+ // Printed on the register's own receipt printer, so offered only when it has one.
143
+ const printers = await gate.hostRef.collection('printers').where('registerId', '==', gate.registerId).limit(1).get().catch(()=>null);
144
+ if (printers && !printers.empty) channels.push('print');
145
+ channels.push('none');
146
+ return channels;
147
+ }
148
+ async function context(gate) {
149
+ const [branding, currency, reader, receipts] = await Promise.all([
150
+ displayBranding(gate.hostId),
151
+ posDisplayCurrency(gate.hostId),
152
+ kioskReaderId(gate),
153
+ receiptChannels(gate)
154
+ ]);
155
+ const value = {
156
+ branding: _extends({}, branding, {
157
+ message: gate.kiosk.welcome || 'Order here'
158
+ }),
159
+ currency,
160
+ tipping: {
161
+ enabled: gate.settings.tippingEnabled,
162
+ percentages: gate.settings.tipPercentages
163
+ },
164
+ payments: {
165
+ reader: Boolean(reader),
166
+ cardPresent: posTerminalAvailable(),
167
+ payAtCounter: gate.kiosk.payAtCounter
168
+ },
169
+ receipts,
170
+ offerMarketing: gate.settings.displayMarketingOptIn,
171
+ idleSeconds: gate.kiosk.idleSeconds,
172
+ testMode: posStripeTestMode()
173
+ };
174
+ return {
175
+ status: 200,
176
+ body: value
177
+ };
178
+ }
179
+ /**
180
+ * Whether a product may be sold at a kiosk at all: what the register sells,
181
+ * less what a customer cannot be handed at a counter — a subscription-only
182
+ * product, a gift card whose code needs issuing, and a digital download.
183
+ */ export function posKioskSellable(product) {
184
+ var _product_variants;
185
+ if (product.status !== 'active') return false;
186
+ if (product.subscription && !product.subscriptionOptional) return false;
187
+ if (product.giftCard) return false;
188
+ if (product.type === 'digital') return false;
189
+ return ((_product_variants = product.variants) != null ? _product_variants : []).some((variant)=>CommerceModel.variantHasPrice(variant));
190
+ }
191
+ /** A product as the kiosk may show it: names, prices and choices, nothing else. */ export function posKioskProduct(id, product, hostId) {
192
+ var _product_description, _product_name, _product_categoryIds, _product_options, _product_variants;
193
+ var _product_mediaUrls;
194
+ const image = resolveMediaSrc((_product_mediaUrls = product.mediaUrls) == null ? void 0 : _product_mediaUrls[0], {
195
+ hostId
196
+ });
197
+ const description = String((_product_description = product.description) != null ? _product_description : '').trim().slice(0, 280);
198
+ return _extends({
199
+ id,
200
+ name: String((_product_name = product.name) != null ? _product_name : '').slice(0, 120)
201
+ }, description ? {
202
+ description
203
+ } : {}, image ? {
204
+ imageUrl: image
205
+ } : {}, {
206
+ categoryIds: ((_product_categoryIds = product.categoryIds) != null ? _product_categoryIds : []).slice(0, 20),
207
+ options: ((_product_options = product.options) != null ? _product_options : []).map((option)=>({
208
+ name: option.name,
209
+ values: [
210
+ ...option.values
211
+ ]
212
+ })),
213
+ variants: ((_product_variants = product.variants) != null ? _product_variants : []).filter((variant)=>CommerceModel.variantHasPrice(variant)).map((variant)=>{
214
+ var _variant_options;
215
+ return {
216
+ id: variant.id,
217
+ options: _extends({}, (_variant_options = variant.options) != null ? _variant_options : {}),
218
+ priceCents: Math.round(Number(variant.priceUsd) * 100),
219
+ soldOut: Boolean(CommerceModel.stockShortfall(product, variant.id, 1))
220
+ };
221
+ }),
222
+ modifierGroups: CommerceModel.productModifierGroups(product)
223
+ });
224
+ }
225
+ async function catalog(gate, body) {
226
+ var _body_categoryId;
227
+ const categoryId = String((_body_categoryId = body['categoryId']) != null ? _body_categoryId : '');
228
+ if (categoryId && !/^[A-Za-z0-9_-]{1,128}$/.test(categoryId)) return refuse(400, 'Unknown category');
229
+ // The register grid's query (AGL-3321): live, active, by name — and a
230
+ // category is a clause on the query, never a filter over what was read.
231
+ let query = gate.hostRef.collection('products').where('deletedAt', '==', null).where('status', '==', 'active');
232
+ if (categoryId) query = query.where('categoryIds', 'array-contains', categoryId);
233
+ const [products, categories, currency] = await Promise.all([
234
+ query.orderBy('nameLower').limit(CommerceModel.POS_KIOSK_CATALOG_LIMIT).get(),
235
+ gate.hostRef.collection('productCategories').limit(30).get(),
236
+ posDisplayCurrency(gate.hostId)
237
+ ]);
238
+ const value = {
239
+ products: products.docs.map((doc)=>({
240
+ id: String(doc.id),
241
+ product: CommerceModel.liftLegacyProduct(doc.data())
242
+ })).filter(({ product })=>posKioskSellable(product)).map(({ id, product })=>posKioskProduct(id, product, gate.hostId)),
243
+ categories: categories.docs.map((doc)=>{
244
+ var _doc_get;
245
+ return {
246
+ id: String(doc.id),
247
+ name: String((_doc_get = doc.get('name')) != null ? _doc_get : 'Category').slice(0, 60)
248
+ };
249
+ }),
250
+ currency
251
+ };
252
+ return {
253
+ status: 200,
254
+ body: value
255
+ };
256
+ }
257
+ /** An order as its kiosk reads it back; null for any order this kiosk did not open. */ export function posKioskSaleView(order, deviceId) {
258
+ var _order_number, _order_lineItems, _ref, _ref1, _ref2;
259
+ var _order_totals, _order_totals1, _order_totals2;
260
+ if (!order || order.posSource !== 'kiosk' || order.kioskDeviceId !== deviceId) return null;
261
+ const summary = posSaleSummary(order);
262
+ const status = order.status === 'paid' ? 'paid' : order.status === 'pending' ? order.kioskQueueRegisterId ? 'queued' : 'open' : 'voided';
263
+ const card = [
264
+ ...summary.payments
265
+ ].reverse().find((payment)=>CommerceModel.isCardPaymentMethod(payment.method));
266
+ return {
267
+ orderId: order.$id,
268
+ number: Number((_order_number = order.number) != null ? _order_number : 0),
269
+ status,
270
+ lines: ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).map((line)=>_extends({
271
+ name: line.name
272
+ }, line.variantLabel ? {
273
+ variantLabel: line.variantLabel
274
+ } : {}, {
275
+ quantity: line.quantity,
276
+ amountCents: line.unitAmountCents * line.quantity
277
+ })),
278
+ itemsCents: Number((_ref = (_order_totals = order.totals) == null ? void 0 : _order_totals.itemsCents) != null ? _ref : 0),
279
+ discountCents: Number((_ref1 = (_order_totals1 = order.totals) == null ? void 0 : _order_totals1.discountCents) != null ? _ref1 : 0),
280
+ taxCents: Number((_ref2 = (_order_totals2 = order.totals) == null ? void 0 : _order_totals2.taxCents) != null ? _ref2 : 0),
281
+ totalCents: summary.totalCents,
282
+ tipCents: summary.tipCents,
283
+ paidCents: summary.paidCents,
284
+ dueCents: summary.dueCents,
285
+ payment: card ? _extends({
286
+ id: card.id,
287
+ status: card.status
288
+ }, card.failureMessage ? {
289
+ failureMessage: card.failureMessage
290
+ } : {}) : null
291
+ };
292
+ }
293
+ /** This kiosk's order, or a refusal that says nothing about anyone else's. */ async function readOwnSale(gate, rawOrderId) {
294
+ const orderId = String(rawOrderId != null ? rawOrderId : '');
295
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(orderId)) return refuse(404, 'Unknown order');
296
+ const order = await readPosSale(gate.hostId, orderId);
297
+ const view = posKioskSaleView(order, gate.deviceId);
298
+ if (!order || !view) return refuse(404, 'Unknown order');
299
+ return {
300
+ order,
301
+ view
302
+ };
303
+ }
304
+ /** The response the in-process sale route wrote. */ function recorder() {
305
+ const result = {
306
+ status: 500,
307
+ body: {}
308
+ };
309
+ const res = {
310
+ status (code) {
311
+ result.status = code;
312
+ return res;
313
+ },
314
+ json (value) {
315
+ result.body = value != null ? value : {};
316
+ },
317
+ send () {},
318
+ setHeader () {},
319
+ redirect () {},
320
+ end () {}
321
+ };
322
+ return {
323
+ res,
324
+ result
325
+ };
326
+ }
327
+ async function checkout(gate, req, body) {
328
+ const attemptKey = posIdempotencyKey(req);
329
+ if (!attemptKey) return refuse(400, 'Missing Idempotency-Key');
330
+ const cart = CommerceModel.sanitizePosKioskLines(body['lines']);
331
+ if ('error' in cart) return refuse(400, cart.error);
332
+ if (!await withinBudget(gate, 'checkout', 40)) {
333
+ return refuse(429, 'Too many orders from this kiosk. Please order at the counter.');
334
+ }
335
+ // What a customer may buy here, read from the product documents before
336
+ // anything is priced: the sale route warns a cashier about stock and sells
337
+ // anyway (the goods are in their hand); a kiosk refuses what is sold out.
338
+ const ids = [
339
+ ...new Set(cart.lines.map((line)=>line.productId))
340
+ ];
341
+ const snapshots = await Promise.all(ids.map((id)=>gate.hostRef.collection('products').doc(id).get()));
342
+ const products = new Map(snapshots.map((snapshot)=>[
343
+ String(snapshot.id),
344
+ snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data()) : null
345
+ ]));
346
+ for (const line of cart.lines){
347
+ const product = products.get(line.productId);
348
+ if (!product || product.deletedAt || !posKioskSellable(product)) {
349
+ return refuse(409, 'An item in your cart is no longer available. Remove it and try again.');
350
+ }
351
+ const variant = line.variantId ? product.variants.find((candidate)=>candidate.id === line.variantId) : product.variants[0];
352
+ if (!variant || !CommerceModel.variantHasPrice(variant)) {
353
+ return refuse(409, `${product.name} is no longer available. Remove it and try again.`);
354
+ }
355
+ const wanted = cart.lines.filter((other)=>{
356
+ var _other_variantId, _line_variantId;
357
+ return other.productId === line.productId && ((_other_variantId = other.variantId) != null ? _other_variantId : '') === ((_line_variantId = line.variantId) != null ? _line_variantId : '');
358
+ }).reduce((sum, other)=>sum + other.quantity, 0);
359
+ if (CommerceModel.stockShortfall(product, variant.id, wanted)) {
360
+ return refuse(409, `${product.name} is sold out. Remove it and try again.`);
361
+ }
362
+ }
363
+ const { res, result } = recorder();
364
+ const priced = withPosKioskPrincipal({
365
+ method: 'POST',
366
+ headers: {
367
+ 'idempotency-key': `kiosk:${gate.deviceId}:${attemptKey}`
368
+ },
369
+ query: {},
370
+ cookies: {},
371
+ socket: req.socket,
372
+ body: _extends({
373
+ hostId: gate.hostId,
374
+ registerId: gate.registerId,
375
+ payment: 'open',
376
+ lines: cart.lines
377
+ }, typeof gate.register['locationId'] === 'string' && gate.register['locationId'] ? {
378
+ locationId: gate.register['locationId']
379
+ } : {})
380
+ }, {
381
+ uid: gate.uid,
382
+ registerId: gate.registerId,
383
+ deviceId: gate.deviceId
384
+ });
385
+ await posOrderHandler(priced, res);
386
+ if (result.status !== 200 || !result.body['orderId']) {
387
+ // A refusal about a product names it, and the customer can fix it. One
388
+ // about the store — a shift, the tax setup, the plan — is the staff's to
389
+ // fix, and the customer is sent to the counter.
390
+ console.warn('[pos-kiosk] sale refused', gate.hostId, result.status, result.body['error']);
391
+ if (result.status === 400 && result.body['error']) return refuse(400, String(result.body['error']));
392
+ return refuse(result.status === 429 ? 429 : 409, 'This kiosk cannot take orders right now. Please order at the counter.');
393
+ }
394
+ const own = await readOwnSale(gate, result.body['orderId']);
395
+ if ('status' in own) return own;
396
+ if (own.view.totalCents <= 0) {
397
+ // Nothing to take a card for, and nothing a kiosk should hand out free.
398
+ await voidPosSale(kioskStaff(gate), own.order.$id).catch(()=>undefined);
399
+ return refuse(409, 'Please ask at the counter for this order.');
400
+ }
401
+ return {
402
+ status: 200,
403
+ body: {
404
+ sale: own.view
405
+ }
406
+ };
407
+ }
408
+ /** The kiosk, as the register's payment helpers expect the person at the till. */ function kioskStaff(gate) {
409
+ return {
410
+ uid: gate.uid,
411
+ hostId: gate.hostId,
412
+ orgId: gate.orgId,
413
+ org: gate.org
414
+ };
415
+ }
416
+ async function pay(gate, req, body) {
417
+ var _body_method, _pressed_tipCents;
418
+ const attemptKey = posIdempotencyKey(req);
419
+ if (!attemptKey) return refuse(400, 'Missing Idempotency-Key');
420
+ const method = String((_body_method = body['method']) != null ? _body_method : '');
421
+ if (method !== 'reader' && method !== 'tap') return refuse(400, 'Choose how to pay.');
422
+ if (!posTerminalAvailable()) return refuse(409, 'Card payments are not available on this kiosk.');
423
+ if (!await withinBudget(gate, 'pay', 60)) return refuse(429, 'Too many tries. Please pay at the counter.');
424
+ const own = await readOwnSale(gate, body['orderId']);
425
+ if ('status' in own) return own;
426
+ if (own.view.status !== 'open') return refuse(409, 'This order is no longer waiting for payment.');
427
+ const paymentId = posPaymentId(own.order.$id, `kiosk:${attemptKey}`);
428
+ // A retried press finds the payment its first press reserved, for the
429
+ // amount and tip that press chose: the reservation is already out of the
430
+ // balance, so the balance alone would read as nothing left to pay.
431
+ const pressed = CommerceModel.orderPayments(own.order).find((payment)=>payment.id === paymentId);
432
+ const amountCents = pressed ? pressed.amountCents : posSaleSummary(own.order).tenderableCents;
433
+ if (!(amountCents > 0)) return refuse(409, 'Nothing is left to pay on this order.');
434
+ const tipCents = pressed ? Number((_pressed_tipCents = pressed.tipCents) != null ? _pressed_tipCents : 0) : CommerceModel.posKioskTipCents(body['tipChoice'], {
435
+ baseCents: amountCents,
436
+ enabled: gate.settings.tippingEnabled,
437
+ percentages: gate.settings.tipPercentages,
438
+ percent: body['tipPercent'],
439
+ cents: body['tipCents']
440
+ });
441
+ if (tipCents === null) return refuse(400, 'Choose one of the tips shown, or no tip.');
442
+ const start = {
443
+ hostId: gate.hostId,
444
+ orderId: own.order.$id,
445
+ paymentId,
446
+ amountCents,
447
+ tipCents,
448
+ cashierId: gate.uid,
449
+ org: gate.org
450
+ };
451
+ let outcome;
452
+ if (method === 'reader') {
453
+ const readerId = await kioskReaderId(gate);
454
+ if (!readerId) return refuse(409, 'No card reader is set up for this kiosk. Please pay at the counter.');
455
+ // The kiosk asked for the tip on its own screen: the reader must not ask again.
456
+ outcome = await startCardPresentPayment(_extends({}, start, {
457
+ readerId,
458
+ settings: _extends({}, gate.settings, {
459
+ tippingEnabled: false
460
+ })
461
+ }));
462
+ } else {
463
+ // Tap to Pay: collected by the native POS app's Stripe Terminal SDK
464
+ // against this server-made intent (AGL-3618), settled like any reader.
465
+ outcome = await createCardPresentIntent(start);
466
+ }
467
+ if ('error' in outcome) return refuse(outcome.status, outcome.error);
468
+ const view = posKioskSaleView(outcome.order, gate.deviceId);
469
+ return {
470
+ status: 200,
471
+ body: _extends({
472
+ sale: view
473
+ }, outcome.payment ? {
474
+ paymentId: outcome.payment.id
475
+ } : {}, outcome.clientSecret ? {
476
+ clientSecret: outcome.clientSecret
477
+ } : {}, outcome.paymentIntentId ? {
478
+ paymentIntentId: outcome.paymentIntentId
479
+ } : {})
480
+ };
481
+ }
482
+ /** A card payment on this kiosk's own order: refreshed, canceled or (test mode) tapped. */ async function cardStep(gate, body, step) {
483
+ var _body_paymentId;
484
+ var _own_order_payments;
485
+ if (!await withinBudget(gate, 'card', 900)) return refuse(429, 'Too many tries. Wait a moment.');
486
+ const own = await readOwnSale(gate, body['orderId']);
487
+ if ('status' in own) return own;
488
+ const paymentId = String((_body_paymentId = body['paymentId']) != null ? _body_paymentId : '');
489
+ if (!((_own_order_payments = own.order.payments) == null ? void 0 : _own_order_payments.some((payment)=>payment.id === paymentId))) return refuse(404, 'Unknown payment');
490
+ const ref = {
491
+ hostId: gate.hostId,
492
+ orderId: own.order.$id,
493
+ paymentId
494
+ };
495
+ let outcome;
496
+ if (step === 'payment-status') outcome = await refreshPosCardPayment(ref);
497
+ else if (step === 'cancel-payment') outcome = await cancelPosCardPayment(ref);
498
+ else {
499
+ var _body_cardNumber;
500
+ if (!posStripeTestMode()) return refuse(404, 'Unknown action');
501
+ outcome = await simulatePosReaderTap(_extends({}, ref, {
502
+ cardNumber: String((_body_cardNumber = body['cardNumber']) != null ? _body_cardNumber : '')
503
+ }));
504
+ }
505
+ if ('error' in outcome) return refuse(outcome.status, outcome.error);
506
+ return {
507
+ status: 200,
508
+ body: {
509
+ sale: posKioskSaleView(outcome.order, gate.deviceId)
510
+ }
511
+ };
512
+ }
513
+ /** "Pay at counter": the order joins the register's queue with its number. */ async function sendToCounter(gate, body) {
514
+ if (!gate.kiosk.payAtCounter) return refuse(409, 'Please pay here with a card.');
515
+ if (!await withinBudget(gate, 'counter', 40)) return refuse(429, 'Too many orders from this kiosk.');
516
+ const own = await readOwnSale(gate, body['orderId']);
517
+ if ('status' in own) return own;
518
+ if (own.view.status === 'queued') return {
519
+ status: 200,
520
+ body: {
521
+ sale: own.view
522
+ }
523
+ };
524
+ const ref = gate.hostRef.collection('orders').doc(own.order.$id);
525
+ const firestore = firebaseAdmin.app().firestore();
526
+ // In a transaction: a card that settled between the customer's two taps
527
+ // leaves a paid order, which never joins a queue.
528
+ const queued = await firestore.runTransaction(async (transaction)=>{
529
+ const fresh = await transaction.get(ref);
530
+ if (!fresh.exists || fresh.get('status') !== 'pending') return false;
531
+ const payments = CommerceModel.orderPayments(fresh.data());
532
+ if (payments.some((payment)=>payment.status === 'pending' || payment.status === 'succeeded')) {
533
+ return false;
534
+ }
535
+ transaction.set(ref, {
536
+ kioskQueueRegisterId: gate.registerId,
537
+ kioskQueuedAtMs: Date.now(),
538
+ timeline: CommerceModel.appendOrderEvent(_extends({}, fresh.data()), 'pos-kiosk-pay-at-counter', 'Sent to the counter from the self-service kiosk')
539
+ }, {
540
+ merge: true
541
+ });
542
+ return true;
543
+ });
544
+ if (!queued) return refuse(409, 'This order is already being paid. Check the screen.');
545
+ const after = await readOwnSale(gate, own.order.$id);
546
+ if ('status' in after) return after;
547
+ return {
548
+ status: 200,
549
+ body: {
550
+ sale: after.view
551
+ }
552
+ };
553
+ }
554
+ async function receipt(gate, body) {
555
+ var _body_channel;
556
+ if (!await withinBudget(gate, 'receipt', 40)) return refuse(429, 'Too many tries.');
557
+ const own = await readOwnSale(gate, body['orderId']);
558
+ if ('status' in own) return own;
559
+ if (own.view.status !== 'paid') return refuse(409, 'Your receipt is ready once your order is paid.');
560
+ const channel = String((_body_channel = body['channel']) != null ? _body_channel : '');
561
+ if (!(await receiptChannels(gate)).includes(channel)) return refuse(400, 'Choose a receipt shown on the screen.');
562
+ const outcome = await recordReceiptChoice(kioskStaff(gate), own.order.$id, {
563
+ channel,
564
+ to: body['to'],
565
+ marketingOptIn: gate.settings.displayMarketingOptIn && body['marketingOptIn'] === true
566
+ });
567
+ if ('error' in outcome) return refuse(outcome.status, outcome.error);
568
+ // The address is on the order now; nothing of it comes back to the screen.
569
+ return {
570
+ status: 200,
571
+ body: {
572
+ ok: true
573
+ }
574
+ };
575
+ }
576
+ /**
577
+ * The idle reset and "Start over": an order this kiosk opened and nobody
578
+ * paid is voided, so it holds no promotion slot and no stock. A queued or
579
+ * paid order is left exactly as it is.
580
+ */ async function abandon(gate, body) {
581
+ const own = await readOwnSale(gate, body['orderId']);
582
+ if ('status' in own) return own;
583
+ if (own.view.status !== 'open') return {
584
+ status: 200,
585
+ body: {
586
+ sale: own.view
587
+ }
588
+ };
589
+ // Money that already moved is the counter's to settle, never a screen's
590
+ // to refund on its own.
591
+ if (CommerceModel.orderPayments(own.order).some((payment)=>payment.status === 'succeeded')) {
592
+ return {
593
+ status: 200,
594
+ body: {
595
+ sale: own.view
596
+ }
597
+ };
598
+ }
599
+ const outcome = await voidPosSale(kioskStaff(gate), own.order.$id);
600
+ if ('error' in outcome) return refuse(outcome.status, outcome.error);
601
+ return {
602
+ status: 200,
603
+ body: {
604
+ sale: posKioskSaleView(outcome.order, gate.deviceId)
605
+ }
606
+ };
607
+ }
608
+ /*==========================================
609
+ * STAFF UNLOCK.
610
+ *
611
+ * The register's staff PINs (AGL-3609), checked on the server: a PIN at a
612
+ * kiosk unlocks its settings — choosing the card reader, leaving kiosk mode
613
+ * — for a few minutes. A kiosk faces the public, so it shows no roster of
614
+ * names; the PIN alone is checked against every member of the site who may
615
+ * work the register. The attempt is claimed on the device's own token row
616
+ * BEFORE any PIN is checked, so a burst of guesses gets
617
+ * POS_KIOSK_UNLOCK_MAX_ATTEMPTS in the lockout window and no more, and a
618
+ * member whose own PIN is locked at the register cannot unlock a kiosk
619
+ * either.
620
+ *=========================================*/ function unlockSecret() {
621
+ const secret = process.env['TOKEN_SIGNING_SECRET'];
622
+ if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured');
623
+ return secret;
624
+ }
625
+ function signUnlock(payload) {
626
+ return createHmac('sha256', unlockSecret()).update(`pos-kiosk-unlock:${payload}`).digest('hex');
627
+ }
628
+ /** A short-lived, signed "staff unlocked THIS kiosk". */ export function mintPosKioskUnlock(deviceId, memberUid, nowMs) {
629
+ const expiresAtMs = nowMs + CommerceModel.POS_KIOSK_UNLOCK_TTL_MS;
630
+ const payload = Buffer.from(JSON.stringify({
631
+ v: 1,
632
+ d: deviceId,
633
+ u: memberUid,
634
+ e: expiresAtMs
635
+ })).toString('base64url');
636
+ return {
637
+ unlock: `${payload}.${signUnlock(payload)}`,
638
+ expiresAtMs
639
+ };
640
+ }
641
+ /** The member a valid unlock names for this kiosk, or null. */ export function readPosKioskUnlock(token, deviceId, nowMs) {
642
+ const value = typeof token === 'string' ? token.trim() : '';
643
+ const dot = value.lastIndexOf('.');
644
+ if (dot <= 0 || value.length > 1000) return null;
645
+ const payload = value.slice(0, dot);
646
+ let expected;
647
+ try {
648
+ expected = signUnlock(payload);
649
+ } catch (unused) {
650
+ return null;
651
+ }
652
+ const a = Buffer.from(value.slice(dot + 1));
653
+ const b = Buffer.from(expected);
654
+ if (a.length !== b.length || !timingSafeEqual(new Uint8Array(a), new Uint8Array(b))) return null;
655
+ try {
656
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
657
+ if ((claims == null ? void 0 : claims.v) !== 1 || claims.d !== deviceId || typeof claims.u !== 'string' || !(Number(claims.e) > nowMs)) {
658
+ return null;
659
+ }
660
+ return claims.u;
661
+ } catch (unused) {
662
+ return null;
663
+ }
664
+ }
665
+ /** Whether a member may still unlock: their role and `managePos` today. */ async function mayUnlock(gate, memberUid) {
666
+ var _host_get;
667
+ var _membership_permissions;
668
+ const host = await gate.hostRef.get();
669
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[memberUid];
670
+ if (role !== 'admin' && role !== 'editor') return false;
671
+ const membership = await resolveOrgPermissions(memberUid, {
672
+ hostId: gate.hostId
673
+ });
674
+ return (membership == null ? void 0 : (_membership_permissions = membership.permissions) == null ? void 0 : _membership_permissions.managePos) === true;
675
+ }
676
+ async function unlock(gate, req, body) {
677
+ var _body_pin, _ref, _host_get;
678
+ var _req_socket;
679
+ const pin = String((_body_pin = body['pin']) != null ? _body_pin : '');
680
+ if (!/^\d{4,6}$/.test(pin)) return refuse(400, 'Enter your staff PIN.');
681
+ const address = String((_ref = (_req_socket = req.socket) == null ? void 0 : _req_socket.remoteAddress) != null ? _ref : '') || 'no-address';
682
+ const rate = await consumeRateLimit(`pos-kiosk-unlock:${address}`, {
683
+ limit: 20,
684
+ windowMs: 15 * 60 * 1000
685
+ });
686
+ if (!rate.allowed) return refuse(429, 'Too many tries. Wait a few minutes and try again.');
687
+ try {
688
+ unlockSecret();
689
+ } catch (unused) {
690
+ return refuse(501, 'Staff PINs are not configured on this server.');
691
+ }
692
+ const firestore = firebaseAdmin.app().firestore();
693
+ const now = Date.now();
694
+ const claim = await firestore.runTransaction(async (transaction)=>{
695
+ var _snapshot_get, _snapshot_get1;
696
+ const snapshot = await transaction.get(gate.device.ref);
697
+ if (!snapshot.exists) return {
698
+ kind: 'gone'
699
+ };
700
+ const lockedUntilMs = Number((_snapshot_get = snapshot.get('kioskUnlockLockedUntilMs')) != null ? _snapshot_get : 0);
701
+ if (lockedUntilMs > now) return {
702
+ kind: 'locked',
703
+ lockedUntilMs
704
+ };
705
+ const failed = lockedUntilMs ? 0 : Number((_snapshot_get1 = snapshot.get('kioskUnlockFailures')) != null ? _snapshot_get1 : 0);
706
+ if (failed >= CommerceModel.POS_KIOSK_UNLOCK_MAX_ATTEMPTS) {
707
+ const until = now + CommerceModel.POS_KIOSK_UNLOCK_LOCKOUT_MS;
708
+ transaction.update(gate.device.ref, {
709
+ kioskUnlockLockedUntilMs: until,
710
+ kioskUnlockFailures: 0
711
+ });
712
+ return {
713
+ kind: 'locked',
714
+ lockedUntilMs: until
715
+ };
716
+ }
717
+ transaction.update(gate.device.ref, {
718
+ kioskUnlockFailures: failed + 1,
719
+ kioskUnlockLockedUntilMs: null
720
+ });
721
+ return {
722
+ kind: 'claimed',
723
+ attempt: failed + 1
724
+ };
725
+ });
726
+ if (claim.kind === 'gone') return refuse(401, 'This kiosk is not paired.');
727
+ if (claim.kind === 'locked') {
728
+ return {
729
+ status: 423,
730
+ body: {
731
+ error: 'Too many wrong PINs. Try again later.',
732
+ lockedUntilMs: claim.lockedUntilMs
733
+ }
734
+ };
735
+ }
736
+ const host = await gate.hostRef.get();
737
+ const roles = (_host_get = host.get('memberRoles')) != null ? _host_get : {};
738
+ const pins = await gate.hostRef.collection('posStaffPins').limit(50).get();
739
+ let matched = '';
740
+ for (const doc of pins.docs){
741
+ var _doc_get, _doc_get1;
742
+ const uid = String(doc.id);
743
+ if (roles[uid] !== 'admin' && roles[uid] !== 'editor') continue;
744
+ if (Number((_doc_get = doc.get('lockedUntilMs')) != null ? _doc_get : 0) > now) continue;
745
+ const hash = String((_doc_get1 = doc.get('pinScrypt')) != null ? _doc_get1 : '');
746
+ if (hash && verifyMemberPassword(pin, hash)) {
747
+ matched = uid;
748
+ break;
749
+ }
750
+ }
751
+ if (!matched || !await mayUnlock(gate, matched)) {
752
+ const left = CommerceModel.POS_KIOSK_UNLOCK_MAX_ATTEMPTS - claim.attempt;
753
+ if (left <= 0) {
754
+ await gate.device.ref.update({
755
+ kioskUnlockLockedUntilMs: now + CommerceModel.POS_KIOSK_UNLOCK_LOCKOUT_MS,
756
+ kioskUnlockFailures: 0
757
+ });
758
+ return {
759
+ status: 423,
760
+ body: {
761
+ error: 'Too many wrong PINs. Try again later.'
762
+ }
763
+ };
764
+ }
765
+ return {
766
+ status: 401,
767
+ body: {
768
+ error: `That PIN is not right. ${left} ${left === 1 ? 'try' : 'tries'} left.`
769
+ }
770
+ };
771
+ }
772
+ await gate.device.ref.update({
773
+ kioskUnlockFailures: 0,
774
+ kioskUnlockLockedUntilMs: null
775
+ });
776
+ const minted = mintPosKioskUnlock(gate.deviceId, matched, now);
777
+ return {
778
+ status: 200,
779
+ body: minted
780
+ };
781
+ }
782
+ async function staffSettings(gate, body, action) {
783
+ var _gate_device_data_label;
784
+ const memberUid = readPosKioskUnlock(body['unlock'], gate.deviceId, Date.now());
785
+ if (!memberUid || !await mayUnlock(gate, memberUid)) return refuse(401, 'Enter your staff PIN again.');
786
+ if (action === 'exit') {
787
+ // Leaving kiosk mode unpairs the device: a new code from the register
788
+ // is the only way back.
789
+ await gate.device.ref.delete();
790
+ return {
791
+ status: 200,
792
+ body: {
793
+ ok: true
794
+ }
795
+ };
796
+ }
797
+ const readers = await registerReaders(gate);
798
+ let data = gate.device.data;
799
+ if (action === 'set-reader') {
800
+ var _body_readerId;
801
+ const readerId = String((_body_readerId = body['readerId']) != null ? _body_readerId : '');
802
+ if (readerId && !readers.some((reader)=>reader.id === readerId)) {
803
+ return refuse(404, 'That card reader is not on this register.');
804
+ }
805
+ await gate.device.ref.set({
806
+ kioskReaderId: readerId || null
807
+ }, {
808
+ merge: true
809
+ });
810
+ data = _extends({}, data, {
811
+ kioskReaderId: readerId || null
812
+ });
813
+ }
814
+ const chosen = await kioskReaderId(_extends({}, gate, {
815
+ device: _extends({}, gate.device, {
816
+ data
817
+ })
818
+ }));
819
+ return {
820
+ status: 200,
821
+ body: {
822
+ readers,
823
+ readerId: chosen,
824
+ label: String((_gate_device_data_label = gate.device.data['label']) != null ? _gate_device_data_label : 'Self-service kiosk')
825
+ }
826
+ };
827
+ }
828
+ /** A Terminal SDK connection token for the native app's Tap to Pay, scoped to the site's Location. */ async function connectionToken(gate) {
829
+ if (!posTerminalAvailable()) return refuse(409, 'Card payments are not available on this kiosk.');
830
+ if (!await withinBudget(gate, 'connection-token', 60)) return refuse(429, 'Too many tries.');
831
+ const token = await createPosTerminalConnectionToken(gate.hostId);
832
+ if (!token) return refuse(409, 'Set up a card reader location for this site first.');
833
+ return {
834
+ status: 200,
835
+ body: token
836
+ };
837
+ }
838
+ async function staffAction(req, body, action) {
839
+ var _body_hostId, _body_registerId, _body_orderId, _order_number, _order_lineItems;
840
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
841
+ const gate = await authorizePosStaff(req, hostId);
842
+ if ('error' in gate) return refuse(gate.status, gate.error);
843
+ const registerId = String((_body_registerId = body['registerId']) != null ? _body_registerId : '');
844
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(registerId)) return refuse(400, 'Missing registerId');
845
+ const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
846
+ if (!(await hostRef.collection('registers').doc(registerId).get()).exists) {
847
+ return refuse(404, 'Unknown register');
848
+ }
849
+ if (action === 'queue') {
850
+ // `kioskQueueRegisterId ==, status ==, kioskQueuedAtMs ASC`, declared in
851
+ // the index file: the oldest order first, as a counter serves them.
852
+ const snapshot = await hostRef.collection('orders').where('kioskQueueRegisterId', '==', registerId).where('status', '==', 'pending').orderBy('kioskQueuedAtMs', 'asc').limit(25).get();
853
+ const entries = snapshot.docs.map((doc)=>{
854
+ var _order_number, _order_kioskQueuedAtMs, _ref, _order_lineItems;
855
+ var _order_totals;
856
+ const order = CommerceModel.liftLegacyOrder(doc.data());
857
+ return {
858
+ orderId: String(doc.id),
859
+ number: Number((_order_number = order.number) != null ? _order_number : 0),
860
+ queuedAtMs: Number((_order_kioskQueuedAtMs = order.kioskQueuedAtMs) != null ? _order_kioskQueuedAtMs : 0),
861
+ totalCents: Number((_ref = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref : 0),
862
+ itemCount: ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).reduce((sum, line)=>sum + line.quantity, 0)
863
+ };
864
+ });
865
+ return {
866
+ status: 200,
867
+ body: {
868
+ entries,
869
+ currency: await posDisplayCurrency(hostId)
870
+ }
871
+ };
872
+ }
873
+ // `take`: the queued order, as the register loads it into its tender panel.
874
+ const orderId = String((_body_orderId = body['orderId']) != null ? _body_orderId : '');
875
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(orderId)) return refuse(400, 'Missing orderId');
876
+ const order = await readPosSale(hostId, orderId);
877
+ if (!order || order.kioskQueueRegisterId !== registerId) return refuse(404, 'Unknown order');
878
+ if (order.status !== 'pending') return refuse(409, 'This order is no longer waiting for payment.');
879
+ return {
880
+ status: 200,
881
+ body: {
882
+ sale: posSaleSummary(order),
883
+ number: Number((_order_number = order.number) != null ? _order_number : 0),
884
+ lines: ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).map((line)=>{
885
+ var _line_modifiers;
886
+ return _extends({
887
+ productId: line.productId
888
+ }, line.variantId ? {
889
+ variantId: line.variantId
890
+ } : {}, {
891
+ name: line.name
892
+ }, line.variantLabel ? {
893
+ variantLabel: line.variantLabel
894
+ } : {}, {
895
+ unitAmountCents: line.unitAmountCents,
896
+ quantity: line.quantity
897
+ }, ((_line_modifiers = line.modifiers) == null ? void 0 : _line_modifiers.length) ? {
898
+ modifiers: line.modifiers.map((modifier)=>({
899
+ groupId: modifier.groupId,
900
+ optionId: modifier.optionId
901
+ }))
902
+ } : {});
903
+ })
904
+ }
905
+ };
906
+ }
907
+ export async function handlePosKiosk(req) {
908
+ var _body_action;
909
+ const body = req.method === 'GET' ? posQueryBody(req) : posRequestBody(req);
910
+ const action = String((_body_action = body['action']) != null ? _body_action : '');
911
+ const read = DEVICE_READS.has(action) || STAFF_READS.has(action);
912
+ if (!(req.method === 'POST' || req.method === 'GET' && read)) {
913
+ return refuse(405, 'Method not allowed');
914
+ }
915
+ if (action === 'queue' || action === 'take') return await staffAction(req, body, action);
916
+ const gate = await authorizeKiosk(req, body, !read);
917
+ if ('status' in gate) return gate;
918
+ switch(action){
919
+ case 'context':
920
+ return await context(gate);
921
+ case 'catalog':
922
+ return await catalog(gate, body);
923
+ case 'sale':
924
+ {
925
+ const own = await readOwnSale(gate, body['orderId']);
926
+ return 'status' in own ? own : {
927
+ status: 200,
928
+ body: {
929
+ sale: own.view
930
+ }
931
+ };
932
+ }
933
+ case 'checkout':
934
+ return await checkout(gate, req, body);
935
+ case 'pay':
936
+ return await pay(gate, req, body);
937
+ case 'payment-status':
938
+ case 'cancel-payment':
939
+ case 'simulate':
940
+ return await cardStep(gate, body, action);
941
+ case 'counter':
942
+ return await sendToCounter(gate, body);
943
+ case 'receipt':
944
+ return await receipt(gate, body);
945
+ case 'abandon':
946
+ return await abandon(gate, body);
947
+ case 'connection-token':
948
+ return await connectionToken(gate);
949
+ case 'unlock':
950
+ return await unlock(gate, req, body);
951
+ case 'settings':
952
+ case 'set-reader':
953
+ case 'exit':
954
+ return await staffSettings(gate, body, action);
955
+ default:
956
+ return refuse(400, 'Unknown action');
957
+ }
958
+ }
959
+ /** `GET|POST /api/commerce/pos-kiosk`. */ export const posKioskHandler = async (req, res)=>{
960
+ res.setHeader('Cache-Control', 'no-store');
961
+ try {
962
+ const outcome = await handlePosKiosk(req);
963
+ return res.status(outcome.status).json(outcome.body);
964
+ } catch (error) {
965
+ console.error('[pos-kiosk]', error);
966
+ return res.status(500).json({
967
+ error: 'Something went wrong. Please order at the counter.'
968
+ });
969
+ }
970
+ };
971
+
972
+ //# sourceMappingURL=pos-kiosk.js.map