@aglyn/plugins-commerce 1.0.0-beta.231 → 1.0.0-beta.233

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 (266) 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/catalog-organization-card.component.js +1 -0
  13. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  14. package/src/lib/components/console/host-orders-card.component.js +42 -11
  15. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  16. package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
  17. package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
  18. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
  19. package/src/lib/components/console/locations-card.component.js +177 -3
  20. package/src/lib/components/console/locations-card.component.js.map +1 -1
  21. package/src/lib/components/console/order-detail-dialog.component.js +10 -0
  22. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  23. package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
  24. package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
  25. package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
  26. package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
  27. package/src/lib/components/console/order-offline-notice.component.js +53 -0
  28. package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
  30. package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
  31. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
  32. package/src/lib/components/console/pos/pos-api.d.ts +24 -1
  33. package/src/lib/components/console/pos/pos-api.js +13 -0
  34. package/src/lib/components/console/pos/pos-api.js.map +1 -1
  35. package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
  36. package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
  37. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  38. package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
  39. package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
  40. package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
  41. package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
  42. package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
  43. package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
  44. package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
  45. package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
  46. package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
  47. package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
  48. package/src/lib/components/console/pos/use-pos-display.js +3 -2
  49. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  50. package/src/lib/components/console/pos-devices-card.component.js +2 -2
  51. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  52. package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
  53. package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
  54. package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
  55. package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
  56. package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
  57. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  58. package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
  59. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
  60. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
  61. package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
  62. package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
  63. package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
  64. package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
  65. package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
  66. package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
  67. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
  68. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
  69. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
  70. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
  71. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
  72. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
  73. package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
  74. package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
  75. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
  76. package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
  77. package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
  78. package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
  79. package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
  80. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
  81. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
  82. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
  83. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
  84. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
  85. package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
  86. package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
  87. package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
  88. package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
  89. package/src/lib/components/console/pos-offline/register-offline.js +24 -0
  90. package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
  91. package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
  92. package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
  93. package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
  94. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
  95. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
  96. package/src/lib/components/console/pos-page.component.js +228 -51
  97. package/src/lib/components/console/pos-page.component.js.map +1 -1
  98. package/src/lib/components/console/pos-zones.d.ts +23 -0
  99. package/src/lib/components/console/pos-zones.js +31 -0
  100. package/src/lib/components/console/pos-zones.js.map +1 -0
  101. package/src/lib/components/console/product-editor-dialog.component.js +10 -64
  102. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  103. package/src/lib/components/console/products-hub-card.component.js +46 -30
  104. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  105. package/src/lib/components/console/shipping-settings-card.component.js +1 -1
  106. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  107. package/src/lib/components/console/stock-movements-card.component.js +48 -16
  108. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  109. package/src/lib/components/console/store-zones.d.ts +8 -1
  110. package/src/lib/components/console/store-zones.js +11 -1
  111. package/src/lib/components/console/store-zones.js.map +1 -1
  112. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -0
  113. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  114. package/src/lib/components/order-status.js +66 -0
  115. package/src/lib/components/order-status.js.map +1 -1
  116. package/src/lib/components/product-detail.js +42 -15
  117. package/src/lib/components/product-detail.js.map +1 -1
  118. package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
  119. package/src/lib/constants/local-fulfillment-list-query.js +84 -0
  120. package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
  121. package/src/lib/constants/orders-list-query.d.ts +7 -2
  122. package/src/lib/constants/orders-list-query.js +89 -8
  123. package/src/lib/constants/orders-list-query.js.map +1 -1
  124. package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
  125. package/src/lib/constants/pos-shift-list-query.js +115 -0
  126. package/src/lib/constants/pos-shift-list-query.js.map +1 -0
  127. package/src/lib/constants/product-list-query.d.ts +6 -1
  128. package/src/lib/constants/product-list-query.js +77 -8
  129. package/src/lib/constants/product-list-query.js.map +1 -1
  130. package/src/lib/constants/stock-movement-query.d.ts +3 -2
  131. package/src/lib/constants/stock-movement-query.js +52 -8
  132. package/src/lib/constants/stock-movement-query.js.map +1 -1
  133. package/src/lib/declarations.server.js +21 -0
  134. package/src/lib/declarations.server.js.map +1 -1
  135. package/src/lib/model/buyer-notifications.d.ts +5 -3
  136. package/src/lib/model/buyer-notifications.js +25 -4
  137. package/src/lib/model/buyer-notifications.js.map +1 -1
  138. package/src/lib/model/commerce-orders.d.ts +103 -1
  139. package/src/lib/model/commerce-orders.js +2 -1
  140. package/src/lib/model/commerce-orders.js.map +1 -1
  141. package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
  142. package/src/lib/model/commerce-pos-kiosk.js +147 -0
  143. package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
  144. package/src/lib/model/commerce-pos-offline.d.ts +234 -0
  145. package/src/lib/model/commerce-pos-offline.js +301 -0
  146. package/src/lib/model/commerce-pos-offline.js.map +1 -0
  147. package/src/lib/model/commerce-pos-ops.d.ts +16 -6
  148. package/src/lib/model/commerce-pos-ops.js +12 -4
  149. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  150. package/src/lib/model/commerce-pos.d.ts +13 -1
  151. package/src/lib/model/commerce-pos.js +5 -4
  152. package/src/lib/model/commerce-pos.js.map +1 -1
  153. package/src/lib/model/commerce-stock-holds.d.ts +24 -3
  154. package/src/lib/model/commerce-stock-holds.js +26 -8
  155. package/src/lib/model/commerce-stock-holds.js.map +1 -1
  156. package/src/lib/model/commerce.d.ts +7 -1
  157. package/src/lib/model/commerce.js.map +1 -1
  158. package/src/lib/model/index.d.ts +5 -0
  159. package/src/lib/model/index.js +5 -0
  160. package/src/lib/model/index.js.map +1 -1
  161. package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
  162. package/src/lib/model/local-fulfillment-settings.js +361 -0
  163. package/src/lib/model/local-fulfillment-settings.js.map +1 -0
  164. package/src/lib/model/order-list-fields.d.ts +10 -1
  165. package/src/lib/model/order-list-fields.js +1 -0
  166. package/src/lib/model/order-list-fields.js.map +1 -1
  167. package/src/lib/model/order-local-fulfillment.d.ts +131 -0
  168. package/src/lib/model/order-local-fulfillment.js +125 -0
  169. package/src/lib/model/order-local-fulfillment.js.map +1 -0
  170. package/src/lib/model/order-shipping-export.js +4 -0
  171. package/src/lib/model/order-shipping-export.js.map +1 -1
  172. package/src/lib/model/order-status-view.d.ts +20 -1
  173. package/src/lib/model/order-status-view.js +60 -6
  174. package/src/lib/model/order-status-view.js.map +1 -1
  175. package/src/lib/model/product-write.d.ts +186 -0
  176. package/src/lib/model/product-write.js +116 -0
  177. package/src/lib/model/product-write.js.map +1 -0
  178. package/src/lib/plugin-config.js +6 -2
  179. package/src/lib/plugin-config.js.map +1 -1
  180. package/src/lib/plugin.js +11 -0
  181. package/src/lib/plugin.js.map +1 -1
  182. package/src/lib/pos-kiosk-config.d.ts +26 -0
  183. package/src/lib/pos-kiosk-config.js +59 -0
  184. package/src/lib/pos-kiosk-config.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.js +8 -0
  186. package/src/lib/server/api-v1/openapi.js.map +1 -1
  187. package/src/lib/server/api-v1/order-view.d.ts +19 -0
  188. package/src/lib/server/api-v1/order-view.js +35 -0
  189. package/src/lib/server/api-v1/order-view.js.map +1 -1
  190. package/src/lib/server/billing-webhook.js +238 -87
  191. package/src/lib/server/billing-webhook.js.map +1 -1
  192. package/src/lib/server/cart-checkout.d.ts +2 -0
  193. package/src/lib/server/cart-checkout.js +173 -13
  194. package/src/lib/server/cart-checkout.js.map +1 -1
  195. package/src/lib/server/channel-order-updates.d.ts +50 -0
  196. package/src/lib/server/channel-order-updates.js +290 -0
  197. package/src/lib/server/channel-order-updates.js.map +1 -0
  198. package/src/lib/server/channel-orders.d.ts +85 -0
  199. package/src/lib/server/channel-orders.js +543 -0
  200. package/src/lib/server/channel-orders.js.map +1 -0
  201. package/src/lib/server/checkout-extras.js +18 -4
  202. package/src/lib/server/checkout-extras.js.map +1 -1
  203. package/src/lib/server/checkout.js +71 -14
  204. package/src/lib/server/checkout.js.map +1 -1
  205. package/src/lib/server/local-fulfillment-status.d.ts +86 -0
  206. package/src/lib/server/local-fulfillment-status.js +379 -0
  207. package/src/lib/server/local-fulfillment-status.js.map +1 -0
  208. package/src/lib/server/local-fulfillment.d.ts +190 -0
  209. package/src/lib/server/local-fulfillment.js +580 -0
  210. package/src/lib/server/local-fulfillment.js.map +1 -0
  211. package/src/lib/server/membership-register.js +3 -1
  212. package/src/lib/server/membership-register.js.map +1 -1
  213. package/src/lib/server/order-notifications.js +61 -0
  214. package/src/lib/server/order-notifications.js.map +1 -1
  215. package/src/lib/server/pos-display.d.ts +27 -1
  216. package/src/lib/server/pos-display.js +50 -9
  217. package/src/lib/server/pos-display.js.map +1 -1
  218. package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
  219. package/src/lib/server/pos-kiosk-principal.js +42 -0
  220. package/src/lib/server/pos-kiosk-principal.js.map +1 -0
  221. package/src/lib/server/pos-kiosk.d.ts +54 -0
  222. package/src/lib/server/pos-kiosk.js +972 -0
  223. package/src/lib/server/pos-kiosk.js.map +1 -0
  224. package/src/lib/server/pos-offline-sync.d.ts +85 -0
  225. package/src/lib/server/pos-offline-sync.js +765 -0
  226. package/src/lib/server/pos-offline-sync.js.map +1 -0
  227. package/src/lib/server/pos-order.js +22 -4
  228. package/src/lib/server/pos-order.js.map +1 -1
  229. package/src/lib/server/pos-payment.d.ts +6 -0
  230. package/src/lib/server/pos-payment.js +225 -6
  231. package/src/lib/server/pos-payment.js.map +1 -1
  232. package/src/lib/server/pos-print.js +5 -0
  233. package/src/lib/server/pos-print.js.map +1 -1
  234. package/src/lib/server/pos-return.js +27 -0
  235. package/src/lib/server/pos-return.js.map +1 -1
  236. package/src/lib/server/pos-sale.d.ts +6 -0
  237. package/src/lib/server/pos-sale.js +2 -0
  238. package/src/lib/server/pos-sale.js.map +1 -1
  239. package/src/lib/server/pos-shift.js +8 -1
  240. package/src/lib/server/pos-shift.js.map +1 -1
  241. package/src/lib/server/product-catalog.js +5 -1
  242. package/src/lib/server/product-catalog.js.map +1 -1
  243. package/src/lib/server/product-drafts.d.ts +5 -1
  244. package/src/lib/server/product-drafts.js +3 -3
  245. package/src/lib/server/product-drafts.js.map +1 -1
  246. package/src/lib/server/product-writer.d.ts +19 -0
  247. package/src/lib/server/product-writer.js +261 -0
  248. package/src/lib/server/product-writer.js.map +1 -0
  249. package/src/lib/server/products-write.d.ts +61 -0
  250. package/src/lib/server/products-write.js +375 -0
  251. package/src/lib/server/products-write.js.map +1 -0
  252. package/src/lib/server/refund.js +3 -0
  253. package/src/lib/server/refund.js.map +1 -1
  254. package/src/lib/server/stock-hold.d.ts +9 -1
  255. package/src/lib/server/stock-hold.js +15 -7
  256. package/src/lib/server/stock-hold.js.map +1 -1
  257. package/src/lib/server.js +21 -0
  258. package/src/lib/server.js.map +1 -1
  259. package/src/lib/tenant-emails.js +161 -0
  260. package/src/lib/tenant-emails.js.map +1 -1
  261. package/src/lib/testing/memory-firestore.d.ts +16 -3
  262. package/src/lib/testing/memory-firestore.js +11 -3
  263. package/src/lib/testing/memory-firestore.js.map +1 -1
  264. package/src/lib/transfer/products.server.d.ts +5 -0
  265. package/src/lib/transfer/products.server.js +2 -2
  266. package/src/lib/transfer/products.server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/orders-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListFilterClause,\n ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n ORDER_CHANNEL_LABELS,\n ORDER_STATUS_LABELS,\n type OrderChannel,\n type OrderStatus,\n} from '../model/commerce-orders'\n\n/*\n * THE ORDERS LIST, ON ITS QUERY (AGL-3321).\n *\n * `hosts/{hostId}/orders`, newest first. Every filter the grid's panel offers\n * and the quick search's word is a predicate on the ONE Firestore query that\n * pages the list (`planListQuery` through `useListQuery`), over the fields\n * every writer stamps with `orderListFields` — so page two of a filtered list\n * is page two of the matches, not page two of the store narrowed afterwards.\n *\n * One order: `createdAtMs` descending, the field every writer stamps. Each\n * filterable field is then ONE `(field, createdAtMs DESC)` composite, and\n * index merging serves every combination of them; the date range is on that\n * same field, so it adds none. Eight composites in all, pinned by\n * `orders-list-query.spec.ts` against `cloud/firebase-firestore.indexes.json`.\n *\n * What one query cannot hold is refused by name above the grid\n * (`ListQueryNotices`, labelled by `listQueryRefusals` from these fields,\n * headers and options), never matched over the rows it returned: the search,\n * Customer `contains`, Order `contains` and Product each take the query's one\n * array clause, so two of them together keep the first and refuse the second.\n *\n * A second range (a total, say) would be a new `sorts` entry and one more\n * composite per filterable field beneath it — eight — plus the range field's\n * own order; nothing else here moves.\n */\n\n/** The grid columns, each as the query reads it. */\nexport const ORDER_LIST_FIELDS: readonly ListFilterField[] = [\n {\n // The number as typed, `#1042` or `1042`, by word prefix.\n column: 'orderLabel',\n kind: 'text',\n path: 'number',\n tokensPath: 'orderLabelTokens',\n operators: ['contains'],\n },\n {\n // `is` a whole address; `contains` an address's piece or its domain.\n column: 'customerEmail',\n kind: 'text',\n path: 'customerEmail',\n lowerPath: 'customerEmailLower',\n tokensPath: 'customerEmailTokens',\n operators: ['contains', 'equals'],\n },\n {\n column: 'channelKey',\n kind: 'exact',\n path: 'channel',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'statusKey',\n kind: 'exact',\n path: 'status',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'createdAtMs',\n kind: 'date',\n path: 'createdAtMs',\n storedAs: 'millis',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n // Every product in the order, matched whole — ids keep their case.\n column: 'productIds',\n kind: 'exact',\n path: 'productIds',\n tokensPath: 'productIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'disputeKey',\n kind: 'exact',\n path: 'disputeKey',\n operators: ['equals', 'isAnyOf'],\n },\n]\n\n/** The list's query: its fields, its one order and its search. */\nexport const ORDER_LIST_QUERY: ListQueryDeclaration = {\n fields: ORDER_LIST_FIELDS,\n sorts: [{ path: 'createdAtMs', direction: 'desc', column: 'createdAtMs' }],\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const ORDER_LIST_HEADERS: Readonly<Record<string, string>> = {\n orderLabel: 'Order',\n customerEmail: 'Customer',\n channelKey: 'Channel',\n statusKey: 'Status',\n createdAtMs: 'Date',\n productIds: 'Product',\n disputeKey: 'Disputes',\n}\n\n/** The fields picked from choices, which the panel shows as a select. */\nexport const ORDER_LIST_SELECT_FIELDS: readonly string[] = [\n 'channelKey',\n 'statusKey',\n 'productIds',\n 'disputeKey',\n]\n\nexport const ORDER_STATUS_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_STATUS_LABELS) as OrderStatus[]\n).map((status) => ({ value: status, label: ORDER_STATUS_LABELS[status] }))\n\nexport const ORDER_CHANNEL_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_CHANNEL_LABELS) as OrderChannel[]\n).map((channel) => ({ value: channel, label: ORDER_CHANNEL_LABELS[channel] }))\n\n/*\n * The dispute badge's tones (`orderDisputeKey`), labelled as the badge\n * labels them. `lost` is the tone, not the raw dispute status: a dispute\n * that was WON also closes with money untouched, and listing it as charged\n * back would tell a merchant they lost a case they won.\n */\nexport const ORDER_DISPUTE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'open', label: 'Open dispute' },\n { value: 'lost', label: 'Charged back' },\n { value: 'won', label: 'Dispute won' },\n { value: 'settled', label: 'Dispute closed' },\n]\n\n/** The clause the open-dispute banner's \"Show them\" sets. */\nexport const OPEN_DISPUTE_CLAUSE: ListFilterClause = {\n field: 'disputeKey',\n op: 'equals',\n value: 'open',\n}\n\n/**\n * The Customer clause the CRM's link seeds from `?email=` (AGL-2622).\n *\n * A whole address is `is`, served by the lower-cased key, so it finds that\n * buyer and no one whose address merely begins the same way. Anything else —\n * a domain, `@acme.com`, a name — is `contains`, a word prefix of the\n * address, so a domain finds every buyer at a company.\n */\nexport function ordersCustomerClause(raw: string): ListFilterClause | null {\n const value = raw.trim().replace(/^@+/, '')\n if (!value) return null\n const at = value.indexOf('@')\n return at > 0 && at < value.length - 1\n ? { field: 'customerEmail', op: 'equals', value }\n : { field: 'customerEmail', op: 'contains', value }\n}\n"],"names":["ORDER_CHANNEL_LABELS","ORDER_STATUS_LABELS","ORDER_LIST_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","verbatimTokens","ORDER_LIST_QUERY","fields","sorts","direction","search","ORDER_LIST_HEADERS","orderLabel","customerEmail","channelKey","statusKey","createdAtMs","productIds","disputeKey","ORDER_LIST_SELECT_FIELDS","ORDER_STATUS_OPTIONS","Object","keys","map","status","value","label","ORDER_CHANNEL_OPTIONS","channel","ORDER_DISPUTE_OPTIONS","OPEN_DISPUTE_CLAUSE","field","op","ordersCustomerClause","raw","trim","replace","at","indexOf","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAQD,SACEA,oBAAoB,EACpBC,mBAAmB,QAGd,8BAA0B;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GAED,kDAAkD,GAClD,OAAO,MAAMC,oBAAgD;IAC3D;QACE,0DAA0D;QAC1DC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZC,WAAW;YAAC;SAAW;IACzB;IACA;QACE,qEAAqE;QACrEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNG,WAAW;QACXF,YAAY;QACZC,WAAW;YAAC;YAAY;SAAS;IACnC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNI,UAAU;QACVF,WAAW;YAAC;YAAU;SAAU;IAClC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNK,UAAU;QACVD,UAAU;QACVF,WAAW;YAAC;YAAM;YAAS;YAAa;YAAU;SAAa;IACjE;IACA;QACE,mEAAmE;QACnEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,YAAY;QACZK,gBAAgB;QAChBJ,WAAW;YAAC;SAAU;IACxB;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNE,WAAW;YAAC;YAAU;SAAU;IAClC;CACD,CAAA;AAED,gEAAgE,GAChE,OAAO,MAAMK,mBAAyC;IACpDC,QAAQX;IACRY,OAAO;QAAC;YAAET,MAAM;YAAeU,WAAW;YAAQZ,QAAQ;QAAc;KAAE;IAC1Ea,QAAQ;QAAEV,YAAY;IAAe;AACvC,EAAC;AAED,OAAO,MAAMW,qBAAuD;IAClEC,YAAY;IACZC,eAAe;IACfC,YAAY;IACZC,WAAW;IACXC,aAAa;IACbC,YAAY;IACZC,YAAY;AACd,EAAC;AAED,uEAAuE,GACvE,OAAO,MAAMC,2BAA8C;IACzD;IACA;IACA;IACA;CACD,CAAA;AAED,OAAO,MAAMC,uBAAoD,AAC/DC,OAAOC,IAAI,CAAC3B,qBACZ4B,GAAG,CAAC,CAACC,SAAY,CAAA;QAAEC,OAAOD;QAAQE,OAAO/B,mBAAmB,CAAC6B,OAAO;IAAC,CAAA,GAAG;AAE1E,OAAO,MAAMG,wBAAqD,AAChEN,OAAOC,IAAI,CAAC5B,sBACZ6B,GAAG,CAAC,CAACK,UAAa,CAAA;QAAEH,OAAOG;QAASF,OAAOhC,oBAAoB,CAACkC,QAAQ;IAAC,CAAA,GAAG;AAE9E;;;;;CAKC,GACD,OAAO,MAAMC,wBAAqD;IAChE;QAAEJ,OAAO;QAAQC,OAAO;IAAe;IACvC;QAAED,OAAO;QAAQC,OAAO;IAAe;IACvC;QAAED,OAAO;QAAOC,OAAO;IAAc;IACrC;QAAED,OAAO;QAAWC,OAAO;IAAiB;CAC7C,CAAA;AAED,2DAA2D,GAC3D,OAAO,MAAMI,sBAAwC;IACnDC,OAAO;IACPC,IAAI;IACJP,OAAO;AACT,EAAC;AAED;;;;;;;CAOC,GACD,OAAO,SAASQ,qBAAqBC,GAAW;IAC9C,MAAMT,QAAQS,IAAIC,IAAI,GAAGC,OAAO,CAAC,OAAO;IACxC,IAAI,CAACX,OAAO,OAAO;IACnB,MAAMY,KAAKZ,MAAMa,OAAO,CAAC;IACzB,OAAOD,KAAK,KAAKA,KAAKZ,MAAMc,MAAM,GAAG,IACjC;QAAER,OAAO;QAAiBC,IAAI;QAAUP;IAAM,IAC9C;QAAEM,OAAO;QAAiBC,IAAI;QAAYP;IAAM;AACtD"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/orders-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListFilterClause,\n ListFilterOption,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n ORDER_CHANNEL_LABELS,\n ORDER_STATUS_LABELS,\n type OrderChannel,\n type OrderStatus,\n} from '../model/commerce-orders'\n\n/*\n * THE ORDERS LIST, ON ITS QUERY (AGL-3321).\n *\n * `hosts/{hostId}/orders`, newest first. Every filter the grid's panel offers\n * and the quick search's word is a predicate on the ONE Firestore query that\n * pages the list (`planListQuery` through `useListQuery`), over the fields\n * every writer stamps with `orderListFields` — so page two of a filtered list\n * is page two of the matches, not page two of the store narrowed afterwards.\n *\n * One order: `createdAtMs` descending, the field every writer stamps. Each\n * filterable field is then ONE `(field, createdAtMs DESC)` composite, and\n * index merging serves every combination of them; the date range is on that\n * same field, so it adds none. Eight composites in all, pinned by\n * `orders-list-query.spec.ts` against `cloud/firebase-firestore.indexes.json`.\n *\n * What one query cannot hold is refused by name above the grid\n * (`ListQueryNotices`, labelled by `listQueryRefusals` from these fields,\n * headers and options), never matched over the rows it returned: the search,\n * Customer `contains`, Order `contains` and Product each take the query's one\n * array clause, so two of them together keep the first and refuse the second.\n *\n * A second range (a total, say) would be a new `sorts` entry and one more\n * composite per filterable field beneath it — eight — plus the range field's\n * own order; nothing else here moves.\n *\n * ## Every header sorts (AGL-3680)\n *\n * `ORDER_LIST_COLUMN_SORTS`: Date either way, and Order, Customer, Channel and\n * Status, each on the field `orderListFields` stamps on every order —\n * `number` (null on an order with none: POS, draft and channel orders are\n * unnumbered), `customerEmailLower`, `channel`, `status`.\n * `tools/scripts/backfill-orders-list-fields.mjs` stamps `number` and an\n * absent `customerEmailLower` on the orders written before. Every order but\n * the default is `alone`: served with no filter or search on, it costs no\n * composite on this unscoped list, and asked under a filter it falls back to\n * newest first and says so. Total is computed from the refunds, so it sorts\n * the page on screen.\n */\n\n/** The grid columns, each as the query reads it. */\nexport const ORDER_LIST_FIELDS: readonly ListFilterField[] = [\n {\n // The number as typed, `#1042` or `1042`, by word prefix.\n column: 'orderLabel',\n kind: 'text',\n path: 'number',\n tokensPath: 'orderLabelTokens',\n operators: ['contains'],\n },\n {\n // `is` a whole address; `contains` an address's piece or its domain.\n column: 'customerEmail',\n kind: 'text',\n path: 'customerEmail',\n lowerPath: 'customerEmailLower',\n tokensPath: 'customerEmailTokens',\n operators: ['contains', 'equals'],\n },\n {\n column: 'channelKey',\n kind: 'exact',\n path: 'channel',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'statusKey',\n kind: 'exact',\n path: 'status',\n presence: 'always',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: 'createdAtMs',\n kind: 'date',\n path: 'createdAtMs',\n storedAs: 'millis',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n // Every product in the order, matched whole — ids keep their case.\n column: 'productIds',\n kind: 'exact',\n path: 'productIds',\n tokensPath: 'productIds',\n verbatimTokens: true,\n operators: ['isAnyOf'],\n },\n {\n column: 'disputeKey',\n kind: 'exact',\n path: 'disputeKey',\n operators: ['equals', 'isAnyOf'],\n },\n]\n\n/**\n * The orders the grid's headers ask for, newest first FIRST — see \"Every\n * header sorts\" above.\n */\nexport const ORDER_LIST_COLUMN_SORTS: readonly ListQuerySort[] = [\n { path: 'createdAtMs', direction: 'desc', column: 'createdAtMs', label: 'Date' },\n { path: 'createdAtMs', direction: 'asc', column: 'createdAtMs', label: 'Date', alone: true },\n { path: 'number', direction: 'desc', column: 'orderLabel', label: 'Order', alone: true },\n { path: 'number', direction: 'asc', column: 'orderLabel', label: 'Order', alone: true },\n { path: 'customerEmailLower', direction: 'asc', column: 'customerEmail', label: 'Customer', alone: true },\n { path: 'customerEmailLower', direction: 'desc', column: 'customerEmail', label: 'Customer', alone: true },\n { path: 'channel', direction: 'asc', column: 'channelKey', label: 'Channel', alone: true },\n { path: 'channel', direction: 'desc', column: 'channelKey', label: 'Channel', alone: true },\n { path: 'status', direction: 'asc', column: 'statusKey', label: 'Status', alone: true },\n { path: 'status', direction: 'desc', column: 'statusKey', label: 'Status', alone: true },\n]\n\n/** The list's query: its fields, its header orders and its search. */\nexport const ORDER_LIST_QUERY: ListQueryDeclaration = {\n fields: ORDER_LIST_FIELDS,\n sorts: ORDER_LIST_COLUMN_SORTS,\n search: { tokensPath: 'searchTokens' },\n}\n\nexport const ORDER_LIST_HEADERS: Readonly<Record<string, string>> = {\n orderLabel: 'Order',\n customerEmail: 'Customer',\n channelKey: 'Channel',\n statusKey: 'Status',\n createdAtMs: 'Date',\n productIds: 'Product',\n disputeKey: 'Disputes',\n}\n\n/** The fields picked from choices, which the panel shows as a select. */\nexport const ORDER_LIST_SELECT_FIELDS: readonly string[] = [\n 'channelKey',\n 'statusKey',\n 'productIds',\n 'disputeKey',\n]\n\nexport const ORDER_STATUS_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_STATUS_LABELS) as OrderStatus[]\n).map((status) => ({ value: status, label: ORDER_STATUS_LABELS[status] }))\n\nexport const ORDER_CHANNEL_OPTIONS: readonly ListFilterOption[] = (\n Object.keys(ORDER_CHANNEL_LABELS) as OrderChannel[]\n).map((channel) => ({ value: channel, label: ORDER_CHANNEL_LABELS[channel] }))\n\n/*\n * The dispute badge's tones (`orderDisputeKey`), labelled as the badge\n * labels them. `lost` is the tone, not the raw dispute status: a dispute\n * that was WON also closes with money untouched, and listing it as charged\n * back would tell a merchant they lost a case they won.\n */\nexport const ORDER_DISPUTE_OPTIONS: readonly ListFilterOption[] = [\n { value: 'open', label: 'Open dispute' },\n { value: 'lost', label: 'Charged back' },\n { value: 'won', label: 'Dispute won' },\n { value: 'settled', label: 'Dispute closed' },\n]\n\n/** The clause the open-dispute banner's \"Show them\" sets. */\nexport const OPEN_DISPUTE_CLAUSE: ListFilterClause = {\n field: 'disputeKey',\n op: 'equals',\n value: 'open',\n}\n\n/**\n * The Customer clause the CRM's link seeds from `?email=` (AGL-2622).\n *\n * A whole address is `is`, served by the lower-cased key, so it finds that\n * buyer and no one whose address merely begins the same way. Anything else —\n * a domain, `@acme.com`, a name — is `contains`, a word prefix of the\n * address, so a domain finds every buyer at a company.\n */\nexport function ordersCustomerClause(raw: string): ListFilterClause | null {\n const value = raw.trim().replace(/^@+/, '')\n if (!value) return null\n const at = value.indexOf('@')\n return at > 0 && at < value.length - 1\n ? { field: 'customerEmail', op: 'equals', value }\n : { field: 'customerEmail', op: 'contains', value }\n}\n"],"names":["ORDER_CHANNEL_LABELS","ORDER_STATUS_LABELS","ORDER_LIST_FIELDS","column","kind","path","tokensPath","operators","lowerPath","presence","storedAs","verbatimTokens","ORDER_LIST_COLUMN_SORTS","direction","label","alone","ORDER_LIST_QUERY","fields","sorts","search","ORDER_LIST_HEADERS","orderLabel","customerEmail","channelKey","statusKey","createdAtMs","productIds","disputeKey","ORDER_LIST_SELECT_FIELDS","ORDER_STATUS_OPTIONS","Object","keys","map","status","value","ORDER_CHANNEL_OPTIONS","channel","ORDER_DISPUTE_OPTIONS","OPEN_DISPUTE_CLAUSE","field","op","ordersCustomerClause","raw","trim","replace","at","indexOf","length"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
18
+ /** The history's header orders, newest first FIRST. */
19
+ export declare const POS_SHIFT_COLUMN_SORTS: readonly ListQuerySort[];
20
+ /** The fields a shift carries from the moment it opens, so every order reaches it. */
21
+ export declare const POS_SHIFT_SORTED_FIELDS: readonly ["closedAtMs", "netSalesCents", "expectedCashCents", "countedCashCents", "varianceCents"];
22
+ export declare const POS_SHIFT_LIST_QUERY: ListQueryDeclaration;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /*
17
+ * One register's shift history (`hosts/{hostId}/registers/{registerId}/shifts`),
18
+ * newest first, and every header it sorts by (AGL-3680).
19
+ *
20
+ * The history has no filters, so each header order is a single-field
21
+ * `orderBy` on the subcollection: no composite at all. An `orderBy` drops
22
+ * every shift that LACKS the field, so the shift writer (`server/pos-shift.ts`)
23
+ * stamps each of them at open — `null` until the close fills it — and the
24
+ * close flattens the Z report's net sales onto the shift as `netSalesCents`,
25
+ * since a query cannot order by a field inside `report`.
26
+ * `tools/scripts/backfill-pos-shift-list-fields.mjs` stamps the shifts
27
+ * written before. "By" is a name picked from two fields, so it sorts the page
28
+ * on screen.
29
+ */ /** The history's header orders, newest first FIRST. */ export const POS_SHIFT_COLUMN_SORTS = [
30
+ {
31
+ path: 'openedAtMs',
32
+ direction: 'desc',
33
+ column: 'openedAtMs',
34
+ label: 'Opened'
35
+ },
36
+ {
37
+ path: 'openedAtMs',
38
+ direction: 'asc',
39
+ column: 'openedAtMs',
40
+ label: 'Opened'
41
+ },
42
+ {
43
+ path: 'closedAtMs',
44
+ direction: 'desc',
45
+ column: 'closedAtMs',
46
+ label: 'Closed'
47
+ },
48
+ {
49
+ path: 'closedAtMs',
50
+ direction: 'asc',
51
+ column: 'closedAtMs',
52
+ label: 'Closed'
53
+ },
54
+ {
55
+ path: 'netSalesCents',
56
+ direction: 'desc',
57
+ column: 'netSales',
58
+ label: 'Net sales'
59
+ },
60
+ {
61
+ path: 'netSalesCents',
62
+ direction: 'asc',
63
+ column: 'netSales',
64
+ label: 'Net sales'
65
+ },
66
+ {
67
+ path: 'expectedCashCents',
68
+ direction: 'desc',
69
+ column: 'expectedCashCents',
70
+ label: 'Expected'
71
+ },
72
+ {
73
+ path: 'expectedCashCents',
74
+ direction: 'asc',
75
+ column: 'expectedCashCents',
76
+ label: 'Expected'
77
+ },
78
+ {
79
+ path: 'countedCashCents',
80
+ direction: 'desc',
81
+ column: 'countedCashCents',
82
+ label: 'Counted'
83
+ },
84
+ {
85
+ path: 'countedCashCents',
86
+ direction: 'asc',
87
+ column: 'countedCashCents',
88
+ label: 'Counted'
89
+ },
90
+ {
91
+ path: 'varianceCents',
92
+ direction: 'desc',
93
+ column: 'varianceCents',
94
+ label: 'Variance'
95
+ },
96
+ {
97
+ path: 'varianceCents',
98
+ direction: 'asc',
99
+ column: 'varianceCents',
100
+ label: 'Variance'
101
+ }
102
+ ];
103
+ /** The fields a shift carries from the moment it opens, so every order reaches it. */ export const POS_SHIFT_SORTED_FIELDS = [
104
+ 'closedAtMs',
105
+ 'netSalesCents',
106
+ 'expectedCashCents',
107
+ 'countedCashCents',
108
+ 'varianceCents'
109
+ ];
110
+ export const POS_SHIFT_LIST_QUERY = {
111
+ fields: [],
112
+ sorts: POS_SHIFT_COLUMN_SORTS
113
+ };
114
+
115
+ //# sourceMappingURL=pos-shift-list-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/pos-shift-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * One register's shift history (`hosts/{hostId}/registers/{registerId}/shifts`),\n * newest first, and every header it sorts by (AGL-3680).\n *\n * The history has no filters, so each header order is a single-field\n * `orderBy` on the subcollection: no composite at all. An `orderBy` drops\n * every shift that LACKS the field, so the shift writer (`server/pos-shift.ts`)\n * stamps each of them at open — `null` until the close fills it — and the\n * close flattens the Z report's net sales onto the shift as `netSalesCents`,\n * since a query cannot order by a field inside `report`.\n * `tools/scripts/backfill-pos-shift-list-fields.mjs` stamps the shifts\n * written before. \"By\" is a name picked from two fields, so it sorts the page\n * on screen.\n */\n\n/** The history's header orders, newest first FIRST. */\nexport const POS_SHIFT_COLUMN_SORTS: readonly ListQuerySort[] = [\n { path: 'openedAtMs', direction: 'desc', column: 'openedAtMs', label: 'Opened' },\n { path: 'openedAtMs', direction: 'asc', column: 'openedAtMs', label: 'Opened' },\n { path: 'closedAtMs', direction: 'desc', column: 'closedAtMs', label: 'Closed' },\n { path: 'closedAtMs', direction: 'asc', column: 'closedAtMs', label: 'Closed' },\n { path: 'netSalesCents', direction: 'desc', column: 'netSales', label: 'Net sales' },\n { path: 'netSalesCents', direction: 'asc', column: 'netSales', label: 'Net sales' },\n { path: 'expectedCashCents', direction: 'desc', column: 'expectedCashCents', label: 'Expected' },\n { path: 'expectedCashCents', direction: 'asc', column: 'expectedCashCents', label: 'Expected' },\n { path: 'countedCashCents', direction: 'desc', column: 'countedCashCents', label: 'Counted' },\n { path: 'countedCashCents', direction: 'asc', column: 'countedCashCents', label: 'Counted' },\n { path: 'varianceCents', direction: 'desc', column: 'varianceCents', label: 'Variance' },\n { path: 'varianceCents', direction: 'asc', column: 'varianceCents', label: 'Variance' },\n]\n\n/** The fields a shift carries from the moment it opens, so every order reaches it. */\nexport const POS_SHIFT_SORTED_FIELDS = [\n 'closedAtMs',\n 'netSalesCents',\n 'expectedCashCents',\n 'countedCashCents',\n 'varianceCents',\n] as const\n\nexport const POS_SHIFT_LIST_QUERY: ListQueryDeclaration = {\n fields: [],\n sorts: POS_SHIFT_COLUMN_SORTS,\n}\n"],"names":["POS_SHIFT_COLUMN_SORTS","path","direction","column","label","POS_SHIFT_SORTED_FIELDS","POS_SHIFT_LIST_QUERY","fields","sorts"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD;;;;;;;;;;;;;CAaC,GAED,qDAAqD,GACrD,OAAO,MAAMA,yBAAmD;IAC9D;QAAEC,MAAM;QAAcC,WAAW;QAAQC,QAAQ;QAAcC,OAAO;IAAS;IAC/E;QAAEH,MAAM;QAAcC,WAAW;QAAOC,QAAQ;QAAcC,OAAO;IAAS;IAC9E;QAAEH,MAAM;QAAcC,WAAW;QAAQC,QAAQ;QAAcC,OAAO;IAAS;IAC/E;QAAEH,MAAM;QAAcC,WAAW;QAAOC,QAAQ;QAAcC,OAAO;IAAS;IAC9E;QAAEH,MAAM;QAAiBC,WAAW;QAAQC,QAAQ;QAAYC,OAAO;IAAY;IACnF;QAAEH,MAAM;QAAiBC,WAAW;QAAOC,QAAQ;QAAYC,OAAO;IAAY;IAClF;QAAEH,MAAM;QAAqBC,WAAW;QAAQC,QAAQ;QAAqBC,OAAO;IAAW;IAC/F;QAAEH,MAAM;QAAqBC,WAAW;QAAOC,QAAQ;QAAqBC,OAAO;IAAW;IAC9F;QAAEH,MAAM;QAAoBC,WAAW;QAAQC,QAAQ;QAAoBC,OAAO;IAAU;IAC5F;QAAEH,MAAM;QAAoBC,WAAW;QAAOC,QAAQ;QAAoBC,OAAO;IAAU;IAC3F;QAAEH,MAAM;QAAiBC,WAAW;QAAQC,QAAQ;QAAiBC,OAAO;IAAW;IACvF;QAAEH,MAAM;QAAiBC,WAAW;QAAOC,QAAQ;QAAiBC,OAAO;IAAW;CACvF,CAAA;AAED,oFAAoF,GACpF,OAAO,MAAMC,0BAA0B;IACrC;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,OAAO,MAAMC,uBAA6C;IACxDC,QAAQ,EAAE;IACVC,OAAOR;AACT,EAAC"}
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
18
18
  import type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
19
- import type { ListQueryDeclaration, ListQueryFilter } from '@aglyn/shared-ui-jsx/const/list-query-plan';
19
+ import type { ListQueryDeclaration, ListQueryFilter, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
20
20
  /** The list's scope: live products only. */
21
21
  export declare const PRODUCT_LIST_BASE: readonly ListQueryFilter[];
22
22
  /** The scope, as `listQueryIndexes` enumerates it. */
@@ -25,6 +25,11 @@ export declare const PRODUCT_LIST_INDEX_BASE: readonly {
25
25
  array?: boolean;
26
26
  }[];
27
27
  export declare const PRODUCT_LIST_FIELDS: readonly ListFilterField[];
28
+ /**
29
+ * The orders the grid's headers ask for, A to Z FIRST — see "Every header
30
+ * sorts" above.
31
+ */
32
+ export declare const PRODUCT_LIST_COLUMN_SORTS: readonly ListQuerySort[];
28
33
  export declare const PRODUCT_LIST_QUERY: ListQueryDeclaration;
29
34
  export declare const PRODUCT_LIST_HEADERS: Readonly<Record<string, string>>;
30
35
  export declare const PRODUCT_LIST_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>>;
@@ -60,10 +60,25 @@
60
60
  * Tags are stored as typed, so a whole-member match would be case-sensitive
61
61
  * where the reader expects it not to be.
62
62
  *
63
+ * ## Every header sorts (AGL-3680)
64
+ *
65
+ * `PRODUCT_LIST_COLUMN_SORTS`: Product either way, Status, Type and Price —
66
+ * each a field every product carries. `status` and `type` every writer sets
67
+ * (and the products backfill stamped on the legacy rows that had none);
68
+ * `priceFromCents` `productSearchFields` derives at every write that carries
69
+ * the variants, which every create and save does, and the products backfill
70
+ * stamped on the rest — it is the storefront's price order too. Every order
71
+ * but the default is `alone`: served with no filter or search on, under the
72
+ * `deletedAt` scope only, so each costs one `(deletedAt, field)` composite per
73
+ * direction rather than one per filterable field. Stock and Variants are
74
+ * read from inside `variants`, which no query can order by, so they sort the
75
+ * page on screen.
76
+ *
63
77
  * ## The indexes
64
78
  *
65
79
  * One `(field, nameLower ASC)` composite per filterable field and for the
66
- * scope, merged by Firestore for any combination:
80
+ * scope, merged by Firestore for any combination, and the scope's composite
81
+ * per header order:
67
82
  * `listQueryIndexes(PRODUCT_LIST_QUERY, PRODUCT_LIST_INDEX_BASE)`, held to
68
83
  * `cloud/firebase-firestore.indexes.json` by `product-list-query.spec.ts`.
69
84
  */ /** The list's scope: live products only. */ export const PRODUCT_LIST_BASE = [
@@ -139,15 +154,69 @@ export const PRODUCT_LIST_FIELDS = [
139
154
  ]
140
155
  }
141
156
  ];
157
+ /**
158
+ * The orders the grid's headers ask for, A to Z FIRST — see "Every header
159
+ * sorts" above.
160
+ */ export const PRODUCT_LIST_COLUMN_SORTS = [
161
+ {
162
+ path: 'nameLower',
163
+ direction: 'asc',
164
+ column: 'name',
165
+ label: 'Product'
166
+ },
167
+ {
168
+ path: 'nameLower',
169
+ direction: 'desc',
170
+ column: 'name',
171
+ label: 'Product',
172
+ alone: true
173
+ },
174
+ {
175
+ path: 'status',
176
+ direction: 'asc',
177
+ column: 'status',
178
+ label: 'Status',
179
+ alone: true
180
+ },
181
+ {
182
+ path: 'status',
183
+ direction: 'desc',
184
+ column: 'status',
185
+ label: 'Status',
186
+ alone: true
187
+ },
188
+ {
189
+ path: 'type',
190
+ direction: 'asc',
191
+ column: 'type',
192
+ label: 'Type',
193
+ alone: true
194
+ },
195
+ {
196
+ path: 'type',
197
+ direction: 'desc',
198
+ column: 'type',
199
+ label: 'Type',
200
+ alone: true
201
+ },
202
+ {
203
+ path: 'priceFromCents',
204
+ direction: 'asc',
205
+ column: 'priceUsd',
206
+ label: 'Price',
207
+ alone: true
208
+ },
209
+ {
210
+ path: 'priceFromCents',
211
+ direction: 'desc',
212
+ column: 'priceUsd',
213
+ label: 'Price',
214
+ alone: true
215
+ }
216
+ ];
142
217
  export const PRODUCT_LIST_QUERY = {
143
218
  fields: PRODUCT_LIST_FIELDS,
144
- sorts: [
145
- {
146
- path: 'nameLower',
147
- direction: 'asc',
148
- column: 'name'
149
- }
150
- ],
219
+ sorts: PRODUCT_LIST_COLUMN_SORTS,
151
220
  search: {
152
221
  tokensPath: 'nameTokens'
153
222
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/product-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * The products hub's catalog table (`hosts/{hostId}/products`): what its\n * Filters panel and quick search may ask, and the ONE Firestore query that\n * answers all of it (AGL-3321).\n *\n * Every clause and the search word go on the query and the pages come from\n * it, so a product on page forty is found exactly as one on page one is. The\n * card matches nothing over the rows it holds.\n *\n * ## The order, and why every product carries its key\n *\n * `nameLower` ascending, the one order the list offers. Ordering by a field\n * drops every document that lacks it, so every writer stamps it through\n * `productSearchFields` — the editor, Duplicate, the CSV importer and the AI\n * drafts, all of which create through `/api/hosts/resources` — and\n * `tools/scripts/backfill-products-list-fields.mjs` stamps it onto the\n * products written before any of them did.\n *\n * ## Soft-deleted products are excluded BY THE QUERY\n *\n * `deletedAt` is `null` on a live product and a timestamp on a deleted one.\n * Firestore cannot ask for a document that LACKS a field, so the list's scope\n * is `deletedAt == null`, and the create route stamps the explicit `null` on\n * every product it makes (the backfill stamps the rest). Dropping deleted\n * rows after the read would leave a page short by however many it dropped.\n *\n * ## What is offered, and what is not\n *\n * - Name: `contains` (word prefix, over `nameTokens`), `equals` and\n * `startsWith` (over `nameLower`, the field the list is ordered by, so a\n * prefix range keeps the order it already has).\n * - Status and Type: picked, `is` / `is any of`.\n * - Slug: `is`. Slugs are lower-case by construction, so the stored value\n * is its own key.\n * - SKU and Barcode: WHOLE values, `contains` / `is any of`, over the\n * flattened `skus` / `barcodes` arrays — a code is copied or scanned, not\n * half-remembered. Stored lower-cased; see `productListRequestClauses`.\n *\n * Not offered, and why: price and the created/updated dates are ranges, and a\n * range must lead the order, so each would add a whole second order's worth\n * of composites for every field above (and `priceUsd` is the first\n * variant's price, which describes a multi-variant product wrongly). A\n * name `endsWith` needs the reversed key as its order for the same reason.\n * Tags are stored as typed, so a whole-member match would be case-sensitive\n * where the reader expects it not to be.\n *\n * ## The indexes\n *\n * One `(field, nameLower ASC)` composite per filterable field and for the\n * scope, merged by Firestore for any combination:\n * `listQueryIndexes(PRODUCT_LIST_QUERY, PRODUCT_LIST_INDEX_BASE)`, held to\n * `cloud/firebase-firestore.indexes.json` by `product-list-query.spec.ts`.\n */\n\n/** The list's scope: live products only. */\nexport const PRODUCT_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'deletedAt', op: '==', value: null },\n]\n\n/** The scope, as `listQueryIndexes` enumerates it. */\nexport const PRODUCT_LIST_INDEX_BASE: readonly { path: string; array?: boolean }[] = [\n { path: 'deletedAt' },\n]\n\nexport const PRODUCT_LIST_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals', 'startsWith'],\n },\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'type', kind: 'exact', path: 'type', operators: ['equals', 'isAnyOf'] },\n { column: 'slug', kind: 'text', path: 'slug', lowerPath: 'slug', operators: ['equals'] },\n {\n column: 'skus',\n kind: 'exact',\n path: 'skus',\n tokensPath: 'skus',\n operators: ['contains', 'isAnyOf'],\n },\n {\n column: 'barcodes',\n kind: 'exact',\n path: 'barcodes',\n tokensPath: 'barcodes',\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const PRODUCT_LIST_QUERY: ListQueryDeclaration = {\n fields: PRODUCT_LIST_FIELDS,\n sorts: [{ path: 'nameLower', direction: 'asc', column: 'name' }],\n search: { tokensPath: 'nameTokens' },\n}\n\nexport const PRODUCT_LIST_HEADERS: Readonly<Record<string, string>> = {\n name: 'Product',\n status: 'Status',\n type: 'Type',\n slug: 'Slug',\n skus: 'SKU',\n barcodes: 'Barcode',\n}\n\nexport const PRODUCT_LIST_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'draft', label: 'Draft' },\n { value: 'archived', label: 'Archived' },\n ],\n type: [\n { value: 'physical', label: 'Physical' },\n { value: 'digital', label: 'Digital' },\n { value: 'service', label: 'Service' },\n ],\n}\n\n/** The fields whose values are picked, which the panel shows as a select. */\nexport const PRODUCT_LIST_SELECT_FIELDS: readonly string[] = Object.keys(PRODUCT_LIST_OPTIONS)\n\n/** The columns folded to lower case before they reach the query. */\nconst FOLDED = new Set(['skus', 'barcodes'])\n\n/**\n * The clauses as the query must ask them.\n *\n * `productSearchFields` stores every SKU and barcode trimmed and lower-cased,\n * and an array field is matched by whole member, byte for byte — so a SKU\n * typed `ABC-123` would ask for a member nobody stored. The typed value is\n * folded the way the writer folded it; `is any of` folds each of its values.\n * Every other clause passes through as the panel wrote it.\n */\nexport function productListRequestClauses<Clause extends ListFilterRequest>(\n clauses: readonly Clause[],\n): Clause[] {\n const fold = (value: string) => value.trim().toLowerCase()\n return clauses.map((clause) => {\n if (!FOLDED.has(clause.field)) return clause\n const value =\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map(fold).filter(Boolean).join(',')\n : fold(clause.value)\n return { ...clause, value }\n })\n}\n"],"names":["PRODUCT_LIST_BASE","path","op","value","PRODUCT_LIST_INDEX_BASE","PRODUCT_LIST_FIELDS","column","kind","lowerPath","tokensPath","operators","PRODUCT_LIST_QUERY","fields","sorts","direction","search","PRODUCT_LIST_HEADERS","name","status","type","slug","skus","barcodes","PRODUCT_LIST_OPTIONS","label","PRODUCT_LIST_SELECT_FIELDS","Object","keys","FOLDED","Set","productListRequestClauses","clauses","fold","trim","toLowerCase","map","clause","has","field","split","filter","Boolean","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDC,GAED,0CAA0C,GAC1C,OAAO,MAAMA,oBAAgD;IAC3D;QAAEC,MAAM;QAAaC,IAAI;QAAMC,OAAO;IAAK;CAC5C,CAAA;AAED,oDAAoD,GACpD,OAAO,MAAMC,0BAAwE;IACnF;QAAEH,MAAM;IAAY;CACrB,CAAA;AAED,OAAO,MAAMI,sBAAkD;IAC7D;QACEC,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNO,WAAW;QACXC,YAAY;QACZC,WAAW;YAAC;YAAY;YAAU;SAAa;IACjD;IACA;QAAEJ,QAAQ;QAAUC,MAAM;QAASN,MAAM;QAAUS,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEJ,QAAQ;QAAQC,MAAM;QAASN,MAAM;QAAQS,WAAW;YAAC;YAAU;SAAU;IAAC;IAChF;QAAEJ,QAAQ;QAAQC,MAAM;QAAQN,MAAM;QAAQO,WAAW;QAAQE,WAAW;YAAC;SAAS;IAAC;IACvF;QACEJ,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNQ,YAAY;QACZC,WAAW;YAAC;YAAY;SAAU;IACpC;IACA;QACEJ,QAAQ;QACRC,MAAM;QACNN,MAAM;QACNQ,YAAY;QACZC,WAAW;YAAC;YAAY;SAAU;IACpC;CACD,CAAA;AAED,OAAO,MAAMC,qBAA2C;IACtDC,QAAQP;IACRQ,OAAO;QAAC;YAAEZ,MAAM;YAAaa,WAAW;YAAOR,QAAQ;QAAO;KAAE;IAChES,QAAQ;QAAEN,YAAY;IAAa;AACrC,EAAC;AAED,OAAO,MAAMO,uBAAyD;IACpEC,MAAM;IACNC,QAAQ;IACRC,MAAM;IACNC,MAAM;IACNC,MAAM;IACNC,UAAU;AACZ,EAAC;AAED,OAAO,MAAMC,uBAA8E;IACzFL,QAAQ;QACN;YAAEf,OAAO;YAAUqB,OAAO;QAAS;QACnC;YAAErB,OAAO;YAASqB,OAAO;QAAQ;QACjC;YAAErB,OAAO;YAAYqB,OAAO;QAAW;KACxC;IACDL,MAAM;QACJ;YAAEhB,OAAO;YAAYqB,OAAO;QAAW;QACvC;YAAErB,OAAO;YAAWqB,OAAO;QAAU;QACrC;YAAErB,OAAO;YAAWqB,OAAO;QAAU;KACtC;AACH,EAAC;AAED,2EAA2E,GAC3E,OAAO,MAAMC,6BAAgDC,OAAOC,IAAI,CAACJ,sBAAqB;AAE9F,kEAAkE,GAClE,MAAMK,SAAS,IAAIC,IAAI;IAAC;IAAQ;CAAW;AAE3C;;;;;;;;CAQC,GACD,OAAO,SAASC,0BACdC,OAA0B;IAE1B,MAAMC,OAAO,CAAC7B,QAAkBA,MAAM8B,IAAI,GAAGC,WAAW;IACxD,OAAOH,QAAQI,GAAG,CAAC,CAACC;QAClB,IAAI,CAACR,OAAOS,GAAG,CAACD,OAAOE,KAAK,GAAG,OAAOF;QACtC,MAAMjC,QACJiC,OAAOlC,EAAE,KAAK,YACVkC,OAAOjC,KAAK,CAACoC,KAAK,CAAC,KAAKJ,GAAG,CAACH,MAAMQ,MAAM,CAACC,SAASC,IAAI,CAAC,OACvDV,KAAKI,OAAOjC,KAAK;QACvB,OAAO,aAAKiC;YAAQjC;;IACtB;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/product-list-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * The products hub's catalog table (`hosts/{hostId}/products`): what its\n * Filters panel and quick search may ask, and the ONE Firestore query that\n * answers all of it (AGL-3321).\n *\n * Every clause and the search word go on the query and the pages come from\n * it, so a product on page forty is found exactly as one on page one is. The\n * card matches nothing over the rows it holds.\n *\n * ## The order, and why every product carries its key\n *\n * `nameLower` ascending, the one order the list offers. Ordering by a field\n * drops every document that lacks it, so every writer stamps it through\n * `productSearchFields` — the editor, Duplicate, the CSV importer and the AI\n * drafts, all of which create through `/api/hosts/resources` — and\n * `tools/scripts/backfill-products-list-fields.mjs` stamps it onto the\n * products written before any of them did.\n *\n * ## Soft-deleted products are excluded BY THE QUERY\n *\n * `deletedAt` is `null` on a live product and a timestamp on a deleted one.\n * Firestore cannot ask for a document that LACKS a field, so the list's scope\n * is `deletedAt == null`, and the create route stamps the explicit `null` on\n * every product it makes (the backfill stamps the rest). Dropping deleted\n * rows after the read would leave a page short by however many it dropped.\n *\n * ## What is offered, and what is not\n *\n * - Name: `contains` (word prefix, over `nameTokens`), `equals` and\n * `startsWith` (over `nameLower`, the field the list is ordered by, so a\n * prefix range keeps the order it already has).\n * - Status and Type: picked, `is` / `is any of`.\n * - Slug: `is`. Slugs are lower-case by construction, so the stored value\n * is its own key.\n * - SKU and Barcode: WHOLE values, `contains` / `is any of`, over the\n * flattened `skus` / `barcodes` arrays — a code is copied or scanned, not\n * half-remembered. Stored lower-cased; see `productListRequestClauses`.\n *\n * Not offered, and why: price and the created/updated dates are ranges, and a\n * range must lead the order, so each would add a whole second order's worth\n * of composites for every field above (and `priceUsd` is the first\n * variant's price, which describes a multi-variant product wrongly). A\n * name `endsWith` needs the reversed key as its order for the same reason.\n * Tags are stored as typed, so a whole-member match would be case-sensitive\n * where the reader expects it not to be.\n *\n * ## Every header sorts (AGL-3680)\n *\n * `PRODUCT_LIST_COLUMN_SORTS`: Product either way, Status, Type and Price —\n * each a field every product carries. `status` and `type` every writer sets\n * (and the products backfill stamped on the legacy rows that had none);\n * `priceFromCents` `productSearchFields` derives at every write that carries\n * the variants, which every create and save does, and the products backfill\n * stamped on the rest — it is the storefront's price order too. Every order\n * but the default is `alone`: served with no filter or search on, under the\n * `deletedAt` scope only, so each costs one `(deletedAt, field)` composite per\n * direction rather than one per filterable field. Stock and Variants are\n * read from inside `variants`, which no query can order by, so they sort the\n * page on screen.\n *\n * ## The indexes\n *\n * One `(field, nameLower ASC)` composite per filterable field and for the\n * scope, merged by Firestore for any combination, and the scope's composite\n * per header order:\n * `listQueryIndexes(PRODUCT_LIST_QUERY, PRODUCT_LIST_INDEX_BASE)`, held to\n * `cloud/firebase-firestore.indexes.json` by `product-list-query.spec.ts`.\n */\n\n/** The list's scope: live products only. */\nexport const PRODUCT_LIST_BASE: readonly ListQueryFilter[] = [\n { path: 'deletedAt', op: '==', value: null },\n]\n\n/** The scope, as `listQueryIndexes` enumerates it. */\nexport const PRODUCT_LIST_INDEX_BASE: readonly { path: string; array?: boolean }[] = [\n { path: 'deletedAt' },\n]\n\nexport const PRODUCT_LIST_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n operators: ['contains', 'equals', 'startsWith'],\n },\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'type', kind: 'exact', path: 'type', operators: ['equals', 'isAnyOf'] },\n { column: 'slug', kind: 'text', path: 'slug', lowerPath: 'slug', operators: ['equals'] },\n {\n column: 'skus',\n kind: 'exact',\n path: 'skus',\n tokensPath: 'skus',\n operators: ['contains', 'isAnyOf'],\n },\n {\n column: 'barcodes',\n kind: 'exact',\n path: 'barcodes',\n tokensPath: 'barcodes',\n operators: ['contains', 'isAnyOf'],\n },\n]\n\n/**\n * The orders the grid's headers ask for, A to Z FIRST — see \"Every header\n * sorts\" above.\n */\nexport const PRODUCT_LIST_COLUMN_SORTS: readonly ListQuerySort[] = [\n { path: 'nameLower', direction: 'asc', column: 'name', label: 'Product' },\n { path: 'nameLower', direction: 'desc', column: 'name', label: 'Product', alone: true },\n { path: 'status', direction: 'asc', column: 'status', label: 'Status', alone: true },\n { path: 'status', direction: 'desc', column: 'status', label: 'Status', alone: true },\n { path: 'type', direction: 'asc', column: 'type', label: 'Type', alone: true },\n { path: 'type', direction: 'desc', column: 'type', label: 'Type', alone: true },\n { path: 'priceFromCents', direction: 'asc', column: 'priceUsd', label: 'Price', alone: true },\n { path: 'priceFromCents', direction: 'desc', column: 'priceUsd', label: 'Price', alone: true },\n]\n\nexport const PRODUCT_LIST_QUERY: ListQueryDeclaration = {\n fields: PRODUCT_LIST_FIELDS,\n sorts: PRODUCT_LIST_COLUMN_SORTS,\n search: { tokensPath: 'nameTokens' },\n}\n\nexport const PRODUCT_LIST_HEADERS: Readonly<Record<string, string>> = {\n name: 'Product',\n status: 'Status',\n type: 'Type',\n slug: 'Slug',\n skus: 'SKU',\n barcodes: 'Barcode',\n}\n\nexport const PRODUCT_LIST_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {\n status: [\n { value: 'active', label: 'Active' },\n { value: 'draft', label: 'Draft' },\n { value: 'archived', label: 'Archived' },\n ],\n type: [\n { value: 'physical', label: 'Physical' },\n { value: 'digital', label: 'Digital' },\n { value: 'service', label: 'Service' },\n ],\n}\n\n/** The fields whose values are picked, which the panel shows as a select. */\nexport const PRODUCT_LIST_SELECT_FIELDS: readonly string[] = Object.keys(PRODUCT_LIST_OPTIONS)\n\n/** The columns folded to lower case before they reach the query. */\nconst FOLDED = new Set(['skus', 'barcodes'])\n\n/**\n * The clauses as the query must ask them.\n *\n * `productSearchFields` stores every SKU and barcode trimmed and lower-cased,\n * and an array field is matched by whole member, byte for byte — so a SKU\n * typed `ABC-123` would ask for a member nobody stored. The typed value is\n * folded the way the writer folded it; `is any of` folds each of its values.\n * Every other clause passes through as the panel wrote it.\n */\nexport function productListRequestClauses<Clause extends ListFilterRequest>(\n clauses: readonly Clause[],\n): Clause[] {\n const fold = (value: string) => value.trim().toLowerCase()\n return clauses.map((clause) => {\n if (!FOLDED.has(clause.field)) return clause\n const value =\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map(fold).filter(Boolean).join(',')\n : fold(clause.value)\n return { ...clause, value }\n })\n}\n"],"names":["PRODUCT_LIST_BASE","path","op","value","PRODUCT_LIST_INDEX_BASE","PRODUCT_LIST_FIELDS","column","kind","lowerPath","tokensPath","operators","PRODUCT_LIST_COLUMN_SORTS","direction","label","alone","PRODUCT_LIST_QUERY","fields","sorts","search","PRODUCT_LIST_HEADERS","name","status","type","slug","skus","barcodes","PRODUCT_LIST_OPTIONS","PRODUCT_LIST_SELECT_FIELDS","Object","keys","FOLDED","Set","productListRequestClauses","clauses","fold","trim","toLowerCase","map","clause","has","field","split","filter","Boolean","join"],"mappings":"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"}
@@ -15,10 +15,11 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
18
- import type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan';
18
+ import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
19
19
  import type { InventoryAdjustmentReason } from '../model/commerce';
20
20
  export declare const STOCK_MOVEMENT_FILTER_FIELDS: readonly ListFilterField[];
21
- /** The ledger's one order: newest first, the order every writer stamps. */
21
+ /** The ledger's header orders, newest first FIRST: the order every writer stamps. */
22
+ export declare const STOCK_MOVEMENT_COLUMN_SORTS: readonly ListQuerySort[];
22
23
  export declare const STOCK_MOVEMENT_QUERY: ListQueryDeclaration;
23
24
  export declare const STOCK_MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>>;
24
25
  /** The fields picked from choices rather than typed. */
@@ -33,6 +33,13 @@
33
33
  * would type lives on the product, so a search could only ever match the
34
34
  * names of the rows already loaded. Product is a picked filter instead, and
35
35
  * the ids the old search read (order, location) are filters of their own.
36
+ *
37
+ * Every header sorts (AGL-3680): When either way, Change and Reason — `delta`
38
+ * and `reason` every writer stores (`InventoryAdjustment` requires both, and
39
+ * every ledger write is typed as one). Those are `alone`: served with no
40
+ * filter on, at no composite cost on this unscoped list. Product (a name
41
+ * read from the product) and Source (composed from three fields) sort the
42
+ * page on screen.
36
43
  */ export const STOCK_MOVEMENT_FILTER_FIELDS = [
37
44
  {
38
45
  column: 'productId',
@@ -76,15 +83,52 @@
76
83
  ]
77
84
  }
78
85
  ];
79
- /** The ledger's one order: newest first, the order every writer stamps. */ export const STOCK_MOVEMENT_QUERY = {
86
+ /** The ledger's header orders, newest first FIRST: the order every writer stamps. */ export const STOCK_MOVEMENT_COLUMN_SORTS = [
87
+ {
88
+ path: 'atMs',
89
+ direction: 'desc',
90
+ column: 'atMs',
91
+ label: 'When'
92
+ },
93
+ {
94
+ path: 'atMs',
95
+ direction: 'asc',
96
+ column: 'atMs',
97
+ label: 'When',
98
+ alone: true
99
+ },
100
+ {
101
+ path: 'delta',
102
+ direction: 'asc',
103
+ column: 'delta',
104
+ label: 'Change',
105
+ alone: true
106
+ },
107
+ {
108
+ path: 'delta',
109
+ direction: 'desc',
110
+ column: 'delta',
111
+ label: 'Change',
112
+ alone: true
113
+ },
114
+ {
115
+ path: 'reason',
116
+ direction: 'asc',
117
+ column: 'reason',
118
+ label: 'Reason',
119
+ alone: true
120
+ },
121
+ {
122
+ path: 'reason',
123
+ direction: 'desc',
124
+ column: 'reason',
125
+ label: 'Reason',
126
+ alone: true
127
+ }
128
+ ];
129
+ export const STOCK_MOVEMENT_QUERY = {
80
130
  fields: STOCK_MOVEMENT_FILTER_FIELDS,
81
- sorts: [
82
- {
83
- path: 'atMs',
84
- direction: 'desc',
85
- column: 'atMs'
86
- }
87
- ]
131
+ sorts: STOCK_MOVEMENT_COLUMN_SORTS
88
132
  };
89
133
  export const STOCK_MOVEMENT_FILTER_HEADERS = {
90
134
  productId: 'Product',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/stock-movement-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport type { InventoryAdjustmentReason } from '../model/commerce'\n\n/*\n * What the stock movements ledger filters by, and how its query serves it\n * (AGL-3321).\n *\n * Every field is one every writer of `hosts/{hostId}/inventoryAdjustments`\n * already stores — the \"Adjust stock\" dialog, the sale path\n * (`decrementVariantStock`) and the cancellation restock — so nothing new is\n * written and nothing needs a backfill. Each is an EQUALITY beneath the\n * ledger's one order, `atMs` newest first, which is one `(field, atMs DESC)`\n * composite per field under index merging; the date filter is a range on\n * `atMs` itself, the order the ledger is already in, so it costs nothing.\n *\n * Order and location are typed or picked ids, matched whole. A sparse field\n * answers `equals` exactly — a row with no order is simply not that order —\n * and offers no \"is set\", which would be an inequality on a second field.\n *\n * No quick search. The ledger's rows name a product by id; the name a reader\n * would type lives on the product, so a search could only ever match the\n * names of the rows already loaded. Product is a picked filter instead, and\n * the ids the old search read (order, location) are filters of their own.\n */\nexport const STOCK_MOVEMENT_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'productId', kind: 'exact', path: 'productId', operators: ['equals', 'isAnyOf'] },\n { column: 'reason', kind: 'exact', path: 'reason', operators: ['equals', 'isAnyOf'] },\n { column: 'atMs', kind: 'date', path: 'atMs', storedAs: 'millis', presence: 'always' },\n { column: 'orderId', kind: 'exact', path: 'orderId', operators: ['equals'] },\n { column: 'locationId', kind: 'exact', path: 'locationId', operators: ['equals'] },\n]\n\n/** The ledger's one order: newest first, the order every writer stamps. */\nexport const STOCK_MOVEMENT_QUERY: ListQueryDeclaration = {\n fields: STOCK_MOVEMENT_FILTER_FIELDS,\n sorts: [{ path: 'atMs', direction: 'desc', column: 'atMs' }],\n}\n\nexport const STOCK_MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n productId: 'Product',\n reason: 'Reason',\n atMs: 'When',\n orderId: 'Order',\n locationId: 'Location',\n}\n\n/** The fields picked from choices rather than typed. */\nexport const STOCK_MOVEMENT_SELECT_FIELDS: readonly string[] = ['productId', 'reason', 'locationId']\n\n/** The stored key is a closed set; these are what a merchant reads. */\nexport const STOCK_MOVEMENT_REASON_LABEL: Readonly<Record<InventoryAdjustmentReason, string>> = {\n sale: 'Sale',\n refund: 'Refund return',\n restock: 'Restock',\n correction: 'Correction',\n damage: 'Damaged',\n cancellation: 'Order canceled',\n sync: 'Count synced',\n}\n"],"names":["STOCK_MOVEMENT_FILTER_FIELDS","column","kind","path","operators","storedAs","presence","STOCK_MOVEMENT_QUERY","fields","sorts","direction","STOCK_MOVEMENT_FILTER_HEADERS","productId","reason","atMs","orderId","locationId","STOCK_MOVEMENT_SELECT_FIELDS","STOCK_MOVEMENT_REASON_LABEL","sale","refund","restock","correction","damage","cancellation","sync"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAMD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMA,+BAA2D;IACtE;QAAEC,QAAQ;QAAaC,MAAM;QAASC,MAAM;QAAaC,WAAW;YAAC;YAAU;SAAU;IAAC;IAC1F;QAAEH,QAAQ;QAAUC,MAAM;QAASC,MAAM;QAAUC,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEH,QAAQ;QAAQC,MAAM;QAAQC,MAAM;QAAQE,UAAU;QAAUC,UAAU;IAAS;IACrF;QAAEL,QAAQ;QAAWC,MAAM;QAASC,MAAM;QAAWC,WAAW;YAAC;SAAS;IAAC;IAC3E;QAAEH,QAAQ;QAAcC,MAAM;QAASC,MAAM;QAAcC,WAAW;YAAC;SAAS;IAAC;CAClF,CAAA;AAED,yEAAyE,GACzE,OAAO,MAAMG,uBAA6C;IACxDC,QAAQR;IACRS,OAAO;QAAC;YAAEN,MAAM;YAAQO,WAAW;YAAQT,QAAQ;QAAO;KAAE;AAC9D,EAAC;AAED,OAAO,MAAMU,gCAAkE;IAC7EC,WAAW;IACXC,QAAQ;IACRC,MAAM;IACNC,SAAS;IACTC,YAAY;AACd,EAAC;AAED,sDAAsD,GACtD,OAAO,MAAMC,+BAAkD;IAAC;IAAa;IAAU;CAAa,CAAA;AAEpG,qEAAqE,GACrE,OAAO,MAAMC,8BAAmF;IAC9FC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,YAAY;IACZC,QAAQ;IACRC,cAAc;IACdC,MAAM;AACR,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/constants/stock-movement-query.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport type { InventoryAdjustmentReason } from '../model/commerce'\n\n/*\n * What the stock movements ledger filters by, and how its query serves it\n * (AGL-3321).\n *\n * Every field is one every writer of `hosts/{hostId}/inventoryAdjustments`\n * already stores — the \"Adjust stock\" dialog, the sale path\n * (`decrementVariantStock`) and the cancellation restock — so nothing new is\n * written and nothing needs a backfill. Each is an EQUALITY beneath the\n * ledger's one order, `atMs` newest first, which is one `(field, atMs DESC)`\n * composite per field under index merging; the date filter is a range on\n * `atMs` itself, the order the ledger is already in, so it costs nothing.\n *\n * Order and location are typed or picked ids, matched whole. A sparse field\n * answers `equals` exactly — a row with no order is simply not that order —\n * and offers no \"is set\", which would be an inequality on a second field.\n *\n * No quick search. The ledger's rows name a product by id; the name a reader\n * would type lives on the product, so a search could only ever match the\n * names of the rows already loaded. Product is a picked filter instead, and\n * the ids the old search read (order, location) are filters of their own.\n *\n * Every header sorts (AGL-3680): When either way, Change and Reason — `delta`\n * and `reason` every writer stores (`InventoryAdjustment` requires both, and\n * every ledger write is typed as one). Those are `alone`: served with no\n * filter on, at no composite cost on this unscoped list. Product (a name\n * read from the product) and Source (composed from three fields) sort the\n * page on screen.\n */\nexport const STOCK_MOVEMENT_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'productId', kind: 'exact', path: 'productId', operators: ['equals', 'isAnyOf'] },\n { column: 'reason', kind: 'exact', path: 'reason', operators: ['equals', 'isAnyOf'] },\n { column: 'atMs', kind: 'date', path: 'atMs', storedAs: 'millis', presence: 'always' },\n { column: 'orderId', kind: 'exact', path: 'orderId', operators: ['equals'] },\n { column: 'locationId', kind: 'exact', path: 'locationId', operators: ['equals'] },\n]\n\n/** The ledger's header orders, newest first FIRST: the order every writer stamps. */\nexport const STOCK_MOVEMENT_COLUMN_SORTS: readonly ListQuerySort[] = [\n { path: 'atMs', direction: 'desc', column: 'atMs', label: 'When' },\n { path: 'atMs', direction: 'asc', column: 'atMs', label: 'When', alone: true },\n { path: 'delta', direction: 'asc', column: 'delta', label: 'Change', alone: true },\n { path: 'delta', direction: 'desc', column: 'delta', label: 'Change', alone: true },\n { path: 'reason', direction: 'asc', column: 'reason', label: 'Reason', alone: true },\n { path: 'reason', direction: 'desc', column: 'reason', label: 'Reason', alone: true },\n]\n\nexport const STOCK_MOVEMENT_QUERY: ListQueryDeclaration = {\n fields: STOCK_MOVEMENT_FILTER_FIELDS,\n sorts: STOCK_MOVEMENT_COLUMN_SORTS,\n}\n\nexport const STOCK_MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n productId: 'Product',\n reason: 'Reason',\n atMs: 'When',\n orderId: 'Order',\n locationId: 'Location',\n}\n\n/** The fields picked from choices rather than typed. */\nexport const STOCK_MOVEMENT_SELECT_FIELDS: readonly string[] = ['productId', 'reason', 'locationId']\n\n/** The stored key is a closed set; these are what a merchant reads. */\nexport const STOCK_MOVEMENT_REASON_LABEL: Readonly<Record<InventoryAdjustmentReason, string>> = {\n sale: 'Sale',\n refund: 'Refund return',\n restock: 'Restock',\n correction: 'Correction',\n damage: 'Damaged',\n cancellation: 'Order canceled',\n sync: 'Count synced',\n}\n"],"names":["STOCK_MOVEMENT_FILTER_FIELDS","column","kind","path","operators","storedAs","presence","STOCK_MOVEMENT_COLUMN_SORTS","direction","label","alone","STOCK_MOVEMENT_QUERY","fields","sorts","STOCK_MOVEMENT_FILTER_HEADERS","productId","reason","atMs","orderId","locationId","STOCK_MOVEMENT_SELECT_FIELDS","STOCK_MOVEMENT_REASON_LABEL","sale","refund","restock","correction","damage","cancellation","sync"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GASD;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMA,+BAA2D;IACtE;QAAEC,QAAQ;QAAaC,MAAM;QAASC,MAAM;QAAaC,WAAW;YAAC;YAAU;SAAU;IAAC;IAC1F;QAAEH,QAAQ;QAAUC,MAAM;QAASC,MAAM;QAAUC,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEH,QAAQ;QAAQC,MAAM;QAAQC,MAAM;QAAQE,UAAU;QAAUC,UAAU;IAAS;IACrF;QAAEL,QAAQ;QAAWC,MAAM;QAASC,MAAM;QAAWC,WAAW;YAAC;SAAS;IAAC;IAC3E;QAAEH,QAAQ;QAAcC,MAAM;QAASC,MAAM;QAAcC,WAAW;YAAC;SAAS;IAAC;CAClF,CAAA;AAED,mFAAmF,GACnF,OAAO,MAAMG,8BAAwD;IACnE;QAAEJ,MAAM;QAAQK,WAAW;QAAQP,QAAQ;QAAQQ,OAAO;IAAO;IACjE;QAAEN,MAAM;QAAQK,WAAW;QAAOP,QAAQ;QAAQQ,OAAO;QAAQC,OAAO;IAAK;IAC7E;QAAEP,MAAM;QAASK,WAAW;QAAOP,QAAQ;QAASQ,OAAO;QAAUC,OAAO;IAAK;IACjF;QAAEP,MAAM;QAASK,WAAW;QAAQP,QAAQ;QAASQ,OAAO;QAAUC,OAAO;IAAK;IAClF;QAAEP,MAAM;QAAUK,WAAW;QAAOP,QAAQ;QAAUQ,OAAO;QAAUC,OAAO;IAAK;IACnF;QAAEP,MAAM;QAAUK,WAAW;QAAQP,QAAQ;QAAUQ,OAAO;QAAUC,OAAO;IAAK;CACrF,CAAA;AAED,OAAO,MAAMC,uBAA6C;IACxDC,QAAQZ;IACRa,OAAON;AACT,EAAC;AAED,OAAO,MAAMO,gCAAkE;IAC7EC,WAAW;IACXC,QAAQ;IACRC,MAAM;IACNC,SAAS;IACTC,YAAY;AACd,EAAC;AAED,sDAAsD,GACtD,OAAO,MAAMC,+BAAkD;IAAC;IAAa;IAAU;CAAa,CAAA;AAEpG,qEAAqE,GACrE,OAAO,MAAMC,8BAAmF;IAC9FC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,YAAY;IACZC,QAAQ;IACRC,cAAc;IACdC,MAAM;AACR,EAAC"}
@@ -21,6 +21,8 @@ import { registerPluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-re
21
21
  import { registerPluginShipmentRecords } from "@aglyn/aglyn/plugin-manager/plugin-shipment-records";
22
22
  import { registerPluginProductCatalog } from "@aglyn/aglyn/plugin-manager/plugin-product-catalog";
23
23
  import { registerPluginStockLevels } from "@aglyn/aglyn/plugin-manager/plugin-stock-levels";
24
+ import { registerPluginChannelOrders } from "@aglyn/aglyn/plugin-manager/plugin-channel-orders";
25
+ import { registerPluginProductWriter } from "@aglyn/aglyn/plugin-manager/plugin-product-writer";
24
26
  import { BUNDLE_ID } from "./constants/bundle-common.js";
25
27
  import { COMMERCE_OPERATOR_ALERTS } from "./constants/operator-alerts.js";
26
28
  import { registerCommerceEventTriggers } from "./server/order-event-triggers.js";
@@ -71,10 +73,29 @@ import { registerCommerceEventTriggers } from "./server/order-event-triggers.js"
71
73
  registerPluginStockLevels(lazyStockLevels, {
72
74
  pluginId: BUNDLE_ID
73
75
  });
76
+ // Products another plugin brings from a source of its own (AGL-3641),
77
+ // written through the store's own rules.
78
+ registerPluginProductWriter(lazyProductWriter, {
79
+ pluginId: BUNDLE_ID
80
+ });
81
+ // Orders a marketplace sold (AGL-3638), recorded as this plugin's own:
82
+ // numbered, their units off the same shelf in the same write, announced.
83
+ registerPluginChannelOrders(lazyChannelOrders, {
84
+ pluginId: BUNDLE_ID
85
+ });
74
86
  }
87
+ /** Outside orders, with the Admin SDK and the model arriving on the first call. */ const loadChannelOrders = async ()=>(await import("./server/channel-orders.js")).commerceChannelOrders;
88
+ const lazyChannelOrders = {
89
+ importOrder: async (order)=>(await loadChannelOrders()).importOrder(order),
90
+ cancelOrder: async (request)=>(await loadChannelOrders()).cancelOrder(request),
91
+ recordFees: async (request)=>(await loadChannelOrders()).recordFees(request)
92
+ };
75
93
  /** Stock counts, with the Admin SDK and the model arriving on the first call. */ const lazyStockLevels = {
76
94
  setAvailable: async (request)=>(await import("./server/stock-levels.js")).commerceStockLevels.setAvailable(request)
77
95
  };
96
+ /** The writer, with the Admin SDK and the model arriving on the first write. */ const lazyProductWriter = {
97
+ upsertSourced: async (write)=>(await import("./server/product-writer.js")).commerceProductWriter.upsertSourced(write)
98
+ };
78
99
  const loadShipmentRecords = async ()=>(await import("./server/shipment-records.js")).commerceShipmentRecords;
79
100
  const lazyShipmentRecords = {
80
101
  read: async (hostId, recordId)=>(await loadShipmentRecords()).read(hostId, recordId),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/declarations.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The seam from its own module, not the plugin-manager barrel: boot needs the\n// registry and nothing else, and the barrel reaches the client contexts.\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { registerPluginEventHandler } from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport {\n registerPluginShipmentRecords,\n type PluginShipmentRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport {\n registerPluginProductCatalog,\n type PluginProductCatalog,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-catalog'\nimport {\n registerPluginStockLevels,\n type PluginStockLevels,\n} from '@aglyn/aglyn/plugin-manager/plugin-stock-levels'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_OPERATOR_ALERTS } from './constants/operator-alerts'\nimport { registerCommerceEventTriggers } from './server/order-event-triggers'\n\n/**\n * The commerce plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads — among them its orders as a label\n * buyer reads and writes them, through core's `core.shipment-records`.\n *\n * Its operator alerts (AGL-3377), so Staff → Operator alerts lists them and\n * staff can switch them before the first one is ever raised; and the indexes\n * of its products and categories, so another plugin reads them without\n * reaching for this plugin's collections.\n */\nexport function registerCommerceServerDeclarations(): void {\n registerOperatorAlerts(COMMERCE_OPERATOR_ALERTS, { pluginId: BUNDLE_ID })\n // Its order and return events (AGL-3611), and each as a workflow trigger.\n registerCommerceEventTriggers()\n // Products and categories, as another plugin reads them (AGL-3080). The\n // readers and the Admin SDK arrive with the first read, not with the boot.\n registerPluginRecordIndex('product', lazyIndex('productRecordIndex'), { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('productCategory', lazyIndex('productCategoryRecordIndex'), {\n pluginId: BUNDLE_ID,\n })\n // Orders, as a label buyer reads and writes them (AGL-3612): read the\n // lines and the address, write a shipment, record tracking. The module and\n // the Admin SDK load with the first call.\n registerPluginShipmentRecords(lazyShipmentRecords, { pluginId: BUNDLE_ID })\n // A site's domains as Stripe payment method domains (AGL-3629), so Apple\n // Pay and the other wallets show on the merchant's own domain. The Stripe\n // calls arrive with the first event, not with the boot.\n registerPluginEventHandler(\n 'host.domain.attached',\n async (payload) =>\n (await import('./server/payment-method-domains')).onHostDomainAttached(payload),\n { pluginId: BUNDLE_ID },\n )\n registerPluginEventHandler(\n 'host.domain.released',\n async (payload) =>\n (await import('./server/payment-method-domains')).onHostDomainReleased(payload),\n { pluginId: BUNDLE_ID },\n )\n // The store's catalog, as a shopping-channel feed reads it (AGL-3637).\n registerPluginProductCatalog(lazyCatalog, { pluginId: BUNDLE_ID })\n // Counts another warehouse keeps, applied as a stock-take (AGL-3634): a\n // fulfillment network says how many it can ship and the products say so.\n registerPluginStockLevels(lazyStockLevels, { pluginId: BUNDLE_ID })\n}\n\n/** Stock counts, with the Admin SDK and the model arriving on the first call. */\nconst lazyStockLevels: PluginStockLevels = {\n setAvailable: async (request) => (await import('./server/stock-levels')).commerceStockLevels.setAvailable(request),\n}\n\nconst loadShipmentRecords = async () =>\n (await import('./server/shipment-records')).commerceShipmentRecords\n\nconst lazyShipmentRecords: PluginShipmentRecords = {\n read: async (hostId, recordId) => (await loadShipmentRecords()).read(hostId, recordId),\n recordShipment: async (write) => (await loadShipmentRecords()).recordShipment(write),\n recordTracking: async (update) => (await loadShipmentRecords()).recordTracking(update),\n shipFromAddresses: async (hostId) => (await loadShipmentRecords()).shipFromAddresses(hostId),\n}\n\n/** The catalog, with the Admin SDK and the model arriving on the first read. */\nconst lazyCatalog: PluginProductCatalog = {\n store: async (hostId) => (await import('./server/product-catalog')).productCatalog.store(hostId),\n page: async (request) => (await import('./server/product-catalog')).productCatalog.page(request),\n}\n\ntype IndexName = 'productRecordIndex' | 'productCategoryRecordIndex'\n\nfunction lazyIndex(name: IndexName): PluginRecordIndex {\n const load = async () => (await import('./server/product-record-index'))[name]\n return {\n list: async (request) => (await load()).list(request),\n get: async (request) => (await load()).get(request),\n }\n}\n"],"names":["registerOperatorAlerts","registerPluginEventHandler","registerPluginRecordIndex","registerPluginShipmentRecords","registerPluginProductCatalog","registerPluginStockLevels","BUNDLE_ID","COMMERCE_OPERATOR_ALERTS","registerCommerceEventTriggers","registerCommerceServerDeclarations","pluginId","lazyIndex","lazyShipmentRecords","payload","onHostDomainAttached","onHostDomainReleased","lazyCatalog","lazyStockLevels","setAvailable","request","commerceStockLevels","loadShipmentRecords","commerceShipmentRecords","read","hostId","recordId","recordShipment","write","recordTracking","update","shipFromAddresses","store","productCatalog","page","name","load","list","get"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,yEAAyE;AACzE,SAASA,sBAAsB,QAAQ,8CAA6C;AACpF,SAASC,0BAA0B,QAAQ,4CAA2C;AACtF,SACEC,yBAAyB,QAEpB,kDAAiD;AACxD,SACEC,6BAA6B,QAExB,sDAAqD;AAC5D,SACEC,4BAA4B,QAEvB,qDAAoD;AAC3D,SACEC,yBAAyB,QAEpB,kDAAiD;AACxD,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,wBAAwB,QAAQ,iCAA6B;AACtE,SAASC,6BAA6B,QAAQ,mCAA+B;AAE7E;;;;;;;;;CASC,GACD,OAAO,SAASC;IACdT,uBAAuBO,0BAA0B;QAAEG,UAAUJ;IAAU;IACvE,0EAA0E;IAC1EE;IACA,wEAAwE;IACxE,2EAA2E;IAC3EN,0BAA0B,WAAWS,UAAU,uBAAuB;QAAED,UAAUJ;IAAU;IAC5FJ,0BAA0B,mBAAmBS,UAAU,+BAA+B;QACpFD,UAAUJ;IACZ;IACA,sEAAsE;IACtE,2EAA2E;IAC3E,0CAA0C;IAC1CH,8BAA8BS,qBAAqB;QAAEF,UAAUJ;IAAU;IACzE,yEAAyE;IACzE,0EAA0E;IAC1E,wDAAwD;IACxDL,2BACE,wBACA,OAAOY,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,oBAAoB,CAACD,UACzE;QAAEH,UAAUJ;IAAU;IAExBL,2BACE,wBACA,OAAOY,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGE,oBAAoB,CAACF,UACzE;QAAEH,UAAUJ;IAAU;IAExB,uEAAuE;IACvEF,6BAA6BY,aAAa;QAAEN,UAAUJ;IAAU;IAChE,wEAAwE;IACxE,yEAAyE;IACzED,0BAA0BY,iBAAiB;QAAEP,UAAUJ;IAAU;AACnE;AAEA,+EAA+E,GAC/E,MAAMW,kBAAqC;IACzCC,cAAc,OAAOC,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,2BAAuB,EAAGC,mBAAmB,CAACF,YAAY,CAACC;AAC5G;AAEA,MAAME,sBAAsB,UAC1B,AAAC,CAAA,MAAM,MAAM,CAAC,+BAA2B,EAAGC,uBAAuB;AAErE,MAAMV,sBAA6C;IACjDW,MAAM,OAAOC,QAAQC,WAAa,AAAC,CAAA,MAAMJ,qBAAoB,EAAGE,IAAI,CAACC,QAAQC;IAC7EC,gBAAgB,OAAOC,QAAU,AAAC,CAAA,MAAMN,qBAAoB,EAAGK,cAAc,CAACC;IAC9EC,gBAAgB,OAAOC,SAAW,AAAC,CAAA,MAAMR,qBAAoB,EAAGO,cAAc,CAACC;IAC/EC,mBAAmB,OAAON,SAAW,AAAC,CAAA,MAAMH,qBAAoB,EAAGS,iBAAiB,CAACN;AACvF;AAEA,8EAA8E,GAC9E,MAAMR,cAAoC;IACxCe,OAAO,OAAOP,SAAW,AAAC,CAAA,MAAM,MAAM,CAAC,8BAA0B,EAAGQ,cAAc,CAACD,KAAK,CAACP;IACzFS,MAAM,OAAOd,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,8BAA0B,EAAGa,cAAc,CAACC,IAAI,CAACd;AAC1F;AAIA,SAASR,UAAUuB,IAAe;IAChC,MAAMC,OAAO,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,mCAA+B,CAAE,CAACD,KAAK;IAC9E,OAAO;QACLE,MAAM,OAAOjB,UAAY,AAAC,CAAA,MAAMgB,MAAK,EAAGC,IAAI,CAACjB;QAC7CkB,KAAK,OAAOlB,UAAY,AAAC,CAAA,MAAMgB,MAAK,EAAGE,GAAG,CAAClB;IAC7C;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/declarations.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The seam from its own module, not the plugin-manager barrel: boot needs the\n// registry and nothing else, and the barrel reaches the client contexts.\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { registerPluginEventHandler } from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport {\n registerPluginShipmentRecords,\n type PluginShipmentRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport {\n registerPluginProductCatalog,\n type PluginProductCatalog,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-catalog'\nimport {\n registerPluginStockLevels,\n type PluginStockLevels,\n} from '@aglyn/aglyn/plugin-manager/plugin-stock-levels'\nimport {\n registerPluginChannelOrders,\n type PluginChannelOrders,\n} from '@aglyn/aglyn/plugin-manager/plugin-channel-orders'\nimport {\n registerPluginProductWriter,\n type PluginProductWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-writer'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_OPERATOR_ALERTS } from './constants/operator-alerts'\nimport { registerCommerceEventTriggers } from './server/order-event-triggers'\n\n/**\n * The commerce plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads — among them its orders as a label\n * buyer reads and writes them, through core's `core.shipment-records`.\n *\n * Its operator alerts (AGL-3377), so Staff → Operator alerts lists them and\n * staff can switch them before the first one is ever raised; and the indexes\n * of its products and categories, so another plugin reads them without\n * reaching for this plugin's collections.\n */\nexport function registerCommerceServerDeclarations(): void {\n registerOperatorAlerts(COMMERCE_OPERATOR_ALERTS, { pluginId: BUNDLE_ID })\n // Its order and return events (AGL-3611), and each as a workflow trigger.\n registerCommerceEventTriggers()\n // Products and categories, as another plugin reads them (AGL-3080). The\n // readers and the Admin SDK arrive with the first read, not with the boot.\n registerPluginRecordIndex('product', lazyIndex('productRecordIndex'), { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('productCategory', lazyIndex('productCategoryRecordIndex'), {\n pluginId: BUNDLE_ID,\n })\n // Orders, as a label buyer reads and writes them (AGL-3612): read the\n // lines and the address, write a shipment, record tracking. The module and\n // the Admin SDK load with the first call.\n registerPluginShipmentRecords(lazyShipmentRecords, { pluginId: BUNDLE_ID })\n // A site's domains as Stripe payment method domains (AGL-3629), so Apple\n // Pay and the other wallets show on the merchant's own domain. The Stripe\n // calls arrive with the first event, not with the boot.\n registerPluginEventHandler(\n 'host.domain.attached',\n async (payload) =>\n (await import('./server/payment-method-domains')).onHostDomainAttached(payload),\n { pluginId: BUNDLE_ID },\n )\n registerPluginEventHandler(\n 'host.domain.released',\n async (payload) =>\n (await import('./server/payment-method-domains')).onHostDomainReleased(payload),\n { pluginId: BUNDLE_ID },\n )\n // The store's catalog, as a shopping-channel feed reads it (AGL-3637).\n registerPluginProductCatalog(lazyCatalog, { pluginId: BUNDLE_ID })\n // Counts another warehouse keeps, applied as a stock-take (AGL-3634): a\n // fulfillment network says how many it can ship and the products say so.\n registerPluginStockLevels(lazyStockLevels, { pluginId: BUNDLE_ID })\n // Products another plugin brings from a source of its own (AGL-3641),\n // written through the store's own rules.\n registerPluginProductWriter(lazyProductWriter, { pluginId: BUNDLE_ID })\n // Orders a marketplace sold (AGL-3638), recorded as this plugin's own:\n // numbered, their units off the same shelf in the same write, announced.\n registerPluginChannelOrders(lazyChannelOrders, { pluginId: BUNDLE_ID })\n}\n\n/** Outside orders, with the Admin SDK and the model arriving on the first call. */\nconst loadChannelOrders = async () => (await import('./server/channel-orders')).commerceChannelOrders\n\nconst lazyChannelOrders: PluginChannelOrders = {\n importOrder: async (order) => (await loadChannelOrders()).importOrder(order),\n cancelOrder: async (request) => (await loadChannelOrders()).cancelOrder(request),\n recordFees: async (request) => (await loadChannelOrders()).recordFees(request),\n}\n\n/** Stock counts, with the Admin SDK and the model arriving on the first call. */\nconst lazyStockLevels: PluginStockLevels = {\n setAvailable: async (request) => (await import('./server/stock-levels')).commerceStockLevels.setAvailable(request),\n}\n\n/** The writer, with the Admin SDK and the model arriving on the first write. */\nconst lazyProductWriter: PluginProductWriter = {\n upsertSourced: async (write) => (await import('./server/product-writer')).commerceProductWriter.upsertSourced(write),\n}\n\nconst loadShipmentRecords = async () =>\n (await import('./server/shipment-records')).commerceShipmentRecords\n\nconst lazyShipmentRecords: PluginShipmentRecords = {\n read: async (hostId, recordId) => (await loadShipmentRecords()).read(hostId, recordId),\n recordShipment: async (write) => (await loadShipmentRecords()).recordShipment(write),\n recordTracking: async (update) => (await loadShipmentRecords()).recordTracking(update),\n shipFromAddresses: async (hostId) => (await loadShipmentRecords()).shipFromAddresses(hostId),\n}\n\n/** The catalog, with the Admin SDK and the model arriving on the first read. */\nconst lazyCatalog: PluginProductCatalog = {\n store: async (hostId) => (await import('./server/product-catalog')).productCatalog.store(hostId),\n page: async (request) => (await import('./server/product-catalog')).productCatalog.page(request),\n}\n\ntype IndexName = 'productRecordIndex' | 'productCategoryRecordIndex'\n\nfunction lazyIndex(name: IndexName): PluginRecordIndex {\n const load = async () => (await import('./server/product-record-index'))[name]\n return {\n list: async (request) => (await load()).list(request),\n get: async (request) => (await load()).get(request),\n }\n}\n"],"names":["registerOperatorAlerts","registerPluginEventHandler","registerPluginRecordIndex","registerPluginShipmentRecords","registerPluginProductCatalog","registerPluginStockLevels","registerPluginChannelOrders","registerPluginProductWriter","BUNDLE_ID","COMMERCE_OPERATOR_ALERTS","registerCommerceEventTriggers","registerCommerceServerDeclarations","pluginId","lazyIndex","lazyShipmentRecords","payload","onHostDomainAttached","onHostDomainReleased","lazyCatalog","lazyStockLevels","lazyProductWriter","lazyChannelOrders","loadChannelOrders","commerceChannelOrders","importOrder","order","cancelOrder","request","recordFees","setAvailable","commerceStockLevels","upsertSourced","write","commerceProductWriter","loadShipmentRecords","commerceShipmentRecords","read","hostId","recordId","recordShipment","recordTracking","update","shipFromAddresses","store","productCatalog","page","name","load","list","get"],"mappings":"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"}
@@ -17,8 +17,10 @@
17
17
  /**
18
18
  * What a store tells its buyer, and when (AGL-3610).
19
19
  *
20
- * Five moments in an order's life each send the buyer one message: the
21
- * receipt, each shipment, delivery, each refund and a cancellation. A store
20
+ * Each moment in an order's life sends the buyer one message: the receipt,
21
+ * each shipment, delivery, each refund and a cancellation — and, for an
22
+ * order collected or brought by the store (AGL-3624), ready for pickup,
23
+ * picked up and out for delivery. A store
22
24
  * turns any of them off on `hosts/{hostId}/settings/store.buyerNotifications`
23
25
  * — and ONLY off: every moment is ON unless an explicit `false` is stored,
24
26
  * because transaction messages are on by default for every store, existing
@@ -30,7 +32,7 @@
30
32
  * send texts and the order carries the buyer's phone number, each moment that
31
33
  * is on also goes by text unless the store turned texts off.
32
34
  */
33
- export declare const BUYER_NOTIFICATION_EVENTS: readonly ["receipt", "shipped", "delivered", "refunded", "cancelled"];
35
+ export declare const BUYER_NOTIFICATION_EVENTS: readonly ["receipt", "shipped", "delivered", "refunded", "cancelled", "ready_for_pickup", "picked_up", "out_for_delivery"];
34
36
  export type BuyerNotificationEvent = (typeof BUYER_NOTIFICATION_EVENTS)[number];
35
37
  export type BuyerNotificationChannel = 'email' | 'sms';
36
38
  export type BuyerNotificationSettings = Partial<Record<BuyerNotificationEvent | 'texts', boolean>>;