@open-mercato/core 0.9.1-develop.7474.1.51db10479d → 0.9.1-develop.7475.1.28ca93ed97

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 (597) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/catalog_price_history_entry/index.js +53 -0
  3. package/dist/generated/entities/catalog_price_history_entry/index.js.map +7 -0
  4. package/dist/generated/entities/customer_group_terms/index.js +2 -0
  5. package/dist/generated/entities/customer_group_terms/index.js.map +2 -2
  6. package/dist/generated/entities/ecommerce_store/index.js +33 -0
  7. package/dist/generated/entities/ecommerce_store/index.js.map +7 -0
  8. package/dist/generated/entities/ecommerce_store_channel_binding/index.js +29 -0
  9. package/dist/generated/entities/ecommerce_store_channel_binding/index.js.map +7 -0
  10. package/dist/generated/entities/ecommerce_store_domain_binding/index.js +23 -0
  11. package/dist/generated/entities/ecommerce_store_domain_binding/index.js.map +7 -0
  12. package/dist/generated/entities.ids.generated.js +8 -1
  13. package/dist/generated/entities.ids.generated.js.map +2 -2
  14. package/dist/generated/entity-fields-registry.js +72 -0
  15. package/dist/generated/entity-fields-registry.js.map +2 -2
  16. package/dist/helpers/integration/customerGroupsFixtures.js.map +2 -2
  17. package/dist/modules/catalog/acl.js +8 -1
  18. package/dist/modules/catalog/acl.js.map +2 -2
  19. package/dist/modules/catalog/api/config/omnibus/route.js +184 -0
  20. package/dist/modules/catalog/api/config/omnibus/route.js.map +7 -0
  21. package/dist/modules/catalog/api/prices/history/route.js +131 -0
  22. package/dist/modules/catalog/api/prices/history/route.js.map +7 -0
  23. package/dist/modules/catalog/api/prices/omnibus-preview/route.js +184 -0
  24. package/dist/modules/catalog/api/prices/omnibus-preview/route.js.map +7 -0
  25. package/dist/modules/catalog/api/products/route.js +54 -245
  26. package/dist/modules/catalog/api/products/route.js.map +3 -3
  27. package/dist/modules/catalog/api/tags/route.js +6 -0
  28. package/dist/modules/catalog/api/tags/route.js.map +2 -2
  29. package/dist/modules/catalog/backend/config/catalog/page.js +3 -1
  30. package/dist/modules/catalog/backend/config/catalog/page.js.map +2 -2
  31. package/dist/modules/catalog/cli.js +80 -2
  32. package/dist/modules/catalog/cli.js.map +2 -2
  33. package/dist/modules/catalog/commands/categories.js +51 -2
  34. package/dist/modules/catalog/commands/categories.js.map +2 -2
  35. package/dist/modules/catalog/commands/prices.js +43 -1
  36. package/dist/modules/catalog/commands/prices.js.map +2 -2
  37. package/dist/modules/catalog/commands/products.js +71 -0
  38. package/dist/modules/catalog/commands/products.js.map +2 -2
  39. package/dist/modules/catalog/commands/variants.js +7 -0
  40. package/dist/modules/catalog/commands/variants.js.map +2 -2
  41. package/dist/modules/catalog/components/OmnibusSettings.js +703 -0
  42. package/dist/modules/catalog/components/OmnibusSettings.js.map +7 -0
  43. package/dist/modules/catalog/components/PriceEditorOmnibusRow.js +152 -0
  44. package/dist/modules/catalog/components/PriceEditorOmnibusRow.js.map +7 -0
  45. package/dist/modules/catalog/components/lookupSources.js +31 -0
  46. package/dist/modules/catalog/components/lookupSources.js.map +7 -0
  47. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js +8 -2
  48. package/dist/modules/catalog/components/prices/PriceScopeSelectors.js.map +2 -2
  49. package/dist/modules/catalog/components/products/VariantBuilder.js +11 -1
  50. package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
  51. package/dist/modules/catalog/data/entities.js +122 -1
  52. package/dist/modules/catalog/data/entities.js.map +2 -2
  53. package/dist/modules/catalog/data/validators.js +78 -0
  54. package/dist/modules/catalog/data/validators.js.map +2 -2
  55. package/dist/modules/catalog/di.js +18 -0
  56. package/dist/modules/catalog/di.js.map +2 -2
  57. package/dist/modules/catalog/lib/omnibus.js +285 -0
  58. package/dist/modules/catalog/lib/omnibus.js.map +7 -0
  59. package/dist/modules/catalog/lib/omnibusBackfill.js +146 -0
  60. package/dist/modules/catalog/lib/omnibusBackfill.js.map +7 -0
  61. package/dist/modules/catalog/lib/omnibusCache.js +89 -0
  62. package/dist/modules/catalog/lib/omnibusCache.js.map +7 -0
  63. package/dist/modules/catalog/lib/omnibusConfig.js +64 -0
  64. package/dist/modules/catalog/lib/omnibusConfig.js.map +7 -0
  65. package/dist/modules/catalog/lib/omnibusHistoryQueries.js +191 -0
  66. package/dist/modules/catalog/lib/omnibusHistoryQueries.js.map +7 -0
  67. package/dist/modules/catalog/lib/omnibusPresentedEntry.js +73 -0
  68. package/dist/modules/catalog/lib/omnibusPresentedEntry.js.map +7 -0
  69. package/dist/modules/catalog/lib/omnibusProductListEnrichment.js +94 -0
  70. package/dist/modules/catalog/lib/omnibusProductListEnrichment.js.map +7 -0
  71. package/dist/modules/catalog/lib/omnibusTypes.js +115 -0
  72. package/dist/modules/catalog/lib/omnibusTypes.js.map +7 -0
  73. package/dist/modules/catalog/lib/optionSchemaTranslations.js +55 -0
  74. package/dist/modules/catalog/lib/optionSchemaTranslations.js.map +7 -0
  75. package/dist/modules/catalog/lib/priceHistoryQuery.js +103 -0
  76. package/dist/modules/catalog/lib/priceHistoryQuery.js.map +7 -0
  77. package/dist/modules/catalog/lib/pricing.js +26 -2
  78. package/dist/modules/catalog/lib/pricing.js.map +2 -2
  79. package/dist/modules/catalog/lib/productFilters.js +273 -0
  80. package/dist/modules/catalog/lib/productFilters.js.map +7 -0
  81. package/dist/modules/catalog/lib/productScopeKeys.js +105 -0
  82. package/dist/modules/catalog/lib/productScopeKeys.js.map +7 -0
  83. package/dist/modules/catalog/lib/productScopeReindex.js +73 -0
  84. package/dist/modules/catalog/lib/productScopeReindex.js.map +7 -0
  85. package/dist/modules/catalog/migrations/Migration20261005105352_catalog.js +39 -0
  86. package/dist/modules/catalog/migrations/Migration20261005105352_catalog.js.map +7 -0
  87. package/dist/modules/catalog/migrations/Migration20261005150824_catalog.js +39 -0
  88. package/dist/modules/catalog/migrations/Migration20261005150824_catalog.js.map +7 -0
  89. package/dist/modules/catalog/services/catalogOmnibusService.js +480 -0
  90. package/dist/modules/catalog/services/catalogOmnibusService.js.map +7 -0
  91. package/dist/modules/catalog/setup.js +3 -1
  92. package/dist/modules/catalog/setup.js.map +2 -2
  93. package/dist/modules/catalog/subscribers/product-scope-keys-category-deleted.js +16 -0
  94. package/dist/modules/catalog/subscribers/product-scope-keys-category-deleted.js.map +7 -0
  95. package/dist/modules/catalog/subscribers/product-scope-keys-category-updated.js +17 -0
  96. package/dist/modules/catalog/subscribers/product-scope-keys-category-updated.js.map +7 -0
  97. package/dist/modules/catalog/translations.js +10 -0
  98. package/dist/modules/catalog/translations.js.map +2 -2
  99. package/dist/modules/configs/lib/upgrade-actions.js +28 -0
  100. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  101. package/dist/modules/customer_groups/api/customer-groups/[id]/terms/route.js +5 -0
  102. package/dist/modules/customer_groups/api/customer-groups/[id]/terms/route.js.map +2 -2
  103. package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js +111 -3
  104. package/dist/modules/customer_groups/components/CustomerGroupTermsSection.js.map +2 -2
  105. package/dist/modules/customer_groups/components/customerGroupAssortmentScope.js +43 -0
  106. package/dist/modules/customer_groups/components/customerGroupAssortmentScope.js.map +7 -0
  107. package/dist/modules/customer_groups/data/entities.js +3 -0
  108. package/dist/modules/customer_groups/data/entities.js.map +2 -2
  109. package/dist/modules/customer_groups/data/validators.js +9 -0
  110. package/dist/modules/customer_groups/data/validators.js.map +2 -2
  111. package/dist/modules/customer_groups/migrations/Migration20261005110250_customer_groups.js +17 -0
  112. package/dist/modules/customer_groups/migrations/Migration20261005110250_customer_groups.js.map +7 -0
  113. package/dist/modules/customer_groups/services/customerGroupsService.js +62 -15
  114. package/dist/modules/customer_groups/services/customerGroupsService.js.map +2 -2
  115. package/dist/modules/ecommerce/acl.js +13 -0
  116. package/dist/modules/ecommerce/acl.js.map +7 -0
  117. package/dist/modules/ecommerce/api/domain-mappings/route.js +72 -0
  118. package/dist/modules/ecommerce/api/domain-mappings/route.js.map +7 -0
  119. package/dist/modules/ecommerce/api/openapi.js +27 -0
  120. package/dist/modules/ecommerce/api/openapi.js.map +7 -0
  121. package/dist/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.js +109 -0
  122. package/dist/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.js.map +7 -0
  123. package/dist/modules/ecommerce/api/store-channel-bindings/crud.js +265 -0
  124. package/dist/modules/ecommerce/api/store-channel-bindings/crud.js.map +7 -0
  125. package/dist/modules/ecommerce/api/store-channel-bindings/route.js +67 -0
  126. package/dist/modules/ecommerce/api/store-channel-bindings/route.js.map +7 -0
  127. package/dist/modules/ecommerce/api/store-domain-bindings/crud.js +296 -0
  128. package/dist/modules/ecommerce/api/store-domain-bindings/crud.js.map +7 -0
  129. package/dist/modules/ecommerce/api/store-domain-bindings/route.js +73 -0
  130. package/dist/modules/ecommerce/api/store-domain-bindings/route.js.map +7 -0
  131. package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js +87 -0
  132. package/dist/modules/ecommerce/api/storefront/categories/[slug]/route.js.map +7 -0
  133. package/dist/modules/ecommerce/api/storefront/categories/openapiSchemas.js +50 -0
  134. package/dist/modules/ecommerce/api/storefront/categories/openapiSchemas.js.map +7 -0
  135. package/dist/modules/ecommerce/api/storefront/categories/route.js +75 -0
  136. package/dist/modules/ecommerce/api/storefront/categories/route.js.map +7 -0
  137. package/dist/modules/ecommerce/api/storefront/context/route.js +163 -0
  138. package/dist/modules/ecommerce/api/storefront/context/route.js.map +7 -0
  139. package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js +87 -0
  140. package/dist/modules/ecommerce/api/storefront/products/[idOrHandle]/route.js.map +7 -0
  141. package/dist/modules/ecommerce/api/storefront/products/openapiSchemas.js +177 -0
  142. package/dist/modules/ecommerce/api/storefront/products/openapiSchemas.js.map +7 -0
  143. package/dist/modules/ecommerce/api/storefront/products/route.js +78 -0
  144. package/dist/modules/ecommerce/api/storefront/products/route.js.map +7 -0
  145. package/dist/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.js +25 -0
  146. package/dist/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.js.map +7 -0
  147. package/dist/modules/ecommerce/api/storefront/search/suggest/route.js +75 -0
  148. package/dist/modules/ecommerce/api/storefront/search/suggest/route.js.map +7 -0
  149. package/dist/modules/ecommerce/api/storefront/storefrontRouteSupport.js +58 -0
  150. package/dist/modules/ecommerce/api/storefront/storefrontRouteSupport.js.map +7 -0
  151. package/dist/modules/ecommerce/api/stores/[id]/branding/route.js +183 -0
  152. package/dist/modules/ecommerce/api/stores/[id]/branding/route.js.map +7 -0
  153. package/dist/modules/ecommerce/api/stores/[id]/preview-branding/route.js +92 -0
  154. package/dist/modules/ecommerce/api/stores/[id]/preview-branding/route.js.map +7 -0
  155. package/dist/modules/ecommerce/api/stores/crud.js +401 -0
  156. package/dist/modules/ecommerce/api/stores/crud.js.map +7 -0
  157. package/dist/modules/ecommerce/api/stores/route.js +64 -0
  158. package/dist/modules/ecommerce/api/stores/route.js.map +7 -0
  159. package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.js +9 -0
  160. package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.js.map +7 -0
  161. package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.js +16 -0
  162. package/dist/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.js.map +7 -0
  163. package/dist/modules/ecommerce/backend/config/ecommerce/page.js +9 -0
  164. package/dist/modules/ecommerce/backend/config/ecommerce/page.js.map +7 -0
  165. package/dist/modules/ecommerce/backend/config/ecommerce/page.meta.js +16 -0
  166. package/dist/modules/ecommerce/backend/config/ecommerce/page.meta.js.map +7 -0
  167. package/dist/modules/ecommerce/commands/stores.js +110 -0
  168. package/dist/modules/ecommerce/commands/stores.js.map +7 -0
  169. package/dist/modules/ecommerce/components/ChannelAssortmentCount.js +90 -0
  170. package/dist/modules/ecommerce/components/ChannelAssortmentCount.js.map +7 -0
  171. package/dist/modules/ecommerce/components/ChannelBindingFormSections.js +33 -0
  172. package/dist/modules/ecommerce/components/ChannelBindingFormSections.js.map +7 -0
  173. package/dist/modules/ecommerce/components/ChannelLookupPicker.js +11 -0
  174. package/dist/modules/ecommerce/components/ChannelLookupPicker.js.map +7 -0
  175. package/dist/modules/ecommerce/components/DomainStatusWarning.js +24 -0
  176. package/dist/modules/ecommerce/components/DomainStatusWarning.js.map +7 -0
  177. package/dist/modules/ecommerce/components/StoreAvailabilityDefaultsSection.js +190 -0
  178. package/dist/modules/ecommerce/components/StoreAvailabilityDefaultsSection.js.map +7 -0
  179. package/dist/modules/ecommerce/components/StoreBrandingControls.js +67 -0
  180. package/dist/modules/ecommerce/components/StoreBrandingControls.js.map +7 -0
  181. package/dist/modules/ecommerce/components/StoreBrandingPreview.js +69 -0
  182. package/dist/modules/ecommerce/components/StoreBrandingPreview.js.map +7 -0
  183. package/dist/modules/ecommerce/components/StoreBrandingTab.js +218 -0
  184. package/dist/modules/ecommerce/components/StoreBrandingTab.js.map +7 -0
  185. package/dist/modules/ecommerce/components/StoreChannelBindingDialog.js +270 -0
  186. package/dist/modules/ecommerce/components/StoreChannelBindingDialog.js.map +7 -0
  187. package/dist/modules/ecommerce/components/StoreChannelsTab.js +313 -0
  188. package/dist/modules/ecommerce/components/StoreChannelsTab.js.map +7 -0
  189. package/dist/modules/ecommerce/components/StoreCreateDialog.js +164 -0
  190. package/dist/modules/ecommerce/components/StoreCreateDialog.js.map +7 -0
  191. package/dist/modules/ecommerce/components/StoreDomainBindingDialog.js +162 -0
  192. package/dist/modules/ecommerce/components/StoreDomainBindingDialog.js.map +7 -0
  193. package/dist/modules/ecommerce/components/StoreDomainsTab.js +357 -0
  194. package/dist/modules/ecommerce/components/StoreDomainsTab.js.map +7 -0
  195. package/dist/modules/ecommerce/components/StoreEditPage.js +155 -0
  196. package/dist/modules/ecommerce/components/StoreEditPage.js.map +7 -0
  197. package/dist/modules/ecommerce/components/StoreGeneralTab.js +167 -0
  198. package/dist/modules/ecommerce/components/StoreGeneralTab.js.map +7 -0
  199. package/dist/modules/ecommerce/components/StoreListPage.js +290 -0
  200. package/dist/modules/ecommerce/components/StoreListPage.js.map +7 -0
  201. package/dist/modules/ecommerce/components/StoreSeoTab.js +132 -0
  202. package/dist/modules/ecommerce/components/StoreSeoTab.js.map +7 -0
  203. package/dist/modules/ecommerce/components/StoreStatusBadge.js +20 -0
  204. package/dist/modules/ecommerce/components/StoreStatusBadge.js.map +7 -0
  205. package/dist/modules/ecommerce/components/channelLookups.js +28 -0
  206. package/dist/modules/ecommerce/components/channelLookups.js.map +7 -0
  207. package/dist/modules/ecommerce/components/storeAdmin.js +41 -0
  208. package/dist/modules/ecommerce/components/storeAdmin.js.map +7 -0
  209. package/dist/modules/ecommerce/components/storeBrandingForm.js +197 -0
  210. package/dist/modules/ecommerce/components/storeBrandingForm.js.map +7 -0
  211. package/dist/modules/ecommerce/components/storeChannels.js +140 -0
  212. package/dist/modules/ecommerce/components/storeChannels.js.map +7 -0
  213. package/dist/modules/ecommerce/components/storeDomains.js +167 -0
  214. package/dist/modules/ecommerce/components/storeDomains.js.map +7 -0
  215. package/dist/modules/ecommerce/components/storeEditTabs.js +62 -0
  216. package/dist/modules/ecommerce/components/storeEditTabs.js.map +7 -0
  217. package/dist/modules/ecommerce/components/storeGeneral.js +95 -0
  218. package/dist/modules/ecommerce/components/storeGeneral.js.map +7 -0
  219. package/dist/modules/ecommerce/components/storeSeo.js +69 -0
  220. package/dist/modules/ecommerce/components/storeSeo.js.map +7 -0
  221. package/dist/modules/ecommerce/components/useChannelAssortmentCount.js +45 -0
  222. package/dist/modules/ecommerce/components/useChannelAssortmentCount.js.map +7 -0
  223. package/dist/modules/ecommerce/components/useStoreAccess.js +28 -0
  224. package/dist/modules/ecommerce/components/useStoreAccess.js.map +7 -0
  225. package/dist/modules/ecommerce/data/enrichers.js +157 -0
  226. package/dist/modules/ecommerce/data/enrichers.js.map +7 -0
  227. package/dist/modules/ecommerce/data/entities.js +197 -0
  228. package/dist/modules/ecommerce/data/entities.js.map +7 -0
  229. package/dist/modules/ecommerce/data/validators.js +406 -0
  230. package/dist/modules/ecommerce/data/validators.js.map +7 -0
  231. package/dist/modules/ecommerce/di.js +11 -0
  232. package/dist/modules/ecommerce/di.js.map +7 -0
  233. package/dist/modules/ecommerce/events.js +27 -0
  234. package/dist/modules/ecommerce/events.js.map +7 -0
  235. package/dist/modules/ecommerce/index.js +16 -0
  236. package/dist/modules/ecommerce/index.js.map +7 -0
  237. package/dist/modules/ecommerce/lib/assortmentCount.js +112 -0
  238. package/dist/modules/ecommerce/lib/assortmentCount.js.map +7 -0
  239. package/dist/modules/ecommerce/lib/brandingStyles.js +184 -0
  240. package/dist/modules/ecommerce/lib/brandingStyles.js.map +7 -0
  241. package/dist/modules/ecommerce/lib/buyerContext.js +284 -0
  242. package/dist/modules/ecommerce/lib/buyerContext.js.map +7 -0
  243. package/dist/modules/ecommerce/lib/cacheInvalidation.js +186 -0
  244. package/dist/modules/ecommerce/lib/cacheInvalidation.js.map +7 -0
  245. package/dist/modules/ecommerce/lib/cacheKeys.js +236 -0
  246. package/dist/modules/ecommerce/lib/cacheKeys.js.map +7 -0
  247. package/dist/modules/ecommerce/lib/createConflictRecheck.js +29 -0
  248. package/dist/modules/ecommerce/lib/createConflictRecheck.js.map +7 -0
  249. package/dist/modules/ecommerce/lib/crudEvents.js +94 -0
  250. package/dist/modules/ecommerce/lib/crudEvents.js.map +7 -0
  251. package/dist/modules/ecommerce/lib/crudSupport.js +64 -0
  252. package/dist/modules/ecommerce/lib/crudSupport.js.map +7 -0
  253. package/dist/modules/ecommerce/lib/domainBindingRebind.js +99 -0
  254. package/dist/modules/ecommerce/lib/domainBindingRebind.js.map +7 -0
  255. package/dist/modules/ecommerce/lib/domainMappingSummaries.js +39 -0
  256. package/dist/modules/ecommerce/lib/domainMappingSummaries.js.map +7 -0
  257. package/dist/modules/ecommerce/lib/exclusiveFlag.js +57 -0
  258. package/dist/modules/ecommerce/lib/exclusiveFlag.js.map +7 -0
  259. package/dist/modules/ecommerce/lib/references.js +70 -0
  260. package/dist/modules/ecommerce/lib/references.js.map +7 -0
  261. package/dist/modules/ecommerce/lib/seedDraftStore.js +99 -0
  262. package/dist/modules/ecommerce/lib/seedDraftStore.js.map +7 -0
  263. package/dist/modules/ecommerce/lib/storeBindingCascade.js +109 -0
  264. package/dist/modules/ecommerce/lib/storeBindingCascade.js.map +7 -0
  265. package/dist/modules/ecommerce/lib/storeBranding.js +40 -0
  266. package/dist/modules/ecommerce/lib/storeBranding.js.map +7 -0
  267. package/dist/modules/ecommerce/lib/storeBrandingRoute.js +76 -0
  268. package/dist/modules/ecommerce/lib/storeBrandingRoute.js.map +7 -0
  269. package/dist/modules/ecommerce/lib/storeContext.js +393 -0
  270. package/dist/modules/ecommerce/lib/storeContext.js.map +7 -0
  271. package/dist/modules/ecommerce/lib/storeContextService.js +43 -0
  272. package/dist/modules/ecommerce/lib/storeContextService.js.map +7 -0
  273. package/dist/modules/ecommerce/lib/storeLocale.js +60 -0
  274. package/dist/modules/ecommerce/lib/storeLocale.js.map +7 -0
  275. package/dist/modules/ecommerce/lib/storeNotifications.js +41 -0
  276. package/dist/modules/ecommerce/lib/storeNotifications.js.map +7 -0
  277. package/dist/modules/ecommerce/lib/storefrontCatalogSupport.js +248 -0
  278. package/dist/modules/ecommerce/lib/storefrontCatalogSupport.js.map +7 -0
  279. package/dist/modules/ecommerce/lib/storefrontCategories.js +241 -0
  280. package/dist/modules/ecommerce/lib/storefrontCategories.js.map +7 -0
  281. package/dist/modules/ecommerce/lib/storefrontCategoryCache.js +71 -0
  282. package/dist/modules/ecommerce/lib/storefrontCategoryCache.js.map +7 -0
  283. package/dist/modules/ecommerce/lib/storefrontDetail.js +447 -0
  284. package/dist/modules/ecommerce/lib/storefrontDetail.js.map +7 -0
  285. package/dist/modules/ecommerce/lib/storefrontFacets.js +441 -0
  286. package/dist/modules/ecommerce/lib/storefrontFacets.js.map +7 -0
  287. package/dist/modules/ecommerce/lib/storefrontOptionFilter.js +15 -0
  288. package/dist/modules/ecommerce/lib/storefrontOptionFilter.js.map +7 -0
  289. package/dist/modules/ecommerce/lib/storefrontPricing.js +393 -0
  290. package/dist/modules/ecommerce/lib/storefrontPricing.js.map +7 -0
  291. package/dist/modules/ecommerce/lib/storefrontProductCache.js +90 -0
  292. package/dist/modules/ecommerce/lib/storefrontProductCache.js.map +7 -0
  293. package/dist/modules/ecommerce/lib/storefrontProductScope.js +107 -0
  294. package/dist/modules/ecommerce/lib/storefrontProductScope.js.map +7 -0
  295. package/dist/modules/ecommerce/lib/storefrontProducts.js +670 -0
  296. package/dist/modules/ecommerce/lib/storefrontProducts.js.map +7 -0
  297. package/dist/modules/ecommerce/lib/storefrontQuery.js +156 -0
  298. package/dist/modules/ecommerce/lib/storefrontQuery.js.map +7 -0
  299. package/dist/modules/ecommerce/lib/storefrontRateLimit.js +99 -0
  300. package/dist/modules/ecommerce/lib/storefrontRateLimit.js.map +7 -0
  301. package/dist/modules/ecommerce/lib/storefrontSearch.js +303 -0
  302. package/dist/modules/ecommerce/lib/storefrontSearch.js.map +7 -0
  303. package/dist/modules/ecommerce/lib/storefrontSearchCache.js +35 -0
  304. package/dist/modules/ecommerce/lib/storefrontSearchCache.js.map +7 -0
  305. package/dist/modules/ecommerce/lib/subscriberSupport.js +33 -0
  306. package/dist/modules/ecommerce/lib/subscriberSupport.js.map +7 -0
  307. package/dist/modules/ecommerce/lib/types.js +1 -0
  308. package/dist/modules/ecommerce/lib/types.js.map +7 -0
  309. package/dist/modules/ecommerce/migrations/Migration20261005113233_ecommerce.js +32 -0
  310. package/dist/modules/ecommerce/migrations/Migration20261005113233_ecommerce.js.map +7 -0
  311. package/dist/modules/ecommerce/migrations/Migration20261006112733_ecommerce.js +17 -0
  312. package/dist/modules/ecommerce/migrations/Migration20261006112733_ecommerce.js.map +7 -0
  313. package/dist/modules/ecommerce/notifications.client.js +47 -0
  314. package/dist/modules/ecommerce/notifications.client.js.map +7 -0
  315. package/dist/modules/ecommerce/notifications.js +48 -0
  316. package/dist/modules/ecommerce/notifications.js.map +7 -0
  317. package/dist/modules/ecommerce/search.js +83 -0
  318. package/dist/modules/ecommerce/search.js.map +7 -0
  319. package/dist/modules/ecommerce/setup.js +17 -0
  320. package/dist/modules/ecommerce/setup.js.map +7 -0
  321. package/dist/modules/ecommerce/subscribers/assortment-empty-notifications.js +14 -0
  322. package/dist/modules/ecommerce/subscribers/assortment-empty-notifications.js.map +7 -0
  323. package/dist/modules/ecommerce/subscribers/availability-policy-cache-invalidation.js +14 -0
  324. package/dist/modules/ecommerce/subscribers/availability-policy-cache-invalidation.js.map +7 -0
  325. package/dist/modules/ecommerce/subscribers/catalog-category-cache-invalidation.js +14 -0
  326. package/dist/modules/ecommerce/subscribers/catalog-category-cache-invalidation.js.map +7 -0
  327. package/dist/modules/ecommerce/subscribers/catalog-price-cache-invalidation.js +14 -0
  328. package/dist/modules/ecommerce/subscribers/catalog-price-cache-invalidation.js.map +7 -0
  329. package/dist/modules/ecommerce/subscribers/catalog-product-cache-invalidation.js +14 -0
  330. package/dist/modules/ecommerce/subscribers/catalog-product-cache-invalidation.js.map +7 -0
  331. package/dist/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.js +14 -0
  332. package/dist/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.js.map +7 -0
  333. package/dist/modules/ecommerce/subscribers/customer-group-cache-invalidation.js +14 -0
  334. package/dist/modules/ecommerce/subscribers/customer-group-cache-invalidation.js.map +7 -0
  335. package/dist/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.js +14 -0
  336. package/dist/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.js.map +7 -0
  337. package/dist/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.js +14 -0
  338. package/dist/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.js.map +7 -0
  339. package/dist/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.js +14 -0
  340. package/dist/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.js.map +7 -0
  341. package/dist/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.js +14 -0
  342. package/dist/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.js.map +7 -0
  343. package/dist/modules/ecommerce/subscribers/domain-binding-rebind.js +16 -0
  344. package/dist/modules/ecommerce/subscribers/domain-binding-rebind.js.map +7 -0
  345. package/dist/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.js +14 -0
  346. package/dist/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.js.map +7 -0
  347. package/dist/modules/ecommerce/subscribers/store-cache-invalidation.js +14 -0
  348. package/dist/modules/ecommerce/subscribers/store-cache-invalidation.js.map +7 -0
  349. package/dist/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.js +14 -0
  350. package/dist/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.js.map +7 -0
  351. package/dist/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.js +14 -0
  352. package/dist/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.js.map +7 -0
  353. package/dist/modules/ecommerce/subscribers/store-notifications.js +14 -0
  354. package/dist/modules/ecommerce/subscribers/store-notifications.js.map +7 -0
  355. package/dist/modules/query_index/lib/batch.js +23 -9
  356. package/dist/modules/query_index/lib/batch.js.map +2 -2
  357. package/dist/modules/query_index/lib/doc-enrichers.js +212 -0
  358. package/dist/modules/query_index/lib/doc-enrichers.js.map +7 -0
  359. package/dist/modules/query_index/lib/document.js +2 -0
  360. package/dist/modules/query_index/lib/document.js.map +2 -2
  361. package/dist/modules/query_index/lib/engine.js +68 -1
  362. package/dist/modules/query_index/lib/engine.js.map +2 -2
  363. package/dist/modules/query_index/lib/indexer.js +7 -0
  364. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  365. package/dist/modules/query_index/lib/search-tokens.js +2 -0
  366. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  367. package/dist/modules/query_index/migrations/Migration20261005201500_query_index.js +17 -0
  368. package/dist/modules/query_index/migrations/Migration20261005201500_query_index.js.map +7 -0
  369. package/dist/modules/translations/components/TranslationManager.js +3 -3
  370. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  371. package/dist/modules/translations/data/validators.js +1 -1
  372. package/dist/modules/translations/data/validators.js.map +1 -1
  373. package/dist/modules/translations/lib/batch.js +21 -1
  374. package/dist/modules/translations/lib/batch.js.map +2 -2
  375. package/dist/modules/translations/lib/helpers.js +3 -0
  376. package/dist/modules/translations/lib/helpers.js.map +2 -2
  377. package/dist/modules/translations/lib/resolve-field-list.js +14 -2
  378. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  379. package/generated/entities/catalog_price_history_entry/index.ts +25 -0
  380. package/generated/entities/customer_group_terms/index.ts +1 -0
  381. package/generated/entities/ecommerce_store/index.ts +15 -0
  382. package/generated/entities/ecommerce_store_channel_binding/index.ts +13 -0
  383. package/generated/entities/ecommerce_store_domain_binding/index.ts +10 -0
  384. package/generated/entities.ids.generated.ts +8 -1
  385. package/generated/entity-fields-registry.ts +72 -0
  386. package/package.json +7 -7
  387. package/src/helpers/integration/customerGroupsFixtures.ts +2 -0
  388. package/src/modules/auth/i18n/de.json +8 -0
  389. package/src/modules/auth/i18n/en.json +8 -0
  390. package/src/modules/auth/i18n/es.json +8 -0
  391. package/src/modules/auth/i18n/ko.json +8 -0
  392. package/src/modules/auth/i18n/pl.json +8 -0
  393. package/src/modules/catalog/AGENTS.md +5 -2
  394. package/src/modules/catalog/acl.ts +7 -0
  395. package/src/modules/catalog/api/config/omnibus/route.ts +204 -0
  396. package/src/modules/catalog/api/prices/history/route.ts +139 -0
  397. package/src/modules/catalog/api/prices/omnibus-preview/route.ts +202 -0
  398. package/src/modules/catalog/api/products/route.ts +53 -308
  399. package/src/modules/catalog/api/tags/route.ts +6 -0
  400. package/src/modules/catalog/backend/config/catalog/page.tsx +2 -0
  401. package/src/modules/catalog/cli.ts +84 -1
  402. package/src/modules/catalog/commands/categories.ts +54 -2
  403. package/src/modules/catalog/commands/prices.ts +43 -1
  404. package/src/modules/catalog/commands/products.ts +84 -1
  405. package/src/modules/catalog/commands/variants.ts +7 -0
  406. package/src/modules/catalog/components/OmnibusSettings.tsx +832 -0
  407. package/src/modules/catalog/components/PriceEditorOmnibusRow.tsx +219 -0
  408. package/src/modules/catalog/components/lookupSources.ts +29 -0
  409. package/src/modules/catalog/components/prices/PriceScopeSelectors.tsx +9 -0
  410. package/src/modules/catalog/components/products/VariantBuilder.tsx +9 -0
  411. package/src/modules/catalog/data/entities.ts +123 -1
  412. package/src/modules/catalog/data/validators.ts +118 -0
  413. package/src/modules/catalog/di.ts +23 -0
  414. package/src/modules/catalog/i18n/de.json +77 -0
  415. package/src/modules/catalog/i18n/en.json +77 -0
  416. package/src/modules/catalog/i18n/es.json +77 -0
  417. package/src/modules/catalog/i18n/ko.json +77 -0
  418. package/src/modules/catalog/i18n/pl.json +77 -0
  419. package/src/modules/catalog/lib/omnibus.ts +406 -0
  420. package/src/modules/catalog/lib/omnibusBackfill.ts +198 -0
  421. package/src/modules/catalog/lib/omnibusCache.ts +111 -0
  422. package/src/modules/catalog/lib/omnibusConfig.ts +73 -0
  423. package/src/modules/catalog/lib/omnibusHistoryQueries.ts +248 -0
  424. package/src/modules/catalog/lib/omnibusPresentedEntry.ts +76 -0
  425. package/src/modules/catalog/lib/omnibusProductListEnrichment.ts +125 -0
  426. package/src/modules/catalog/lib/omnibusTypes.ts +202 -0
  427. package/src/modules/catalog/lib/optionSchemaTranslations.ts +55 -0
  428. package/src/modules/catalog/lib/priceHistoryQuery.ts +155 -0
  429. package/src/modules/catalog/lib/pricing.ts +42 -4
  430. package/src/modules/catalog/lib/productFilters.ts +349 -0
  431. package/src/modules/catalog/lib/productScopeKeys.ts +162 -0
  432. package/src/modules/catalog/lib/productScopeReindex.ts +139 -0
  433. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +572 -0
  434. package/src/modules/catalog/migrations/Migration20261005105352_catalog.ts +39 -0
  435. package/src/modules/catalog/migrations/Migration20261005150824_catalog.ts +39 -0
  436. package/src/modules/catalog/services/catalogOmnibusService.ts +642 -0
  437. package/src/modules/catalog/setup.ts +2 -0
  438. package/src/modules/catalog/subscribers/product-scope-keys-category-deleted.ts +18 -0
  439. package/src/modules/catalog/subscribers/product-scope-keys-category-updated.ts +19 -0
  440. package/src/modules/catalog/translations.ts +12 -0
  441. package/src/modules/configs/i18n/de.json +5 -1
  442. package/src/modules/configs/i18n/en.json +5 -1
  443. package/src/modules/configs/i18n/es.json +5 -1
  444. package/src/modules/configs/i18n/ko.json +5 -1
  445. package/src/modules/configs/i18n/pl.json +5 -1
  446. package/src/modules/configs/lib/upgrade-actions.ts +35 -0
  447. package/src/modules/customer_groups/api/customer-groups/[id]/terms/route.ts +7 -0
  448. package/src/modules/customer_groups/components/CustomerGroupTermsSection.tsx +124 -2
  449. package/src/modules/customer_groups/components/customerGroupAssortmentScope.ts +46 -0
  450. package/src/modules/customer_groups/data/entities.ts +4 -0
  451. package/src/modules/customer_groups/data/validators.ts +14 -0
  452. package/src/modules/customer_groups/i18n/de.json +12 -0
  453. package/src/modules/customer_groups/i18n/en.json +12 -0
  454. package/src/modules/customer_groups/i18n/es.json +12 -0
  455. package/src/modules/customer_groups/i18n/ko.json +12 -0
  456. package/src/modules/customer_groups/i18n/pl.json +12 -0
  457. package/src/modules/customer_groups/migrations/.snapshot-open-mercato.json +17 -0
  458. package/src/modules/customer_groups/migrations/Migration20261005110250_customer_groups.ts +15 -0
  459. package/src/modules/customer_groups/services/customerGroupsService.ts +74 -23
  460. package/src/modules/ecommerce/acl.ts +9 -0
  461. package/src/modules/ecommerce/api/domain-mappings/route.ts +76 -0
  462. package/src/modules/ecommerce/api/openapi.ts +28 -0
  463. package/src/modules/ecommerce/api/store-channel-bindings/[id]/assortment-count/route.ts +141 -0
  464. package/src/modules/ecommerce/api/store-channel-bindings/crud.ts +283 -0
  465. package/src/modules/ecommerce/api/store-channel-bindings/route.ts +67 -0
  466. package/src/modules/ecommerce/api/store-domain-bindings/crud.ts +324 -0
  467. package/src/modules/ecommerce/api/store-domain-bindings/route.ts +73 -0
  468. package/src/modules/ecommerce/api/storefront/categories/[slug]/route.ts +94 -0
  469. package/src/modules/ecommerce/api/storefront/categories/openapiSchemas.ts +63 -0
  470. package/src/modules/ecommerce/api/storefront/categories/route.ts +78 -0
  471. package/src/modules/ecommerce/api/storefront/context/route.ts +203 -0
  472. package/src/modules/ecommerce/api/storefront/products/[idOrHandle]/route.ts +94 -0
  473. package/src/modules/ecommerce/api/storefront/products/openapiSchemas.ts +193 -0
  474. package/src/modules/ecommerce/api/storefront/products/route.ts +81 -0
  475. package/src/modules/ecommerce/api/storefront/search/suggest/openapiSchemas.ts +25 -0
  476. package/src/modules/ecommerce/api/storefront/search/suggest/route.ts +78 -0
  477. package/src/modules/ecommerce/api/storefront/storefrontRouteSupport.ts +68 -0
  478. package/src/modules/ecommerce/api/stores/[id]/branding/route.ts +205 -0
  479. package/src/modules/ecommerce/api/stores/[id]/preview-branding/route.ts +99 -0
  480. package/src/modules/ecommerce/api/stores/crud.ts +457 -0
  481. package/src/modules/ecommerce/api/stores/route.ts +66 -0
  482. package/src/modules/ecommerce/backend/config/ecommerce/[id]/page.meta.ts +12 -0
  483. package/src/modules/ecommerce/backend/config/ecommerce/[id]/page.tsx +5 -0
  484. package/src/modules/ecommerce/backend/config/ecommerce/page.meta.ts +12 -0
  485. package/src/modules/ecommerce/backend/config/ecommerce/page.tsx +5 -0
  486. package/src/modules/ecommerce/commands/stores.ts +154 -0
  487. package/src/modules/ecommerce/components/ChannelAssortmentCount.tsx +138 -0
  488. package/src/modules/ecommerce/components/ChannelBindingFormSections.tsx +40 -0
  489. package/src/modules/ecommerce/components/ChannelLookupPicker.tsx +5 -0
  490. package/src/modules/ecommerce/components/DomainStatusWarning.tsx +32 -0
  491. package/src/modules/ecommerce/components/StoreAvailabilityDefaultsSection.tsx +219 -0
  492. package/src/modules/ecommerce/components/StoreBrandingControls.tsx +73 -0
  493. package/src/modules/ecommerce/components/StoreBrandingPreview.tsx +77 -0
  494. package/src/modules/ecommerce/components/StoreBrandingTab.tsx +242 -0
  495. package/src/modules/ecommerce/components/StoreChannelBindingDialog.tsx +306 -0
  496. package/src/modules/ecommerce/components/StoreChannelsTab.tsx +375 -0
  497. package/src/modules/ecommerce/components/StoreCreateDialog.tsx +204 -0
  498. package/src/modules/ecommerce/components/StoreDomainBindingDialog.tsx +198 -0
  499. package/src/modules/ecommerce/components/StoreDomainsTab.tsx +426 -0
  500. package/src/modules/ecommerce/components/StoreEditPage.tsx +193 -0
  501. package/src/modules/ecommerce/components/StoreGeneralTab.tsx +189 -0
  502. package/src/modules/ecommerce/components/StoreListPage.tsx +326 -0
  503. package/src/modules/ecommerce/components/StoreSeoTab.tsx +152 -0
  504. package/src/modules/ecommerce/components/StoreStatusBadge.tsx +22 -0
  505. package/src/modules/ecommerce/components/channelLookups.ts +24 -0
  506. package/src/modules/ecommerce/components/storeAdmin.ts +56 -0
  507. package/src/modules/ecommerce/components/storeBrandingForm.ts +230 -0
  508. package/src/modules/ecommerce/components/storeChannels.ts +210 -0
  509. package/src/modules/ecommerce/components/storeDomains.ts +221 -0
  510. package/src/modules/ecommerce/components/storeEditTabs.tsx +76 -0
  511. package/src/modules/ecommerce/components/storeGeneral.ts +155 -0
  512. package/src/modules/ecommerce/components/storeSeo.ts +78 -0
  513. package/src/modules/ecommerce/components/useChannelAssortmentCount.ts +67 -0
  514. package/src/modules/ecommerce/components/useStoreAccess.ts +36 -0
  515. package/src/modules/ecommerce/data/enrichers.ts +213 -0
  516. package/src/modules/ecommerce/data/entities.ts +168 -0
  517. package/src/modules/ecommerce/data/validators.ts +587 -0
  518. package/src/modules/ecommerce/di.ts +9 -0
  519. package/src/modules/ecommerce/events.ts +27 -0
  520. package/src/modules/ecommerce/i18n/de.json +341 -0
  521. package/src/modules/ecommerce/i18n/en.json +341 -0
  522. package/src/modules/ecommerce/i18n/es.json +341 -0
  523. package/src/modules/ecommerce/i18n/ko.json +341 -0
  524. package/src/modules/ecommerce/i18n/pl.json +341 -0
  525. package/src/modules/ecommerce/index.ts +14 -0
  526. package/src/modules/ecommerce/lib/assortmentCount.ts +175 -0
  527. package/src/modules/ecommerce/lib/brandingStyles.ts +215 -0
  528. package/src/modules/ecommerce/lib/buyerContext.ts +401 -0
  529. package/src/modules/ecommerce/lib/cacheInvalidation.ts +302 -0
  530. package/src/modules/ecommerce/lib/cacheKeys.ts +340 -0
  531. package/src/modules/ecommerce/lib/createConflictRecheck.ts +31 -0
  532. package/src/modules/ecommerce/lib/crudEvents.ts +128 -0
  533. package/src/modules/ecommerce/lib/crudSupport.ts +85 -0
  534. package/src/modules/ecommerce/lib/domainBindingRebind.ts +138 -0
  535. package/src/modules/ecommerce/lib/domainMappingSummaries.ts +71 -0
  536. package/src/modules/ecommerce/lib/exclusiveFlag.ts +90 -0
  537. package/src/modules/ecommerce/lib/references.ts +93 -0
  538. package/src/modules/ecommerce/lib/seedDraftStore.ts +125 -0
  539. package/src/modules/ecommerce/lib/storeBindingCascade.ts +138 -0
  540. package/src/modules/ecommerce/lib/storeBranding.ts +48 -0
  541. package/src/modules/ecommerce/lib/storeBrandingRoute.ts +91 -0
  542. package/src/modules/ecommerce/lib/storeContext.ts +669 -0
  543. package/src/modules/ecommerce/lib/storeContextService.ts +71 -0
  544. package/src/modules/ecommerce/lib/storeLocale.ts +88 -0
  545. package/src/modules/ecommerce/lib/storeNotifications.ts +55 -0
  546. package/src/modules/ecommerce/lib/storefrontCatalogSupport.ts +427 -0
  547. package/src/modules/ecommerce/lib/storefrontCategories.ts +419 -0
  548. package/src/modules/ecommerce/lib/storefrontCategoryCache.ts +113 -0
  549. package/src/modules/ecommerce/lib/storefrontDetail.ts +654 -0
  550. package/src/modules/ecommerce/lib/storefrontFacets.ts +697 -0
  551. package/src/modules/ecommerce/lib/storefrontOptionFilter.ts +44 -0
  552. package/src/modules/ecommerce/lib/storefrontPricing.ts +583 -0
  553. package/src/modules/ecommerce/lib/storefrontProductCache.ts +126 -0
  554. package/src/modules/ecommerce/lib/storefrontProductScope.ts +181 -0
  555. package/src/modules/ecommerce/lib/storefrontProducts.ts +963 -0
  556. package/src/modules/ecommerce/lib/storefrontQuery.ts +215 -0
  557. package/src/modules/ecommerce/lib/storefrontRateLimit.ts +126 -0
  558. package/src/modules/ecommerce/lib/storefrontSearch.ts +454 -0
  559. package/src/modules/ecommerce/lib/storefrontSearchCache.ts +49 -0
  560. package/src/modules/ecommerce/lib/subscriberSupport.ts +41 -0
  561. package/src/modules/ecommerce/lib/types.ts +69 -0
  562. package/src/modules/ecommerce/migrations/.snapshot-open-mercato.json +829 -0
  563. package/src/modules/ecommerce/migrations/Migration20261005113233_ecommerce.ts +32 -0
  564. package/src/modules/ecommerce/migrations/Migration20261006112733_ecommerce.ts +15 -0
  565. package/src/modules/ecommerce/notifications.client.ts +46 -0
  566. package/src/modules/ecommerce/notifications.ts +46 -0
  567. package/src/modules/ecommerce/search.ts +93 -0
  568. package/src/modules/ecommerce/setup.ts +15 -0
  569. package/src/modules/ecommerce/subscribers/assortment-empty-notifications.ts +12 -0
  570. package/src/modules/ecommerce/subscribers/availability-policy-cache-invalidation.ts +12 -0
  571. package/src/modules/ecommerce/subscribers/catalog-category-cache-invalidation.ts +12 -0
  572. package/src/modules/ecommerce/subscribers/catalog-price-cache-invalidation.ts +12 -0
  573. package/src/modules/ecommerce/subscribers/catalog-product-cache-invalidation.ts +12 -0
  574. package/src/modules/ecommerce/subscribers/catalog-variant-cache-invalidation.ts +12 -0
  575. package/src/modules/ecommerce/subscribers/customer-group-cache-invalidation.ts +12 -0
  576. package/src/modules/ecommerce/subscribers/customer-group-membership-cache-invalidation.ts +12 -0
  577. package/src/modules/ecommerce/subscribers/customer-group-terms-cache-invalidation.ts +12 -0
  578. package/src/modules/ecommerce/subscribers/customer-user-deleted-cache-invalidation.ts +12 -0
  579. package/src/modules/ecommerce/subscribers/customer-user-updated-cache-invalidation.ts +12 -0
  580. package/src/modules/ecommerce/subscribers/domain-binding-rebind.ts +14 -0
  581. package/src/modules/ecommerce/subscribers/domain-mapping-cache-invalidation.ts +12 -0
  582. package/src/modules/ecommerce/subscribers/store-cache-invalidation.ts +12 -0
  583. package/src/modules/ecommerce/subscribers/store-channel-binding-cache-invalidation.ts +12 -0
  584. package/src/modules/ecommerce/subscribers/store-domain-binding-cache-invalidation.ts +12 -0
  585. package/src/modules/ecommerce/subscribers/store-notifications.ts +12 -0
  586. package/src/modules/query_index/lib/batch.ts +25 -9
  587. package/src/modules/query_index/lib/doc-enrichers.ts +306 -0
  588. package/src/modules/query_index/lib/document.ts +2 -0
  589. package/src/modules/query_index/lib/engine.ts +58 -1
  590. package/src/modules/query_index/lib/indexer.ts +8 -0
  591. package/src/modules/query_index/lib/search-tokens.ts +2 -0
  592. package/src/modules/query_index/migrations/Migration20261005201500_query_index.ts +42 -0
  593. package/src/modules/translations/components/TranslationManager.tsx +3 -3
  594. package/src/modules/translations/data/validators.ts +1 -1
  595. package/src/modules/translations/lib/batch.ts +47 -0
  596. package/src/modules/translations/lib/helpers.ts +4 -0
  597. package/src/modules/translations/lib/resolve-field-list.ts +16 -2
@@ -0,0 +1,963 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
3
+ import type { AvailabilityItemResult, AvailabilityState } from '@open-mercato/shared/lib/availability'
4
+ import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
5
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
6
+ import { sanitizeSearchTerm } from '@open-mercato/shared/lib/query/sanitizeSearchTerm'
7
+ import { SortDir, type QueryEngine, type Sort, type Where } from '@open-mercato/shared/lib/query/types'
8
+ import { E } from '#generated/entities.ids.generated'
9
+ import {
10
+ CatalogProductCategory,
11
+ CatalogProductCategoryAssignment,
12
+ CatalogProductPrice,
13
+ CatalogProductTag,
14
+ CatalogProductTagAssignment,
15
+ CatalogProductVariant,
16
+ } from '@open-mercato/core/modules/catalog/data/entities'
17
+ import type { CatalogProductType } from '@open-mercato/core/modules/catalog/data/types'
18
+ import {
19
+ buildProductFilters,
20
+ scoreProductSearchRelevance,
21
+ type CatalogProductFilterQuery,
22
+ } from '@open-mercato/core/modules/catalog/lib/productFilters'
23
+ import type {
24
+ EcommercePriceDisplayMode,
25
+ EcommercePriceSortFallback,
26
+ EcommerceStorefrontProductListQuery,
27
+ EcommerceStorefrontProductSort,
28
+ } from '../data/validators'
29
+ import {
30
+ STOREFRONT_FACET_UNIVERSE_CAP,
31
+ countStorefrontFacets,
32
+ labelStorefrontCountFacets,
33
+ loadStorefrontFacetSource,
34
+ loadStorefrontProductTypeLabeler,
35
+ readCachedStorefrontCountFacets,
36
+ storefrontAvailabilityFacet,
37
+ storefrontFacetTranslationRequests,
38
+ storefrontFacetVariantIndex,
39
+ storefrontPriceRangeFacet,
40
+ writeCachedStorefrontCountFacets,
41
+ type StorefrontCountFacets,
42
+ type StorefrontFacetSelection,
43
+ type StorefrontFacetSource,
44
+ type StorefrontFacets,
45
+ } from './storefrontFacets'
46
+ import { buildStorefrontProductScope, composeStorefrontProductFilters, type StorefrontProductScope } from './storefrontProductScope'
47
+ import { resolveStorefrontPrices, type StorefrontProductPricing } from './storefrontPricing'
48
+ import { queryStorefrontOptionProductIds } from './storefrontOptionFilter'
49
+ import {
50
+ STOREFRONT_CATEGORY_ENTITY_TYPE,
51
+ STOREFRONT_PRODUCT_ENTITY_TYPE,
52
+ STOREFRONT_TAG_ENTITY_TYPE,
53
+ buildStorefrontListItem,
54
+ isCategoryInAssortment,
55
+ selectCategoriesWithActiveAncestors,
56
+ loadStorefrontTranslations,
57
+ localeChain,
58
+ nonEmptyString,
59
+ referenceId,
60
+ resolvePublicDefaultMediaUrls,
61
+ resolveStorefrontAvailability,
62
+ storefrontAvailabilityKey,
63
+ stringList,
64
+ toStorefrontAvailability,
65
+ tryResolve,
66
+ type CategoryLineage,
67
+ type CategoryRef,
68
+ type StorefrontAvailability,
69
+ type StorefrontProductListItem,
70
+ type TagRef,
71
+ type TranslationMap,
72
+ } from './storefrontCatalogSupport'
73
+ import type { StoreContext } from './types'
74
+
75
+ export { isCategoryInAssortment } from './storefrontCatalogSupport'
76
+ export type { StorefrontAvailability, StorefrontProductListItem } from './storefrontCatalogSupport'
77
+ export type { StorefrontFacets } from './storefrontFacets'
78
+
79
+ /** Storefront Public API §6.3: the pre-sort id set that is priced and sorted in memory. */
80
+ export const STOREFRONT_PRICE_SORT_CAP = 5000
81
+
82
+ /**
83
+ * The largest filtered set the `'approximate'` fallback still loads in full (candidates plus their
84
+ * list-price rows); past it the listing answers as under `'unavailable'`.
85
+ */
86
+ export const STOREFRONT_APPROXIMATE_SORT_CEILING = 50_000
87
+
88
+ const APPROXIMATE_PRICE_CHUNK = 1000
89
+
90
+ const NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'
91
+
92
+ const LIST_FIELDS = [
93
+ 'id',
94
+ 'title',
95
+ 'subtitle',
96
+ 'handle',
97
+ 'sku',
98
+ 'product_type',
99
+ 'is_configurable',
100
+ 'default_media_id',
101
+ 'default_media_url',
102
+ 'created_at',
103
+ ]
104
+
105
+ const CANDIDATE_FIELDS = ['id', 'title', 'sku']
106
+
107
+ const SORT_ORDER: readonly EcommerceStorefrontProductSort[] = [
108
+ 'relevance',
109
+ 'price_asc',
110
+ 'price_desc',
111
+ 'title_asc',
112
+ 'title_desc',
113
+ 'newest',
114
+ 'featured',
115
+ ]
116
+
117
+ export type StorefrontAppliedFilters = {
118
+ search?: string
119
+ category?: { id: string; slug: string | null; includesDescendants: true }
120
+ tagSlugs?: string[]
121
+ price?: { min: number | null; max: number | null; approximate: boolean }
122
+ options?: Record<string, string[]>
123
+ productType?: CatalogProductType
124
+ availability?: { value: 'in_stock' | 'available'; scope: 'page' }
125
+ }
126
+
127
+ export type StorefrontPriceSortPolicy = {
128
+ cap: number
129
+ fallback: EcommercePriceSortFallback
130
+ capExceeded: boolean
131
+ }
132
+
133
+ export type StorefrontProductListResponse = {
134
+ items: StorefrontProductListItem[]
135
+ total: number
136
+ page: number
137
+ pageSize: number
138
+ totalPages: number
139
+ facets: StorefrontFacets
140
+ effectiveLocale: string
141
+ requestedLocale: string | null
142
+ currencyCode: string
143
+ taxMode: EcommercePriceDisplayMode
144
+ appliedFilters: StorefrontAppliedFilters
145
+ availableSorts: EcommerceStorefrontProductSort[]
146
+ appliedSort: EcommerceStorefrontProductSort
147
+ priceSort: StorefrontPriceSortPolicy
148
+ /** The applied order is not exact — the route emits `X-Sort-Approximate: true` (§6.3). */
149
+ sortApproximate: boolean
150
+ /** A requested price sort was declined by the channel's `'unavailable'` policy — `X-Sort-Unavailable: true`. */
151
+ sortUnavailable: boolean
152
+ }
153
+
154
+ export type ListStorefrontProductsOptions = {
155
+ /** Overrides `STOREFRONT_PRICE_SORT_CAP`; intended for tests. */
156
+ priceSortCap?: number
157
+ /** Overrides `STOREFRONT_APPROXIMATE_SORT_CEILING`; intended for tests. */
158
+ approximateSortCeiling?: number
159
+ /** Overrides `STOREFRONT_FACET_UNIVERSE_CAP`; intended for tests. */
160
+ facetUniverseCap?: number
161
+ date?: Date
162
+ }
163
+
164
+ type ProductRecord = {
165
+ id: string
166
+ title?: string | null
167
+ subtitle?: string | null
168
+ handle?: string | null
169
+ sku?: string | null
170
+ product_type?: string | null
171
+ is_configurable?: unknown
172
+ default_media_id?: string | null
173
+ default_media_url?: string | null
174
+ }
175
+
176
+ type Candidate = { id: string; title: string; sku: string | null }
177
+
178
+ type Runtime = {
179
+ container: AwilixContainer
180
+ ctx: StoreContext
181
+ em: EntityManager
182
+ queryEngine: QueryEngine
183
+ scope: StorefrontProductScope
184
+ decryptionScope: { tenantId: string; organizationId: string }
185
+ date: Date
186
+ facetUniverseCap?: number
187
+ }
188
+
189
+ type ResolvedFilters = {
190
+ extra: Where | null
191
+ universe: Where | null
192
+ selection: StorefrontFacetSelection
193
+ applied: StorefrontAppliedFilters
194
+ searchTerm: string | null
195
+ }
196
+
197
+ type HydratedPage = {
198
+ items: StorefrontProductListItem[]
199
+ availabilityStates: AvailabilityState[]
200
+ translations: Map<string, TranslationMap>
201
+ }
202
+
203
+ type CountFacetState = {
204
+ cached: StorefrontCountFacets | null
205
+ source: StorefrontFacetSource | null
206
+ overCap: boolean
207
+ }
208
+
209
+ const EMPTY_COUNT_FACETS: StorefrontCountFacets = { categories: [], tags: [], options: [], productTypes: [] }
210
+
211
+ type PagePrefetch = {
212
+ variants?: Map<string, string[]>
213
+ pricing?: Map<string, StorefrontProductPricing>
214
+ }
215
+
216
+ function isPriceSort(sort: EcommerceStorefrontProductSort): boolean {
217
+ return sort === 'price_asc' || sort === 'price_desc'
218
+ }
219
+
220
+ function sqlSortFor(sort: EcommerceStorefrontProductSort): Sort[] {
221
+ switch (sort) {
222
+ case 'title_desc':
223
+ return [{ field: 'title', dir: SortDir.Desc }]
224
+ case 'newest':
225
+ case 'featured':
226
+ return [{ field: 'created_at', dir: SortDir.Desc }]
227
+ default:
228
+ return [{ field: 'title', dir: SortDir.Asc }]
229
+ }
230
+ }
231
+
232
+ async function resolveCategoryFilter(
233
+ runtime: Runtime,
234
+ query: EcommerceStorefrontProductListQuery,
235
+ ): Promise<{ id: string; slug: string | null } | null> {
236
+ if (!query.categoryId && !query.categorySlug) return null
237
+ const { ctx } = runtime
238
+ const where: FilterQuery<CatalogProductCategory> = {
239
+ tenantId: ctx.tenantId,
240
+ organizationId: ctx.organizationId,
241
+ deletedAt: null,
242
+ isActive: true,
243
+ ...(query.categoryId ? { id: query.categoryId } : { slug: query.categorySlug }),
244
+ }
245
+ const [category] = await findWithDecryption(
246
+ runtime.em,
247
+ CatalogProductCategory,
248
+ where,
249
+ { fields: ['id', 'slug', 'ancestorIds', 'descendantIds'], limit: 1 },
250
+ runtime.decryptionScope,
251
+ )
252
+ if (!category) return null
253
+ const lineage: CategoryLineage = {
254
+ id: category.id,
255
+ ancestorIds: stringList(category.ancestorIds),
256
+ descendantIds: stringList(category.descendantIds),
257
+ }
258
+ if (!isCategoryInAssortment(lineage, ctx.buyer.assortmentScope)) return null
259
+ const { visibleIds } = await selectCategoriesWithActiveAncestors(runtime.em, ctx, [lineage])
260
+ if (!visibleIds.has(category.id)) return null
261
+ return { id: category.id, slug: category.slug ?? null }
262
+ }
263
+
264
+ async function resolveTagFilter(runtime: Runtime, slugs: string[] | undefined): Promise<Array<{ id: string; slug: string }>> {
265
+ if (!slugs?.length) return []
266
+ const { ctx } = runtime
267
+ const tags = await findWithDecryption(
268
+ runtime.em,
269
+ CatalogProductTag,
270
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId, slug: { $in: slugs } },
271
+ { fields: ['id', 'slug'] },
272
+ runtime.decryptionScope,
273
+ )
274
+ const bySlug = new Map(tags.map((tag) => [tag.slug, tag.id]))
275
+ return slugs.flatMap((slug) => {
276
+ const id = bySlug.get(slug)
277
+ return id ? [{ id, slug }] : []
278
+ })
279
+ }
280
+
281
+ const OPTION_FILTER_OVER_CAP = 'over_cap'
282
+
283
+ type OptionFilterResolution = string[] | typeof OPTION_FILTER_OVER_CAP | null
284
+
285
+ async function resolveOptionProductIds(
286
+ runtime: Runtime,
287
+ options: Record<string, string[]> | undefined,
288
+ ): Promise<OptionFilterResolution> {
289
+ if (!options) return null
290
+ const ids = await queryStorefrontOptionProductIds(
291
+ runtime.em,
292
+ runtime.decryptionScope,
293
+ options,
294
+ runtime.facetUniverseCap ?? STOREFRONT_FACET_UNIVERSE_CAP,
295
+ )
296
+ return ids ?? OPTION_FILTER_OVER_CAP
297
+ }
298
+
299
+ async function productFilterClause(
300
+ runtime: Runtime,
301
+ filterQuery: Omit<CatalogProductFilterQuery, 'page' | 'pageSize'>,
302
+ ): Promise<Where | null> {
303
+ return buildStorefrontProductFilterClause(runtime.container, runtime.ctx, filterQuery)
304
+ }
305
+
306
+ /**
307
+ * `catalog`'s product filters (§3.2 reuse) for the store's tenant and organization — for `search`
308
+ * the Phase 1 escaped, accent-insensitive `ILIKE` over title, subtitle, description, SKU and handle
309
+ * (§8.1). `null` when no filter applies. Compose the result with the storefront scope.
310
+ */
311
+ export async function buildStorefrontProductFilterClause(
312
+ container: AwilixContainer,
313
+ ctx: StoreContext,
314
+ filterQuery: Omit<CatalogProductFilterQuery, 'page' | 'pageSize'>,
315
+ ): Promise<Where | null> {
316
+ const crudCtx: CrudCtx = {
317
+ container,
318
+ auth: { sub: 'ecommerce:storefront', tenantId: ctx.tenantId, orgId: ctx.organizationId },
319
+ organizationScope: null,
320
+ selectedOrganizationId: ctx.organizationId,
321
+ organizationIds: [ctx.organizationId],
322
+ }
323
+ const productFilters = await buildProductFilters({ page: 1, pageSize: 1, ...filterQuery }, crudCtx, {
324
+ includeCategoryDescendants: true,
325
+ })
326
+ return Object.keys(productFilters).length ? productFilters : null
327
+ }
328
+
329
+ function andClauses(clauses: Array<Where | null>): Where | null {
330
+ const present = clauses.filter((clause): clause is Where => clause !== null)
331
+ if (present.length === 0) return null
332
+ return present.length === 1 ? present[0] : { $and: present }
333
+ }
334
+
335
+ /**
336
+ * Resolves the listing's filters. `extra` is every product filter AND'ed (composed with the scope
337
+ * by the caller); `universe` is the search clause alone — the facet universe (§5.4), since search
338
+ * is the one filter no facet dimension excludes; `selection` is what the facets cross-exclude.
339
+ */
340
+ async function resolveListFilters(
341
+ runtime: Runtime,
342
+ query: EcommerceStorefrontProductListQuery,
343
+ ): Promise<ResolvedFilters> {
344
+ const applied: StorefrontAppliedFilters = {}
345
+ const searchTerm = sanitizeSearchTerm(query.search)
346
+ const [category, resolvedTags, optionProductIds] = await Promise.all([
347
+ resolveCategoryFilter(runtime, query),
348
+ resolveTagFilter(runtime, query.tagSlugs),
349
+ resolveOptionProductIds(runtime, query.options),
350
+ ])
351
+ if (searchTerm) applied.search = searchTerm
352
+ if (category) applied.category = { id: category.id, slug: category.slug, includesDescendants: true }
353
+ if (resolvedTags.length) applied.tagSlugs = resolvedTags.map((tag) => tag.slug)
354
+ if (query.productType) applied.productType = query.productType
355
+ const optionsApplied = Array.isArray(optionProductIds)
356
+ if (optionsApplied && query.options) applied.options = query.options
357
+
358
+ const [searchClause, facetClause] = await Promise.all([
359
+ searchTerm ? productFilterClause(runtime, { search: searchTerm }) : Promise.resolve(null),
360
+ category || resolvedTags.length || query.productType
361
+ ? productFilterClause(runtime, {
362
+ categoryIds: category?.id,
363
+ tagIds: resolvedTags.length ? resolvedTags.map((tag) => tag.id).join(',') : undefined,
364
+ productType: query.productType,
365
+ })
366
+ : Promise.resolve(null),
367
+ ])
368
+ const optionClause: Where | null = Array.isArray(optionProductIds)
369
+ ? optionProductIds.length
370
+ ? { id: { $in: optionProductIds } }
371
+ : { id: { $eq: NO_MATCH_ID } }
372
+ : null
373
+ return {
374
+ extra: andClauses([searchClause, facetClause, optionClause]),
375
+ universe: searchClause,
376
+ selection: {
377
+ categoryId: category?.id ?? null,
378
+ tagIds: resolvedTags.map((tag) => tag.id),
379
+ productType: query.productType ?? null,
380
+ options: optionsApplied ? query.options ?? {} : {},
381
+ },
382
+ applied,
383
+ searchTerm: searchTerm || null,
384
+ }
385
+ }
386
+
387
+ async function queryCandidates(
388
+ runtime: Runtime,
389
+ filters: Where,
390
+ sort: Sort[],
391
+ pageSize: number,
392
+ ): Promise<{ items: Candidate[]; total: number }> {
393
+ const result = await runtime.queryEngine.query<ProductRecord>(E.catalog.catalog_product, {
394
+ tenantId: runtime.scope.tenantId,
395
+ organizationId: runtime.scope.organizationId,
396
+ withDeleted: runtime.scope.withDeleted,
397
+ filters,
398
+ fields: CANDIDATE_FIELDS,
399
+ sort,
400
+ page: { page: 1, pageSize: Math.max(1, pageSize) },
401
+ })
402
+ const items = result.items
403
+ .filter((item) => typeof item.id === 'string')
404
+ .map((item) => ({ id: item.id, title: nonEmptyString(item.title) ?? '', sku: nonEmptyString(item.sku) }))
405
+ return { items, total: result.total }
406
+ }
407
+
408
+ async function queryProductsByIds(runtime: Runtime, ids: string[]): Promise<ProductRecord[]> {
409
+ if (!ids.length) return []
410
+ const result = await runtime.queryEngine.query<ProductRecord>(E.catalog.catalog_product, {
411
+ tenantId: runtime.scope.tenantId,
412
+ organizationId: runtime.scope.organizationId,
413
+ withDeleted: runtime.scope.withDeleted,
414
+ filters: composeStorefrontProductFilters(runtime.scope, { id: { $in: ids } }),
415
+ fields: LIST_FIELDS,
416
+ page: { page: 1, pageSize: ids.length },
417
+ })
418
+ const byId = new Map(result.items.map((item) => [item.id, item]))
419
+ return ids.map((id) => byId.get(id)).filter((item): item is ProductRecord => !!item)
420
+ }
421
+
422
+ async function loadVariantIds(runtime: Runtime, productIds: string[]): Promise<Map<string, string[]>> {
423
+ const byProduct = new Map<string, string[]>()
424
+ if (!productIds.length) return byProduct
425
+ const { ctx } = runtime
426
+ const variants = await findWithDecryption(
427
+ runtime.em,
428
+ CatalogProductVariant,
429
+ {
430
+ tenantId: ctx.tenantId,
431
+ organizationId: ctx.organizationId,
432
+ deletedAt: null,
433
+ isActive: true,
434
+ product: { $in: productIds },
435
+ },
436
+ { fields: ['id', 'product'] },
437
+ runtime.decryptionScope,
438
+ )
439
+ for (const variant of variants) {
440
+ const productId = referenceId(variant.product)
441
+ if (!productId) continue
442
+ const bucket = byProduct.get(productId) ?? []
443
+ bucket.push(variant.id)
444
+ byProduct.set(productId, bucket)
445
+ }
446
+ return byProduct
447
+ }
448
+
449
+ async function loadCategories(
450
+ runtime: Runtime,
451
+ productIds: string[],
452
+ ): Promise<{ byProduct: Map<string, CategoryRef[]>; categoryIds: string[] }> {
453
+ const candidates = new Map<string, Array<CategoryRef & { ancestorIds: string[] }>>()
454
+ const { ctx } = runtime
455
+ const assignments = await findWithDecryption(
456
+ runtime.em,
457
+ CatalogProductCategoryAssignment,
458
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId, product: { $in: productIds } },
459
+ { populate: ['category'], orderBy: { position: 'asc' } },
460
+ runtime.decryptionScope,
461
+ )
462
+ const categoryIds = new Set<string>()
463
+ for (const assignment of assignments) {
464
+ const productId = referenceId(assignment.product)
465
+ const category = typeof assignment.category === 'object' ? assignment.category : null
466
+ if (!productId || !category || category.deletedAt || category.isActive === false) continue
467
+ const lineage: CategoryLineage = {
468
+ id: category.id,
469
+ ancestorIds: stringList(category.ancestorIds),
470
+ descendantIds: stringList(category.descendantIds),
471
+ }
472
+ if (!isCategoryInAssortment(lineage, ctx.buyer.assortmentScope)) continue
473
+ const bucket = candidates.get(productId) ?? []
474
+ if (bucket.some((entry) => entry.id === category.id)) continue
475
+ bucket.push({ id: category.id, name: category.name, slug: category.slug ?? null, ancestorIds: lineage.ancestorIds })
476
+ candidates.set(productId, bucket)
477
+ categoryIds.add(category.id)
478
+ }
479
+ const allCandidates = Array.from(candidates.values()).flat()
480
+ const { visibleIds: visible } = await selectCategoriesWithActiveAncestors(runtime.em, ctx, allCandidates, {
481
+ activeIds: categoryIds,
482
+ })
483
+ const byProduct = new Map<string, CategoryRef[]>()
484
+ for (const [productId, bucket] of candidates) {
485
+ const refs = bucket.filter((entry) => visible.has(entry.id)).map(({ id, name, slug }) => ({ id, name, slug }))
486
+ if (refs.length > 0) byProduct.set(productId, refs)
487
+ }
488
+ return { byProduct, categoryIds: Array.from(categoryIds).filter((id) => visible.has(id)) }
489
+ }
490
+
491
+ async function loadTags(
492
+ runtime: Runtime,
493
+ productIds: string[],
494
+ ): Promise<{ byProduct: Map<string, TagRef[]>; tagIds: string[] }> {
495
+ const byProduct = new Map<string, TagRef[]>()
496
+ const { ctx } = runtime
497
+ const assignments = await findWithDecryption(
498
+ runtime.em,
499
+ CatalogProductTagAssignment,
500
+ { tenantId: ctx.tenantId, organizationId: ctx.organizationId, product: { $in: productIds } },
501
+ { populate: ['tag'] },
502
+ runtime.decryptionScope,
503
+ )
504
+ const tagIds = new Set<string>()
505
+ for (const assignment of assignments) {
506
+ const productId = referenceId(assignment.product)
507
+ const tag = typeof assignment.tag === 'object' ? assignment.tag : null
508
+ if (!productId || !tag) continue
509
+ const bucket = byProduct.get(productId) ?? []
510
+ if (bucket.some((entry) => entry.id === tag.id)) continue
511
+ bucket.push({ id: tag.id, label: tag.label })
512
+ byProduct.set(productId, bucket)
513
+ tagIds.add(tag.id)
514
+ }
515
+ return { byProduct, tagIds: Array.from(tagIds) }
516
+ }
517
+
518
+ type PageAvailability = Map<string, { result: AvailabilityItemResult | null; hideWhenOutOfStock: boolean }>
519
+
520
+ async function resolvePageAvailability(runtime: Runtime, productIds: string[]): Promise<PageAvailability> {
521
+ const results: PageAvailability = new Map()
522
+ if (!productIds.length) return results
523
+ const targets = productIds.map((productId) => ({ productId, variantId: null }))
524
+ const resolved = await resolveStorefrontAvailability(runtime.container, runtime.ctx, runtime.em, targets)
525
+ for (const target of targets) {
526
+ const entry = resolved.get(storefrontAvailabilityKey(target))
527
+ results.set(target.productId, {
528
+ result: entry?.result ?? null,
529
+ hideWhenOutOfStock: entry?.policy?.hideWhenOutOfStock?.value === true,
530
+ })
531
+ }
532
+ return results
533
+ }
534
+
535
+ const IN_STOCK_STATES: ReadonlySet<AvailabilityState> = new Set(['in_stock', 'low_stock', 'not_tracked'])
536
+
537
+ function matchesAvailabilityFilter(
538
+ availability: StorefrontAvailability,
539
+ filter: EcommerceStorefrontProductListQuery['availability'],
540
+ ): boolean {
541
+ if (filter === 'in_stock') return IN_STOCK_STATES.has(availability.state) && availability.canFulfil
542
+ if (filter === 'available') return availability.state !== 'out_of_stock' && availability.canFulfil
543
+ return true
544
+ }
545
+
546
+ /**
547
+ * The page's items. `extraTranslations` (the count facets' overlays) ride on the page's single
548
+ * translation query; `availabilityStates` are the page's states after `hideWhenOutOfStock` and
549
+ * before the `availability=` filter — what the page-scoped availability facet counts (§5.3).
550
+ */
551
+ async function hydratePage(
552
+ runtime: Runtime,
553
+ records: ProductRecord[],
554
+ query: EcommerceStorefrontProductListQuery,
555
+ prefetch: PagePrefetch,
556
+ extraTranslations: Array<{ entityType: string; ids: string[] }>,
557
+ ): Promise<HydratedPage> {
558
+ const { ctx, container } = runtime
559
+ if (!records.length) {
560
+ const translations = extraTranslations.length
561
+ ? await loadStorefrontTranslations(runtime.em, ctx, extraTranslations)
562
+ : new Map<string, TranslationMap>()
563
+ return { items: [], availabilityStates: [], translations }
564
+ }
565
+ const productIds = records.map((record) => record.id)
566
+ const [variantsByProduct, categories, tags] = await Promise.all([
567
+ prefetch.variants ?? loadVariantIds(runtime, productIds),
568
+ loadCategories(runtime, productIds),
569
+ loadTags(runtime, productIds),
570
+ ])
571
+ const [pricing, translations, availability, mediaUrls] = await Promise.all([
572
+ prefetch.pricing ??
573
+ resolveStorefrontPrices(
574
+ container,
575
+ ctx,
576
+ productIds.map((productId) => ({ productId, variantIds: variantsByProduct.get(productId) ?? [] })),
577
+ { date: runtime.date },
578
+ ),
579
+ loadStorefrontTranslations(runtime.em, ctx, [
580
+ { entityType: STOREFRONT_PRODUCT_ENTITY_TYPE, ids: productIds },
581
+ { entityType: STOREFRONT_CATEGORY_ENTITY_TYPE, ids: categories.categoryIds },
582
+ { entityType: STOREFRONT_TAG_ENTITY_TYPE, ids: tags.tagIds },
583
+ ...extraTranslations,
584
+ ]),
585
+ resolvePageAvailability(runtime, productIds),
586
+ resolvePublicDefaultMediaUrls(
587
+ runtime.em,
588
+ ctx,
589
+ records.map((record) => ({
590
+ id: record.id,
591
+ defaultMediaId: record.default_media_id,
592
+ defaultMediaUrl: record.default_media_url,
593
+ })),
594
+ ),
595
+ ])
596
+ const locales = localeChain(ctx)
597
+ const productTranslations = translations.get(STOREFRONT_PRODUCT_ENTITY_TYPE)
598
+ const categoryTranslations = translations.get(STOREFRONT_CATEGORY_ENTITY_TYPE)
599
+ const tagTranslations = translations.get(STOREFRONT_TAG_ENTITY_TYPE)
600
+
601
+ const items: StorefrontProductListItem[] = []
602
+ const availabilityStates: AvailabilityState[] = []
603
+ for (const record of records) {
604
+ const productAvailability = availability.get(record.id)
605
+ const storefrontAvailability = toStorefrontAvailability(productAvailability?.result ?? null)
606
+ if (productAvailability?.hideWhenOutOfStock && storefrontAvailability.state === 'out_of_stock') continue
607
+ availabilityStates.push(storefrontAvailability.state)
608
+ if (!matchesAvailabilityFilter(storefrontAvailability, query.availability)) continue
609
+ items.push(
610
+ buildStorefrontListItem(
611
+ {
612
+ id: record.id,
613
+ handle: record.handle,
614
+ title: record.title,
615
+ subtitle: record.subtitle,
616
+ defaultMediaUrl: mediaUrls.get(record.id) ?? null,
617
+ productType: record.product_type,
618
+ isConfigurable: record.is_configurable,
619
+ },
620
+ {
621
+ locales,
622
+ overlay: productTranslations?.get(record.id),
623
+ variantCount: variantsByProduct.get(record.id)?.length ?? 0,
624
+ categories: categories.byProduct.get(record.id) ?? [],
625
+ categoryTranslations,
626
+ tags: tags.byProduct.get(record.id) ?? [],
627
+ tagTranslations,
628
+ pricing: pricing.get(record.id),
629
+ availability: storefrontAvailability,
630
+ },
631
+ ),
632
+ )
633
+ }
634
+ return { items, availabilityStates, translations }
635
+ }
636
+
637
+ function pricingAmount(pricing: StorefrontProductPricing | undefined): number | null {
638
+ if (!pricing) return null
639
+ return pricing.price?.amount ?? pricing.priceRange?.min ?? null
640
+ }
641
+
642
+ function parseAmount(value: unknown): number | null {
643
+ if (value === null || value === undefined || value === '') return null
644
+ const numeric = typeof value === 'number' ? value : Number(value)
645
+ return Number.isFinite(numeric) ? numeric : null
646
+ }
647
+
648
+ function chunk<T>(values: T[], size: number): T[][] {
649
+ const chunks: T[][] = []
650
+ for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size))
651
+ return chunks
652
+ }
653
+
654
+ /**
655
+ * Past the cap with `price_sort_fallback = 'approximate'` (§6.3): each candidate's lowest list
656
+ * price among the channel default price kind's unscoped rows (no customer, group or user), read on
657
+ * the buyer's `taxMode` side. Only the candidates' rows are read — those on the product itself or on
658
+ * one of its variants — in sequential chunks of `APPROXIMATE_PRICE_CHUNK` products, so one request
659
+ * holds at most one connection; never buyer prices.
660
+ */
661
+ async function loadApproximateAmounts(runtime: Runtime, productIds: string[]): Promise<Map<string, number>> {
662
+ const { ctx } = runtime
663
+ const amounts = new Map<string, number>()
664
+ const priceKindId = ctx.channel?.priceKindId ?? null
665
+ if (!priceKindId || !productIds.length) return amounts
666
+ const rows: CatalogProductPrice[] = []
667
+ for (const ids of chunk(productIds, APPROXIMATE_PRICE_CHUNK)) {
668
+ rows.push(
669
+ ...(await findWithDecryption(
670
+ runtime.em,
671
+ CatalogProductPrice,
672
+ {
673
+ tenantId: ctx.tenantId,
674
+ organizationId: ctx.organizationId,
675
+ priceKind: priceKindId,
676
+ currencyCode: ctx.currencyCode,
677
+ customerId: null,
678
+ customerGroupId: null,
679
+ userId: null,
680
+ userGroupId: null,
681
+ $or: [{ product: { $in: ids } }, { variant: { product: { $in: ids } } }],
682
+ },
683
+ { populate: ['variant'] },
684
+ runtime.decryptionScope,
685
+ )),
686
+ )
687
+ }
688
+ const wantedIds = new Set(productIds)
689
+ const channelId = ctx.channel?.salesChannelId ?? null
690
+ const now = runtime.date
691
+ for (const row of rows) {
692
+ if (row.channelId && row.channelId !== channelId) continue
693
+ if ((row.minQuantity ?? 1) > 1) continue
694
+ if (row.startsAt && row.startsAt > now) continue
695
+ if (row.endsAt && row.endsAt < now) continue
696
+ const productId =
697
+ referenceId(row.product) ?? (row.variant && typeof row.variant === 'object' ? referenceId(row.variant.product) : null)
698
+ if (!productId || !wantedIds.has(productId)) continue
699
+ const wanted = parseAmount(ctx.buyer.taxMode === 'gross' ? row.unitPriceGross : row.unitPriceNet)
700
+ const amount = wanted ?? parseAmount(ctx.buyer.taxMode === 'gross' ? row.unitPriceNet : row.unitPriceGross)
701
+ if (amount === null) continue
702
+ const current = amounts.get(productId)
703
+ if (current === undefined || amount < current) amounts.set(productId, amount)
704
+ }
705
+ return amounts
706
+ }
707
+
708
+ function compareAmounts(left: number | null, right: number | null, direction: 1 | -1): number {
709
+ if (left === null && right === null) return 0
710
+ if (left === null) return 1
711
+ if (right === null) return -1
712
+ return (left - right) * direction
713
+ }
714
+
715
+ function rankByRelevance<T extends { title: string; sku: string | null }>(items: T[], searchTerm: string): T[] {
716
+ const needle = searchTerm.toLowerCase()
717
+ return items
718
+ .map((item, index) => ({ item, index, score: scoreProductSearchRelevance(needle, item.title, item.sku) }))
719
+ .sort((left, right) => left.score - right.score || left.item.title.localeCompare(right.item.title) || left.index - right.index)
720
+ .map((entry) => entry.item)
721
+ }
722
+
723
+ function matchesPriceFilter(amount: number | null, query: EcommerceStorefrontProductListQuery): boolean {
724
+ if (amount === null) return false
725
+ if (query.priceMin !== undefined && amount < query.priceMin) return false
726
+ if (query.priceMax !== undefined && amount > query.priceMax) return false
727
+ return true
728
+ }
729
+
730
+ function availableSortsFor(searchTerm: string | null, priceSortsOffered: boolean): EcommerceStorefrontProductSort[] {
731
+ return SORT_ORDER.filter((sort) => {
732
+ if (sort === 'relevance') return !!searchTerm
733
+ if (isPriceSort(sort)) return priceSortsOffered
734
+ return true
735
+ })
736
+ }
737
+
738
+ async function resolveCountFacetState(
739
+ runtime: Runtime,
740
+ selection: StorefrontFacetSelection,
741
+ searchTerm: string | null,
742
+ universe: Where | null,
743
+ ): Promise<CountFacetState> {
744
+ const cached = await readCachedStorefrontCountFacets(runtime.container, runtime.ctx, selection, searchTerm)
745
+ if (cached) return { cached, source: null, overCap: false }
746
+ const source = await loadStorefrontFacetSource(
747
+ runtime,
748
+ composeStorefrontProductFilters(runtime.scope, universe),
749
+ runtime.facetUniverseCap,
750
+ )
751
+ return { cached: null, source, overCap: source === null }
752
+ }
753
+
754
+ /**
755
+ * `GET /products` listing (Storefront Public API rev 4 §4.1, §5.1, §5.3, §6.3, §7, §8.1, §10).
756
+ *
757
+ * Every product query composes `buildStorefrontProductScope` (§3.3). The filtered set is read as
758
+ * ordered candidates (title, newest and featured order in SQL — `featured` orders by `created_at
759
+ * desc` until merchandising supplies a featured rank). While it is at most
760
+ * `STOREFRONT_PRICE_SORT_CAP` products it is priced for the buyer in one batch: that pricing feeds
761
+ * the price sorts, the price filter, relevance ranking and `facets.priceRange` (the filtered set
762
+ * without the price filter, §5.4) and is reused for the page. Past the cap the channel's
763
+ * `priceSortFallback` decides: `'approximate'` orders, filters and ranges by the channel default
764
+ * price kind's list rows and flags `sortApproximate` — loading every candidate and those candidates'
765
+ * default-kind rows only when a price sort or price filter asks for it, otherwise `priceRange` is
766
+ * `null` — `'unavailable'` withdraws the price sorts from `availableSorts`, applies the default sort
767
+ * instead (`sortUnavailable`), leaves the price filter unapplied and returns `priceRange: null`. A
768
+ * filtered set larger than `STOREFRONT_APPROXIMATE_SORT_CEILING` is never loaded in full: there
769
+ * `'approximate'` answers as `'unavailable'` and `priceSort.fallback` reports `'unavailable'`. `availability=` and `hideWhenOutOfStock` are
770
+ * page-scoped (D21): the page is chosen first, then filtered, so `total` counts the
771
+ * pre-availability set and a page may be short; `facets.availability` counts the page.
772
+ *
773
+ * The count facets come from `storefrontFacets` — cached per assortment scope (§9.1); on a miss
774
+ * their universe load also supplies the variant index the pricing reads, and their labels ride on
775
+ * the page's translation query. A facet universe past `STOREFRONT_FACET_UNIVERSE_CAP` products is
776
+ * not loaded and the count facets are empty. An `options[code]` filter is resolved in one query of
777
+ * at most `STOREFRONT_FACET_UNIVERSE_CAP` product ids; when more products match, it is left
778
+ * unapplied like a price filter past the cap — absent from `appliedFilters` and from the facet
779
+ * selection. The whole response, `priceRange` included, is cached by the caller on the full digest.
780
+ */
781
+ export async function listStorefrontProducts(
782
+ container: AwilixContainer,
783
+ ctx: StoreContext,
784
+ query: EcommerceStorefrontProductListQuery,
785
+ options: ListStorefrontProductsOptions = {},
786
+ ): Promise<StorefrontProductListResponse> {
787
+ const em = tryResolve<EntityManager>(container, 'em')
788
+ const queryEngine = tryResolve<QueryEngine>(container, 'queryEngine')
789
+ if (!em || !queryEngine) throw new Error('[internal] ecommerce storefront listing requires em and queryEngine')
790
+ const scope = buildStorefrontProductScope(ctx)
791
+ const runtime: Runtime = {
792
+ container,
793
+ ctx,
794
+ em,
795
+ queryEngine,
796
+ scope,
797
+ decryptionScope: { tenantId: ctx.tenantId, organizationId: ctx.organizationId },
798
+ date: options.date ?? new Date(),
799
+ facetUniverseCap: options.facetUniverseCap,
800
+ }
801
+ const cap = Math.max(1, options.priceSortCap ?? STOREFRONT_PRICE_SORT_CAP)
802
+ const approximateCeiling = Math.max(cap, options.approximateSortCeiling ?? STOREFRONT_APPROXIMATE_SORT_CEILING)
803
+ const channelFallback: EcommercePriceSortFallback = ctx.channel?.priceSortFallback ?? 'approximate'
804
+ const { page, pageSize } = query
805
+
806
+ const { extra, universe, selection, applied, searchTerm } = await resolveListFilters(runtime, query)
807
+ const filters = composeStorefrontProductFilters(scope, extra)
808
+ if (query.availability === 'in_stock' || query.availability === 'available') {
809
+ applied.availability = { value: query.availability, scope: 'page' }
810
+ }
811
+
812
+ const defaultSort: EcommerceStorefrontProductSort = searchTerm ? 'relevance' : 'featured'
813
+ const requestedSort = query.sort ?? defaultSort
814
+ const priceSortRequested = isPriceSort(requestedSort)
815
+ const priceFilterRequested = query.priceMin !== undefined || query.priceMax !== undefined
816
+ const baseSort: EcommerceStorefrontProductSort =
817
+ priceSortRequested || requestedSort === 'relevance' ? 'title_asc' : requestedSort
818
+
819
+ const [countFacetState, first] = await Promise.all([
820
+ resolveCountFacetState(runtime, selection, searchTerm, universe),
821
+ queryCandidates(runtime, filters, sqlSortFor(baseSort), cap),
822
+ ])
823
+ const facetVariants = countFacetState.source ? storefrontFacetVariantIndex(countFacetState.source) : null
824
+ const capExceeded = first.total > cap
825
+ const fallback: EcommercePriceSortFallback =
826
+ capExceeded && first.total > approximateCeiling ? 'unavailable' : channelFallback
827
+ const priceOnOffer = !capExceeded || fallback === 'approximate'
828
+
829
+ let sortApproximate = false
830
+ let sortUnavailable = false
831
+ let appliedSort: EcommerceStorefrontProductSort = requestedSort === 'relevance' && !searchTerm ? defaultSort : requestedSort
832
+ let total = 0
833
+ let records: ProductRecord[] = []
834
+ let prefetch: PagePrefetch = {}
835
+ let candidates: Candidate[] | null = null
836
+ let amounts: Map<string, number | null> | null = null
837
+
838
+ if (!capExceeded) {
839
+ candidates = first.items
840
+ const candidateIds = candidates.map((candidate) => candidate.id)
841
+ const variants = facetVariants
842
+ ? new Map(candidateIds.map((id) => [id, facetVariants.get(id) ?? []]))
843
+ : await loadVariantIds(runtime, candidateIds)
844
+ const pricing = await resolveStorefrontPrices(
845
+ container,
846
+ ctx,
847
+ candidateIds.map((id) => ({ productId: id, variantIds: variants.get(id) ?? [] })),
848
+ { date: runtime.date },
849
+ )
850
+ prefetch = { variants, pricing }
851
+ amounts = new Map(candidateIds.map((id) => [id, pricingAmount(pricing.get(id))]))
852
+ } else if (priceOnOffer && (priceSortRequested || priceFilterRequested)) {
853
+ const all = await queryCandidates(runtime, filters, sqlSortFor(baseSort), first.total)
854
+ const approximate = await loadApproximateAmounts(runtime, all.items.map((candidate) => candidate.id))
855
+ amounts = new Map(all.items.map((candidate) => [candidate.id, approximate.get(candidate.id) ?? null]))
856
+ candidates = all.items
857
+ }
858
+ const priceRange = amounts ? storefrontPriceRangeFacet(amounts.values(), ctx.currencyCode) : null
859
+
860
+ if (priceFilterRequested && priceOnOffer) {
861
+ applied.price = { min: query.priceMin ?? null, max: query.priceMax ?? null, approximate: capExceeded }
862
+ }
863
+ if (priceSortRequested && !priceOnOffer) {
864
+ sortUnavailable = true
865
+ appliedSort = defaultSort
866
+ }
867
+
868
+ if (candidates) {
869
+ let ordered = priceFilterRequested && priceOnOffer
870
+ ? candidates.filter((candidate) => matchesPriceFilter(amounts?.get(candidate.id) ?? null, query))
871
+ : candidates
872
+ if (appliedSort === 'relevance' && searchTerm) {
873
+ ordered = rankByRelevance(ordered, searchTerm)
874
+ } else if (isPriceSort(appliedSort)) {
875
+ const direction = appliedSort === 'price_asc' ? 1 : -1
876
+ ordered = ordered
877
+ .map((candidate, index) => ({ candidate, index }))
878
+ .sort(
879
+ (left, right) =>
880
+ compareAmounts(amounts?.get(left.candidate.id) ?? null, amounts?.get(right.candidate.id) ?? null, direction) ||
881
+ left.index - right.index,
882
+ )
883
+ .map((entry) => entry.candidate)
884
+ sortApproximate = capExceeded
885
+ }
886
+ total = ordered.length
887
+ const pageIds = ordered.slice((page - 1) * pageSize, page * pageSize).map((candidate) => candidate.id)
888
+ records = await queryProductsByIds(runtime, pageIds)
889
+ } else {
890
+ const sqlOrder: EcommerceStorefrontProductSort = appliedSort === 'relevance' ? 'title_asc' : appliedSort
891
+ const result = await runtime.queryEngine.query<ProductRecord>(E.catalog.catalog_product, {
892
+ tenantId: scope.tenantId,
893
+ organizationId: scope.organizationId,
894
+ withDeleted: scope.withDeleted,
895
+ filters,
896
+ fields: LIST_FIELDS,
897
+ sort: sqlSortFor(sqlOrder),
898
+ page: { page, pageSize },
899
+ })
900
+ total = result.total
901
+ records = result.items.filter((item) => typeof item.id === 'string')
902
+ if (appliedSort === 'relevance' && searchTerm) {
903
+ records = rankByRelevance(
904
+ records.map((record) => ({ record, title: nonEmptyString(record.title) ?? '', sku: nonEmptyString(record.sku) })),
905
+ searchTerm,
906
+ ).map((entry) => entry.record)
907
+ sortApproximate = true
908
+ }
909
+ if (facetVariants) prefetch = { variants: new Map(records.map((record) => [record.id, facetVariants.get(record.id) ?? []])) }
910
+ }
911
+
912
+ const availableSorts = availableSortsFor(searchTerm, !(capExceeded && fallback === 'unavailable'))
913
+ const { source, cached, overCap } = countFacetState
914
+ const rawCounts = source ? countStorefrontFacets(source, selection) : null
915
+ const hydrated = await hydratePage(
916
+ runtime,
917
+ records,
918
+ query,
919
+ prefetch,
920
+ source && rawCounts ? storefrontFacetTranslationRequests(source, rawCounts) : [],
921
+ )
922
+ let countFacets: StorefrontCountFacets = cached ?? EMPTY_COUNT_FACETS
923
+ if (source && rawCounts) {
924
+ countFacets = labelStorefrontCountFacets(source, rawCounts, {
925
+ locales: localeChain(ctx),
926
+ translations: hydrated.translations,
927
+ assortmentScope: ctx.buyer.assortmentScope,
928
+ productTypeLabel: await loadStorefrontProductTypeLabeler(ctx.effectiveLocale),
929
+ })
930
+ await writeCachedStorefrontCountFacets(container, ctx, selection, searchTerm, countFacets)
931
+ } else if (overCap) {
932
+ await writeCachedStorefrontCountFacets(container, ctx, selection, searchTerm, EMPTY_COUNT_FACETS)
933
+ }
934
+ const facets: StorefrontFacets = {
935
+ categories: countFacets.categories,
936
+ tags: countFacets.tags,
937
+ priceRange,
938
+ options: countFacets.options,
939
+ productTypes: countFacets.productTypes,
940
+ availability: storefrontAvailabilityFacet(hydrated.availabilityStates),
941
+ availabilityScope: 'page',
942
+ total,
943
+ }
944
+
945
+ return {
946
+ items: hydrated.items,
947
+ total,
948
+ page,
949
+ pageSize,
950
+ totalPages: Math.ceil(total / pageSize),
951
+ facets,
952
+ effectiveLocale: ctx.effectiveLocale,
953
+ requestedLocale: ctx.requestedLocale,
954
+ currencyCode: ctx.currencyCode,
955
+ taxMode: ctx.buyer.taxMode,
956
+ appliedFilters: applied,
957
+ availableSorts,
958
+ appliedSort,
959
+ priceSort: { cap, fallback, capExceeded },
960
+ sortApproximate,
961
+ sortUnavailable,
962
+ }
963
+ }