@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,96 @@
1
+ /**
2
+ * The port interfaces `catalog` publishes whose signature carries the caller's
3
+ * `EntityManager`, and **nothing that exists at runtime** (D-169, D-171).
4
+ *
5
+ * `tsc` compiles this file to `export {};`. That is the property D-171 makes
6
+ * the boundary decision on — *a subpath is contract surface iff the module it
7
+ * resolves to exports no runtime binding* — so a consumer naming this subpath
8
+ * names a declaration and can name nothing else. Every other `catalog` port is
9
+ * declared in `@endora-commerce/contracts`; one lands here only when its
10
+ * signature stops it living there: that package is compiled by `admin` and
11
+ * `storefront`, and FR-034 keeps it free of `@mikro-orm` imports.
12
+ *
13
+ * No entity class leaves by this door, type-only included (D-168).
14
+ */
15
+ import type { EntityManager } from '@mikro-orm/postgresql';
16
+ /** A baseline value removed from under a rename's destination. */
17
+ export interface CatalogDisplacedAttributeValue {
18
+ readonly productId: string;
19
+ readonly value: unknown;
20
+ }
21
+ /** An override row removed from under a rename's destination. */
22
+ export interface CatalogDisplacedValueOverride {
23
+ readonly productId: string;
24
+ readonly channelId: string;
25
+ readonly languageCode: string | null;
26
+ /** The stored `{ v: … }` wrapper, as it was. */
27
+ readonly value: unknown;
28
+ }
29
+ /**
30
+ * What a rename does when something is already stored under `toKey`. There is
31
+ * no default: the caller states the choice, for the reason D-169 makes the
32
+ * `EntityManager` required — a default is a decision the caller never saw.
33
+ *
34
+ * - `refuse` — throw `target_occupied`, naming the key and both counts, before
35
+ * anything is written.
36
+ * - `displace` — remove everything under `toKey`, baseline values and override
37
+ * rows alike, on every product that holds it, and return it in
38
+ * {@link CatalogAttributeValueKeyMove.displaced}; then move the key.
39
+ */
40
+ export interface CatalogAttributeValueKeyRenameOptions {
41
+ readonly occupied: 'refuse' | 'displace';
42
+ }
43
+ /** How many rows a value-key rename moved, per place the key is stored. */
44
+ export interface CatalogAttributeValueKeyMove {
45
+ /** Products whose `attribute_values` map carried the old key. */
46
+ readonly products: number;
47
+ /** Channel/locale override rows keyed by the old key. */
48
+ readonly overrides: number;
49
+ /** What `displace` removed from under `toKey`; both lists empty otherwise. */
50
+ readonly displaced: {
51
+ readonly values: readonly CatalogDisplacedAttributeValue[];
52
+ readonly overrides: readonly CatalogDisplacedValueOverride[];
53
+ };
54
+ }
55
+ /**
56
+ * Container name: `catalogAttributeValueKeyPort`. Owner: `catalog`.
57
+ *
58
+ * Renames an attribute's **value key** everywhere the catalogue stores it as a
59
+ * string — the `products.attribute_values` JSONB map and
60
+ * `product_value_overrides.attribute_key` — inside the **caller's**
61
+ * transaction (`specs/134-paid-module-extraction/research.md` D12, *Ergonode
62
+ * fallback boundary*).
63
+ *
64
+ * It is one half of a key rename and never the whole of one. The definition's
65
+ * key is `custom_fields`' row, renamed through `applyRenameKey` on
66
+ * `@endora-commerce/mod-custom-fields/ports` in the same transaction, and the
67
+ * caller publishes that seam's invalidation after it commits. The seam exists
68
+ * for a module repairing keys **it derived itself** — the connector owns which
69
+ * rows are candidates and where each one goes; `catalog` owns the rows.
70
+ *
71
+ * **The `EntityManager` is required and never optional** (D-169): the two
72
+ * statements and the definition rename must commit or roll back together, and
73
+ * an optional transaction lets a caller hand one to an implementation that
74
+ * ignores it and receive a silently non-atomic write.
75
+ *
76
+ * **Data already under `toKey`** is never merged into the moved value and never
77
+ * left to collide with it (`specs/134-paid-module-extraction/research.md` D12,
78
+ * *what a rename does to data already under its destination*). The caller
79
+ * chooses with {@link CatalogAttributeValueKeyRenameOptions}: `refuse` writes
80
+ * nothing, `displace` removes it — on every product that holds it, not only
81
+ * those also holding `fromKey` — and hands it back so the caller can record
82
+ * it. The destination rows are read `for update`, on the caller's transaction,
83
+ * before anything is written.
84
+ *
85
+ * The moves are conditioned on `fromKey`, so re-running is a no-op; a rename
86
+ * to the same key issues nothing. `name`, `description` and any key outside
87
+ * the attribute grammar are refused before a statement is issued.
88
+ *
89
+ * **Owner off:** the seam fails closed — resolving this port throws
90
+ * `ModuleDisabledError`, so nothing half-executes. Whether `catalog` has an off
91
+ * state at all is its manifest's `activation` to say, not this line's.
92
+ */
93
+ export interface CatalogAttributeValueKeyApi {
94
+ renameValueKey(em: EntityManager, fromKey: string, toKey: string, options: CatalogAttributeValueKeyRenameOptions): Promise<CatalogAttributeValueKeyMove>;
95
+ }
96
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,kEAAkE;AAClE,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,iEAAiE;AACjE,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qCAAqC;IACpD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU,CAAC;CAC1C;AAED,2EAA2E;AAC3E,MAAM,WAAW,4BAA4B;IAC3C,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE;QAClB,QAAQ,CAAC,MAAM,EAAE,SAAS,8BAA8B,EAAE,CAAC;QAC3D,QAAQ,CAAC,SAAS,EAAE,SAAS,6BAA6B,EAAE,CAAC;KAC9D,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CACZ,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,qCAAqC,GAC7C,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC1C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Product Attachments
3
+ ---
4
+
5
+ # Product Attachments
6
+
7
+ Product Attachments are downloadable files (certificates, technical
8
+ specifications, product cards, generic PDFs) attached to a Product.
9
+ They wrap an underlying `Asset` row, so the same file can be linked to
10
+ multiple Products without duplication — the `assets` row survives even
11
+ when an attachment is deleted.
12
+
13
+ ## Attachment Types
14
+
15
+ The `attachment_types` dictionary categorizes attachments. Migration
16
+ 021 seeds four standard rows with deterministic UUIDs so seeds and
17
+ tests can reference them stably:
18
+
19
+ - `certificate` — Certificate / Certyfikat
20
+ - `tech_spec` — Technical specification / Specyfikacja techniczna
21
+ - `product_card` — Product card / Karta produktu
22
+ - `pdf` — generic PDF
23
+
24
+ Admins can add custom types via the **Attachment Types** page in the
25
+ admin panel. Deleting a type that any product still references returns
26
+ `409 ATTACHMENT_TYPE_IN_USE`.
27
+
28
+ ## Public surface
29
+
30
+ | Verb + Path | Audience | Purpose |
31
+ | --- | --- | --- |
32
+ | `GET /api/v1/admin/catalog/attachment-types` | admin | List types with usage counts |
33
+ | `POST /api/v1/admin/catalog/attachment-types` | admin | Create custom type |
34
+ | `PATCH /api/v1/admin/catalog/attachment-types/:id` | admin | Update name / position |
35
+ | `DELETE /api/v1/admin/catalog/attachment-types/:id` | admin | Delete; rejects when in use |
36
+ | `GET /api/v1/admin/catalog/products/:id/attachments` | admin | List attachments on a product |
37
+ | `POST /api/v1/admin/catalog/products/:id/attachments` | admin | Attach an existing Asset |
38
+ | `PATCH /api/v1/admin/catalog/products/:id/attachments/:attachmentId` | admin | Update name / description / type / position |
39
+ | `DELETE /api/v1/admin/catalog/products/:id/attachments/:attachmentId` | admin | Remove (Asset survives) |
40
+
41
+ ## Errors
42
+
43
+ | Code | Status | When |
44
+ | --- | --- | --- |
45
+ | `ASSET_KIND_NOT_SUPPORTED` | 400 | Attachment Assets must be `pdf`, `certificate`, or `other` (image/video are gallery-only) |
46
+ | `ATTACHMENT_TYPE_IN_USE` | 409 | Delete blocked by usage |
47
+ | `ATTACHMENT_TYPE_CODE_TAKEN` | 409 | `code` collision on create |
48
+ | `ATTACHMENT_TYPE_NOT_FOUND` | 404 | `:id` missing |
49
+ | `ATTACHMENT_NOT_FOUND` | 404 | `:attachmentId` missing |
50
+
51
+ ## Storefront integration
52
+
53
+ `productDetail.attachments[]` carries the full type and Asset metadata
54
+ (filename, sizeBytes, mimeType, url). The `<AttachmentsList>` component
55
+ groups by AttachmentType (preserving first-seen order so admin-curated
56
+ positions shape the section ordering), renders one heading per type,
57
+ and emits download anchors with `download={filename}` for clean file
58
+ names.
59
+
60
+ ## Storage
61
+
62
+ `attachment_types` (id, code unique, name jsonb, position) +
63
+ `product_attachments` (id, product_id FK CASCADE, asset_id FK RESTRICT,
64
+ attachment_type_id FK RESTRICT, name, description, position).
65
+
66
+ The Foundation has no public `AssetsService` — the catalog uses direct
67
+ `Asset` entity imports across modules. An admin Asset upload UI is a
68
+ foundation-level follow-up; until then, admins paste an existing
69
+ asset id when creating an attachment.
@@ -0,0 +1,63 @@
1
+ ---
2
+ title: Attribute Sets
3
+ ---
4
+
5
+ # Attribute Sets
6
+
7
+ Attribute Sets group `ProductAttribute` definitions into reusable
8
+ schemas. Every Product is wired to exactly one Attribute Set; the system
9
+ ships a `default` set that applies when admins do not pick one.
10
+
11
+ ## Why they exist
12
+
13
+ Without sets, every Product carries its full attribute graph in
14
+ `attributeValues`. Different product types (electronics, apparel,
15
+ chemicals) need different attributes, but the foundation schema
16
+ treated every key as global. Sets let admins curate a focused authoring
17
+ experience per category and let the storefront render a tighter
18
+ attribute table.
19
+
20
+ ## Public surface
21
+
22
+ | Verb + Path | Audience | Purpose |
23
+ | --- | --- | --- |
24
+ | `GET /api/v1/admin/catalog/attribute-sets` | admin | List all sets including `default` |
25
+ | `GET /api/v1/admin/catalog/attribute-sets/:id` | admin | Set detail with assigned attributes |
26
+ | `POST /api/v1/admin/catalog/attribute-sets` | admin | Create custom set (`code` snake_case, unique) |
27
+ | `PATCH /api/v1/admin/catalog/attribute-sets/:id` | admin | Update name; system Default is immutable |
28
+ | `DELETE /api/v1/admin/catalog/attribute-sets/:id` | admin | Delete; rejects with 409 when any Product references the set |
29
+ | `POST /api/v1/admin/catalog/attribute-sets/:id/attributes` | admin | Assign attributes to the set |
30
+ | `DELETE /api/v1/admin/catalog/attribute-sets/:id/attributes/:key` | admin | Unassign |
31
+
32
+ The Product create/update payload accepts `attributeSetId`. When
33
+ omitted, the system Default Set is used.
34
+
35
+ ## Errors
36
+
37
+ | Code | Status | When |
38
+ | --- | --- | --- |
39
+ | `ATTRIBUTE_SET_CODE_TAKEN` | 409 | `code` already exists |
40
+ | `ATTRIBUTE_SET_IN_USE` | 409 | Delete blocked: at least one Product references the set |
41
+ | `SYSTEM_ATTRIBUTE_SET_IMMUTABLE` | 409 | Mutating the seeded Default Set |
42
+ | `ATTRIBUTE_SET_NOT_FOUND` | 404 | `:id` missing |
43
+ | `ATTRIBUTE_NOT_FOUND` | 404 | `:key` missing on assignment |
44
+
45
+ ## Storefront integration
46
+
47
+ `productDetail.attributeSet` carries `{id, code, name}` so themes can
48
+ render the set label above the attribute table. Foundation reference
49
+ theme renders the localized set name as a small subheading.
50
+
51
+ ## Storage
52
+
53
+ `attribute_sets` (id, code unique, name jsonb, is_system bool) +
54
+ `attribute_set_attributes` (composite PK on (set_id, attribute_id)).
55
+ The system Default row is seeded by migration 017 with deterministic
56
+ UUID `defa0017-0000-4000-8000-000000000000` so seeds and tests can
57
+ reference it stably.
58
+
59
+ ## Audit log
60
+
61
+ AttributeSet CRUD writes one `AuditLogEntry` per mutation with
62
+ `stateBefore` and `stateAfter` so the audit page surfaces who changed
63
+ what.
@@ -0,0 +1,244 @@
1
+ ---
2
+ title: Attributes
3
+ ---
4
+
5
+ # Attributes
6
+
7
+ An **Attribute** is a single named property that can be attached to a
8
+ Product — `color`, `gear_ratio`, `material`, `weight`. Attributes
9
+ are the lower-level primitive consumed by everything that decorates a
10
+ product: the storefront filter sidebar, the PDP "Parametry produktu"
11
+ tab, the Compare page, the Promotion Rule editor, the search index,
12
+ and the configurable-product variant picker.
13
+
14
+ Attributes are grouped into [Attribute Sets](./attribute-sets.md) which
15
+ are then pinned to a Product so the admin editor renders just the
16
+ fields that family actually needs. This page covers the attribute
17
+ itself; the set ↔ product wiring lives on the Attribute Sets page.
18
+
19
+ ## Anatomy
20
+
21
+ Every attribute carries:
22
+
23
+ | Field | Purpose |
24
+ | --- | --- |
25
+ | `key` | Stable, URL-safe identifier — `^[a-z][a-z0-9_]*$`, unique platform-wide (case-insensitive). Used in filter URLs, search payloads, and the product JSONB key. |
26
+ | `label` | `Record<bcp47-tag, string>` per-locale label map. |
27
+ | `labelDefault` | Fallback label used when the active storefront / admin locale is missing from `label`. |
28
+ | `valueType` | One of `string`, `number`, `boolean`, `price`, `date`, `select`, `multiselect`, `enum`. |
29
+ | `options[]` | Ordered list of selectable options — only for `select` / `multiselect` / `enum`. See [Option lists](#option-lists) below. |
30
+
31
+ Plus the behavioural flags and a numeric position:
32
+
33
+ | Flag | Default | Consumed by |
34
+ | --- | --- | --- |
35
+ | `isFilterable` | `false` | Storefront filter sidebar |
36
+ | `isSearchable` | `false` | Search indexer (Meilisearch) |
37
+ | `isComparable` | `false` | Storefront Compare page |
38
+ | `isVariantAxis` | `false` | Configurable-product variant picker |
39
+ | `isRequired` | `false` | Product-save validator (only when the attribute is in the assigned Attribute Set) |
40
+ | `isPromoRule` | `false` | Promotion Rule criterion picker |
41
+ | `isVisibleOnProductPage` | `false` | PDP "Parametry produktu" tab |
42
+ | `displayAsSlider` | `false` | Storefront sidebar — renders a range slider; only valid for `valueType ∈ ('number','price')` |
43
+ | `filterPosition` | `0` | Storefront sidebar sort key (ascending; ties broken alphabetically by the resolved label) |
44
+
45
+ ## Value types
46
+
47
+ | Type | Storage | Notes |
48
+ | --- | --- | --- |
49
+ | `string` | string | Free-text. |
50
+ | `number` | number | Numeric; supports `displayAsSlider` for range filters. |
51
+ | `boolean` | boolean | Two-state. |
52
+ | `price` | number | Money amount — formatted in the active currency / locale. Supports `displayAsSlider`. |
53
+ | `date` | ISO 8601 date string | |
54
+ | `select` | option `value` | Single choice from `options[]`. At most one option may carry `isDefault = true`. |
55
+ | `multiselect` | array of option `value`s | Multiple choices from `options[]`. Any number of options may carry `isDefault = true`. |
56
+ | `enum` | option `value` | Same storage shape as `select`; renders as a compact pill / segmented control on storefront filters and the PDP rather than a dropdown. |
57
+
58
+ Changing `valueType` while any product carries a value the new type
59
+ cannot represent is refused with `attribute_type_change_unsafe`.
60
+
61
+ ## Option lists
62
+
63
+ For `select` / `multiselect` / `enum` attributes the option list is
64
+ authored inline on the attribute editor. Each option carries:
65
+
66
+ | Field | Purpose |
67
+ | --- | --- |
68
+ | `value` | Stable identifier — `^[a-z0-9_-]{1,200}$`, unique within the attribute. Encoded into filter URLs and stored on every product that carries that value. |
69
+ | `label` | `Record<bcp47-tag, string>` per-locale label map. |
70
+ | `labelDefault` | Fallback label when the active locale is missing from `label`. |
71
+ | `isDefault` | Optional pre-selection on new products. `select` / `enum` allow at most one; `multiselect` allows any number. |
72
+ | `sortOrder` | Render order; ties broken by `value` ASC. |
73
+
74
+ Option **values** are immutable while any product still carries them —
75
+ the operator must migrate dependent values first. Option **labels** can
76
+ always be renamed. Deleting an option is refused with
77
+ `409 option_in_use` while any product still carries that value.
78
+
79
+ ## Public surface
80
+
81
+ Admin routes are gated by `catalog:read` (list / get) /
82
+ `catalog:write` (mutations).
83
+
84
+ | Verb + Path | Audience | Purpose |
85
+ | --- | --- | --- |
86
+ | `GET /api/v1/admin/catalog/attributes` | admin | List attributes |
87
+ | `GET /api/v1/admin/catalog/attributes/by-flag?flag=isPromoRule\|isComparable\|...` | admin | Picker payload — every attribute carrying the requested flag |
88
+ | `GET /api/v1/admin/catalog/attributes/:idOrKey` | admin | Single attribute read |
89
+ | `POST /api/v1/admin/catalog/attributes` | admin | Create attribute (accepts the flags + inline `options[]` for select-style types) |
90
+ | `PATCH /api/v1/admin/catalog/attributes/:key` | admin | Update labels and hot-toggle `isFilterable` / `isSearchable` / `isVariantAxis` / `isPromoRule` / `isComparable` / `isVisibleOnProductPage` / `isRequired` / `filterPosition`. Re-emits `attribute.updated.v1`. |
91
+ | `DELETE /api/v1/admin/catalog/attributes/:idOrKey` | admin | Delete; refused with `409 attribute_in_use_by_set` while any Attribute Set still references it |
92
+ | `GET /api/v1/admin/catalog/attributes/:idOrKey/options` | admin | List option rows for select / enum / multiselect attributes |
93
+ | `POST /api/v1/admin/catalog/attributes/:idOrKey/options` | admin | Append an option |
94
+ | `PATCH /api/v1/admin/catalog/attribute-options/:optionId` | admin | Patch `label` / `labelDefault` / `isDefault` / `sortOrder` (option `value` is immutable) |
95
+ | `DELETE /api/v1/admin/catalog/attribute-options/:optionId` | admin | Remove; refused with `409 option_in_use` while any product still carries the value |
96
+
97
+ Filter sidebar reads consume `GET /api/v1/catalog/filters` (defined on
98
+ the [Catalog](../catalog.md) page) — that endpoint resolves the
99
+ currently visible filterable attributes per Sales Channel and orders
100
+ them by `filterPosition`.
101
+
102
+ ## Errors
103
+
104
+ | Code | Status | When |
105
+ | --- | --- | --- |
106
+ | `attribute_key_invalid` | 400 | `key` does not match `^[a-z][a-z0-9_]*$` |
107
+ | `duplicate_key` | 409 | Attribute key already exists (case-insensitive) |
108
+ | `attribute_in_use_by_set` | 409 | Delete blocked: at least one Attribute Set still references the attribute |
109
+ | `attribute_type_change_unsafe` | 409 | `valueType` change refused — some product carries a value the new type cannot represent |
110
+ | `default_option_ambiguous` | 409 | More than one option flagged `isDefault = true` on a `select` / `enum` attribute |
111
+ | `option_in_use` | 409 | Delete (or value rename) blocked: a product still carries this option value |
112
+ | `attribute_not_found` | 404 | `:idOrKey` missing |
113
+ | `option_not_found` | 404 | `:optionId` missing |
114
+
115
+ ## Storefront integration
116
+
117
+ ### Filter sidebar
118
+
119
+ Category and search pages render a chip per `isFilterable` attribute
120
+ that has at least one value across the currently visible products.
121
+ Chips appear ordered by `filterPosition` ascending, ties broken
122
+ alphabetically by the resolved per-locale label. Attributes with no
123
+ values across the current page are omitted (no empty filter).
124
+
125
+ ### PDP "Parametry produktu" tab
126
+
127
+ Every PDP carries a `Parametry produktu` tab that lists every
128
+ attribute meeting both:
129
+
130
+ - the product has a value for the attribute, **and**
131
+ - the attribute is flagged `isVisibleOnProductPage = true`.
132
+
133
+ For `select` / `multiselect` / `enum` values the tab renders the
134
+ option's per-locale label, not the raw `value`. The detail payload
135
+ field is assembled by `CatalogQueryService.buildVisibleAttributesProjection()`.
136
+
137
+ ### Search index
138
+
139
+ Toggling `isSearchable` propagates into the Meilisearch indexer's
140
+ payload on the next refresh cycle. Textual types (`string`, plus the
141
+ option labels of `select` / `multiselect` / `enum`) feed the lexical
142
+ and (when enabled) semantic index; numeric / boolean / price / date
143
+ types feed range / exact-match filters.
144
+
145
+ ### Compare page
146
+
147
+ Comparison rows on the storefront Compare page list every attribute
148
+ flagged `isComparable = true` for which at least one product in the
149
+ comparison carries a value. The list is sourced via
150
+ `CatalogQueryService.comparableAttributeKeys()`.
151
+
152
+ ### Variant picker
153
+
154
+ Configurable products expose a variant picker whose axes come from the
155
+ attributes flagged `isVariantAxis = true` on the product's currently
156
+ assigned Attribute Set.
157
+
158
+ ## Storage
159
+
160
+ Since migration `102` an attribute is split between the
161
+ generic Custom Fields layer that the `custom_fields` module owns and
162
+ a catalog-owned extension row. The API shape above is unchanged — the
163
+ admin surface composes the two back into the legacy form.
164
+
165
+ `custom_field_definitions` (owned by `custom_fields`, rows with
166
+ `entity_type = 'product'`):
167
+
168
+ - `id uuid PK`, `key` (unique per entity type), `label jsonb`,
169
+ `label_default`, `value_type` (generic six-type set: `text` |
170
+ `number` | `boolean` | `date` | `select` | `multiselect`),
171
+ `required`, `sort_order`. The legacy eight-value `valueType` form is
172
+ derived bijectively from the generic type plus the extension
173
+ refinements below (`enum` = `select` + `select_display='pill'`,
174
+ `price` = `number` + `numeric_kind='price'`, ...).
175
+
176
+ `custom_field_options` (owned by `custom_fields`):
177
+
178
+ - Option rows keyed UNIQUE `(definition_id, value)` with per-locale
179
+ `label`, `label_default`, `is_default`, `sort_order`. The
180
+ catalog-owned `attribute_options` table (migration `032`) was dropped
181
+ by migration `102` after its rows moved here.
182
+
183
+ `product_attributes` (owned by `catalog`) — the 1:1 **extension**:
184
+
185
+ - `id uuid PK` (stable — admin API attribute ids survived the
186
+ migration), `custom_field_definition_id uuid NOT NULL UNIQUE` FK →
187
+ `custom_field_definitions.id` ON DELETE RESTRICT.
188
+ - Boolean flags: `is_searchable`, `is_filterable`, `is_variant_axis`,
189
+ `is_comparable`, `quick_searchable`, `is_promo_rule`,
190
+ `is_visible_on_product_page`, `display_as_slider`,
191
+ `channel_scoped`, `language_scoped`, `mass_editable`.
192
+ - `filter_position int NOT NULL DEFAULT 0`.
193
+ - Presentation refinements: `select_display varchar(16) NULL`
194
+ (`pill` = legacy `enum`, `dropdown` = legacy `select`) and
195
+ `numeric_kind varchar(8) NULL` (`number` | `price`).
196
+ - The duplicated definition columns (`key`, `label`, `label_default`,
197
+ `value_type`, `is_required`) were dropped by migration `102` — the
198
+ definition row is the single source of truth for them.
199
+
200
+ `products.attribute_values jsonb` carries the per-product map keyed by
201
+ attribute `key`; the values themselves have never moved out of it.
202
+ Values are retained server-side even when the attribute leaves the
203
+ product's currently assigned Attribute Set — switching back surfaces
204
+ them again.
205
+
206
+ All attribute and option mutations flow through the catalog Commands
207
+ behind `/catalog/attributes` — the generic Custom Fields admin surface
208
+ lists product definitions read-only and refuses mutations with
209
+ `409 host_managed`.
210
+
211
+ ## Events emitted
212
+
213
+ - `attribute.updated.v1` — picked up by the search indexer and
214
+ bridged to webhook subscribers.
215
+
216
+ ## Audit log
217
+
218
+ Attribute and option CRUD writes one `AuditLogEntry` per mutation
219
+ with `stateBefore` and `stateAfter` so the audit page surfaces who
220
+ flipped which flag.
221
+
222
+ ## Cross-module consumers
223
+
224
+ Three methods on `CatalogQueryService` are the documented service
225
+ ports other modules call — a module never reaches into catalog
226
+ internals:
227
+
228
+ - `comparableAttributeKeys(): string[]` — the storefront Compare page.
229
+ - `promoRuleAttributeKeys(): string[]` + `getAttributeWithOptions(key)`
230
+ — the Promotion Rule criterion picker and resolver.
231
+ - `buildVisibleAttributesProjection()` — internal, used by the PDP
232
+ detail response to assemble the `visibleAttributes[]` payload.
233
+
234
+ ## See also
235
+
236
+ - [Attribute Sets](./attribute-sets.md) — bundling attributes into
237
+ per-family schemas pinned to a Product.
238
+ - [Catalog](../catalog.md) — the parent module, including the
239
+ storefront `GET /api/v1/catalog/filters` endpoint that consumes
240
+ `filterPosition`.
241
+ - The `search` module's Meilisearch indexer, which consumes the
242
+ `isSearchable` flag.
243
+ - The `promotions` module's Promotion Rule editor, which consumes the
244
+ `isPromoRule` flag.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Composite Products
3
+ ---
4
+
5
+ # Composite Products: grouped, bundle, virtual
6
+
7
+ The Catalog supports three product types beyond `simple` /
8
+ `configurable`. Each carries a type-discriminated payload on the PDP
9
+ and renders a dedicated component in the storefront's action zone.
10
+
11
+ ## Types
12
+
13
+ ### Grouped
14
+
15
+ A parent product with a fixed list of child products at fixed
16
+ quantities. The buyer cannot configure which children — they buy the
17
+ whole set. Use case: "Starter kit" SKU that bundles two simple
18
+ products at quantities 2 and 1.
19
+
20
+ ### Bundle
21
+
22
+ A parent product with named slots, each with min / max quantity ranges
23
+ and one or more option products to choose between. Use case:
24
+ configurable workstation where the buyer picks a CPU (slot, min=1
25
+ max=1) and add-on accessories (slot, min=0 max=3).
26
+
27
+ ### Virtual
28
+
29
+ A digital product with `downloadAssetId` (server-hosted file) **or**
30
+ `downloadUrl` (external link) — exactly one of the two MUST be set
31
+ (Zod refine + service-layer guard). Use case: e-book PDF, license key
32
+ fulfilment, link to a third-party download portal.
33
+
34
+ ## No nested composites
35
+
36
+ A grouped product cannot include another grouped or bundle product as
37
+ a child. A bundle's slot options cannot be grouped or bundle products
38
+ either. The rule is enforced at the service layer because PostgreSQL
39
+ CHECK constraints cannot JOIN `products` to inspect the foreign row's
40
+ `type`. Surfaces as `NESTED_COMPOSITE_NOT_ALLOWED` (400).
41
+
42
+ ## Public surface
43
+
44
+ ### Admin
45
+
46
+ | Verb + Path | Purpose |
47
+ | --- | --- |
48
+ | `GET / POST / PATCH / DELETE /products/:id/grouped-items[/:itemId]` | Grouped CRUD |
49
+ | `GET / POST / PATCH / DELETE /products/:id/bundle-slots[/:slotId]` | Bundle slot CRUD |
50
+ | `POST / DELETE /products/:id/bundle-slots/:slotId/options[/:optionId]` | Slot option CRUD |
51
+
52
+ ### Storefront
53
+
54
+ | Verb + Path | Purpose |
55
+ | --- | --- |
56
+ | `POST /api/v1/catalog/products/:idOrSlug/bundle-configuration/validate` | Pure compute: validate a buyer's bundle configuration |
57
+
58
+ The validation endpoint is the only POST on the public catalog surface.
59
+ It returns `{valid, errors[], resolvedSelections}` rather than
60
+ non-2xx — the storefront wants the structured error list so it can
61
+ highlight every offending slot.
62
+
63
+ ## Validation errors (inside the response envelope)
64
+
65
+ | Code | When |
66
+ | --- | --- |
67
+ | `MIN_NOT_MET` | Total selected qty for a slot < `minQuantity` |
68
+ | `MAX_EXCEEDED` | Total selected qty for a slot > `maxQuantity` |
69
+ | `UNKNOWN_OPTION` | `optionId` not part of the slot |
70
+
71
+ HTTP-level rejections (400 `PRODUCT_TYPE_MISMATCH` when the product is
72
+ not a bundle, 404 `PRODUCT_NOT_FOUND`) still apply.
73
+
74
+ ## CRUD-level errors
75
+
76
+ | Code | Status | When |
77
+ | --- | --- | --- |
78
+ | `PRODUCT_TYPE_MISMATCH` | 400 | Calling grouped/bundle endpoints against the wrong parent type |
79
+ | `NESTED_COMPOSITE_NOT_ALLOWED` | 400 | Child / option product is itself grouped or bundle |
80
+ | `INVALID_QUANTITY_RANGE` | 400 | Slot `minQuantity > maxQuantity` |
81
+ | `OPTION_ALREADY_EXISTS` | 409 | Same option product reused in a slot |
82
+ | `GROUPED_ITEM_NOT_FOUND` | 404 | `:itemId` missing |
83
+ | `BUNDLE_SLOT_NOT_FOUND` | 404 | `:slotId` missing |
84
+ | `BUNDLE_SLOT_OPTION_NOT_FOUND` | 404 | `:optionId` missing |
85
+
86
+ ## Storefront integration
87
+
88
+ `productDetail` carries one of three branches based on `type`:
89
+
90
+ - `groupedItems[]` when type='grouped' — `{id, position, quantity, product{...}}`
91
+ - `bundleSlots[]` when type='bundle' — `{id, name, minQuantity, maxQuantity, position, options: [{id, defaultQuantity, position, product{...}}]}`
92
+ - `virtual` when type='virtual' — `{downloadAssetId, downloadUrl}`
93
+
94
+ The PDP type-switches the action zone:
95
+
96
+ | `product.type` | Component | UX |
97
+ | --- | --- | --- |
98
+ | `simple`, `configurable` | legacy Add-to-cart + RFQ + VariantPicker | unchanged |
99
+ | `grouped` | `<GroupedSummary>` | Read-only list + "Add bundle to cart" CTA |
100
+ | `bundle` | `<BundleConfigurator>` | Per-slot select + qty input; CTA disabled when any slot is required |
101
+ | `virtual` | `<VirtualCta>` | "Buy and download" CTA + delivery copy |
102
+
103
+ ## Storage
104
+
105
+ - `grouped_items` (id, parent_product_id FK CASCADE, child_product_id
106
+ FK RESTRICT, quantity, position; UNIQUE (parent, child); CHECK
107
+ `quantity > 0` + `parent <> child`)
108
+ - `bundle_slots` (id, parent_product_id FK CASCADE, name jsonb,
109
+ minQuantity / maxQuantity int; CHECK `min_quantity <= max_quantity` +
110
+ `min_quantity >= 0` + `max_quantity > 0`)
111
+ - `bundle_slot_options` (id, slot_id FK CASCADE, option_product_id FK
112
+ RESTRICT, default_quantity, position; UNIQUE (slot, option_product))
113
+
114
+ Virtual download fields live on the `products` table itself:
115
+ `download_asset_id` FK nullable + `download_url` varchar nullable.
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: Product Gallery
3
+ ---
4
+
5
+ # Product Gallery
6
+
7
+ Each Product can carry a curated gallery of image and video Assets, with
8
+ three labels that pin the storefront's resolution chain:
9
+
10
+ - **Base Image** — the primary hero shown on the PDP. Exactly one per Product.
11
+ - **Small Image** — emphasized in the thumbnail strip. Exactly one per Product.
12
+ - **Thumbnail** — the listing-card image. Exactly one per Product.
13
+
14
+ Each Gallery Item can carry **one to three** of these labels (the same
15
+ asset can be both Base Image and Thumbnail, for example). The "exactly
16
+ one per Product per label" invariant is enforced by a database
17
+ `UNIQUE (product_id, label)` constraint, so collisions surface as a
18
+ typed conflict error rather than silent data corruption.
19
+
20
+ ## Public surface
21
+
22
+ | Verb + Path | Audience | Purpose |
23
+ | --- | --- | --- |
24
+ | `GET /api/v1/admin/catalog/products/:id/gallery` | admin | List the Product's gallery |
25
+ | `POST /api/v1/admin/catalog/products/:id/gallery` | admin | Attach an existing image/video Asset with optional labels |
26
+ | `POST .../gallery?replace=true` | admin | Atomic-swap: remove conflicting labels from other items in this Product, then assign |
27
+ | `PATCH /api/v1/admin/catalog/products/:id/gallery/:itemId` | admin | Update labels (same `?replace=true` toggle) |
28
+ | `DELETE /api/v1/admin/catalog/products/:id/gallery/:itemId` | admin | Remove from gallery (Asset row survives) |
29
+ | `PUT /api/v1/admin/catalog/products/:id/gallery/order` | admin | Reorder by id list |
30
+
31
+ ## Atomic label swap
32
+
33
+ Without the `?replace=true` query, assigning a label that already exists
34
+ on another item returns `409 GALLERY_LABEL_ALREADY_TAKEN`. With it, the
35
+ service drops the conflicting assignments first then inserts the new
36
+ one — all in a single transaction, so concurrent admins never see a
37
+ half-applied state.
38
+
39
+ ## Errors
40
+
41
+ | Code | Status | When |
42
+ | --- | --- | --- |
43
+ | `GALLERY_LABEL_ALREADY_TAKEN` | 409 | Label conflict without `?replace=true` |
44
+ | `GALLERY_LABEL_LIMIT_EXCEEDED` | 400 | More than 3 labels on a single item |
45
+ | `ASSET_KIND_NOT_SUPPORTED` | 400 | Asset kind ∉ `{image, video}` |
46
+ | `GALLERY_ITEM_NOT_FOUND` | 404 | `:itemId` missing |
47
+ | `PRODUCT_NOT_FOUND` | 404 | `:id` missing |
48
+
49
+ ## Storefront integration
50
+
51
+ `productDetail.gallery[]` carries `{id, position, labels[], asset{id, kind, url}}`.
52
+ The `<GallerySwitcher>` component renders the Base Image as the primary
53
+ `<img>` (or the first item if no Base Image is set) and emits a thumbnail
54
+ strip with the Small Image marked via `data-small-image="true"` for
55
+ themes to highlight.
56
+
57
+ ## Listing card thumbnail resolution
58
+
59
+ `ProductSummary.primaryAssetUrl` (used by the listing card) is resolved
60
+ server-side via the chain: Thumbnail → Base Image → first gallery item
61
+ → legacy `product_assets` row → null.
62
+
63
+ ## OG image resolution
64
+
65
+ `productDetail.seo.openGraph.imageUrl` prefers the gallery's Base Image
66
+ over the listing thumbnail so social shares get the marketer's hero shot.
67
+
68
+ ## Storage
69
+
70
+ `gallery_items` (id, product_id FK CASCADE, asset_id FK RESTRICT,
71
+ position, timestamps) + `gallery_item_labels` (composite PK on
72
+ (gallery_item_id, label), `UNIQUE (product_id, label)`, CHECK label IN
73
+ ('base_image', 'small_image', 'thumbnail')).