@endora-commerce/mod-catalog 0.100.0

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 (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/admin/components/PackagingUnitsEditor.d.ts +12 -0
  4. package/dist/admin/components/PackagingUnitsEditor.d.ts.map +1 -0
  5. package/dist/admin/components/PackagingUnitsEditor.js +99 -0
  6. package/dist/admin/components/PackagingUnitsEditor.js.map +1 -0
  7. package/dist/admin/components/ProductAttributesTab.d.ts +13 -0
  8. package/dist/admin/components/ProductAttributesTab.d.ts.map +1 -0
  9. package/dist/admin/components/ProductAttributesTab.js +140 -0
  10. package/dist/admin/components/ProductAttributesTab.js.map +1 -0
  11. package/dist/admin/components/ProductInventoryTab.d.ts +14 -0
  12. package/dist/admin/components/ProductInventoryTab.d.ts.map +1 -0
  13. package/dist/admin/components/ProductInventoryTab.js +284 -0
  14. package/dist/admin/components/ProductInventoryTab.js.map +1 -0
  15. package/dist/admin/components/ProductScopeEditor.d.ts +94 -0
  16. package/dist/admin/components/ProductScopeEditor.d.ts.map +1 -0
  17. package/dist/admin/components/ProductScopeEditor.js +395 -0
  18. package/dist/admin/components/ProductScopeEditor.js.map +1 -0
  19. package/dist/admin/components/ProductsBulkEditDialog.d.ts +11 -0
  20. package/dist/admin/components/ProductsBulkEditDialog.d.ts.map +1 -0
  21. package/dist/admin/components/ProductsBulkEditDialog.js +304 -0
  22. package/dist/admin/components/ProductsBulkEditDialog.js.map +1 -0
  23. package/dist/admin/index.d.ts +45 -0
  24. package/dist/admin/index.d.ts.map +1 -0
  25. package/dist/admin/index.js +184 -0
  26. package/dist/admin/index.js.map +1 -0
  27. package/dist/admin/lib/resolve-product-selection.d.ts +21 -0
  28. package/dist/admin/lib/resolve-product-selection.d.ts.map +1 -0
  29. package/dist/admin/lib/resolve-product-selection.js +25 -0
  30. package/dist/admin/lib/resolve-product-selection.js.map +1 -0
  31. package/dist/admin/pages/AttachmentTypesPage.d.ts +18 -0
  32. package/dist/admin/pages/AttachmentTypesPage.d.ts.map +1 -0
  33. package/dist/admin/pages/AttachmentTypesPage.js +92 -0
  34. package/dist/admin/pages/AttachmentTypesPage.js.map +1 -0
  35. package/dist/admin/pages/AttributeSetsPage.d.ts +21 -0
  36. package/dist/admin/pages/AttributeSetsPage.d.ts.map +1 -0
  37. package/dist/admin/pages/AttributeSetsPage.js +158 -0
  38. package/dist/admin/pages/AttributeSetsPage.js.map +1 -0
  39. package/dist/admin/pages/AttributesManager.d.ts +9 -0
  40. package/dist/admin/pages/AttributesManager.d.ts.map +1 -0
  41. package/dist/admin/pages/AttributesManager.js +305 -0
  42. package/dist/admin/pages/AttributesManager.js.map +1 -0
  43. package/dist/admin/pages/BulkOperationDetailPage.d.ts +9 -0
  44. package/dist/admin/pages/BulkOperationDetailPage.d.ts.map +1 -0
  45. package/dist/admin/pages/BulkOperationDetailPage.js +187 -0
  46. package/dist/admin/pages/BulkOperationDetailPage.js.map +1 -0
  47. package/dist/admin/pages/BulkOperationsPage.d.ts +9 -0
  48. package/dist/admin/pages/BulkOperationsPage.d.ts.map +1 -0
  49. package/dist/admin/pages/BulkOperationsPage.js +98 -0
  50. package/dist/admin/pages/BulkOperationsPage.js.map +1 -0
  51. package/dist/admin/pages/CategoriesTree.d.ts +9 -0
  52. package/dist/admin/pages/CategoriesTree.d.ts.map +1 -0
  53. package/dist/admin/pages/CategoriesTree.js +176 -0
  54. package/dist/admin/pages/CategoriesTree.js.map +1 -0
  55. package/dist/admin/pages/ProductEditor.d.ts +13 -0
  56. package/dist/admin/pages/ProductEditor.d.ts.map +1 -0
  57. package/dist/admin/pages/ProductEditor.js +1050 -0
  58. package/dist/admin/pages/ProductEditor.js.map +1 -0
  59. package/dist/admin/pages/ProductsList.d.ts +9 -0
  60. package/dist/admin/pages/ProductsList.d.ts.map +1 -0
  61. package/dist/admin/pages/ProductsList.js +399 -0
  62. package/dist/admin/pages/ProductsList.js.map +1 -0
  63. package/dist/backend/commands/attribute-commands.d.ts +111 -0
  64. package/dist/backend/commands/attribute-commands.d.ts.map +1 -0
  65. package/dist/backend/commands/attribute-commands.js +482 -0
  66. package/dist/backend/commands/attribute-commands.js.map +1 -0
  67. package/dist/backend/demo/reset.d.ts +23 -0
  68. package/dist/backend/demo/reset.d.ts.map +1 -0
  69. package/dist/backend/demo/reset.js +41 -0
  70. package/dist/backend/demo/reset.js.map +1 -0
  71. package/dist/backend/demo/rows.d.ts +103 -0
  72. package/dist/backend/demo/rows.d.ts.map +1 -0
  73. package/dist/backend/demo/rows.js +162 -0
  74. package/dist/backend/demo/rows.js.map +1 -0
  75. package/dist/backend/demo/seed.d.ts +35 -0
  76. package/dist/backend/demo/seed.d.ts.map +1 -0
  77. package/dist/backend/demo/seed.js +177 -0
  78. package/dist/backend/demo/seed.js.map +1 -0
  79. package/dist/backend/entities/attachment-type.entity.d.ts +16 -0
  80. package/dist/backend/entities/attachment-type.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/attachment-type.entity.js +57 -0
  82. package/dist/backend/entities/attachment-type.entity.js.map +1 -0
  83. package/dist/backend/entities/attribute-set-attribute.entity.d.ts +26 -0
  84. package/dist/backend/entities/attribute-set-attribute.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/attribute-set-attribute.entity.js +55 -0
  86. package/dist/backend/entities/attribute-set-attribute.entity.js.map +1 -0
  87. package/dist/backend/entities/attribute-set.entity.d.ts +22 -0
  88. package/dist/backend/entities/attribute-set.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/attribute-set.entity.js +67 -0
  90. package/dist/backend/entities/attribute-set.entity.js.map +1 -0
  91. package/dist/backend/entities/bulk-operation.entity.d.ts +78 -0
  92. package/dist/backend/entities/bulk-operation.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/bulk-operation.entity.js +138 -0
  94. package/dist/backend/entities/bulk-operation.entity.js.map +1 -0
  95. package/dist/backend/entities/bundle-slot-option.entity.d.ts +23 -0
  96. package/dist/backend/entities/bundle-slot-option.entity.d.ts.map +1 -0
  97. package/dist/backend/entities/bundle-slot-option.entity.js +68 -0
  98. package/dist/backend/entities/bundle-slot-option.entity.js.map +1 -0
  99. package/dist/backend/entities/bundle-slot.entity.d.ts +25 -0
  100. package/dist/backend/entities/bundle-slot.entity.d.ts.map +1 -0
  101. package/dist/backend/entities/bundle-slot.entity.js +74 -0
  102. package/dist/backend/entities/bundle-slot.entity.js.map +1 -0
  103. package/dist/backend/entities/category.entity.d.ts +26 -0
  104. package/dist/backend/entities/category.entity.d.ts.map +1 -0
  105. package/dist/backend/entities/category.entity.js +119 -0
  106. package/dist/backend/entities/category.entity.js.map +1 -0
  107. package/dist/backend/entities/gallery-item-label.entity.d.ts +22 -0
  108. package/dist/backend/entities/gallery-item-label.entity.d.ts.map +1 -0
  109. package/dist/backend/entities/gallery-item-label.entity.js +50 -0
  110. package/dist/backend/entities/gallery-item-label.entity.js.map +1 -0
  111. package/dist/backend/entities/gallery-item.entity.d.ts +17 -0
  112. package/dist/backend/entities/gallery-item.entity.d.ts.map +1 -0
  113. package/dist/backend/entities/gallery-item.entity.js +58 -0
  114. package/dist/backend/entities/gallery-item.entity.js.map +1 -0
  115. package/dist/backend/entities/grouped-item.entity.d.ts +23 -0
  116. package/dist/backend/entities/grouped-item.entity.d.ts.map +1 -0
  117. package/dist/backend/entities/grouped-item.entity.js +68 -0
  118. package/dist/backend/entities/grouped-item.entity.js.map +1 -0
  119. package/dist/backend/entities/product-attachment.entity.d.ts +23 -0
  120. package/dist/backend/entities/product-attachment.entity.d.ts.map +1 -0
  121. package/dist/backend/entities/product-attachment.entity.js +77 -0
  122. package/dist/backend/entities/product-attachment.entity.js.map +1 -0
  123. package/dist/backend/entities/product-attribute.entity.d.ts +72 -0
  124. package/dist/backend/entities/product-attribute.entity.d.ts.map +1 -0
  125. package/dist/backend/entities/product-attribute.entity.js +161 -0
  126. package/dist/backend/entities/product-attribute.entity.js.map +1 -0
  127. package/dist/backend/entities/product-editor-preference.entity.d.ts +28 -0
  128. package/dist/backend/entities/product-editor-preference.entity.d.ts.map +1 -0
  129. package/dist/backend/entities/product-editor-preference.entity.js +63 -0
  130. package/dist/backend/entities/product-editor-preference.entity.js.map +1 -0
  131. package/dist/backend/entities/product-link.entity.d.ts +25 -0
  132. package/dist/backend/entities/product-link.entity.d.ts.map +1 -0
  133. package/dist/backend/entities/product-link.entity.js +70 -0
  134. package/dist/backend/entities/product-link.entity.js.map +1 -0
  135. package/dist/backend/entities/product-packaging-unit.entity.d.ts +26 -0
  136. package/dist/backend/entities/product-packaging-unit.entity.d.ts.map +1 -0
  137. package/dist/backend/entities/product-packaging-unit.entity.js +76 -0
  138. package/dist/backend/entities/product-packaging-unit.entity.js.map +1 -0
  139. package/dist/backend/entities/product-value-override.entity.d.ts +43 -0
  140. package/dist/backend/entities/product-value-override.entity.d.ts.map +1 -0
  141. package/dist/backend/entities/product-value-override.entity.js +90 -0
  142. package/dist/backend/entities/product-value-override.entity.js.map +1 -0
  143. package/dist/backend/entities/product-variant.entity.d.ts +18 -0
  144. package/dist/backend/entities/product-variant.entity.d.ts.map +1 -0
  145. package/dist/backend/entities/product-variant.entity.js +68 -0
  146. package/dist/backend/entities/product-variant.entity.js.map +1 -0
  147. package/dist/backend/entities/product.entity.d.ts +108 -0
  148. package/dist/backend/entities/product.entity.d.ts.map +1 -0
  149. package/dist/backend/entities/product.entity.js +230 -0
  150. package/dist/backend/entities/product.entity.js.map +1 -0
  151. package/dist/backend/index.d.ts +262 -0
  152. package/dist/backend/index.d.ts.map +1 -0
  153. package/dist/backend/index.js +642 -0
  154. package/dist/backend/index.js.map +1 -0
  155. package/dist/backend/plugin.d.ts +216 -0
  156. package/dist/backend/plugin.d.ts.map +1 -0
  157. package/dist/backend/plugin.js +223 -0
  158. package/dist/backend/plugin.js.map +1 -0
  159. package/dist/backend/prompt-tools.d.ts +47 -0
  160. package/dist/backend/prompt-tools.d.ts.map +1 -0
  161. package/dist/backend/prompt-tools.js +284 -0
  162. package/dist/backend/prompt-tools.js.map +1 -0
  163. package/dist/backend/routes.admin.d.ts +105 -0
  164. package/dist/backend/routes.admin.d.ts.map +1 -0
  165. package/dist/backend/routes.admin.js +1162 -0
  166. package/dist/backend/routes.admin.js.map +1 -0
  167. package/dist/backend/routes.api-key.d.ts +33 -0
  168. package/dist/backend/routes.api-key.d.ts.map +1 -0
  169. package/dist/backend/routes.api-key.js +43 -0
  170. package/dist/backend/routes.api-key.js.map +1 -0
  171. package/dist/backend/routes.external.d.ts +32 -0
  172. package/dist/backend/routes.external.d.ts.map +1 -0
  173. package/dist/backend/routes.external.js +115 -0
  174. package/dist/backend/routes.external.js.map +1 -0
  175. package/dist/backend/routes.public.d.ts +46 -0
  176. package/dist/backend/routes.public.d.ts.map +1 -0
  177. package/dist/backend/routes.public.js +318 -0
  178. package/dist/backend/routes.public.js.map +1 -0
  179. package/dist/backend/services/asset-references.d.ts +4 -0
  180. package/dist/backend/services/asset-references.d.ts.map +1 -0
  181. package/dist/backend/services/asset-references.js +104 -0
  182. package/dist/backend/services/asset-references.js.map +1 -0
  183. package/dist/backend/services/attachment.service.d.ts +45 -0
  184. package/dist/backend/services/attachment.service.d.ts.map +1 -0
  185. package/dist/backend/services/attachment.service.js +268 -0
  186. package/dist/backend/services/attachment.service.js.map +1 -0
  187. package/dist/backend/services/attribute-option-validator.d.ts +47 -0
  188. package/dist/backend/services/attribute-option-validator.d.ts.map +1 -0
  189. package/dist/backend/services/attribute-option-validator.js +69 -0
  190. package/dist/backend/services/attribute-option-validator.js.map +1 -0
  191. package/dist/backend/services/attribute-set-validations.d.ts +65 -0
  192. package/dist/backend/services/attribute-set-validations.d.ts.map +1 -0
  193. package/dist/backend/services/attribute-set-validations.js +89 -0
  194. package/dist/backend/services/attribute-set-validations.js.map +1 -0
  195. package/dist/backend/services/attribute-set.service.d.ts +50 -0
  196. package/dist/backend/services/attribute-set.service.d.ts.map +1 -0
  197. package/dist/backend/services/attribute-set.service.js +401 -0
  198. package/dist/backend/services/attribute-set.service.js.map +1 -0
  199. package/dist/backend/services/attribute-type-mapping.d.ts +66 -0
  200. package/dist/backend/services/attribute-type-mapping.d.ts.map +1 -0
  201. package/dist/backend/services/attribute-type-mapping.js +121 -0
  202. package/dist/backend/services/attribute-type-mapping.js.map +1 -0
  203. package/dist/backend/services/attribute-value-key.service.d.ts +48 -0
  204. package/dist/backend/services/attribute-value-key.service.d.ts.map +1 -0
  205. package/dist/backend/services/attribute-value-key.service.js +118 -0
  206. package/dist/backend/services/attribute-value-key.service.js.map +1 -0
  207. package/dist/backend/services/audit-references.d.ts +13 -0
  208. package/dist/backend/services/audit-references.d.ts.map +1 -0
  209. package/dist/backend/services/audit-references.js +51 -0
  210. package/dist/backend/services/audit-references.js.map +1 -0
  211. package/dist/backend/services/bulk-operation-queue.d.ts +26 -0
  212. package/dist/backend/services/bulk-operation-queue.d.ts.map +1 -0
  213. package/dist/backend/services/bulk-operation-queue.js +34 -0
  214. package/dist/backend/services/bulk-operation-queue.js.map +1 -0
  215. package/dist/backend/services/bulk-operation.service.d.ts +195 -0
  216. package/dist/backend/services/bulk-operation.service.d.ts.map +1 -0
  217. package/dist/backend/services/bulk-operation.service.js +510 -0
  218. package/dist/backend/services/bulk-operation.service.js.map +1 -0
  219. package/dist/backend/services/bundle.service.d.ts +87 -0
  220. package/dist/backend/services/bundle.service.d.ts.map +1 -0
  221. package/dist/backend/services/bundle.service.js +286 -0
  222. package/dist/backend/services/bundle.service.js.map +1 -0
  223. package/dist/backend/services/catalog-admin.service.d.ts +398 -0
  224. package/dist/backend/services/catalog-admin.service.d.ts.map +1 -0
  225. package/dist/backend/services/catalog-admin.service.js +1516 -0
  226. package/dist/backend/services/catalog-admin.service.js.map +1 -0
  227. package/dist/backend/services/catalog-attribute-read.service.d.ts +78 -0
  228. package/dist/backend/services/catalog-attribute-read.service.d.ts.map +1 -0
  229. package/dist/backend/services/catalog-attribute-read.service.js +216 -0
  230. package/dist/backend/services/catalog-attribute-read.service.js.map +1 -0
  231. package/dist/backend/services/catalog-bulk-import.service.d.ts +30 -0
  232. package/dist/backend/services/catalog-bulk-import.service.d.ts.map +1 -0
  233. package/dist/backend/services/catalog-bulk-import.service.js +136 -0
  234. package/dist/backend/services/catalog-bulk-import.service.js.map +1 -0
  235. package/dist/backend/services/catalog-bulk-update.service.d.ts +91 -0
  236. package/dist/backend/services/catalog-bulk-update.service.d.ts.map +1 -0
  237. package/dist/backend/services/catalog-bulk-update.service.js +330 -0
  238. package/dist/backend/services/catalog-bulk-update.service.js.map +1 -0
  239. package/dist/backend/services/catalog-category-read.service.d.ts +66 -0
  240. package/dist/backend/services/catalog-category-read.service.d.ts.map +1 -0
  241. package/dist/backend/services/catalog-category-read.service.js +248 -0
  242. package/dist/backend/services/catalog-category-read.service.js.map +1 -0
  243. package/dist/backend/services/catalog-org-price-decorator.d.ts +90 -0
  244. package/dist/backend/services/catalog-org-price-decorator.d.ts.map +1 -0
  245. package/dist/backend/services/catalog-org-price-decorator.js +184 -0
  246. package/dist/backend/services/catalog-org-price-decorator.js.map +1 -0
  247. package/dist/backend/services/catalog-product-filter.service.d.ts +82 -0
  248. package/dist/backend/services/catalog-product-filter.service.d.ts.map +1 -0
  249. package/dist/backend/services/catalog-product-filter.service.js +275 -0
  250. package/dist/backend/services/catalog-product-filter.service.js.map +1 -0
  251. package/dist/backend/services/catalog-product-read.service.d.ts +46 -0
  252. package/dist/backend/services/catalog-product-read.service.d.ts.map +1 -0
  253. package/dist/backend/services/catalog-product-read.service.js +197 -0
  254. package/dist/backend/services/catalog-product-read.service.js.map +1 -0
  255. package/dist/backend/services/catalog-query.service.d.ts +444 -0
  256. package/dist/backend/services/catalog-query.service.d.ts.map +1 -0
  257. package/dist/backend/services/catalog-query.service.js +1633 -0
  258. package/dist/backend/services/catalog-query.service.js.map +1 -0
  259. package/dist/backend/services/catalog-quick-search.service.d.ts +78 -0
  260. package/dist/backend/services/catalog-quick-search.service.d.ts.map +1 -0
  261. package/dist/backend/services/catalog-quick-search.service.js +159 -0
  262. package/dist/backend/services/catalog-quick-search.service.js.map +1 -0
  263. package/dist/backend/services/catalog-write-ports.d.ts +25 -0
  264. package/dist/backend/services/catalog-write-ports.d.ts.map +1 -0
  265. package/dist/backend/services/catalog-write-ports.js +63 -0
  266. package/dist/backend/services/catalog-write-ports.js.map +1 -0
  267. package/dist/backend/services/category-admin.service.d.ts +102 -0
  268. package/dist/backend/services/category-admin.service.d.ts.map +1 -0
  269. package/dist/backend/services/category-admin.service.js +271 -0
  270. package/dist/backend/services/category-admin.service.js.map +1 -0
  271. package/dist/backend/services/gallery.service.d.ts +85 -0
  272. package/dist/backend/services/gallery.service.d.ts.map +1 -0
  273. package/dist/backend/services/gallery.service.js +326 -0
  274. package/dist/backend/services/gallery.service.js.map +1 -0
  275. package/dist/backend/services/grouped.service.d.ts +39 -0
  276. package/dist/backend/services/grouped.service.d.ts.map +1 -0
  277. package/dist/backend/services/grouped.service.js +136 -0
  278. package/dist/backend/services/grouped.service.js.map +1 -0
  279. package/dist/backend/services/label-resolver.d.ts +16 -0
  280. package/dist/backend/services/label-resolver.d.ts.map +1 -0
  281. package/dist/backend/services/label-resolver.js +43 -0
  282. package/dist/backend/services/label-resolver.js.map +1 -0
  283. package/dist/backend/services/packaging-unit.service.d.ts +28 -0
  284. package/dist/backend/services/packaging-unit.service.d.ts.map +1 -0
  285. package/dist/backend/services/packaging-unit.service.js +197 -0
  286. package/dist/backend/services/packaging-unit.service.js.map +1 -0
  287. package/dist/backend/services/primary-asset-url.d.ts +4 -0
  288. package/dist/backend/services/primary-asset-url.d.ts.map +1 -0
  289. package/dist/backend/services/primary-asset-url.js +55 -0
  290. package/dist/backend/services/primary-asset-url.js.map +1 -0
  291. package/dist/backend/services/product-editor-preferences.service.d.ts +20 -0
  292. package/dist/backend/services/product-editor-preferences.service.d.ts.map +1 -0
  293. package/dist/backend/services/product-editor-preferences.service.js +44 -0
  294. package/dist/backend/services/product-editor-preferences.service.js.map +1 -0
  295. package/dist/backend/services/product-link.service.d.ts +169 -0
  296. package/dist/backend/services/product-link.service.d.ts.map +1 -0
  297. package/dist/backend/services/product-link.service.js +297 -0
  298. package/dist/backend/services/product-link.service.js.map +1 -0
  299. package/dist/backend/services/product-overrides.service.d.ts +77 -0
  300. package/dist/backend/services/product-overrides.service.d.ts.map +1 -0
  301. package/dist/backend/services/product-overrides.service.js +204 -0
  302. package/dist/backend/services/product-overrides.service.js.map +1 -0
  303. package/dist/backend/services/product-scope-context.service.d.ts +36 -0
  304. package/dist/backend/services/product-scope-context.service.d.ts.map +1 -0
  305. package/dist/backend/services/product-scope-context.service.js +106 -0
  306. package/dist/backend/services/product-scope-context.service.js.map +1 -0
  307. package/dist/backend/services/product-type-validations.d.ts +42 -0
  308. package/dist/backend/services/product-type-validations.d.ts.map +1 -0
  309. package/dist/backend/services/product-type-validations.js +55 -0
  310. package/dist/backend/services/product-type-validations.js.map +1 -0
  311. package/dist/backend/services/product-value-resolver.service.d.ts +59 -0
  312. package/dist/backend/services/product-value-resolver.service.d.ts.map +1 -0
  313. package/dist/backend/services/product-value-resolver.service.js +111 -0
  314. package/dist/backend/services/product-value-resolver.service.js.map +1 -0
  315. package/dist/backend/services/system-attribute-scopes.d.ts +18 -0
  316. package/dist/backend/services/system-attribute-scopes.d.ts.map +1 -0
  317. package/dist/backend/services/system-attribute-scopes.js +18 -0
  318. package/dist/backend/services/system-attribute-scopes.js.map +1 -0
  319. package/dist/backend/services/viewer-organization.d.ts +25 -0
  320. package/dist/backend/services/viewer-organization.d.ts.map +1 -0
  321. package/dist/backend/services/viewer-organization.js +31 -0
  322. package/dist/backend/services/viewer-organization.js.map +1 -0
  323. package/dist/manifest.d.ts +204 -0
  324. package/dist/manifest.d.ts.map +1 -0
  325. package/dist/manifest.js +686 -0
  326. package/dist/manifest.js.map +1 -0
  327. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts +33 -0
  328. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts.map +1 -0
  329. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js +118 -0
  330. package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js.map +1 -0
  331. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts +24 -0
  332. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts.map +1 -0
  333. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js +31 -0
  334. package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js.map +1 -0
  335. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts +32 -0
  336. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts.map +1 -0
  337. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js +58 -0
  338. package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js.map +1 -0
  339. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts +22 -0
  340. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts.map +1 -0
  341. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js +66 -0
  342. package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js.map +1 -0
  343. package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts +18 -0
  344. package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts.map +1 -0
  345. package/dist/migrations/20260429T112543_catalog_product_attachments.js +73 -0
  346. package/dist/migrations/20260429T112543_catalog_product_attachments.js.map +1 -0
  347. package/dist/migrations/20260429T123726_catalog_product_links.d.ts +21 -0
  348. package/dist/migrations/20260429T123726_catalog_product_links.d.ts.map +1 -0
  349. package/dist/migrations/20260429T123726_catalog_product_links.js +48 -0
  350. package/dist/migrations/20260429T123726_catalog_product_links.js.map +1 -0
  351. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts +23 -0
  352. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts.map +1 -0
  353. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js +94 -0
  354. package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js.map +1 -0
  355. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts +24 -0
  356. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts.map +1 -0
  357. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js +31 -0
  358. package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js.map +1 -0
  359. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts +31 -0
  360. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts.map +1 -0
  361. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js +133 -0
  362. package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js.map +1 -0
  363. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts +16 -0
  364. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts.map +1 -0
  365. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js +23 -0
  366. package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js.map +1 -0
  367. package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts +10 -0
  368. package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts.map +1 -0
  369. package/dist/migrations/20260526T124736_catalog_product_status_inactive.js +22 -0
  370. package/dist/migrations/20260526T124736_catalog_product_status_inactive.js.map +1 -0
  371. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts +38 -0
  372. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts.map +1 -0
  373. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js +98 -0
  374. package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js.map +1 -0
  375. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts +13 -0
  376. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts.map +1 -0
  377. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js +17 -0
  378. package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js.map +1 -0
  379. package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts +16 -0
  380. package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts.map +1 -0
  381. package/dist/migrations/20260611T140407_catalog_bulk_operations.js +42 -0
  382. package/dist/migrations/20260611T140407_catalog_bulk_operations.js.map +1 -0
  383. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts +14 -0
  384. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts.map +1 -0
  385. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js +18 -0
  386. package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js.map +1 -0
  387. package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts +11 -0
  388. package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts.map +1 -0
  389. package/dist/migrations/20260611T140412_catalog_product_packaging_units.js +29 -0
  390. package/dist/migrations/20260611T140412_catalog_product_packaging_units.js.map +1 -0
  391. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts +23 -0
  392. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts.map +1 -0
  393. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js +35 -0
  394. package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js.map +1 -0
  395. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts +12 -0
  396. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts.map +1 -0
  397. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js +16 -0
  398. package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js.map +1 -0
  399. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts +32 -0
  400. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts.map +1 -0
  401. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js +286 -0
  402. package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js.map +1 -0
  403. package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts +36 -0
  404. package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts.map +1 -0
  405. package/dist/migrations/20260804T152604_catalog_widen_product_sku.js +63 -0
  406. package/dist/migrations/20260804T152604_catalog_widen_product_sku.js.map +1 -0
  407. package/dist/migrations/20260804T160244_catalog_category_activation.d.ts +18 -0
  408. package/dist/migrations/20260804T160244_catalog_category_activation.d.ts.map +1 -0
  409. package/dist/migrations/20260804T160244_catalog_category_activation.js +22 -0
  410. package/dist/migrations/20260804T160244_catalog_category_activation.js.map +1 -0
  411. package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts +27 -0
  412. package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts.map +1 -0
  413. package/dist/migrations/20260912T094557_catalog_sales_channel_products.js +44 -0
  414. package/dist/migrations/20260912T094557_catalog_sales_channel_products.js.map +1 -0
  415. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts +27 -0
  416. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts.map +1 -0
  417. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js +44 -0
  418. package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js.map +1 -0
  419. package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts +36 -0
  420. package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts.map +1 -0
  421. package/dist/migrations/20260925T125527_catalog_inventory_columns.js +87 -0
  422. package/dist/migrations/20260925T125527_catalog_inventory_columns.js.map +1 -0
  423. package/dist/migrations/index.d.ts +55 -0
  424. package/dist/migrations/index.d.ts.map +1 -0
  425. package/dist/migrations/index.js +80 -0
  426. package/dist/migrations/index.js.map +1 -0
  427. package/dist/ports/index.d.ts +96 -0
  428. package/dist/ports/index.d.ts.map +1 -0
  429. package/dist/ports/index.js +2 -0
  430. package/dist/ports/index.js.map +1 -0
  431. package/docs/catalog/attachments.md +69 -0
  432. package/docs/catalog/attribute-sets.md +63 -0
  433. package/docs/catalog/attributes.md +244 -0
  434. package/docs/catalog/composite-products.md +115 -0
  435. package/docs/catalog/gallery-and-labels.md +73 -0
  436. package/docs/catalog/packaging-units.md +67 -0
  437. package/docs/catalog/per-channel-per-language-overrides.md +234 -0
  438. package/docs/catalog/product-links.md +79 -0
  439. package/docs/catalog.md +207 -0
  440. package/i18n/en.json +649 -0
  441. package/i18n/pl.json +649 -0
  442. package/package.json +109 -0
  443. package/tailwind.css +14 -0
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: Packaging Units
3
+ ---
4
+
5
+ # Packaging Units
6
+
7
+ Named ordering units attached to a product — for example a **Paleta** worth
8
+ 480 pieces. They let B2B buyers order in bulk units without typing the exact
9
+ piece count, and carry that context all the way into the order and quote
10
+ request.
11
+
12
+ ## For operators
13
+
14
+ Manage packaging units in the **Inventory** section of the product card
15
+ (simple and configurable products only). Each unit has:
16
+
17
+ - a **name** (operator text, e.g. `Paleta`, `Karton`) — unique within the product,
18
+ - a **base quantity** (whole number ≥ 1) — how many base pieces the unit holds,
19
+ - a **default** flag — the unit pre-selected on the storefront,
20
+ - a **position** — the order units appear in.
21
+
22
+ ## For buyers
23
+
24
+ On the product page a selector offers the available units plus a *single piece*
25
+ option. Ordering a unit adds `baseQuantity × units` pieces to the cart as one
26
+ line, whose displayed name gets the unit appended — e.g.
27
+ `Łożysko 6205-2RS (Paleta)`. A packaging-unit line and a plain single-piece
28
+ line of the same product stay separate.
29
+
30
+ ## How the label travels
31
+
32
+ The unit name and base quantity are **snapshotted** on the line when it is
33
+ created, so later edits to (or deletion of) a product's packaging units never
34
+ change historical carts, orders, or quote requests.
35
+
36
+ - **Cart** — `cart_items` snapshot the unit; the cart serializer composes a
37
+ `displayName` with the suffix.
38
+ - **Order** — `order_items` get a `packaging_unit_snapshot`, and the unit name
39
+ is appended to `product_snapshot.name`, so every order document (detail,
40
+ invoice, CSV, email) shows it.
41
+ - **Quote request** — `quote_request_items` snapshot the unit and append it to
42
+ `product_name`; cart → quote-request conversion carries the context through.
43
+
44
+ ## Pricing & availability
45
+
46
+ Pricing reuses the existing engine on the resulting base-piece quantity
47
+ (including price-list quantity tiers) — there is no separate per-unit price.
48
+ Stock availability and per-line limits are evaluated on the resulting piece
49
+ count.
50
+
51
+ ## API surface
52
+
53
+ - Admin CRUD: `GET/POST /api/v1/admin/catalog/products/:id/packaging-units`,
54
+ `PATCH/DELETE …/:unitId`, `PATCH …/packaging-units/reorder`
55
+ (gated by `catalog:read` / `catalog:write`).
56
+ - Public: the product detail (`GET /api/v1/catalog/products/:idOrSlug`)
57
+ includes an optional `packagingUnits` array.
58
+ - Cart: `POST /api/v1/cart/items` accepts an optional `packagingUnitId`; the
59
+ resulting line quantity is `baseQuantity × quantity`.
60
+
61
+ ## Schema
62
+
63
+ Table `product_packaging_units` (migration 068): `id`, `product_id`
64
+ (FK → `products`, cascade delete), `name`, `base_quantity`
65
+ (`CHECK >= 1`), `position`, `is_default`, timestamps; `UNIQUE (product_id,
66
+ name)`. Additive snapshot columns: `cart_items` (069), `order_items` (070),
67
+ `quote_request_items` (071).
@@ -0,0 +1,234 @@
1
+ ---
2
+ title: Per-Channel + Per-Language Overrides
3
+ ---
4
+
5
+ # Per-Channel + Per-Language Overrides
6
+
7
+ The Catalog uses a **four-scope value model** for product attribute
8
+ values. The same attribute key can hold up to four addressable slots
9
+ per product:
10
+
11
+ - `global` — one value across every Sales Channel and every language;
12
+ - `language` — one value per language (e.g. system Name, Description);
13
+ - `channel` — one value per Sales Channel the product is assigned to;
14
+ - `channel+language` — one value per `(channel, language)` pair.
15
+
16
+ Which slots are addressable for a given attribute is governed by two
17
+ flags on the attribute definition, `channelScoped` and `languageScoped`.
18
+ System attributes `name` and `description` are pinned channel-scoped +
19
+ language-scoped via a backend constant
20
+ (`SYSTEM_ATTRIBUTE_SCOPES`). User-defined attributes default to
21
+ global-only and opt in via the attribute editor.
22
+
23
+ ## Why
24
+
25
+ A B2B catalog routinely needs to overlay channel-specific marketing
26
+ copy (wholesale vs. retail) and language-specific translation on the
27
+ same product. The legacy storage — JSONB on `products.name` /
28
+ `products.description` + a flat `products.attribute_values` JSONB —
29
+ could express only the language dimension. The four-scope model lifts
30
+ that ceiling without rewriting baseline storage; it adds a sibling
31
+ overrides table that holds the channel-aware slots, and a single
32
+ resolver function fuses the two on read.
33
+
34
+ ## Storage shape
35
+
36
+ The global baseline keeps living where it has always lived:
37
+
38
+ - `products.name` (`Record<lang, string>` JSONB) — language-scoped.
39
+ - `products.description` (`Record<lang, string>` JSONB) — language-scoped.
40
+ - `products.attribute_values` (`Record<key, value>` JSONB) — global by
41
+ default. When the attribute's `languageScoped` flag is `true`, the
42
+ inner value is itself keyed by language: `Record<key, Record<lang,
43
+ value>>`.
44
+
45
+ Channel-aware slots land in a new table:
46
+
47
+ ```sql
48
+ create table "product_value_overrides" (
49
+ "id" uuid primary key default gen_random_uuid(),
50
+ "product_id" uuid not null references "products"("id") on delete cascade,
51
+ "attribute_key" varchar(64) not null,
52
+ "channel_id" uuid not null references "sales_channels"("id") on delete cascade,
53
+ "language_code" varchar(16) null,
54
+ "value" jsonb not null, -- always wrapped { "v": <scalar | object> }
55
+ "created_at" timestamptz not null default now(),
56
+ "updated_at" timestamptz not null default now()
57
+ );
58
+ ```
59
+
60
+ The (product, attribute, channel, language) tuple is unique under
61
+ **two partial indexes**:
62
+
63
+ ```sql
64
+ create unique index "product_value_overrides_channel_only_uniq"
65
+ on "product_value_overrides" ("product_id", "attribute_key", "channel_id")
66
+ where "language_code" is null;
67
+
68
+ create unique index "product_value_overrides_channel_lang_uniq"
69
+ on "product_value_overrides" ("product_id", "attribute_key", "channel_id", "language_code")
70
+ where "language_code" is not null;
71
+ ```
72
+
73
+ PostgreSQL treats `NULL` as distinct in a regular UNIQUE constraint, so
74
+ the only correct way to model "(channel, NULL) and (channel, NULL) are
75
+ the same row" is two partial indexes — one for each NULL semantic.
76
+
77
+ The lookup path used by the resolver in tight loops is a plain btree:
78
+
79
+ ```sql
80
+ create index "product_value_overrides_product_attr_idx"
81
+ on "product_value_overrides" ("product_id", "attribute_key");
82
+ ```
83
+
84
+ A second table — `product_editor_preferences` — stores the per-(admin
85
+ user, product) last-selected `(channelId, languageCode)` so the
86
+ product edit page seeds the switchers from the editor's prior visit.
87
+ It carries no FK to `sales_channels` so a deleted / un-assigned
88
+ channel is silently tolerated on read.
89
+
90
+ ## The resolver
91
+
92
+ The single source of truth for "what value should I show?" lives in
93
+ `packages/contracts/src/product-value-resolver.ts`. The same TypeScript
94
+ file is imported by the backend (admin endpoints, storefront read
95
+ path, search indexer) and the admin SPA. By construction, the admin's
96
+ effective-value preview cannot drift from what the storefront renders.
97
+
98
+ The algorithm — `resolveAttribute({ baseline, overrides, scope, ctx })`
99
+ — walks four steps:
100
+
101
+ 1. `(channel + language)` slot — when the attribute is channel-scoped
102
+ AND language-scoped AND the context carries both;
103
+ 2. `(channel-only)` slot — when channel-scoped AND a channel is in the
104
+ context;
105
+ 3. `(global + language)` baseline — for language-scoped attributes,
106
+ pick the requested language out of the JSONB;
107
+ 4. `(global)` baseline — single value (or primary-language pick for
108
+ language-scoped attrs).
109
+
110
+ The first non-empty slot wins. Empty string, `null`, empty object,
111
+ empty array are all treated as "absent" and fall through. The function
112
+ returns both the resolved value AND a `source` tag indicating which
113
+ slot produced the answer — the admin UI consumes this to render the
114
+ "channel + language override" / "global baseline" badge per field.
115
+
116
+ ### Orphan tolerance
117
+
118
+ An administrator can flip an attribute's `channelScoped` flag back to
119
+ `false` after channel overrides have already been written. The
120
+ resolver simply skips override rows whose attribute is no longer
121
+ channel-scoped, so the storefront falls back to the baseline without
122
+ any data-loss event. A future maintenance pass can drop orphan rows;
123
+ this feature does not require it.
124
+
125
+ ## Write path
126
+
127
+ Channel-aware overrides land through one admin endpoint:
128
+
129
+ ```
130
+ PATCH /api/v1/admin/catalog/products/:id/value-overrides
131
+ {
132
+ "upserts": [
133
+ { "attributeKey": "name", "channelId": "<vip-uuid>", "languageCode": "en-US", "value": { "v": "VIP wholesale name (EN)" } },
134
+ { "attributeKey": "description", "channelId": "<vip-uuid>", "languageCode": "pl-PL", "value": { "v": "Wholesale-only Polish copy" } }
135
+ ],
136
+ "deletes": [
137
+ { "attributeKey": "description", "channelId": "<retail-uuid>", "languageCode": "en-US" }
138
+ ]
139
+ }
140
+ ```
141
+
142
+ All requested operations run in a single `em.transactional` pass.
143
+ The handler validates every entry up front against six rules and
144
+ rolls everything back on the first failure:
145
+
146
+ | 422 code | Triggered when |
147
+ | ----------------------------------- | -------------- |
148
+ | `attribute_unknown` | `attributeKey` is neither a system attr (`name`, `description`) nor a row in `product_attributes`. |
149
+ | `attribute_not_channel_scoped` | the attribute's `channelScoped=false`, so it has no channel slot. |
150
+ | `attribute_missing_language` | the attribute's `languageScoped=true`, but the slot has `languageCode=null`. |
151
+ | `channel_not_assigned_to_product` | `channelId` is not in `sales_channel_products` for the product. |
152
+ | `language_not_in_channel` | `languageCode` is non-null and not in the channel's `SalesChannel.languages` array. |
153
+ | `value_invalid` | the wrapped `value.v` does not match the attribute's `valueType`. |
154
+
155
+ The baseline write path (Name / Description per language; global
156
+ `attribute_values`) is unchanged — editors still use the existing
157
+ locale-side-by-side inputs on the Details tab. Channel overrides are
158
+ additive.
159
+
160
+ ## Read paths
161
+
162
+ Three consumers go through the resolver:
163
+
164
+ - **Admin endpoint** — `GET /api/v1/admin/catalog/products/:id?channelId=&languageCode=&includeOverridesMap=true`
165
+ returns the baseline product PLUS, when context params are passed, a
166
+ `resolved` block with `name`, `description`, `attributeValues`,
167
+ `sources`. When `includeOverridesMap=true`, the full override list
168
+ is attached for client-side switcher previews.
169
+ - **Storefront public read** — pulls the channel from the
170
+ `x-sales-channel` header (the existing convention) and
171
+ the language from `Accept-Language`. The override layer is invisible
172
+ to the public client.
173
+ - **Search indexer (Meilisearch)** — builds one document per
174
+ `(product, channel)` pair. Each document's `name` and `description`
175
+ fields are run through the resolver with `channel.defaultLanguage`
176
+ as the active language, so per-channel overrides flow into
177
+ storefront search ranking.
178
+
179
+ ## Admin UI
180
+
181
+ The product edit page's **Details** tab gains a
182
+ `<ProductScopeEditor>` panel above the existing locale inputs. It:
183
+
184
+ 1. Loads `GET /scope-context` (channels assigned to the product + each
185
+ channel's languages + the platform primary admin language + the
186
+ editor's remembered preference) and `GET /value-overrides` in
187
+ parallel.
188
+ 2. Renders a Sales Channel switcher (Global + each assigned channel)
189
+ and a Language switcher narrowed to the active channel's languages
190
+ (or the union of all channel languages when Global).
191
+ 3. Shows the resolved Name + Description for the active context with a
192
+ source badge per field — `channel + language override` / `channel
193
+ override` / `global baseline (language)` / `global baseline
194
+ (fallback)` / `no value`.
195
+ 4. Offers "Add override" / "Edit override" / "Reset to Global"
196
+ affordances per field. Edits go through `PATCH /value-overrides`
197
+ with optimistic refresh.
198
+ 5. Persists the editor's `(channel, language)` selection back via
199
+ `PUT /editor-preference` (debounced, best-effort) so the next visit
200
+ seeds the same context.
201
+
202
+ Edit affordances are gated on a specific channel being selected.
203
+ Under Global / no channel, the panel is read-only and a help string
204
+ explains that overrides only apply per-channel.
205
+
206
+ ## Migration
207
+
208
+ One migration `043_product_value_overrides_init.ts` ships everything:
209
+
210
+ - adds `channel_scoped` + `language_scoped` boolean columns to
211
+ `product_attributes` (default `false`);
212
+ - creates `product_value_overrides` with the two partial UNIQUE
213
+ indexes and the `(product_id, attribute_key)` btree;
214
+ - creates `product_editor_preferences` (composite PK
215
+ `(admin_user_id, product_id)`).
216
+
217
+ No baseline data migration. No data backfill. The `down()` is the
218
+ exact inverse — drop the two tables and the two columns.
219
+
220
+ ## What is not in scope
221
+
222
+ - **User-defined attribute overrides in the admin UI** — the panel
223
+ surfaces only the system Name + Description today. User attribute
224
+ overrides are accepted by the PATCH endpoint and resolved on read,
225
+ but the admin SPA does not yet expose a write affordance for them.
226
+ - **Storefront public read shape change** — `products.name` and
227
+ `products.description` still ship as `Record<lang, string>` JSONB
228
+ on the public catalog endpoint. Switching to a resolved scalar is
229
+ a breaking change for storefront consumers and is scoped as a
230
+ follow-up.
231
+ - **Channel-aware reindex enqueue** — the PATCH endpoint does not
232
+ yet enqueue a Meilisearch reindex job per touched channel; the
233
+ next per-product upsert (driven by any catalog mutation) picks it
234
+ up. A dedicated reindex on override write is a small follow-up.
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: Product Links
3
+ ---
4
+
5
+ # Product Links
6
+
7
+ Directed links between Products that drive cross-merchandising on the
8
+ PDP and cart. Three kinds:
9
+
10
+ - **Related** — "Related products" section on the PDP
11
+ - **Up-sell** — "You might also like" on the PDP
12
+ - **Cross-sell** — "You may also need" on the cart page
13
+
14
+ ## DB-level guarantees
15
+
16
+ Migration 022 ships three constraints that make incorrect link data
17
+ impossible:
18
+
19
+ - `UNIQUE (source_product_id, target_product_id, kind)` — same pair can
20
+ appear once per kind, never twice for the same kind
21
+ - `CHECK source_product_id <> target_product_id` — defence-in-depth
22
+ against self-links beyond the API-layer guard
23
+ - `CHECK kind IN ('related','up_sell','cross_sell')`
24
+
25
+ Both FK columns cascade on product delete: link rows have no value when
26
+ either side is gone.
27
+
28
+ ## Public surface
29
+
30
+ | Verb + Path | Audience | Purpose |
31
+ | --- | --- | --- |
32
+ | `GET /api/v1/admin/catalog/products/:id/links?kind=...` | admin | List links optionally filtered by kind |
33
+ | `POST /api/v1/admin/catalog/products/:id/links` | admin | Bulk create — all-or-nothing transaction |
34
+ | `DELETE /api/v1/admin/catalog/products/:id/links/:linkId` | admin | Remove a single link |
35
+ | `PUT /api/v1/admin/catalog/products/:id/links/:kind/order` | admin | Reorder by id list |
36
+ | `GET /api/v1/catalog/products/:idOrSlug/links?kind=...` | storefront | Storefront read; archived/channel-restricted targets filtered server-side |
37
+
38
+ ## Bulk create semantics
39
+
40
+ `POST .../links` accepts a `links[]` array where each entry has
41
+ `{targetProductId, kind, position?}`. The service validates the whole
42
+ batch before inserting any row:
43
+
44
+ 1. Source product exists and is not archived (404 otherwise)
45
+ 2. No entry has `targetProductId === sourceProductId` (400)
46
+ 3. All targets exist (404 with the offending id)
47
+ 4. No `(source, target, kind)` already exists (409)
48
+
49
+ If any check fails, **no rows are inserted**. This matches the admin UX
50
+ of submitting a curated batch — partial inserts would surprise an
51
+ admin who's still finishing their picks.
52
+
53
+ ## Errors
54
+
55
+ | Code | Status | When |
56
+ | --- | --- | --- |
57
+ | `SELF_LINK_NOT_ALLOWED` | 400 | source = target |
58
+ | `LINK_ALREADY_EXISTS` | 409 | Duplicate `(source, target, kind)` |
59
+ | `TARGET_PRODUCT_NOT_FOUND` | 404 | Any target id missing |
60
+ | `PRODUCT_LINK_NOT_FOUND` | 404 | `:linkId` missing |
61
+
62
+ ## Storefront integration
63
+
64
+ `productDetail.links` carries pre-grouped arrays sliced to default page
65
+ sizes: `related[8]`, `upSell[4]`, `crossSell[4]`. Each entry is a
66
+ storefront-shape link
67
+ summary `{id, kind, position, product{id, sku, slug, name,
68
+ primaryAssetUrl, price}}` so the listing card renders without a
69
+ follow-up fetch.
70
+
71
+ The `<ProductLinksSections>` PDP component renders Related and Up-sell
72
+ under their own headings. The `<CrossSellSection>` cart component
73
+ fetches cross-sell links per cart item and dedupes by product id so the
74
+ same target shows up once even when reached via multiple cart entries.
75
+
76
+ ## Storage
77
+
78
+ Single `product_links` table with the constraints above plus indexes on
79
+ `(source_product_id, kind)` and `(target_product_id)`.
@@ -0,0 +1,207 @@
1
+ ---
2
+ title: catalog
3
+ sidebar_label: Catalog
4
+ description: Products, variants, categories, attributes, sales channels
5
+ ---
6
+
7
+ # `catalog`
8
+
9
+ The product catalog: Products, ProductVariants, Categories,
10
+ ProductAttributes, and SalesChannels. Owns all read paths the storefront
11
+ depends on and the admin-side authoring surface.
12
+
13
+ ## Public surface
14
+
15
+ Admin routes are gated by `catalog:read` (list / get) /
16
+ `catalog:write` (mutations).
17
+
18
+ | Verb + Path | Audience | Purpose |
19
+ | --- | --- | --- |
20
+ | `GET /api/v1/catalog/products` | storefront / API key | List/search/filter products in the active Sales Channel |
21
+ | `GET /api/v1/catalog/products/:idOrSlug` | storefront | Product detail (price omitted on non-public Sales Channels) |
22
+ | `GET /api/v1/catalog/categories` | storefront | Nested category tree |
23
+ | `GET /api/v1/catalog/filters` | storefront | Filterable attributes for the active Sales Channel |
24
+ | `GET /api/v1/catalog/sitemap.xml` | crawlers | SEO sitemap |
25
+ | `GET /api/v1/admin/catalog/products?includeArchived` | admin | Admin product list (includes drafts; archived rows opt-in) |
26
+ | `GET /api/v1/admin/catalog/products/:id` | admin | Product detail |
27
+ | `POST /api/v1/admin/catalog/products` | admin | Create product (`type` immutable post-create; `sku` is editable) |
28
+ | `PATCH /api/v1/admin/catalog/products/:id` | admin | Update (incl. `sku`); writes an audit row with stateBefore / stateAfter; refuses with `409 sku_in_use` if the new SKU already belongs to another product |
29
+ | `DELETE /api/v1/admin/catalog/products/:id` | admin | Archive (soft) |
30
+ | `GET /api/v1/admin/catalog/attributes` | admin | List attributes |
31
+ | `GET /api/v1/admin/catalog/attributes/by-flag?flag=isPromoRule\|isComparable\|...` | admin | Picker payload — every attribute carrying the requested flag |
32
+ | `GET /api/v1/admin/catalog/attributes/:idOrKey` | admin | Single attribute read |
33
+ | `POST /api/v1/admin/catalog/attributes` | admin | Create attribute (accepts the new flags + inline `options[]` for select-style types) |
34
+ | `PATCH /api/v1/admin/catalog/attributes/:key` | admin | Hot-toggle `isFilterable` / `isSearchable` / `isVariantAxis` / `isPromoRule` / `isComparable` / `isVisibleOnProductPage` / `isRequired` / `filterPosition` (re-emits `attribute.updated.v1`) |
35
+ | `DELETE /api/v1/admin/catalog/attributes/:idOrKey` | admin | Delete; refused with `409 attribute_in_use_by_set` while any Attribute Set still references it |
36
+ | `GET /api/v1/admin/catalog/attributes/:idOrKey/options` | admin | List option-list rows for select/enum/multiselect attributes |
37
+ | `POST /api/v1/admin/catalog/attributes/:idOrKey/options` | admin | Append an option |
38
+ | `PATCH /api/v1/admin/catalog/attribute-options/:optionId` | admin | Patch label / labelDefault / isDefault / sortOrder (option `value` is immutable) |
39
+ | `DELETE /api/v1/admin/catalog/attribute-options/:optionId` | admin | Remove; refused with `409 option_in_use` while any product still carries the value |
40
+ | `POST /api/v1/admin/catalog/attribute-set-preview` | admin | Preview which Set's attributes will be edited / hidden when an operator switches a product's Attribute Set |
41
+ | `GET /api/v1/admin/catalog/categories` | admin | Flat list, the UI folds into a tree |
42
+ | `POST /api/v1/admin/catalog/categories` | admin | Create (parent must exist) |
43
+ | `PATCH /api/v1/admin/catalog/categories/:id` | admin | Update; reparenting walks the new parent's chain to refuse cycles (409) |
44
+ | `DELETE /api/v1/admin/catalog/categories/:id` | admin | Soft-delete; rejects with 409 if any active child still references the row |
45
+ | `PUT /api/v1/catalog/products/by-sku/:sku` | API key | Idempotent upsert (PIM sync) |
46
+
47
+ ## Entities
48
+
49
+ `Product`, `ProductVariant`, `Category`, `ProductAttribute`,
50
+ `SalesChannel`, plus the M:N bridges
51
+ `product_categories`, `sales_channel_products`, `product_assets`.
52
+
53
+ ## Events emitted
54
+
55
+ `product.created.v1`, `product.updated.v1`, `product.archived.v1`,
56
+ `attribute.updated.v1`. Picked up by the search indexer and bridged to
57
+ webhook subscribers.
58
+
59
+ ## Extension points
60
+
61
+ - **Per-Sales-Channel pricing** — query service receives a SalesChannel
62
+ context; new gating (e.g. customer-segment-specific catalogs) is added by
63
+ composing into `catalog-query.service.ts`.
64
+ - **Slug uniqueness** — the slug is unique across all Sales Channels by
65
+ default; override the slugifier in `catalog-admin.service.ts` if locale
66
+ collisions become a concern.
67
+
68
+ ## Product structure and composition surfaces
69
+
70
+ The catalog grew several capability surfaces, each with its own page:
71
+
72
+ - [Attribute Sets](./catalog/attribute-sets.md) — reusable attribute
73
+ schemas pinned to Products, with a system Default
74
+ - [Product Gallery](./catalog/gallery-and-labels.md) — image / video
75
+ gallery with Base / Small / Thumbnail label invariants enforced at
76
+ the database level
77
+ - [Attachments](./catalog/attachments.md) — downloadable files
78
+ (certificates, tech specs, ...) with a typed dictionary
79
+ - [Product Links](./catalog/product-links.md) — Related, Up-sell,
80
+ Cross-sell pairings driving cross-merchandising on the PDP and cart
81
+ - [Composite Products](./catalog/composite-products.md) — `grouped`
82
+ (fixed children), `bundle` (configurable slots), `virtual` (digital
83
+ delivery)
84
+
85
+ Five product types are now supported: `simple`, `configurable`,
86
+ `grouped`, `bundle`, `virtual`. `simple` and `configurable` are the
87
+ original pair; the other three were added later.
88
+
89
+ ## Attribute extensions on the Catalog read paths
90
+
91
+ The Attributes work added the operational surface the storefront
92
+ needs to render rich product information and the search / promotions
93
+ modules need to resolve customer queries. The dedicated
94
+ [Attributes](./catalog/attributes.md) page covers the attribute
95
+ authoring surface in full — this section only summarises what changed
96
+ on the Catalog read paths.
97
+
98
+ ### New attribute flags
99
+
100
+ `ProductAttribute` gains four behavioural flags + a numeric position +
101
+ a per-locale label fallback:
102
+
103
+ - `isPromoRule` (boolean) — picker eligibility for the Promotion Rule
104
+ editor's `attribute` criterion variant
105
+ - `isVisibleOnProductPage` (boolean) — surface the attribute on the
106
+ storefront PDP "Parametry produktu" tab when the product carries a
107
+ value
108
+ - `isRequired` (boolean) — enforced at product save time when the
109
+ attribute is part of the product's Attribute Set
110
+ - `filterPosition` (number) — sort key for the storefront filter sidebar
111
+ (lower comes first; ties broken by label)
112
+ - `labelDefault` (string) — fallback used when the active locale has no
113
+ matching key in the per-locale `label` JSONB
114
+
115
+ ### Option lists
116
+
117
+ Select-style attribute types (`select`, `enum`, `multiselect`) carry an
118
+ ordered option list — each row keyed by `(definition, value)` with
119
+ per-locale label + fallback + sort order + default flag. The legacy
120
+ `enum_values: string[]` JSONB column on `product_attributes` was
121
+ decommissioned by migration 032 (into the catalog-owned
122
+ `attribute_options` table), and migration 102 moved the
123
+ rows into the generic `custom_field_options` table. Existing readers
124
+ project the option list back into the legacy form for backward
125
+ compatibility at the API boundary.
126
+
127
+ ### Editable SKU
128
+
129
+ Product `sku` is mutable. The internal canonical reference for every
130
+ cross-module link (assets, links, RFQ items, ...) is the `Product.id`
131
+ UUID, which never changes. Updating the SKU writes an audit row and
132
+ refuses with `409 sku_in_use` if the new value already belongs to
133
+ another product.
134
+
135
+ ### Attribute Set swap
136
+
137
+ When an operator assigns a different Attribute Set to a Product, the
138
+ admin form re-renders to show only the new Set's attributes. Values
139
+ for attributes outside the new Set stay in the JSONB column server-side
140
+ — switching back surfaces them again. The
141
+ `attribute-set-preview` endpoint lets the editor warn the operator
142
+ which fields will be hidden vs. retained before they confirm.
143
+
144
+ ### Cross-module read surface
145
+
146
+ Two methods on `CatalogQueryService` cross module boundaries (the
147
+ documented service ports):
148
+
149
+ - `comparableAttributeKeys(): string[]` — Compare
150
+ - `promoRuleAttributeKeys(): string[]` + `getAttributeWithOptions(key)`
151
+ — Promotions
152
+ - `buildVisibleAttributesProjection()` — internal, used by the PDP
153
+ detail response to assemble the `visibleAttributes[]` payload
154
+
155
+ ## Attributes as Custom Field extensions
156
+
157
+ The attribute definition store has converged onto the generic Custom
158
+ Fields layer that the `custom_fields` module owns, adapter-shaped rather
159
+ than rewritten. Nothing changed on the
160
+ HTTP surface — every endpoint above keeps its shape — but the storage
161
+ and ownership model is different:
162
+
163
+ - **A product attribute is a catalog extension of a product-host Custom
164
+ Field definition.** The generic identity (`key`, per-locale `label` +
165
+ `labelDefault`, `valueType`, `required`) lives on a
166
+ `custom_field_definitions` row with `entity_type = 'product'`. The
167
+ `product_attributes` table remains, rebuilt as a thin 1:1 extension
168
+ row (`custom_field_definition_id` UNIQUE FK) carrying only the
169
+ catalog behaviour flags (`isSearchable`, `isFilterable`,
170
+ `isVariantAxis`, `displayAsSlider`, `isComparable`,
171
+ `quickSearchable`, `isPromoRule`, `filterPosition`,
172
+ `isVisibleOnProductPage`, `channelScoped`, `languageScoped`,
173
+ `massEditable`) plus two presentation refinements (`selectDisplay`,
174
+ `numericKind`) that keep the legacy `enum`/`select` and
175
+ `number`/`price` distinctions lossless. **Flags stay catalog-owned**
176
+ — the generic core never interprets them.
177
+ - **Options live in `custom_field_options`.** The catalog-owned
178
+ `attribute_options` table is gone; option lists are ordinary Custom
179
+ Field option rows on the product-host definition.
180
+ - **Single write surface: `/catalog/attributes`.** Attribute and
181
+ option mutations are catalog Commands that create/update/delete the
182
+ definition and the extension together in one transaction (one audit
183
+ row), using the transactional apply seam exported by
184
+ `custom_fields`. The generic Custom Fields admin surface lists
185
+ product definitions read-only and refuses mutations with
186
+ `409 host_managed`.
187
+ - **Migration `102_attributes_on_custom_fields.ts`** performed the
188
+ one-time convergence in a single transaction: backfilled one
189
+ definition per legacy attribute (key, labels, mapped value type,
190
+ required, deterministic sort order), moved `attribute_options` rows
191
+ into `custom_field_options`, re-keyed `attribute_set_attributes` to
192
+ definition ids, added `custom_field_definition_id` /
193
+ `select_display` / `numeric_kind` to `product_attributes`, dropped
194
+ the duplicated columns (`key`, `label`, `label_default`,
195
+ `value_type`, `is_required`), and dropped `attribute_options`. The
196
+ migration is reversible (`down()` restores the legacy shape) and
197
+ aborts loudly on a reserved-key collision.
198
+ - **Attribute values did not move** — `products.attribute_values`,
199
+ `product_variants.variant_attribute_values`, and
200
+ `product_value_overrides` keep their shape and catalog ownership
201
+ (the host owns its data).
202
+
203
+ Internal consumers (search, quick order, comparisons, bulk edit, the
204
+ promotions port, the scope editor) read attributes through the
205
+ catalog-exported `CatalogAttributeReadService`, which composes the
206
+ definition and the extension into the legacy-shaped
207
+ `CatalogAttributeView`.