@open-mercato/core 0.9.1-develop.7473.1.de5b22888f → 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,17 @@
1
+ import {
2
+ reindexProductsForCategorySubtree
3
+ } from "../lib/productScopeReindex.js";
4
+ const metadata = {
5
+ event: "catalog.category.updated",
6
+ persistent: true,
7
+ id: "catalog:product-scope-keys-category-updated"
8
+ };
9
+ async function handle(payload, ctx) {
10
+ if (payload?.hierarchyChanged !== true) return;
11
+ await reindexProductsForCategorySubtree(payload, ctx);
12
+ }
13
+ export {
14
+ handle as default,
15
+ metadata
16
+ };
17
+ //# sourceMappingURL=product-scope-keys-category-updated.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/catalog/subscribers/product-scope-keys-category-updated.ts"],
4
+ "sourcesContent": ["import {\n reindexProductsForCategorySubtree,\n type CategoryScopeChangePayload,\n type ProductScopeReindexContext,\n} from '../lib/productScopeReindex'\n\nexport const metadata = {\n event: 'catalog.category.updated',\n persistent: true,\n id: 'catalog:product-scope-keys-category-updated',\n}\n\nexport default async function handle(\n payload: CategoryScopeChangePayload,\n ctx: ProductScopeReindexContext,\n): Promise<void> {\n if (payload?.hierarchyChanged !== true) return\n await reindexProductsForCategorySubtree(payload, ctx)\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,OAGK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,OACL,SACA,KACe;AACf,MAAI,SAAS,qBAAqB,KAAM;AACxC,QAAM,kCAAkC,SAAS,GAAG;AACtD;",
6
+ "names": []
7
+ }
@@ -1,3 +1,11 @@
1
+ import {
2
+ registerTranslatableEntityListPath,
3
+ registerTranslatableFieldExpander
4
+ } from "@open-mercato/shared/lib/localization/translatable-fields";
5
+ import {
6
+ OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE,
7
+ expandOptionSchemaTranslationFields
8
+ } from "./lib/optionSchemaTranslations.js";
1
9
  const translatableFields = {
2
10
  "catalog:catalog_product": ["title", "subtitle", "description", "seoTitle", "seoDescription"],
3
11
  "catalog:catalog_product_variant": ["name"],
@@ -6,6 +14,8 @@ const translatableFields = {
6
14
  "catalog:catalog_product_category": ["name", "description"],
7
15
  "catalog:catalog_product_tag": ["label"]
8
16
  };
17
+ registerTranslatableFieldExpander(OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE, expandOptionSchemaTranslationFields);
18
+ registerTranslatableEntityListPath(OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE, "/api/catalog/option-schemas");
9
19
  var translations_default = translatableFields;
10
20
  export {
11
21
  translations_default as default,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/catalog/translations.ts"],
4
- "sourcesContent": ["export const translatableFields: Record<string, string[]> = {\n 'catalog:catalog_product': ['title', 'subtitle', 'description', 'seoTitle', 'seoDescription'],\n 'catalog:catalog_product_variant': ['name'],\n 'catalog:catalog_offer': ['title', 'subtitle', 'description'],\n 'catalog:catalog_option_schema_template': ['name', 'description'],\n 'catalog:catalog_product_category': ['name', 'description'],\n 'catalog:catalog_product_tag': ['label'],\n}\n\nexport default translatableFields\n"],
5
- "mappings": "AAAO,MAAM,qBAA+C;AAAA,EAC1D,2BAA2B,CAAC,SAAS,YAAY,eAAe,YAAY,gBAAgB;AAAA,EAC5F,mCAAmC,CAAC,MAAM;AAAA,EAC1C,yBAAyB,CAAC,SAAS,YAAY,aAAa;AAAA,EAC5D,0CAA0C,CAAC,QAAQ,aAAa;AAAA,EAChE,oCAAoC,CAAC,QAAQ,aAAa;AAAA,EAC1D,+BAA+B,CAAC,OAAO;AACzC;AAEA,IAAO,uBAAQ;",
4
+ "sourcesContent": ["import {\n registerTranslatableEntityListPath,\n registerTranslatableFieldExpander,\n} from '@open-mercato/shared/lib/localization/translatable-fields'\nimport {\n OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE,\n expandOptionSchemaTranslationFields,\n} from './lib/optionSchemaTranslations'\n\nexport const translatableFields: Record<string, string[]> = {\n 'catalog:catalog_product': ['title', 'subtitle', 'description', 'seoTitle', 'seoDescription'],\n 'catalog:catalog_product_variant': ['name'],\n 'catalog:catalog_offer': ['title', 'subtitle', 'description'],\n 'catalog:catalog_option_schema_template': ['name', 'description'],\n 'catalog:catalog_product_category': ['name', 'description'],\n 'catalog:catalog_product_tag': ['label'],\n}\n\nregisterTranslatableFieldExpander(OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE, expandOptionSchemaTranslationFields)\nregisterTranslatableEntityListPath(OPTION_SCHEMA_TEMPLATE_ENTITY_TYPE, '/api/catalog/option-schemas')\n\nexport default translatableFields\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,MAAM,qBAA+C;AAAA,EAC1D,2BAA2B,CAAC,SAAS,YAAY,eAAe,YAAY,gBAAgB;AAAA,EAC5F,mCAAmC,CAAC,MAAM;AAAA,EAC1C,yBAAyB,CAAC,SAAS,YAAY,aAAa;AAAA,EAC5D,0CAA0C,CAAC,QAAQ,aAAa;AAAA,EAChE,oCAAoC,CAAC,QAAQ,aAAa;AAAA,EAC1D,+BAA+B,CAAC,OAAO;AACzC;AAEA,kCAAkC,oCAAoC,mCAAmC;AACzG,mCAAmC,oCAAoC,6BAA6B;AAEpG,IAAO,uBAAQ;",
6
6
  "names": []
7
7
  }
@@ -137,6 +137,34 @@ const upgradeActions = [
137
137
  if (deferAfterCommit) deferAfterCommit(invalidate);
138
138
  else await invalidate();
139
139
  }
140
+ },
141
+ {
142
+ id: "ecommerce.seed-draft-store",
143
+ version: "0.9.1",
144
+ messageKey: "configs.upgrades.ecommerceDraftStore.message",
145
+ ctaKey: "configs.upgrades.ecommerceDraftStore.cta",
146
+ successKey: "configs.upgrades.ecommerceDraftStore.success",
147
+ loadingKey: "configs.upgrades.ecommerceDraftStore.loading",
148
+ requiredModules: ["ecommerce"],
149
+ run: async ({ container, em, tenantId, organizationId }) => {
150
+ const { seedDraftStore } = await import("@open-mercato/core/modules/ecommerce/lib/seedDraftStore");
151
+ const result = await seedDraftStore(
152
+ em,
153
+ { tenantId, organizationId },
154
+ {
155
+ resolveCurrencyCode: async () => {
156
+ try {
157
+ const resolver = container.resolve("baseCurrencyService");
158
+ const resolution = await resolver.resolveBaseCurrency({ tenantId, organizationIds: [organizationId] });
159
+ return resolution.status === "resolved" ? resolution.code : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ }
165
+ );
166
+ logger.info("ecommerce draft store seed completed", { tenantId, organizationId, status: result.status });
167
+ }
140
168
  }
141
169
  ];
142
170
  function actionsUpToVersion(version) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/configs/lib/upgrade-actions.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n deferAfterCommit?: (callback: () => void | Promise<void>) => void\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n /**\n * Modules that must be enabled for this action to be offered/executed.\n * Mirrors `InjectionWidgetMetadata.requiredModules` \u2014 omit for actions\n * owned by `configs` itself.\n */\n requiredModules?: string[]\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'devices.seed-push-token-encryption-map',\n version: '0.6.6',\n messageKey: 'configs.upgrades.devicesPushTokenEncryption.message',\n ctaKey: 'configs.upgrades.devicesPushTokenEncryption.cta',\n successKey: 'configs.upgrades.devicesPushTokenEncryption.success',\n loadingKey: 'configs.upgrades.devicesPushTokenEncryption.loading',\n // Encryption maps are seeded once at tenant creation (`entities seed-encryption`), so a tenant\n // that predates the devices push-token feature has no `devices:user_device` map row. Without it\n // `encryptEntityPayload` no-ops and `push_token` is written as PLAINTEXT (the map is a declaration\n // of which fields to encrypt; the tenant DEK still drives the actual crypto, so seeding it is safe\n // even when encryption is currently disabled \u2014 it only takes effect once encryption is on).\n // Lazy-imported so configs stays decoupled from devices/entities (mirrors the customers action).\n run: async ({ container, em, tenantId, organizationId, deferAfterCommit }) => {\n const [{ default: devicesEncryptionMaps }, { invalidateEncryptionMapScopes, upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/devices/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n const materializedScopes = await upsertEncryptionMapSpecs(\n em,\n tenantId,\n organizationId ?? null,\n devicesEncryptionMaps,\n )\n const invalidate = async () => {\n const encryptionService = container.resolve('tenantEncryptionService') as Parameters<typeof invalidateEncryptionMapScopes>[0]\n await invalidateEncryptionMapScopes(encryptionService, materializedScopes)\n }\n if (deferAfterCommit) deferAfterCommit(invalidate)\n else await invalidate()\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n {\n id: 'payment_gateways.register-session-initialization-prune',\n version: '0.6.6',\n messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',\n ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',\n successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',\n loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',\n requiredModules: ['payment_gateways'],\n run: async ({ container, tenantId, organizationId }) => {\n const { registerSessionInitializationPruneSchedule } = await import(\n '@open-mercato/core/modules/payment_gateways/setup'\n )\n await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })\n },\n },\n {\n id: 'phone_calls.seed-call-encryption-maps',\n version: '0.7.1',\n messageKey: 'phone_calls.config.upgradeActions.encryptionMaps.message',\n ctaKey: 'phone_calls.config.upgradeActions.encryptionMaps.cta',\n successKey: 'phone_calls.config.upgradeActions.encryptionMaps.success',\n loadingKey: 'phone_calls.config.upgradeActions.encryptionMaps.loading',\n // `Migration20260822120000` backfills these maps, but only for scopes that had active maps when\n // it ran. A tenant that upgraded with encryption disabled and turned it on afterwards is left\n // without a map, so ingest writes call PII as plaintext with nothing to signal it. This is the\n // heal for that case, and the same re-assert path devices exposes for its own map.\n // Lazy-imported so configs stays decoupled from phone_calls/entities (mirrors the devices action).\n run: async ({ container, em, tenantId, organizationId, deferAfterCommit }) => {\n const [{ default: phoneCallsEncryptionMaps }, { invalidateEncryptionMapScopes, upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/phone_calls/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n const materializedScopes = await upsertEncryptionMapSpecs(\n em,\n tenantId,\n organizationId ?? null,\n phoneCallsEncryptionMaps,\n )\n const invalidate = async () => {\n const encryptionService = container.resolve('tenantEncryptionService') as Parameters<typeof invalidateEncryptionMapScopes>[0]\n await invalidateEncryptionMapScopes(encryptionService, materializedScopes)\n }\n if (deferAfterCommit) deferAfterCommit(invalidate)\n else await invalidate()\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\ntype BaseCurrencyResolver = {\n resolveBaseCurrency(scope: {\n tenantId: string\n organizationIds: string[]\n }): Promise<{ status: 'resolved'; code: string } | { status: 'missing' | 'ambiguous' | 'unavailable' }>\n}\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n deferAfterCommit?: (callback: () => void | Promise<void>) => void\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n /**\n * Modules that must be enabled for this action to be offered/executed.\n * Mirrors `InjectionWidgetMetadata.requiredModules` \u2014 omit for actions\n * owned by `configs` itself.\n */\n requiredModules?: string[]\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'devices.seed-push-token-encryption-map',\n version: '0.6.6',\n messageKey: 'configs.upgrades.devicesPushTokenEncryption.message',\n ctaKey: 'configs.upgrades.devicesPushTokenEncryption.cta',\n successKey: 'configs.upgrades.devicesPushTokenEncryption.success',\n loadingKey: 'configs.upgrades.devicesPushTokenEncryption.loading',\n // Encryption maps are seeded once at tenant creation (`entities seed-encryption`), so a tenant\n // that predates the devices push-token feature has no `devices:user_device` map row. Without it\n // `encryptEntityPayload` no-ops and `push_token` is written as PLAINTEXT (the map is a declaration\n // of which fields to encrypt; the tenant DEK still drives the actual crypto, so seeding it is safe\n // even when encryption is currently disabled \u2014 it only takes effect once encryption is on).\n // Lazy-imported so configs stays decoupled from devices/entities (mirrors the customers action).\n run: async ({ container, em, tenantId, organizationId, deferAfterCommit }) => {\n const [{ default: devicesEncryptionMaps }, { invalidateEncryptionMapScopes, upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/devices/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n const materializedScopes = await upsertEncryptionMapSpecs(\n em,\n tenantId,\n organizationId ?? null,\n devicesEncryptionMaps,\n )\n const invalidate = async () => {\n const encryptionService = container.resolve('tenantEncryptionService') as Parameters<typeof invalidateEncryptionMapScopes>[0]\n await invalidateEncryptionMapScopes(encryptionService, materializedScopes)\n }\n if (deferAfterCommit) deferAfterCommit(invalidate)\n else await invalidate()\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n {\n id: 'payment_gateways.register-session-initialization-prune',\n version: '0.6.6',\n messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',\n ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',\n successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',\n loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',\n requiredModules: ['payment_gateways'],\n run: async ({ container, tenantId, organizationId }) => {\n const { registerSessionInitializationPruneSchedule } = await import(\n '@open-mercato/core/modules/payment_gateways/setup'\n )\n await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })\n },\n },\n {\n id: 'phone_calls.seed-call-encryption-maps',\n version: '0.7.1',\n messageKey: 'phone_calls.config.upgradeActions.encryptionMaps.message',\n ctaKey: 'phone_calls.config.upgradeActions.encryptionMaps.cta',\n successKey: 'phone_calls.config.upgradeActions.encryptionMaps.success',\n loadingKey: 'phone_calls.config.upgradeActions.encryptionMaps.loading',\n // `Migration20260822120000` backfills these maps, but only for scopes that had active maps when\n // it ran. A tenant that upgraded with encryption disabled and turned it on afterwards is left\n // without a map, so ingest writes call PII as plaintext with nothing to signal it. This is the\n // heal for that case, and the same re-assert path devices exposes for its own map.\n // Lazy-imported so configs stays decoupled from phone_calls/entities (mirrors the devices action).\n run: async ({ container, em, tenantId, organizationId, deferAfterCommit }) => {\n const [{ default: phoneCallsEncryptionMaps }, { invalidateEncryptionMapScopes, upsertEncryptionMapSpecs }] = await Promise.all([\n import('@open-mercato/core/modules/phone_calls/encryption'),\n import('@open-mercato/core/modules/entities/cli'),\n ])\n const materializedScopes = await upsertEncryptionMapSpecs(\n em,\n tenantId,\n organizationId ?? null,\n phoneCallsEncryptionMaps,\n )\n const invalidate = async () => {\n const encryptionService = container.resolve('tenantEncryptionService') as Parameters<typeof invalidateEncryptionMapScopes>[0]\n await invalidateEncryptionMapScopes(encryptionService, materializedScopes)\n }\n if (deferAfterCommit) deferAfterCommit(invalidate)\n else await invalidate()\n },\n },\n {\n id: 'ecommerce.seed-draft-store',\n version: '0.9.1',\n messageKey: 'configs.upgrades.ecommerceDraftStore.message',\n ctaKey: 'configs.upgrades.ecommerceDraftStore.cta',\n successKey: 'configs.upgrades.ecommerceDraftStore.success',\n loadingKey: 'configs.upgrades.ecommerceDraftStore.loading',\n requiredModules: ['ecommerce'],\n run: async ({ container, em, tenantId, organizationId }) => {\n const { seedDraftStore } = await import('@open-mercato/core/modules/ecommerce/lib/seedDraftStore')\n const result = await seedDraftStore(\n em,\n { tenantId, organizationId },\n {\n resolveCurrencyCode: async () => {\n try {\n const resolver = container.resolve<BaseCurrencyResolver>('baseCurrencyService')\n const resolution = await resolver.resolveBaseCurrency({ tenantId, organizationIds: [organizationId] })\n return resolution.status === 'resolved' ? resolution.code : null\n } catch {\n return null\n }\n },\n },\n )\n logger.info('ecommerce draft store seed completed', { tenantId, organizationId, status: result.status })\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,7 @@ import { CustomerGroup, CustomerGroupTerms } from "../../../../data/entities.js"
13
13
  import { isPriceKindInTenant } from "../../../../lib/priceKindScope.js";
14
14
  import { announceCustomerGroupTermsUpdated } from "../../../../lib/groupEvents.js";
15
15
  import {
16
+ assortmentScopeSchema,
16
17
  customerGroupTermsCreateSchema,
17
18
  customerGroupTermsUpdateSchema
18
19
  } from "../../../../data/validators.js";
@@ -67,6 +68,7 @@ function serializeTerms(terms) {
67
68
  creditCurrencyCode: terms.creditCurrencyCode ?? null,
68
69
  approvalRequiredAbove: toNumberOrNull(terms.approvalRequiredAbove),
69
70
  minOrderValue: toNumberOrNull(terms.minOrderValue),
71
+ assortmentScope: terms.assortmentScope ?? null,
70
72
  metadata: terms.metadata ?? null,
71
73
  createdAt: terms.createdAt.toISOString(),
72
74
  updatedAt: terms.updatedAt.toISOString()
@@ -105,6 +107,7 @@ function toEntityData(input) {
105
107
  creditCurrencyCode: input.creditCurrencyCode ?? null,
106
108
  approvalRequiredAbove: toNumericString(input.approvalRequiredAbove),
107
109
  minOrderValue: toNumericString(input.minOrderValue),
110
+ assortmentScope: input.assortmentScope ?? null,
108
111
  metadata: input.metadata ?? null
109
112
  };
110
113
  }
@@ -119,6 +122,7 @@ function applyTermsUpdate(entity, input) {
119
122
  if (hasOwn(input, "creditCurrencyCode")) entity.creditCurrencyCode = input.creditCurrencyCode ?? null;
120
123
  if (hasOwn(input, "approvalRequiredAbove")) entity.approvalRequiredAbove = toNumericString(input.approvalRequiredAbove);
121
124
  if (hasOwn(input, "minOrderValue")) entity.minOrderValue = toNumericString(input.minOrderValue);
125
+ if (hasOwn(input, "assortmentScope")) entity.assortmentScope = input.assortmentScope ?? null;
122
126
  if (hasOwn(input, "metadata")) entity.metadata = input.metadata ?? null;
123
127
  }
124
128
  async function PUT(req, context) {
@@ -220,6 +224,7 @@ const termsItemSchema = z.object({
220
224
  creditCurrencyCode: z.string().nullable(),
221
225
  approvalRequiredAbove: z.number().nullable(),
222
226
  minOrderValue: z.number().nullable(),
227
+ assortmentScope: assortmentScopeSchema,
223
228
  metadata: z.record(z.string(), z.unknown()).nullable(),
224
229
  createdAt: z.string(),
225
230
  updatedAt: z.string()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/customer_groups/api/customer-groups/%5Bid%5D/terms/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { CrudHttpError, badRequest, conflict, isCrudHttpError, isUniqueViolation, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { CustomerGroup, CustomerGroupTerms } from '../../../../data/entities'\nimport { isPriceKindInTenant } from '../../../../lib/priceKindScope'\nimport { announceCustomerGroupTermsUpdated } from '../../../../lib/groupEvents'\nimport {\n customerGroupTermsCreateSchema,\n customerGroupTermsUpdateSchema,\n type CustomerGroupTermsCreateInput,\n type CustomerGroupTermsUpdateInput,\n} from '../../../../data/validators'\n\nconst logger = createLogger('customer_groups')\n\n// Terms are a strict 1:1 sub-resource of a group (`customer_group_terms_group_unique`\n// is a partial unique index on `group_id`, see `data/entities.ts`) with no independent\n// list/pagination/search use case \u2014 there is no \"list all terms rows\" screen, only\n// \"this group's terms\". The spec's own literal path (`GET/PUT\n// /api/customer-groups/:id/terms`) nests terms under a specific group, unlike\n// `../crud.ts` / `memberships/crud.ts`, which are genuine tenant-wide collections with\n// their own list/filter semantics. A `[id]` dynamic segment (id = the GROUP's id) doing\n// a plain `findOne`/upsert against `CustomerGroupTerms` is therefore the better fit here\n// than a `makeCrudRoute` keyed by `groupId` (Option A from the Step 2.4 task) \u2014 the\n// latter would need to invent list/create/delete semantics this resource never uses,\n// and `makeCrudRoute`'s auto-registered optimistic-lock reader is keyed by the route's\n// own resourceKind, not a caller-chosen `groupId` lookup, so it would not save any\n// wiring over the explicit `enforceCommandOptimisticLock` call used below anyway.\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customer_groups.terms.view'] },\n PUT: { requireAuth: true, requireFeatures: ['customer_groups.terms.manage'] },\n}\n\n// Matches the indexer entityType naming convention already used for the other two\n// entities in this module (`customer_groups:customer_group`,\n// `customer_groups:customer_group_membership` in `../crud.ts` /\n// `memberships/crud.ts`), so `OM_OPTIMISTIC_LOCK`'s allow-list mode can target this\n// resource by the same family of names.\nconst TERMS_RESOURCE_KIND = 'customer_groups:customer_group_terms'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\ntype RouteContext = { params: Promise<{ id: string }> | { id: string } }\n\ntype Scope = { tenantId: string; organizationId: string | null }\n\ntype Translate = (key: string, fallback?: string) => string\n\nasync function resolveGroupId(context: RouteContext, translate: Translate): Promise<string> {\n const raw = await context.params\n const parsed = paramsSchema.safeParse(raw)\n if (!parsed.success) {\n throw notFound(translate('customer_groups.errors.group_not_found', 'Customer group not found'))\n }\n return parsed.data.id\n}\n\nasync function resolveAuthScope(req: Request, translate: Translate): Promise<{ userId: string; scope: Scope }> {\n const auth = await getAuthFromRequest(req)\n if (!auth) throw new CrudHttpError(401, { error: translate('customer_groups.errors.unauthorized', 'Unauthorized') })\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n throw new CrudHttpError(400, { error: translate('customer_groups.errors.tenant_required', 'Tenant context is required') })\n }\n return { userId: auth.sub, scope: { tenantId, organizationId: auth.orgId ?? null } }\n}\n\nasync function loadGroupOrThrow(\n em: EntityManager,\n id: string,\n scope: Scope,\n translate: Translate,\n): Promise<CustomerGroup> {\n const group = await em.findOne(CustomerGroup, { id, tenantId: scope.tenantId, deletedAt: null })\n if (!group) throw notFound(translate('customer_groups.errors.group_not_found', 'Customer group not found'))\n return group\n}\n\n// Mirrors `toNumericString` in `catalog/commands/shared.ts` \u2014 this module has no\n// shared equivalent yet, so a small module-local copy follows the same established\n// pattern rather than introducing a new cross-module dependency for one line.\nfunction toNumericString(value: number | null | undefined): string | null {\n if (value === undefined || value === null) return null\n return value.toString()\n}\n\nfunction toNumberOrNull(value: string | null | undefined): number | null {\n if (value == null) return null\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction serializeTerms(terms: CustomerGroupTerms) {\n return {\n id: terms.id,\n groupId: terms.groupId,\n organizationId: terms.organizationId ?? null,\n tenantId: terms.tenantId,\n priceKindId: terms.priceKindId ?? null,\n paymentTermsDays: terms.paymentTermsDays ?? null,\n allowPurchaseOnAccount: terms.allowPurchaseOnAccount ?? null,\n defaultCreditLimit: toNumberOrNull(terms.defaultCreditLimit),\n creditCurrencyCode: terms.creditCurrencyCode ?? null,\n approvalRequiredAbove: toNumberOrNull(terms.approvalRequiredAbove),\n minOrderValue: toNumberOrNull(terms.minOrderValue),\n metadata: terms.metadata ?? null,\n createdAt: terms.createdAt.toISOString(),\n updatedAt: terms.updatedAt.toISOString(),\n }\n}\n\nexport async function GET(req: Request, context: RouteContext) {\n const { translate } = await resolveTranslations()\n try {\n const groupId = await resolveGroupId(context, translate)\n const { scope } = await resolveAuthScope(req, translate)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n await loadGroupOrThrow(em, groupId, scope, translate)\n\n // No terms row yet is a normal, expected state (spec: \"No terms set \u2014 inheriting\n // from parent / tenant defaults\") \u2014 answered as 200 with `terms: null` rather than\n // 404, since the sub-resource's *absence* is itself meaningful data for the caller\n // to render, not an error.\n const terms = await em.findOne(CustomerGroupTerms, { groupId, tenantId: scope.tenantId, deletedAt: null })\n return NextResponse.json({ terms: terms ? serializeTerms(terms) : null })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n logger.error('customer_groups.terms.get failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'customer_groups', code: 'customer_groups.terms_get_failed' })\n return NextResponse.json(\n { error: translate('customer_groups.errors.load_failed', 'Failed to load commercial terms') },\n { status: 500 },\n )\n }\n}\n\nconst termsBodySchema = z.object({}).passthrough()\n\ntype CustomerGroupTermsEntityData = {\n organizationId: string | null\n tenantId: string\n groupId: string\n priceKindId: string | null\n paymentTermsDays: number | null\n allowPurchaseOnAccount: boolean | null\n defaultCreditLimit: string | null\n creditCurrencyCode: string | null\n approvalRequiredAbove: string | null\n minOrderValue: string | null\n metadata: Record<string, unknown> | null\n}\n\nfunction toEntityData(input: CustomerGroupTermsCreateInput): CustomerGroupTermsEntityData {\n return {\n organizationId: input.organizationId ?? null,\n tenantId: input.tenantId,\n groupId: input.groupId,\n priceKindId: input.priceKindId ?? null,\n paymentTermsDays: input.paymentTermsDays ?? null,\n allowPurchaseOnAccount: input.allowPurchaseOnAccount ?? null,\n defaultCreditLimit: toNumericString(input.defaultCreditLimit),\n creditCurrencyCode: input.creditCurrencyCode ?? null,\n approvalRequiredAbove: toNumericString(input.approvalRequiredAbove),\n minOrderValue: toNumericString(input.minOrderValue),\n metadata: input.metadata ?? null,\n }\n}\n\nfunction hasOwn(input: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(input, key)\n}\n\n// Scope fields (tenantId/organizationId/groupId) are deliberately excluded \u2014 same rule\n// as `applyCustomerGroupUpdate` in `../../crud.ts`: a terms row's group/tenant is fixed\n// at creation and MUST NOT change on update.\nfunction applyTermsUpdate(entity: CustomerGroupTerms, input: CustomerGroupTermsUpdateInput): void {\n if (hasOwn(input, 'priceKindId')) entity.priceKindId = input.priceKindId ?? null\n if (hasOwn(input, 'paymentTermsDays')) entity.paymentTermsDays = input.paymentTermsDays ?? null\n if (hasOwn(input, 'allowPurchaseOnAccount')) entity.allowPurchaseOnAccount = input.allowPurchaseOnAccount ?? null\n if (hasOwn(input, 'defaultCreditLimit')) entity.defaultCreditLimit = toNumericString(input.defaultCreditLimit)\n if (hasOwn(input, 'creditCurrencyCode')) entity.creditCurrencyCode = input.creditCurrencyCode ?? null\n if (hasOwn(input, 'approvalRequiredAbove')) entity.approvalRequiredAbove = toNumericString(input.approvalRequiredAbove)\n if (hasOwn(input, 'minOrderValue')) entity.minOrderValue = toNumericString(input.minOrderValue)\n if (hasOwn(input, 'metadata')) entity.metadata = input.metadata ?? null\n}\n\nexport async function PUT(req: Request, context: RouteContext) {\n const { translate } = await resolveTranslations()\n try {\n const groupId = await resolveGroupId(context, translate)\n const { userId, scope } = await resolveAuthScope(req, translate)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n await loadGroupOrThrow(em, groupId, scope, translate)\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(req, {})) ?? {}\n const rawInput = termsBodySchema.parse(rawBody)\n\n const existing = await em.findOne(CustomerGroupTerms, { groupId, tenantId: scope.tenantId, deletedAt: null })\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: { userId, tenantId: scope.tenantId, organizationId: scope.organizationId },\n input: {\n resourceKind: TERMS_RESOURCE_KIND,\n resourceId: existing?.id ?? groupId,\n operation: existing ? 'update' : 'create',\n mutationPayload: rawInput,\n },\n })\n if (!guarded.ok) return guarded.response\n\n const payload = guarded.modifiedPayload ?? rawInput\n\n const scopedPayload = { ...payload, groupId, tenantId: scope.tenantId, organizationId: scope.organizationId }\n\n // Runs on the validated payload only, so a malformed `priceKindId` is a 400 from\n // the schema instead of reaching the uuid column as a raw query.\n const assertPriceKindAllowed = async (priceKindId: string | null | undefined): Promise<void> => {\n if (!priceKindId || priceKindId === existing?.priceKindId) return\n if (await isPriceKindInTenant(em, priceKindId, scope.tenantId)) return\n throw badRequest(\n translate('customer_groups.errors.priceKindNotFound', 'The selected price kind does not exist.'),\n )\n }\n\n let terms: CustomerGroupTerms\n if (existing) {\n // The terms row has its own `updated_at`, independent of the parent group's \u2014\n // guard THIS row against a concurrent edit of the same terms, not the group.\n // No lock check on creation below: there is nothing to conflict with yet.\n enforceCommandOptimisticLock({\n resourceKind: TERMS_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n const parsed = customerGroupTermsUpdateSchema.parse({ ...scopedPayload, id: existing.id })\n await assertPriceKindAllowed(parsed.priceKindId)\n applyTermsUpdate(existing, parsed)\n terms = existing\n } else {\n const parsed = customerGroupTermsCreateSchema.parse(scopedPayload)\n await assertPriceKindAllowed(parsed.priceKindId)\n terms = em.create(CustomerGroupTerms, toEntityData(parsed))\n em.persist(terms)\n }\n\n await em.flush()\n await guarded.runAfterSuccess()\n try {\n await announceCustomerGroupTermsUpdated(container, terms)\n } catch (announceErr) {\n logger.error('customer_groups.terms.put announce failed', { err: announceErr })\n getTelemetryRuntime()?.reportError(announceErr, {\n module: 'customer_groups',\n code: 'customer_groups.terms_announce_failed',\n })\n }\n\n return NextResponse.json({ terms: serializeTerms(terms) })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('customer_groups.errors.invalid_input', 'Invalid input') },\n { status: 400 },\n )\n }\n // `customer_group_terms_group_unique` (one live terms row per group): two\n // concurrent first saves for the same group both see \"no row yet\" and both insert.\n // The loser gets a clean 409 so the client reloads and retries as an update.\n if (isUniqueViolation(err)) {\n const conflictError = conflict(\n translate(\n 'customer_groups.errors.terms_conflict',\n 'These commercial terms were changed at the same time by someone else. Reload and try again.',\n ),\n )\n return NextResponse.json(conflictError.body, { status: conflictError.status })\n }\n logger.error('customer_groups.terms.put failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'customer_groups', code: 'customer_groups.terms_put_failed' })\n return NextResponse.json(\n { error: translate('customer_groups.errors.save_failed', 'Failed to save commercial terms') },\n { status: 500 },\n )\n }\n}\n\nconst termsItemSchema = z.object({\n id: z.string().uuid(),\n groupId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid(),\n priceKindId: z.string().uuid().nullable(),\n paymentTermsDays: z.number().int().nullable(),\n allowPurchaseOnAccount: z.boolean().nullable(),\n defaultCreditLimit: z.number().nullable(),\n creditCurrencyCode: z.string().nullable(),\n approvalRequiredAbove: z.number().nullable(),\n minOrderValue: z.number().nullable(),\n metadata: z.record(z.string(), z.unknown()).nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst termsGetResponseSchema = z.object({ terms: termsItemSchema.nullable() })\nconst termsPutResponseSchema = z.object({ terms: termsItemSchema })\nconst termsErrorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerGroups',\n summary: 'Commercial terms for a customer group',\n methods: {\n GET: {\n summary: 'Get a customer group\u2019s commercial terms',\n description: 'Returns the group\u2019s own terms row, or `{ terms: null }` when the group has none and inherits from its parent/tenant defaults.',\n responses: [{ status: 200, description: 'Terms (or null)', schema: termsGetResponseSchema }],\n errors: [\n { status: 400, description: 'Tenant context is required', schema: termsErrorSchema },\n { status: 401, description: 'Unauthorized', schema: termsErrorSchema },\n { status: 404, description: 'Customer group not found', schema: termsErrorSchema },\n { status: 500, description: 'Unexpected failure', schema: termsErrorSchema },\n ],\n },\n PUT: {\n summary: 'Create or update a customer group\u2019s commercial terms',\n description: 'Upserts the group\u2019s terms row (creates it on first save). Supports optimistic locking via the extension header once a row exists.',\n requestBody: { contentType: 'application/json', schema: customerGroupTermsCreateSchema.partial() },\n responses: [{ status: 200, description: 'Terms created or updated', schema: termsPutResponseSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: termsErrorSchema },\n { status: 401, description: 'Unauthorized', schema: termsErrorSchema },\n { status: 404, description: 'Customer group not found', schema: termsErrorSchema },\n { status: 409, description: 'Optimistic lock or concurrent-create conflict', schema: termsErrorSchema },\n { status: 500, description: 'Unexpected failure', schema: termsErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { CrudHttpError, badRequest, conflict, isCrudHttpError, isUniqueViolation, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { AssortmentScope } from '@open-mercato/shared/lib/catalog-visibility'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { CustomerGroup, CustomerGroupTerms } from '../../../../data/entities'\nimport { isPriceKindInTenant } from '../../../../lib/priceKindScope'\nimport { announceCustomerGroupTermsUpdated } from '../../../../lib/groupEvents'\nimport {\n assortmentScopeSchema,\n customerGroupTermsCreateSchema,\n customerGroupTermsUpdateSchema,\n type CustomerGroupTermsCreateInput,\n type CustomerGroupTermsUpdateInput,\n} from '../../../../data/validators'\n\nconst logger = createLogger('customer_groups')\n\n// Terms are a strict 1:1 sub-resource of a group (`customer_group_terms_group_unique`\n// is a partial unique index on `group_id`, see `data/entities.ts`) with no independent\n// list/pagination/search use case \u2014 there is no \"list all terms rows\" screen, only\n// \"this group's terms\". The spec's own literal path (`GET/PUT\n// /api/customer-groups/:id/terms`) nests terms under a specific group, unlike\n// `../crud.ts` / `memberships/crud.ts`, which are genuine tenant-wide collections with\n// their own list/filter semantics. A `[id]` dynamic segment (id = the GROUP's id) doing\n// a plain `findOne`/upsert against `CustomerGroupTerms` is therefore the better fit here\n// than a `makeCrudRoute` keyed by `groupId` (Option A from the Step 2.4 task) \u2014 the\n// latter would need to invent list/create/delete semantics this resource never uses,\n// and `makeCrudRoute`'s auto-registered optimistic-lock reader is keyed by the route's\n// own resourceKind, not a caller-chosen `groupId` lookup, so it would not save any\n// wiring over the explicit `enforceCommandOptimisticLock` call used below anyway.\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customer_groups.terms.view'] },\n PUT: { requireAuth: true, requireFeatures: ['customer_groups.terms.manage'] },\n}\n\n// Matches the indexer entityType naming convention already used for the other two\n// entities in this module (`customer_groups:customer_group`,\n// `customer_groups:customer_group_membership` in `../crud.ts` /\n// `memberships/crud.ts`), so `OM_OPTIMISTIC_LOCK`'s allow-list mode can target this\n// resource by the same family of names.\nconst TERMS_RESOURCE_KIND = 'customer_groups:customer_group_terms'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\ntype RouteContext = { params: Promise<{ id: string }> | { id: string } }\n\ntype Scope = { tenantId: string; organizationId: string | null }\n\ntype Translate = (key: string, fallback?: string) => string\n\nasync function resolveGroupId(context: RouteContext, translate: Translate): Promise<string> {\n const raw = await context.params\n const parsed = paramsSchema.safeParse(raw)\n if (!parsed.success) {\n throw notFound(translate('customer_groups.errors.group_not_found', 'Customer group not found'))\n }\n return parsed.data.id\n}\n\nasync function resolveAuthScope(req: Request, translate: Translate): Promise<{ userId: string; scope: Scope }> {\n const auth = await getAuthFromRequest(req)\n if (!auth) throw new CrudHttpError(401, { error: translate('customer_groups.errors.unauthorized', 'Unauthorized') })\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n throw new CrudHttpError(400, { error: translate('customer_groups.errors.tenant_required', 'Tenant context is required') })\n }\n return { userId: auth.sub, scope: { tenantId, organizationId: auth.orgId ?? null } }\n}\n\nasync function loadGroupOrThrow(\n em: EntityManager,\n id: string,\n scope: Scope,\n translate: Translate,\n): Promise<CustomerGroup> {\n const group = await em.findOne(CustomerGroup, { id, tenantId: scope.tenantId, deletedAt: null })\n if (!group) throw notFound(translate('customer_groups.errors.group_not_found', 'Customer group not found'))\n return group\n}\n\n// Mirrors `toNumericString` in `catalog/commands/shared.ts` \u2014 this module has no\n// shared equivalent yet, so a small module-local copy follows the same established\n// pattern rather than introducing a new cross-module dependency for one line.\nfunction toNumericString(value: number | null | undefined): string | null {\n if (value === undefined || value === null) return null\n return value.toString()\n}\n\nfunction toNumberOrNull(value: string | null | undefined): number | null {\n if (value == null) return null\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction serializeTerms(terms: CustomerGroupTerms) {\n return {\n id: terms.id,\n groupId: terms.groupId,\n organizationId: terms.organizationId ?? null,\n tenantId: terms.tenantId,\n priceKindId: terms.priceKindId ?? null,\n paymentTermsDays: terms.paymentTermsDays ?? null,\n allowPurchaseOnAccount: terms.allowPurchaseOnAccount ?? null,\n defaultCreditLimit: toNumberOrNull(terms.defaultCreditLimit),\n creditCurrencyCode: terms.creditCurrencyCode ?? null,\n approvalRequiredAbove: toNumberOrNull(terms.approvalRequiredAbove),\n minOrderValue: toNumberOrNull(terms.minOrderValue),\n assortmentScope: terms.assortmentScope ?? null,\n metadata: terms.metadata ?? null,\n createdAt: terms.createdAt.toISOString(),\n updatedAt: terms.updatedAt.toISOString(),\n }\n}\n\nexport async function GET(req: Request, context: RouteContext) {\n const { translate } = await resolveTranslations()\n try {\n const groupId = await resolveGroupId(context, translate)\n const { scope } = await resolveAuthScope(req, translate)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n await loadGroupOrThrow(em, groupId, scope, translate)\n\n // No terms row yet is a normal, expected state (spec: \"No terms set \u2014 inheriting\n // from parent / tenant defaults\") \u2014 answered as 200 with `terms: null` rather than\n // 404, since the sub-resource's *absence* is itself meaningful data for the caller\n // to render, not an error.\n const terms = await em.findOne(CustomerGroupTerms, { groupId, tenantId: scope.tenantId, deletedAt: null })\n return NextResponse.json({ terms: terms ? serializeTerms(terms) : null })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n logger.error('customer_groups.terms.get failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'customer_groups', code: 'customer_groups.terms_get_failed' })\n return NextResponse.json(\n { error: translate('customer_groups.errors.load_failed', 'Failed to load commercial terms') },\n { status: 500 },\n )\n }\n}\n\nconst termsBodySchema = z.object({}).passthrough()\n\ntype CustomerGroupTermsEntityData = {\n organizationId: string | null\n tenantId: string\n groupId: string\n priceKindId: string | null\n paymentTermsDays: number | null\n allowPurchaseOnAccount: boolean | null\n defaultCreditLimit: string | null\n creditCurrencyCode: string | null\n approvalRequiredAbove: string | null\n minOrderValue: string | null\n assortmentScope: AssortmentScope | null\n metadata: Record<string, unknown> | null\n}\n\nfunction toEntityData(input: CustomerGroupTermsCreateInput): CustomerGroupTermsEntityData {\n return {\n organizationId: input.organizationId ?? null,\n tenantId: input.tenantId,\n groupId: input.groupId,\n priceKindId: input.priceKindId ?? null,\n paymentTermsDays: input.paymentTermsDays ?? null,\n allowPurchaseOnAccount: input.allowPurchaseOnAccount ?? null,\n defaultCreditLimit: toNumericString(input.defaultCreditLimit),\n creditCurrencyCode: input.creditCurrencyCode ?? null,\n approvalRequiredAbove: toNumericString(input.approvalRequiredAbove),\n minOrderValue: toNumericString(input.minOrderValue),\n assortmentScope: input.assortmentScope ?? null,\n metadata: input.metadata ?? null,\n }\n}\n\nfunction hasOwn(input: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(input, key)\n}\n\n// Scope fields (tenantId/organizationId/groupId) are deliberately excluded \u2014 same rule\n// as `applyCustomerGroupUpdate` in `../../crud.ts`: a terms row's group/tenant is fixed\n// at creation and MUST NOT change on update.\nfunction applyTermsUpdate(entity: CustomerGroupTerms, input: CustomerGroupTermsUpdateInput): void {\n if (hasOwn(input, 'priceKindId')) entity.priceKindId = input.priceKindId ?? null\n if (hasOwn(input, 'paymentTermsDays')) entity.paymentTermsDays = input.paymentTermsDays ?? null\n if (hasOwn(input, 'allowPurchaseOnAccount')) entity.allowPurchaseOnAccount = input.allowPurchaseOnAccount ?? null\n if (hasOwn(input, 'defaultCreditLimit')) entity.defaultCreditLimit = toNumericString(input.defaultCreditLimit)\n if (hasOwn(input, 'creditCurrencyCode')) entity.creditCurrencyCode = input.creditCurrencyCode ?? null\n if (hasOwn(input, 'approvalRequiredAbove')) entity.approvalRequiredAbove = toNumericString(input.approvalRequiredAbove)\n if (hasOwn(input, 'minOrderValue')) entity.minOrderValue = toNumericString(input.minOrderValue)\n if (hasOwn(input, 'assortmentScope')) entity.assortmentScope = input.assortmentScope ?? null\n if (hasOwn(input, 'metadata')) entity.metadata = input.metadata ?? null\n}\n\nexport async function PUT(req: Request, context: RouteContext) {\n const { translate } = await resolveTranslations()\n try {\n const groupId = await resolveGroupId(context, translate)\n const { userId, scope } = await resolveAuthScope(req, translate)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n await loadGroupOrThrow(em, groupId, scope, translate)\n\n const rawBody = (await readJsonSafe<Record<string, unknown>>(req, {})) ?? {}\n const rawInput = termsBodySchema.parse(rawBody)\n\n const existing = await em.findOne(CustomerGroupTerms, { groupId, tenantId: scope.tenantId, deletedAt: null })\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: { userId, tenantId: scope.tenantId, organizationId: scope.organizationId },\n input: {\n resourceKind: TERMS_RESOURCE_KIND,\n resourceId: existing?.id ?? groupId,\n operation: existing ? 'update' : 'create',\n mutationPayload: rawInput,\n },\n })\n if (!guarded.ok) return guarded.response\n\n const payload = guarded.modifiedPayload ?? rawInput\n\n const scopedPayload = { ...payload, groupId, tenantId: scope.tenantId, organizationId: scope.organizationId }\n\n // Runs on the validated payload only, so a malformed `priceKindId` is a 400 from\n // the schema instead of reaching the uuid column as a raw query.\n const assertPriceKindAllowed = async (priceKindId: string | null | undefined): Promise<void> => {\n if (!priceKindId || priceKindId === existing?.priceKindId) return\n if (await isPriceKindInTenant(em, priceKindId, scope.tenantId)) return\n throw badRequest(\n translate('customer_groups.errors.priceKindNotFound', 'The selected price kind does not exist.'),\n )\n }\n\n let terms: CustomerGroupTerms\n if (existing) {\n // The terms row has its own `updated_at`, independent of the parent group's \u2014\n // guard THIS row against a concurrent edit of the same terms, not the group.\n // No lock check on creation below: there is nothing to conflict with yet.\n enforceCommandOptimisticLock({\n resourceKind: TERMS_RESOURCE_KIND,\n resourceId: existing.id,\n current: existing.updatedAt,\n request: req,\n })\n const parsed = customerGroupTermsUpdateSchema.parse({ ...scopedPayload, id: existing.id })\n await assertPriceKindAllowed(parsed.priceKindId)\n applyTermsUpdate(existing, parsed)\n terms = existing\n } else {\n const parsed = customerGroupTermsCreateSchema.parse(scopedPayload)\n await assertPriceKindAllowed(parsed.priceKindId)\n terms = em.create(CustomerGroupTerms, toEntityData(parsed))\n em.persist(terms)\n }\n\n await em.flush()\n await guarded.runAfterSuccess()\n try {\n await announceCustomerGroupTermsUpdated(container, terms)\n } catch (announceErr) {\n logger.error('customer_groups.terms.put announce failed', { err: announceErr })\n getTelemetryRuntime()?.reportError(announceErr, {\n module: 'customer_groups',\n code: 'customer_groups.terms_announce_failed',\n })\n }\n\n return NextResponse.json({ terms: serializeTerms(terms) })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('customer_groups.errors.invalid_input', 'Invalid input') },\n { status: 400 },\n )\n }\n // `customer_group_terms_group_unique` (one live terms row per group): two\n // concurrent first saves for the same group both see \"no row yet\" and both insert.\n // The loser gets a clean 409 so the client reloads and retries as an update.\n if (isUniqueViolation(err)) {\n const conflictError = conflict(\n translate(\n 'customer_groups.errors.terms_conflict',\n 'These commercial terms were changed at the same time by someone else. Reload and try again.',\n ),\n )\n return NextResponse.json(conflictError.body, { status: conflictError.status })\n }\n logger.error('customer_groups.terms.put failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'customer_groups', code: 'customer_groups.terms_put_failed' })\n return NextResponse.json(\n { error: translate('customer_groups.errors.save_failed', 'Failed to save commercial terms') },\n { status: 500 },\n )\n }\n}\n\nconst termsItemSchema = z.object({\n id: z.string().uuid(),\n groupId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid(),\n priceKindId: z.string().uuid().nullable(),\n paymentTermsDays: z.number().int().nullable(),\n allowPurchaseOnAccount: z.boolean().nullable(),\n defaultCreditLimit: z.number().nullable(),\n creditCurrencyCode: z.string().nullable(),\n approvalRequiredAbove: z.number().nullable(),\n minOrderValue: z.number().nullable(),\n assortmentScope: assortmentScopeSchema,\n metadata: z.record(z.string(), z.unknown()).nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst termsGetResponseSchema = z.object({ terms: termsItemSchema.nullable() })\nconst termsPutResponseSchema = z.object({ terms: termsItemSchema })\nconst termsErrorSchema = z.object({ error: z.string() })\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'CustomerGroups',\n summary: 'Commercial terms for a customer group',\n methods: {\n GET: {\n summary: 'Get a customer group\u2019s commercial terms',\n description: 'Returns the group\u2019s own terms row, or `{ terms: null }` when the group has none and inherits from its parent/tenant defaults.',\n responses: [{ status: 200, description: 'Terms (or null)', schema: termsGetResponseSchema }],\n errors: [\n { status: 400, description: 'Tenant context is required', schema: termsErrorSchema },\n { status: 401, description: 'Unauthorized', schema: termsErrorSchema },\n { status: 404, description: 'Customer group not found', schema: termsErrorSchema },\n { status: 500, description: 'Unexpected failure', schema: termsErrorSchema },\n ],\n },\n PUT: {\n summary: 'Create or update a customer group\u2019s commercial terms',\n description: 'Upserts the group\u2019s terms row (creates it on first save). Supports optimistic locking via the extension header once a row exists.',\n requestBody: { contentType: 'application/json', schema: customerGroupTermsCreateSchema.partial() },\n responses: [{ status: 200, description: 'Terms created or updated', schema: termsPutResponseSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: termsErrorSchema },\n { status: 401, description: 'Unauthorized', schema: termsErrorSchema },\n { status: 404, description: 'Customer group not found', schema: termsErrorSchema },\n { status: 409, description: 'Optimistic lock or concurrent-create conflict', schema: termsErrorSchema },\n { status: 500, description: 'Unexpected failure', schema: termsErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -10,8 +10,18 @@ import { ErrorMessage, LoadingMessage } from "@open-mercato/ui/backend/detail";
10
10
  import { SectionHeader } from "@open-mercato/ui/backend/SectionHeader";
11
11
  import { EmptyState } from "@open-mercato/ui/primitives/empty-state";
12
12
  import { Button } from "@open-mercato/ui/primitives/button";
13
+ import { LookupMultiPicker } from "@open-mercato/ui/backend/inputs/LookupPickers";
14
+ import {
15
+ categoryLookupSource,
16
+ productLookupSource,
17
+ tagLookupSource
18
+ } from "@open-mercato/core/modules/catalog/components/lookupSources";
13
19
  import { useT } from "@open-mercato/shared/lib/i18n/context";
14
20
  import { CustomerGroupTermsPriceKindField } from "./CustomerGroupTermsPriceKindField.js";
21
+ import {
22
+ buildAssortmentScopePayload,
23
+ mapAssortmentScopeToFormValues
24
+ } from "./customerGroupAssortmentScope.js";
15
25
  const TERMS_NOT_YET_CREATED_LOCK_TOKEN = (/* @__PURE__ */ new Date(0)).toISOString();
16
26
  const PURCHASE_ON_ACCOUNT_VALUES = ["inherit", "allow", "deny"];
17
27
  function toPurchaseOnAccountValue(value) {
@@ -32,7 +42,38 @@ function mapTermsToFormValues(terms) {
32
42
  defaultCreditLimit: terms?.defaultCreditLimit ?? void 0,
33
43
  creditCurrencyCode: terms?.creditCurrencyCode ?? "",
34
44
  approvalRequiredAbove: terms?.approvalRequiredAbove ?? void 0,
35
- minOrderValue: terms?.minOrderValue ?? void 0
45
+ minOrderValue: terms?.minOrderValue ?? void 0,
46
+ ...mapAssortmentScopeToFormValues(terms?.assortmentScope)
47
+ };
48
+ }
49
+ const TERMS_FIELD_IDS = [
50
+ "priceKindId",
51
+ "paymentTermsDays",
52
+ "allowPurchaseOnAccount",
53
+ "defaultCreditLimit",
54
+ "creditCurrencyCode",
55
+ "approvalRequiredAbove",
56
+ "minOrderValue"
57
+ ];
58
+ function toIdList(value) {
59
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
60
+ }
61
+ function scopePickerField(id, label, placeholder, source, readOnly) {
62
+ return {
63
+ id,
64
+ type: "custom",
65
+ label,
66
+ disabled: readOnly,
67
+ component: ({ value, setValue, disabled }) => /* @__PURE__ */ jsx(
68
+ LookupMultiPicker,
69
+ {
70
+ source,
71
+ value: toIdList(value),
72
+ onChange: (next) => setValue(next),
73
+ placeholder,
74
+ disabled: disabled || readOnly
75
+ }
76
+ )
36
77
  };
37
78
  }
38
79
  function CustomerGroupTermsSection({
@@ -56,7 +97,12 @@ function CustomerGroupTermsSection({
56
97
  defaultCreditLimit: z.coerce.number().min(0, negativeMessage).optional(),
57
98
  creditCurrencyCode: z.string().trim().max(4, t("customer_groups.groups.form.terms.errors.currencyLength", "Use up to 4 characters.")),
58
99
  approvalRequiredAbove: z.coerce.number().min(0, negativeMessage).optional(),
59
- minOrderValue: z.coerce.number().min(0, negativeMessage).optional()
100
+ minOrderValue: z.coerce.number().min(0, negativeMessage).optional(),
101
+ categoryIds: z.array(z.string()).default([]),
102
+ tagIds: z.array(z.string()).default([]),
103
+ excludeProductIds: z.array(z.string()).default([]),
104
+ excludeCategoryIds: z.array(z.string()).default([]),
105
+ excludeTagIds: z.array(z.string()).default([])
60
106
  }),
61
107
  [negativeMessage, t]
62
108
  );
@@ -127,6 +173,65 @@ function CustomerGroupTermsSection({
127
173
  label: t("customer_groups.groups.form.terms.field.minOrderValue", "Minimum order value"),
128
174
  type: "number",
129
175
  layout: "half"
176
+ },
177
+ scopePickerField(
178
+ "categoryIds",
179
+ t("customer_groups.groups.form.terms.assortment.field.categoryIds", "Categories"),
180
+ t("customer_groups.groups.form.terms.assortment.placeholder.category", "Search categories"),
181
+ categoryLookupSource,
182
+ !canManage
183
+ ),
184
+ scopePickerField(
185
+ "tagIds",
186
+ t("customer_groups.groups.form.terms.assortment.field.tagIds", "Tags"),
187
+ t("customer_groups.groups.form.terms.assortment.placeholder.tag", "Search tags"),
188
+ tagLookupSource,
189
+ !canManage
190
+ ),
191
+ scopePickerField(
192
+ "excludeProductIds",
193
+ t("customer_groups.groups.form.terms.assortment.field.excludeProductIds", "Products"),
194
+ t("customer_groups.groups.form.terms.assortment.placeholder.product", "Search products"),
195
+ productLookupSource,
196
+ !canManage
197
+ ),
198
+ scopePickerField(
199
+ "excludeCategoryIds",
200
+ t("customer_groups.groups.form.terms.assortment.field.excludeCategoryIds", "Categories"),
201
+ t("customer_groups.groups.form.terms.assortment.placeholder.category", "Search categories"),
202
+ categoryLookupSource,
203
+ !canManage
204
+ ),
205
+ scopePickerField(
206
+ "excludeTagIds",
207
+ t("customer_groups.groups.form.terms.assortment.field.excludeTagIds", "Tags"),
208
+ t("customer_groups.groups.form.terms.assortment.placeholder.tag", "Search tags"),
209
+ tagLookupSource,
210
+ !canManage
211
+ )
212
+ ],
213
+ [canManage, t]
214
+ );
215
+ const groups = React.useMemo(
216
+ () => [
217
+ { id: "terms", fields: TERMS_FIELD_IDS },
218
+ {
219
+ id: "assortmentInclude",
220
+ title: t("customer_groups.groups.form.terms.assortment.includeTitle", "Assortment scope: show only products in"),
221
+ description: t(
222
+ "customer_groups.groups.form.terms.assortment.includeHint",
223
+ "A product is visible to this group when it is in any of the chosen categories and carries any of the chosen tags. An empty picker does not filter, so leave both empty for no restriction; clearing a picker never hides everything. The storefront assortment a buyer sees is the channel's scope narrowed by this group's scope, and a buyer in several groups sees the union of their scopes."
224
+ ),
225
+ fields: ["categoryIds", "tagIds"]
226
+ },
227
+ {
228
+ id: "assortmentExclude",
229
+ title: t("customer_groups.groups.form.terms.assortment.excludeTitle", "Always hide from this group"),
230
+ description: t(
231
+ "customer_groups.groups.form.terms.assortment.excludeHint",
232
+ "Hidden products, categories and tags win over the selection above."
233
+ ),
234
+ fields: ["excludeProductIds", "excludeCategoryIds", "excludeTagIds"]
130
235
  }
131
236
  ],
132
237
  [t]
@@ -141,7 +246,8 @@ function CustomerGroupTermsSection({
141
246
  defaultCreditLimit: typeof values.defaultCreditLimit === "number" ? values.defaultCreditLimit : null,
142
247
  creditCurrencyCode: values.creditCurrencyCode && values.creditCurrencyCode.trim().length ? values.creditCurrencyCode.trim().toUpperCase() : null,
143
248
  approvalRequiredAbove: typeof values.approvalRequiredAbove === "number" ? values.approvalRequiredAbove : null,
144
- minOrderValue: typeof values.minOrderValue === "number" ? values.minOrderValue : null
249
+ minOrderValue: typeof values.minOrderValue === "number" ? values.minOrderValue : null,
250
+ assortmentScope: buildAssortmentScopePayload(values)
145
251
  };
146
252
  const call = await updateCrud(`customer_groups/customer-groups/${groupId}/terms`, payload, {
147
253
  errorMessage: t("customer_groups.groups.form.terms.errors.save", "Failed to save commercial terms.")
@@ -174,8 +280,10 @@ function CustomerGroupTermsSection({
174
280
  CrudForm,
175
281
  {
176
282
  embedded: true,
283
+ disableInitialFocus: true,
177
284
  schema,
178
285
  fields,
286
+ groups,
179
287
  initialValues,
180
288
  optimisticLockUpdatedAt: terms?.updatedAt ?? TERMS_NOT_YET_CREATED_LOCK_TOKEN,
181
289
  readOnly: !canManage,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_groups/components/CustomerGroupTermsSection.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { FileText } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { SectionHeader } from '@open-mercato/ui/backend/SectionHeader'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CustomerGroupTermsPriceKindField } from './CustomerGroupTermsPriceKindField'\n\nexport type CustomerGroupTermsDTO = {\n id: string\n groupId: string\n organizationId: string | null\n tenantId: string\n priceKindId: string | null\n paymentTermsDays: number | null\n allowPurchaseOnAccount: boolean | null\n defaultCreditLimit: number | null\n creditCurrencyCode: string | null\n approvalRequiredAbove: number | null\n minOrderValue: number | null\n metadata: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n}\n\n// Lock token sent with the first save of a group that has no terms row yet. No real\n// row can carry this `updated_at`, so if another editor created the row meanwhile the\n// route's lock check against that row answers 409 instead of silently overwriting it.\nexport const TERMS_NOT_YET_CREATED_LOCK_TOKEN = new Date(0).toISOString()\n\n// `inherit` maps to `null` on the wire: the field is left unset on this group and\n// resolves from the parent chain / tenant default, like the other terms fields.\nconst PURCHASE_ON_ACCOUNT_VALUES = ['inherit', 'allow', 'deny'] as const\ntype PurchaseOnAccountValue = (typeof PURCHASE_ON_ACCOUNT_VALUES)[number]\n\nfunction toPurchaseOnAccountValue(value: boolean | null | undefined): PurchaseOnAccountValue {\n if (value === true) return 'allow'\n if (value === false) return 'deny'\n return 'inherit'\n}\n\nfunction fromPurchaseOnAccountValue(value: string | undefined): boolean | null {\n if (value === 'allow') return true\n if (value === 'deny') return false\n return null\n}\n\ntype CustomerGroupTermsFormValues = {\n priceKindId: string\n paymentTermsDays?: number\n allowPurchaseOnAccount: PurchaseOnAccountValue\n defaultCreditLimit?: number\n creditCurrencyCode: string\n approvalRequiredAbove?: number\n minOrderValue?: number\n}\n\nfunction mapTermsToFormValues(terms: CustomerGroupTermsDTO | null): CustomerGroupTermsFormValues {\n return {\n priceKindId: terms?.priceKindId ?? '',\n paymentTermsDays: terms?.paymentTermsDays ?? undefined,\n allowPurchaseOnAccount: toPurchaseOnAccountValue(terms?.allowPurchaseOnAccount),\n defaultCreditLimit: terms?.defaultCreditLimit ?? undefined,\n creditCurrencyCode: terms?.creditCurrencyCode ?? '',\n approvalRequiredAbove: terms?.approvalRequiredAbove ?? undefined,\n minOrderValue: terms?.minOrderValue ?? undefined,\n }\n}\n\nexport function CustomerGroupTermsSection({\n groupId,\n terms,\n loading,\n loadError,\n canManage,\n onSaved,\n}: {\n groupId: string\n // `undefined` = still loading, `null` = confirmed no terms row for this group\n // (spec: \"No terms set \u2014 inheriting from parent / tenant defaults\"), object = loaded row.\n terms: CustomerGroupTermsDTO | null | undefined\n loading: boolean\n // Set when the terms GET failed for a reason other than \"no row\" \u2014 the form is\n // then withheld so a save can never overwrite terms it could not read.\n loadError?: string | null\n // Without `customer_groups.terms.manage` the terms stay viewable but the form is\n // read-only and the empty state offers no \"set terms\" action \u2014 a save would 403.\n canManage: boolean\n onSaved: (next: CustomerGroupTermsDTO) => void\n}) {\n const t = useT()\n // Once the user starts filling out terms for a group that has none yet, keep the\n // form mounted even while the in-flight save briefly leaves `terms` as `null` \u2014\n // otherwise the empty state would flash back before `onSaved` lands.\n const [editing, setEditing] = React.useState(false)\n const hasTerms = terms != null\n const showForm = hasTerms || editing\n\n const negativeMessage = t('customer_groups.groups.form.terms.errors.negative', 'Must be zero or greater.')\n\n const schema = React.useMemo(\n () =>\n z.object({\n priceKindId: z.string().trim(),\n paymentTermsDays: z.coerce.number().int().min(0, negativeMessage).optional(),\n allowPurchaseOnAccount: z.enum(PURCHASE_ON_ACCOUNT_VALUES),\n defaultCreditLimit: z.coerce.number().min(0, negativeMessage).optional(),\n creditCurrencyCode: z\n .string()\n .trim()\n .max(4, t('customer_groups.groups.form.terms.errors.currencyLength', 'Use up to 4 characters.')),\n approvalRequiredAbove: z.coerce.number().min(0, negativeMessage).optional(),\n minOrderValue: z.coerce.number().min(0, negativeMessage).optional(),\n }),\n [negativeMessage, t],\n )\n\n const fields = React.useMemo<CrudField[]>(\n () => [\n {\n id: 'priceKindId',\n label: t('customer_groups.groups.form.terms.field.priceKind', 'Price kind'),\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <CustomerGroupTermsPriceKindField\n value={typeof value === 'string' ? value : ''}\n onChange={setValue}\n disabled={disabled}\n />\n ),\n },\n {\n id: 'paymentTermsDays',\n label: t('customer_groups.groups.form.terms.field.paymentTermsDays', 'Payment terms (days)'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'allowPurchaseOnAccount',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccount', 'Allow purchase on account'),\n type: 'select',\n layout: 'half',\n required: true,\n options: [\n {\n value: 'inherit',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.inherit', 'Inherit'),\n },\n {\n value: 'allow',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.allow', 'Allow'),\n },\n {\n value: 'deny',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.deny', 'Do not allow'),\n },\n ],\n },\n {\n id: 'defaultCreditLimit',\n label: t('customer_groups.groups.form.terms.field.defaultCreditLimit', 'Default credit limit'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'creditCurrencyCode',\n label: t('customer_groups.groups.form.terms.field.creditCurrencyCode', 'Credit currency'),\n type: 'text',\n layout: 'half',\n placeholder: t('customer_groups.groups.form.terms.field.creditCurrencyCodePlaceholder', 'e.g., USD'),\n maxLength: 4,\n },\n {\n id: 'approvalRequiredAbove',\n label: t('customer_groups.groups.form.terms.field.approvalRequiredAbove', 'Approval required above'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'minOrderValue',\n label: t('customer_groups.groups.form.terms.field.minOrderValue', 'Minimum order value'),\n type: 'number',\n layout: 'half',\n },\n ],\n [t],\n )\n\n const initialValues = React.useMemo(() => mapTermsToFormValues(terms ?? null), [terms])\n\n const handleSubmit = React.useCallback(\n async (values: CustomerGroupTermsFormValues) => {\n const payload: Record<string, unknown> = {\n priceKindId: values.priceKindId && values.priceKindId.trim().length ? values.priceKindId : null,\n paymentTermsDays: typeof values.paymentTermsDays === 'number' ? values.paymentTermsDays : null,\n allowPurchaseOnAccount: fromPurchaseOnAccountValue(values.allowPurchaseOnAccount),\n defaultCreditLimit: typeof values.defaultCreditLimit === 'number' ? values.defaultCreditLimit : null,\n creditCurrencyCode:\n values.creditCurrencyCode && values.creditCurrencyCode.trim().length\n ? values.creditCurrencyCode.trim().toUpperCase()\n : null,\n approvalRequiredAbove:\n typeof values.approvalRequiredAbove === 'number' ? values.approvalRequiredAbove : null,\n minOrderValue: typeof values.minOrderValue === 'number' ? values.minOrderValue : null,\n }\n // `updateCrud` PUTs to `/api/customer_groups/customer-groups/{groupId}/terms`, matching the\n // upsert route (Step 2.4). `optimisticLockUpdatedAt` below is\n // `TERMS_NOT_YET_CREATED_LOCK_TOKEN` on the first save (no `terms` row yet).\n const call = await updateCrud<{ terms: CustomerGroupTermsDTO }>(`customer_groups/customer-groups/${groupId}/terms`, payload, {\n errorMessage: t('customer_groups.groups.form.terms.errors.save', 'Failed to save commercial terms.'),\n })\n if (call.result?.terms) onSaved(call.result.terms)\n flash(t('customer_groups.groups.form.terms.flash.saved', 'Commercial terms saved.'), 'success')\n },\n [groupId, onSaved, t],\n )\n\n return (\n <div className=\"rounded-md border border-border bg-card p-4 space-y-4\">\n <SectionHeader title={t('customer_groups.groups.form.terms.title', 'Commercial terms')} />\n {loading ? (\n <LoadingMessage label={t('customer_groups.groups.form.terms.loading', 'Loading commercial terms\u2026')} />\n ) : loadError ? (\n <ErrorMessage label={loadError} />\n ) : !showForm ? (\n <EmptyState\n size=\"sm\"\n variant=\"subtle\"\n icon={<FileText className=\"h-5 w-5\" aria-hidden />}\n title={t(\n 'customer_groups.groups.form.terms.empty.title',\n 'No terms set \u2014 inheriting from parent / tenant defaults',\n )}\n description={t(\n 'customer_groups.groups.form.terms.empty.description',\n 'This group currently uses commercial terms inherited from its parent group or the tenant defaults. Set explicit terms to override them for this group.',\n )}\n actions={canManage ? (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setEditing(true)}>\n {t('customer_groups.groups.form.terms.empty.action', 'Set terms for this group')}\n </Button>\n ) : undefined}\n className=\"border border-dashed border-border\"\n />\n ) : (\n <CrudForm<CustomerGroupTermsFormValues>\n key={terms?.updatedAt ?? 'new'}\n embedded\n schema={schema}\n fields={fields}\n initialValues={initialValues}\n optimisticLockUpdatedAt={terms?.updatedAt ?? TERMS_NOT_YET_CREATED_LOCK_TOKEN}\n readOnly={!canManage}\n readOnlyOverlay={(\n <div className=\"rounded-xl border border-border/70 bg-background/95 px-4 py-3 text-sm text-muted-foreground shadow-sm\">\n {t(\n 'customer_groups.groups.form.terms.readOnly',\n 'You do not have permission to change commercial terms.',\n )}\n </div>\n )}\n submitLabel={t('customer_groups.groups.form.terms.action.save', 'Save terms')}\n onSubmit={handleSubmit}\n />\n )}\n </div>\n )\n}\n\nexport default CustomerGroupTermsSection\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { FileText } from 'lucide-react'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { SectionHeader } from '@open-mercato/ui/backend/SectionHeader'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LookupMultiPicker } from '@open-mercato/ui/backend/inputs/LookupPickers'\nimport type { LookupSource } from '@open-mercato/ui/backend/inputs/lookupSources'\nimport {\n categoryLookupSource,\n productLookupSource,\n tagLookupSource,\n} from '@open-mercato/core/modules/catalog/components/lookupSources'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CustomerGroupTermsPriceKindField } from './CustomerGroupTermsPriceKindField'\nimport {\n buildAssortmentScopePayload,\n mapAssortmentScopeToFormValues,\n type AssortmentScopeField,\n type AssortmentScopeFormValues,\n type GroupAssortmentScope,\n} from './customerGroupAssortmentScope'\n\nexport type CustomerGroupTermsDTO = {\n id: string\n groupId: string\n organizationId: string | null\n tenantId: string\n priceKindId: string | null\n paymentTermsDays: number | null\n allowPurchaseOnAccount: boolean | null\n defaultCreditLimit: number | null\n creditCurrencyCode: string | null\n approvalRequiredAbove: number | null\n minOrderValue: number | null\n assortmentScope?: GroupAssortmentScope | null\n metadata: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n}\n\n// Lock token sent with the first save of a group that has no terms row yet. No real\n// row can carry this `updated_at`, so if another editor created the row meanwhile the\n// route's lock check against that row answers 409 instead of silently overwriting it.\nexport const TERMS_NOT_YET_CREATED_LOCK_TOKEN = new Date(0).toISOString()\n\n// `inherit` maps to `null` on the wire: the field is left unset on this group and\n// resolves from the parent chain / tenant default, like the other terms fields.\nconst PURCHASE_ON_ACCOUNT_VALUES = ['inherit', 'allow', 'deny'] as const\ntype PurchaseOnAccountValue = (typeof PURCHASE_ON_ACCOUNT_VALUES)[number]\n\nfunction toPurchaseOnAccountValue(value: boolean | null | undefined): PurchaseOnAccountValue {\n if (value === true) return 'allow'\n if (value === false) return 'deny'\n return 'inherit'\n}\n\nfunction fromPurchaseOnAccountValue(value: string | undefined): boolean | null {\n if (value === 'allow') return true\n if (value === 'deny') return false\n return null\n}\n\ntype CustomerGroupTermsFormValues = AssortmentScopeFormValues & {\n priceKindId: string\n paymentTermsDays?: number\n allowPurchaseOnAccount: PurchaseOnAccountValue\n defaultCreditLimit?: number\n creditCurrencyCode: string\n approvalRequiredAbove?: number\n minOrderValue?: number\n}\n\nfunction mapTermsToFormValues(terms: CustomerGroupTermsDTO | null): CustomerGroupTermsFormValues {\n return {\n priceKindId: terms?.priceKindId ?? '',\n paymentTermsDays: terms?.paymentTermsDays ?? undefined,\n allowPurchaseOnAccount: toPurchaseOnAccountValue(terms?.allowPurchaseOnAccount),\n defaultCreditLimit: terms?.defaultCreditLimit ?? undefined,\n creditCurrencyCode: terms?.creditCurrencyCode ?? '',\n approvalRequiredAbove: terms?.approvalRequiredAbove ?? undefined,\n minOrderValue: terms?.minOrderValue ?? undefined,\n ...mapAssortmentScopeToFormValues(terms?.assortmentScope),\n }\n}\n\nconst TERMS_FIELD_IDS = [\n 'priceKindId',\n 'paymentTermsDays',\n 'allowPurchaseOnAccount',\n 'defaultCreditLimit',\n 'creditCurrencyCode',\n 'approvalRequiredAbove',\n 'minOrderValue',\n]\n\nfunction toIdList(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n}\n\nfunction scopePickerField(\n id: AssortmentScopeField,\n label: string,\n placeholder: string,\n source: LookupSource,\n readOnly: boolean,\n): CrudField {\n return {\n id,\n type: 'custom',\n label,\n disabled: readOnly,\n component: ({ value, setValue, disabled }) => (\n <LookupMultiPicker\n source={source}\n value={toIdList(value)}\n onChange={(next) => setValue(next)}\n placeholder={placeholder}\n disabled={disabled || readOnly}\n />\n ),\n }\n}\n\nexport function CustomerGroupTermsSection({\n groupId,\n terms,\n loading,\n loadError,\n canManage,\n onSaved,\n}: {\n groupId: string\n // `undefined` = still loading, `null` = confirmed no terms row for this group\n // (spec: \"No terms set \u2014 inheriting from parent / tenant defaults\"), object = loaded row.\n terms: CustomerGroupTermsDTO | null | undefined\n loading: boolean\n // Set when the terms GET failed for a reason other than \"no row\" \u2014 the form is\n // then withheld so a save can never overwrite terms it could not read.\n loadError?: string | null\n // Without `customer_groups.terms.manage` the terms stay viewable but the form is\n // read-only and the empty state offers no \"set terms\" action \u2014 a save would 403.\n canManage: boolean\n onSaved: (next: CustomerGroupTermsDTO) => void\n}) {\n const t = useT()\n // Once the user starts filling out terms for a group that has none yet, keep the\n // form mounted even while the in-flight save briefly leaves `terms` as `null` \u2014\n // otherwise the empty state would flash back before `onSaved` lands.\n const [editing, setEditing] = React.useState(false)\n const hasTerms = terms != null\n const showForm = hasTerms || editing\n\n const negativeMessage = t('customer_groups.groups.form.terms.errors.negative', 'Must be zero or greater.')\n\n const schema = React.useMemo(\n () =>\n z.object({\n priceKindId: z.string().trim(),\n paymentTermsDays: z.coerce.number().int().min(0, negativeMessage).optional(),\n allowPurchaseOnAccount: z.enum(PURCHASE_ON_ACCOUNT_VALUES),\n defaultCreditLimit: z.coerce.number().min(0, negativeMessage).optional(),\n creditCurrencyCode: z\n .string()\n .trim()\n .max(4, t('customer_groups.groups.form.terms.errors.currencyLength', 'Use up to 4 characters.')),\n approvalRequiredAbove: z.coerce.number().min(0, negativeMessage).optional(),\n minOrderValue: z.coerce.number().min(0, negativeMessage).optional(),\n categoryIds: z.array(z.string()).default([]),\n tagIds: z.array(z.string()).default([]),\n excludeProductIds: z.array(z.string()).default([]),\n excludeCategoryIds: z.array(z.string()).default([]),\n excludeTagIds: z.array(z.string()).default([]),\n }),\n [negativeMessage, t],\n )\n\n const fields = React.useMemo<CrudField[]>(\n () => [\n {\n id: 'priceKindId',\n label: t('customer_groups.groups.form.terms.field.priceKind', 'Price kind'),\n type: 'custom',\n component: ({ value, setValue, disabled }) => (\n <CustomerGroupTermsPriceKindField\n value={typeof value === 'string' ? value : ''}\n onChange={setValue}\n disabled={disabled}\n />\n ),\n },\n {\n id: 'paymentTermsDays',\n label: t('customer_groups.groups.form.terms.field.paymentTermsDays', 'Payment terms (days)'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'allowPurchaseOnAccount',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccount', 'Allow purchase on account'),\n type: 'select',\n layout: 'half',\n required: true,\n options: [\n {\n value: 'inherit',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.inherit', 'Inherit'),\n },\n {\n value: 'allow',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.allow', 'Allow'),\n },\n {\n value: 'deny',\n label: t('customer_groups.groups.form.terms.field.allowPurchaseOnAccountOptions.deny', 'Do not allow'),\n },\n ],\n },\n {\n id: 'defaultCreditLimit',\n label: t('customer_groups.groups.form.terms.field.defaultCreditLimit', 'Default credit limit'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'creditCurrencyCode',\n label: t('customer_groups.groups.form.terms.field.creditCurrencyCode', 'Credit currency'),\n type: 'text',\n layout: 'half',\n placeholder: t('customer_groups.groups.form.terms.field.creditCurrencyCodePlaceholder', 'e.g., USD'),\n maxLength: 4,\n },\n {\n id: 'approvalRequiredAbove',\n label: t('customer_groups.groups.form.terms.field.approvalRequiredAbove', 'Approval required above'),\n type: 'number',\n layout: 'half',\n },\n {\n id: 'minOrderValue',\n label: t('customer_groups.groups.form.terms.field.minOrderValue', 'Minimum order value'),\n type: 'number',\n layout: 'half',\n },\n scopePickerField(\n 'categoryIds',\n t('customer_groups.groups.form.terms.assortment.field.categoryIds', 'Categories'),\n t('customer_groups.groups.form.terms.assortment.placeholder.category', 'Search categories'),\n categoryLookupSource,\n !canManage,\n ),\n scopePickerField(\n 'tagIds',\n t('customer_groups.groups.form.terms.assortment.field.tagIds', 'Tags'),\n t('customer_groups.groups.form.terms.assortment.placeholder.tag', 'Search tags'),\n tagLookupSource,\n !canManage,\n ),\n scopePickerField(\n 'excludeProductIds',\n t('customer_groups.groups.form.terms.assortment.field.excludeProductIds', 'Products'),\n t('customer_groups.groups.form.terms.assortment.placeholder.product', 'Search products'),\n productLookupSource,\n !canManage,\n ),\n scopePickerField(\n 'excludeCategoryIds',\n t('customer_groups.groups.form.terms.assortment.field.excludeCategoryIds', 'Categories'),\n t('customer_groups.groups.form.terms.assortment.placeholder.category', 'Search categories'),\n categoryLookupSource,\n !canManage,\n ),\n scopePickerField(\n 'excludeTagIds',\n t('customer_groups.groups.form.terms.assortment.field.excludeTagIds', 'Tags'),\n t('customer_groups.groups.form.terms.assortment.placeholder.tag', 'Search tags'),\n tagLookupSource,\n !canManage,\n ),\n ],\n [canManage, t],\n )\n\n const groups = React.useMemo<CrudFormGroup[]>(\n () => [\n { id: 'terms', fields: TERMS_FIELD_IDS },\n {\n id: 'assortmentInclude',\n title: t('customer_groups.groups.form.terms.assortment.includeTitle', 'Assortment scope: show only products in'),\n description: t(\n 'customer_groups.groups.form.terms.assortment.includeHint',\n \"A product is visible to this group when it is in any of the chosen categories and carries any of the chosen tags. An empty picker does not filter, so leave both empty for no restriction; clearing a picker never hides everything. The storefront assortment a buyer sees is the channel's scope narrowed by this group's scope, and a buyer in several groups sees the union of their scopes.\",\n ),\n fields: ['categoryIds', 'tagIds'],\n },\n {\n id: 'assortmentExclude',\n title: t('customer_groups.groups.form.terms.assortment.excludeTitle', 'Always hide from this group'),\n description: t(\n 'customer_groups.groups.form.terms.assortment.excludeHint',\n 'Hidden products, categories and tags win over the selection above.',\n ),\n fields: ['excludeProductIds', 'excludeCategoryIds', 'excludeTagIds'],\n },\n ],\n [t],\n )\n\n const initialValues = React.useMemo(() => mapTermsToFormValues(terms ?? null), [terms])\n\n const handleSubmit = React.useCallback(\n async (values: CustomerGroupTermsFormValues) => {\n const payload: Record<string, unknown> = {\n priceKindId: values.priceKindId && values.priceKindId.trim().length ? values.priceKindId : null,\n paymentTermsDays: typeof values.paymentTermsDays === 'number' ? values.paymentTermsDays : null,\n allowPurchaseOnAccount: fromPurchaseOnAccountValue(values.allowPurchaseOnAccount),\n defaultCreditLimit: typeof values.defaultCreditLimit === 'number' ? values.defaultCreditLimit : null,\n creditCurrencyCode:\n values.creditCurrencyCode && values.creditCurrencyCode.trim().length\n ? values.creditCurrencyCode.trim().toUpperCase()\n : null,\n approvalRequiredAbove:\n typeof values.approvalRequiredAbove === 'number' ? values.approvalRequiredAbove : null,\n minOrderValue: typeof values.minOrderValue === 'number' ? values.minOrderValue : null,\n assortmentScope: buildAssortmentScopePayload(values),\n }\n // `updateCrud` PUTs to `/api/customer_groups/customer-groups/{groupId}/terms`, matching the\n // upsert route (Step 2.4). `optimisticLockUpdatedAt` below is\n // `TERMS_NOT_YET_CREATED_LOCK_TOKEN` on the first save (no `terms` row yet).\n const call = await updateCrud<{ terms: CustomerGroupTermsDTO }>(`customer_groups/customer-groups/${groupId}/terms`, payload, {\n errorMessage: t('customer_groups.groups.form.terms.errors.save', 'Failed to save commercial terms.'),\n })\n if (call.result?.terms) onSaved(call.result.terms)\n flash(t('customer_groups.groups.form.terms.flash.saved', 'Commercial terms saved.'), 'success')\n },\n [groupId, onSaved, t],\n )\n\n return (\n <div className=\"rounded-md border border-border bg-card p-4 space-y-4\">\n <SectionHeader title={t('customer_groups.groups.form.terms.title', 'Commercial terms')} />\n {loading ? (\n <LoadingMessage label={t('customer_groups.groups.form.terms.loading', 'Loading commercial terms\u2026')} />\n ) : loadError ? (\n <ErrorMessage label={loadError} />\n ) : !showForm ? (\n <EmptyState\n size=\"sm\"\n variant=\"subtle\"\n icon={<FileText className=\"h-5 w-5\" aria-hidden />}\n title={t(\n 'customer_groups.groups.form.terms.empty.title',\n 'No terms set \u2014 inheriting from parent / tenant defaults',\n )}\n description={t(\n 'customer_groups.groups.form.terms.empty.description',\n 'This group currently uses commercial terms inherited from its parent group or the tenant defaults. Set explicit terms to override them for this group.',\n )}\n actions={canManage ? (\n <Button type=\"button\" size=\"sm\" variant=\"outline\" onClick={() => setEditing(true)}>\n {t('customer_groups.groups.form.terms.empty.action', 'Set terms for this group')}\n </Button>\n ) : undefined}\n className=\"border border-dashed border-border\"\n />\n ) : (\n <CrudForm<CustomerGroupTermsFormValues>\n key={terms?.updatedAt ?? 'new'}\n embedded\n disableInitialFocus\n schema={schema}\n fields={fields}\n groups={groups}\n initialValues={initialValues}\n optimisticLockUpdatedAt={terms?.updatedAt ?? TERMS_NOT_YET_CREATED_LOCK_TOKEN}\n readOnly={!canManage}\n readOnlyOverlay={(\n <div className=\"rounded-xl border border-border/70 bg-background/95 px-4 py-3 text-sm text-muted-foreground shadow-sm\">\n {t(\n 'customer_groups.groups.form.terms.readOnly',\n 'You do not have permission to change commercial terms.',\n )}\n </div>\n )}\n submitLabel={t('customer_groups.groups.form.terms.action.save', 'Save terms')}\n onSubmit={handleSubmit}\n />\n )}\n </div>\n )\n}\n\nexport default CustomerGroupTermsSection\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }