@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/admin/components/PackagingUnitsEditor.d.ts +12 -0
- package/dist/admin/components/PackagingUnitsEditor.d.ts.map +1 -0
- package/dist/admin/components/PackagingUnitsEditor.js +99 -0
- package/dist/admin/components/PackagingUnitsEditor.js.map +1 -0
- package/dist/admin/components/ProductAttributesTab.d.ts +13 -0
- package/dist/admin/components/ProductAttributesTab.d.ts.map +1 -0
- package/dist/admin/components/ProductAttributesTab.js +140 -0
- package/dist/admin/components/ProductAttributesTab.js.map +1 -0
- package/dist/admin/components/ProductInventoryTab.d.ts +14 -0
- package/dist/admin/components/ProductInventoryTab.d.ts.map +1 -0
- package/dist/admin/components/ProductInventoryTab.js +284 -0
- package/dist/admin/components/ProductInventoryTab.js.map +1 -0
- package/dist/admin/components/ProductScopeEditor.d.ts +94 -0
- package/dist/admin/components/ProductScopeEditor.d.ts.map +1 -0
- package/dist/admin/components/ProductScopeEditor.js +395 -0
- package/dist/admin/components/ProductScopeEditor.js.map +1 -0
- package/dist/admin/components/ProductsBulkEditDialog.d.ts +11 -0
- package/dist/admin/components/ProductsBulkEditDialog.d.ts.map +1 -0
- package/dist/admin/components/ProductsBulkEditDialog.js +304 -0
- package/dist/admin/components/ProductsBulkEditDialog.js.map +1 -0
- package/dist/admin/index.d.ts +45 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +184 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/lib/resolve-product-selection.d.ts +21 -0
- package/dist/admin/lib/resolve-product-selection.d.ts.map +1 -0
- package/dist/admin/lib/resolve-product-selection.js +25 -0
- package/dist/admin/lib/resolve-product-selection.js.map +1 -0
- package/dist/admin/pages/AttachmentTypesPage.d.ts +18 -0
- package/dist/admin/pages/AttachmentTypesPage.d.ts.map +1 -0
- package/dist/admin/pages/AttachmentTypesPage.js +92 -0
- package/dist/admin/pages/AttachmentTypesPage.js.map +1 -0
- package/dist/admin/pages/AttributeSetsPage.d.ts +21 -0
- package/dist/admin/pages/AttributeSetsPage.d.ts.map +1 -0
- package/dist/admin/pages/AttributeSetsPage.js +158 -0
- package/dist/admin/pages/AttributeSetsPage.js.map +1 -0
- package/dist/admin/pages/AttributesManager.d.ts +9 -0
- package/dist/admin/pages/AttributesManager.d.ts.map +1 -0
- package/dist/admin/pages/AttributesManager.js +305 -0
- package/dist/admin/pages/AttributesManager.js.map +1 -0
- package/dist/admin/pages/BulkOperationDetailPage.d.ts +9 -0
- package/dist/admin/pages/BulkOperationDetailPage.d.ts.map +1 -0
- package/dist/admin/pages/BulkOperationDetailPage.js +187 -0
- package/dist/admin/pages/BulkOperationDetailPage.js.map +1 -0
- package/dist/admin/pages/BulkOperationsPage.d.ts +9 -0
- package/dist/admin/pages/BulkOperationsPage.d.ts.map +1 -0
- package/dist/admin/pages/BulkOperationsPage.js +98 -0
- package/dist/admin/pages/BulkOperationsPage.js.map +1 -0
- package/dist/admin/pages/CategoriesTree.d.ts +9 -0
- package/dist/admin/pages/CategoriesTree.d.ts.map +1 -0
- package/dist/admin/pages/CategoriesTree.js +176 -0
- package/dist/admin/pages/CategoriesTree.js.map +1 -0
- package/dist/admin/pages/ProductEditor.d.ts +13 -0
- package/dist/admin/pages/ProductEditor.d.ts.map +1 -0
- package/dist/admin/pages/ProductEditor.js +1050 -0
- package/dist/admin/pages/ProductEditor.js.map +1 -0
- package/dist/admin/pages/ProductsList.d.ts +9 -0
- package/dist/admin/pages/ProductsList.d.ts.map +1 -0
- package/dist/admin/pages/ProductsList.js +399 -0
- package/dist/admin/pages/ProductsList.js.map +1 -0
- package/dist/backend/commands/attribute-commands.d.ts +111 -0
- package/dist/backend/commands/attribute-commands.d.ts.map +1 -0
- package/dist/backend/commands/attribute-commands.js +482 -0
- package/dist/backend/commands/attribute-commands.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +23 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +41 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +103 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +162 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +35 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +177 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/attachment-type.entity.d.ts +16 -0
- package/dist/backend/entities/attachment-type.entity.d.ts.map +1 -0
- package/dist/backend/entities/attachment-type.entity.js +57 -0
- package/dist/backend/entities/attachment-type.entity.js.map +1 -0
- package/dist/backend/entities/attribute-set-attribute.entity.d.ts +26 -0
- package/dist/backend/entities/attribute-set-attribute.entity.d.ts.map +1 -0
- package/dist/backend/entities/attribute-set-attribute.entity.js +55 -0
- package/dist/backend/entities/attribute-set-attribute.entity.js.map +1 -0
- package/dist/backend/entities/attribute-set.entity.d.ts +22 -0
- package/dist/backend/entities/attribute-set.entity.d.ts.map +1 -0
- package/dist/backend/entities/attribute-set.entity.js +67 -0
- package/dist/backend/entities/attribute-set.entity.js.map +1 -0
- package/dist/backend/entities/bulk-operation.entity.d.ts +78 -0
- package/dist/backend/entities/bulk-operation.entity.d.ts.map +1 -0
- package/dist/backend/entities/bulk-operation.entity.js +138 -0
- package/dist/backend/entities/bulk-operation.entity.js.map +1 -0
- package/dist/backend/entities/bundle-slot-option.entity.d.ts +23 -0
- package/dist/backend/entities/bundle-slot-option.entity.d.ts.map +1 -0
- package/dist/backend/entities/bundle-slot-option.entity.js +68 -0
- package/dist/backend/entities/bundle-slot-option.entity.js.map +1 -0
- package/dist/backend/entities/bundle-slot.entity.d.ts +25 -0
- package/dist/backend/entities/bundle-slot.entity.d.ts.map +1 -0
- package/dist/backend/entities/bundle-slot.entity.js +74 -0
- package/dist/backend/entities/bundle-slot.entity.js.map +1 -0
- package/dist/backend/entities/category.entity.d.ts +26 -0
- package/dist/backend/entities/category.entity.d.ts.map +1 -0
- package/dist/backend/entities/category.entity.js +119 -0
- package/dist/backend/entities/category.entity.js.map +1 -0
- package/dist/backend/entities/gallery-item-label.entity.d.ts +22 -0
- package/dist/backend/entities/gallery-item-label.entity.d.ts.map +1 -0
- package/dist/backend/entities/gallery-item-label.entity.js +50 -0
- package/dist/backend/entities/gallery-item-label.entity.js.map +1 -0
- package/dist/backend/entities/gallery-item.entity.d.ts +17 -0
- package/dist/backend/entities/gallery-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/gallery-item.entity.js +58 -0
- package/dist/backend/entities/gallery-item.entity.js.map +1 -0
- package/dist/backend/entities/grouped-item.entity.d.ts +23 -0
- package/dist/backend/entities/grouped-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/grouped-item.entity.js +68 -0
- package/dist/backend/entities/grouped-item.entity.js.map +1 -0
- package/dist/backend/entities/product-attachment.entity.d.ts +23 -0
- package/dist/backend/entities/product-attachment.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-attachment.entity.js +77 -0
- package/dist/backend/entities/product-attachment.entity.js.map +1 -0
- package/dist/backend/entities/product-attribute.entity.d.ts +72 -0
- package/dist/backend/entities/product-attribute.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-attribute.entity.js +161 -0
- package/dist/backend/entities/product-attribute.entity.js.map +1 -0
- package/dist/backend/entities/product-editor-preference.entity.d.ts +28 -0
- package/dist/backend/entities/product-editor-preference.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-editor-preference.entity.js +63 -0
- package/dist/backend/entities/product-editor-preference.entity.js.map +1 -0
- package/dist/backend/entities/product-link.entity.d.ts +25 -0
- package/dist/backend/entities/product-link.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-link.entity.js +70 -0
- package/dist/backend/entities/product-link.entity.js.map +1 -0
- package/dist/backend/entities/product-packaging-unit.entity.d.ts +26 -0
- package/dist/backend/entities/product-packaging-unit.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-packaging-unit.entity.js +76 -0
- package/dist/backend/entities/product-packaging-unit.entity.js.map +1 -0
- package/dist/backend/entities/product-value-override.entity.d.ts +43 -0
- package/dist/backend/entities/product-value-override.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-value-override.entity.js +90 -0
- package/dist/backend/entities/product-value-override.entity.js.map +1 -0
- package/dist/backend/entities/product-variant.entity.d.ts +18 -0
- package/dist/backend/entities/product-variant.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-variant.entity.js +68 -0
- package/dist/backend/entities/product-variant.entity.js.map +1 -0
- package/dist/backend/entities/product.entity.d.ts +108 -0
- package/dist/backend/entities/product.entity.d.ts.map +1 -0
- package/dist/backend/entities/product.entity.js +230 -0
- package/dist/backend/entities/product.entity.js.map +1 -0
- package/dist/backend/index.d.ts +262 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +642 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +216 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +223 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/prompt-tools.d.ts +47 -0
- package/dist/backend/prompt-tools.d.ts.map +1 -0
- package/dist/backend/prompt-tools.js +284 -0
- package/dist/backend/prompt-tools.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +105 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +1162 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.api-key.d.ts +33 -0
- package/dist/backend/routes.api-key.d.ts.map +1 -0
- package/dist/backend/routes.api-key.js +43 -0
- package/dist/backend/routes.api-key.js.map +1 -0
- package/dist/backend/routes.external.d.ts +32 -0
- package/dist/backend/routes.external.d.ts.map +1 -0
- package/dist/backend/routes.external.js +115 -0
- package/dist/backend/routes.external.js.map +1 -0
- package/dist/backend/routes.public.d.ts +46 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +318 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/asset-references.d.ts +4 -0
- package/dist/backend/services/asset-references.d.ts.map +1 -0
- package/dist/backend/services/asset-references.js +104 -0
- package/dist/backend/services/asset-references.js.map +1 -0
- package/dist/backend/services/attachment.service.d.ts +45 -0
- package/dist/backend/services/attachment.service.d.ts.map +1 -0
- package/dist/backend/services/attachment.service.js +268 -0
- package/dist/backend/services/attachment.service.js.map +1 -0
- package/dist/backend/services/attribute-option-validator.d.ts +47 -0
- package/dist/backend/services/attribute-option-validator.d.ts.map +1 -0
- package/dist/backend/services/attribute-option-validator.js +69 -0
- package/dist/backend/services/attribute-option-validator.js.map +1 -0
- package/dist/backend/services/attribute-set-validations.d.ts +65 -0
- package/dist/backend/services/attribute-set-validations.d.ts.map +1 -0
- package/dist/backend/services/attribute-set-validations.js +89 -0
- package/dist/backend/services/attribute-set-validations.js.map +1 -0
- package/dist/backend/services/attribute-set.service.d.ts +50 -0
- package/dist/backend/services/attribute-set.service.d.ts.map +1 -0
- package/dist/backend/services/attribute-set.service.js +401 -0
- package/dist/backend/services/attribute-set.service.js.map +1 -0
- package/dist/backend/services/attribute-type-mapping.d.ts +66 -0
- package/dist/backend/services/attribute-type-mapping.d.ts.map +1 -0
- package/dist/backend/services/attribute-type-mapping.js +121 -0
- package/dist/backend/services/attribute-type-mapping.js.map +1 -0
- package/dist/backend/services/attribute-value-key.service.d.ts +48 -0
- package/dist/backend/services/attribute-value-key.service.d.ts.map +1 -0
- package/dist/backend/services/attribute-value-key.service.js +118 -0
- package/dist/backend/services/attribute-value-key.service.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +13 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +51 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/bulk-operation-queue.d.ts +26 -0
- package/dist/backend/services/bulk-operation-queue.d.ts.map +1 -0
- package/dist/backend/services/bulk-operation-queue.js +34 -0
- package/dist/backend/services/bulk-operation-queue.js.map +1 -0
- package/dist/backend/services/bulk-operation.service.d.ts +195 -0
- package/dist/backend/services/bulk-operation.service.d.ts.map +1 -0
- package/dist/backend/services/bulk-operation.service.js +510 -0
- package/dist/backend/services/bulk-operation.service.js.map +1 -0
- package/dist/backend/services/bundle.service.d.ts +87 -0
- package/dist/backend/services/bundle.service.d.ts.map +1 -0
- package/dist/backend/services/bundle.service.js +286 -0
- package/dist/backend/services/bundle.service.js.map +1 -0
- package/dist/backend/services/catalog-admin.service.d.ts +398 -0
- package/dist/backend/services/catalog-admin.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-admin.service.js +1516 -0
- package/dist/backend/services/catalog-admin.service.js.map +1 -0
- package/dist/backend/services/catalog-attribute-read.service.d.ts +78 -0
- package/dist/backend/services/catalog-attribute-read.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-attribute-read.service.js +216 -0
- package/dist/backend/services/catalog-attribute-read.service.js.map +1 -0
- package/dist/backend/services/catalog-bulk-import.service.d.ts +30 -0
- package/dist/backend/services/catalog-bulk-import.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-bulk-import.service.js +136 -0
- package/dist/backend/services/catalog-bulk-import.service.js.map +1 -0
- package/dist/backend/services/catalog-bulk-update.service.d.ts +91 -0
- package/dist/backend/services/catalog-bulk-update.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-bulk-update.service.js +330 -0
- package/dist/backend/services/catalog-bulk-update.service.js.map +1 -0
- package/dist/backend/services/catalog-category-read.service.d.ts +66 -0
- package/dist/backend/services/catalog-category-read.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-category-read.service.js +248 -0
- package/dist/backend/services/catalog-category-read.service.js.map +1 -0
- package/dist/backend/services/catalog-org-price-decorator.d.ts +90 -0
- package/dist/backend/services/catalog-org-price-decorator.d.ts.map +1 -0
- package/dist/backend/services/catalog-org-price-decorator.js +184 -0
- package/dist/backend/services/catalog-org-price-decorator.js.map +1 -0
- package/dist/backend/services/catalog-product-filter.service.d.ts +82 -0
- package/dist/backend/services/catalog-product-filter.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-product-filter.service.js +275 -0
- package/dist/backend/services/catalog-product-filter.service.js.map +1 -0
- package/dist/backend/services/catalog-product-read.service.d.ts +46 -0
- package/dist/backend/services/catalog-product-read.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-product-read.service.js +197 -0
- package/dist/backend/services/catalog-product-read.service.js.map +1 -0
- package/dist/backend/services/catalog-query.service.d.ts +444 -0
- package/dist/backend/services/catalog-query.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-query.service.js +1633 -0
- package/dist/backend/services/catalog-query.service.js.map +1 -0
- package/dist/backend/services/catalog-quick-search.service.d.ts +78 -0
- package/dist/backend/services/catalog-quick-search.service.d.ts.map +1 -0
- package/dist/backend/services/catalog-quick-search.service.js +159 -0
- package/dist/backend/services/catalog-quick-search.service.js.map +1 -0
- package/dist/backend/services/catalog-write-ports.d.ts +25 -0
- package/dist/backend/services/catalog-write-ports.d.ts.map +1 -0
- package/dist/backend/services/catalog-write-ports.js +63 -0
- package/dist/backend/services/catalog-write-ports.js.map +1 -0
- package/dist/backend/services/category-admin.service.d.ts +102 -0
- package/dist/backend/services/category-admin.service.d.ts.map +1 -0
- package/dist/backend/services/category-admin.service.js +271 -0
- package/dist/backend/services/category-admin.service.js.map +1 -0
- package/dist/backend/services/gallery.service.d.ts +85 -0
- package/dist/backend/services/gallery.service.d.ts.map +1 -0
- package/dist/backend/services/gallery.service.js +326 -0
- package/dist/backend/services/gallery.service.js.map +1 -0
- package/dist/backend/services/grouped.service.d.ts +39 -0
- package/dist/backend/services/grouped.service.d.ts.map +1 -0
- package/dist/backend/services/grouped.service.js +136 -0
- package/dist/backend/services/grouped.service.js.map +1 -0
- package/dist/backend/services/label-resolver.d.ts +16 -0
- package/dist/backend/services/label-resolver.d.ts.map +1 -0
- package/dist/backend/services/label-resolver.js +43 -0
- package/dist/backend/services/label-resolver.js.map +1 -0
- package/dist/backend/services/packaging-unit.service.d.ts +28 -0
- package/dist/backend/services/packaging-unit.service.d.ts.map +1 -0
- package/dist/backend/services/packaging-unit.service.js +197 -0
- package/dist/backend/services/packaging-unit.service.js.map +1 -0
- package/dist/backend/services/primary-asset-url.d.ts +4 -0
- package/dist/backend/services/primary-asset-url.d.ts.map +1 -0
- package/dist/backend/services/primary-asset-url.js +55 -0
- package/dist/backend/services/primary-asset-url.js.map +1 -0
- package/dist/backend/services/product-editor-preferences.service.d.ts +20 -0
- package/dist/backend/services/product-editor-preferences.service.d.ts.map +1 -0
- package/dist/backend/services/product-editor-preferences.service.js +44 -0
- package/dist/backend/services/product-editor-preferences.service.js.map +1 -0
- package/dist/backend/services/product-link.service.d.ts +169 -0
- package/dist/backend/services/product-link.service.d.ts.map +1 -0
- package/dist/backend/services/product-link.service.js +297 -0
- package/dist/backend/services/product-link.service.js.map +1 -0
- package/dist/backend/services/product-overrides.service.d.ts +77 -0
- package/dist/backend/services/product-overrides.service.d.ts.map +1 -0
- package/dist/backend/services/product-overrides.service.js +204 -0
- package/dist/backend/services/product-overrides.service.js.map +1 -0
- package/dist/backend/services/product-scope-context.service.d.ts +36 -0
- package/dist/backend/services/product-scope-context.service.d.ts.map +1 -0
- package/dist/backend/services/product-scope-context.service.js +106 -0
- package/dist/backend/services/product-scope-context.service.js.map +1 -0
- package/dist/backend/services/product-type-validations.d.ts +42 -0
- package/dist/backend/services/product-type-validations.d.ts.map +1 -0
- package/dist/backend/services/product-type-validations.js +55 -0
- package/dist/backend/services/product-type-validations.js.map +1 -0
- package/dist/backend/services/product-value-resolver.service.d.ts +59 -0
- package/dist/backend/services/product-value-resolver.service.d.ts.map +1 -0
- package/dist/backend/services/product-value-resolver.service.js +111 -0
- package/dist/backend/services/product-value-resolver.service.js.map +1 -0
- package/dist/backend/services/system-attribute-scopes.d.ts +18 -0
- package/dist/backend/services/system-attribute-scopes.d.ts.map +1 -0
- package/dist/backend/services/system-attribute-scopes.js +18 -0
- package/dist/backend/services/system-attribute-scopes.js.map +1 -0
- package/dist/backend/services/viewer-organization.d.ts +25 -0
- package/dist/backend/services/viewer-organization.d.ts.map +1 -0
- package/dist/backend/services/viewer-organization.js +31 -0
- package/dist/backend/services/viewer-organization.js.map +1 -0
- package/dist/manifest.d.ts +204 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +686 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts +33 -0
- package/dist/migrations/20260429T064146_catalog_attribute_sets_init.d.ts.map +1 -0
- package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js +118 -0
- package/dist/migrations/20260429T064146_catalog_attribute_sets_init.js.map +1 -0
- package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts +24 -0
- package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.d.ts.map +1 -0
- package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js +31 -0
- package/dist/migrations/20260429T070004_catalog_product_attribute_extensions.js.map +1 -0
- package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts +32 -0
- package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.d.ts.map +1 -0
- package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js +58 -0
- package/dist/migrations/20260429T102322_catalog_product_type_and_virtual_fields.js.map +1 -0
- package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts +22 -0
- package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.d.ts.map +1 -0
- package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js +66 -0
- package/dist/migrations/20260429T111839_catalog_gallery_items_and_labels.js.map +1 -0
- package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts +18 -0
- package/dist/migrations/20260429T112543_catalog_product_attachments.d.ts.map +1 -0
- package/dist/migrations/20260429T112543_catalog_product_attachments.js +73 -0
- package/dist/migrations/20260429T112543_catalog_product_attachments.js.map +1 -0
- package/dist/migrations/20260429T123726_catalog_product_links.d.ts +21 -0
- package/dist/migrations/20260429T123726_catalog_product_links.d.ts.map +1 -0
- package/dist/migrations/20260429T123726_catalog_product_links.js +48 -0
- package/dist/migrations/20260429T123726_catalog_product_links.js.map +1 -0
- package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts +23 -0
- package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.d.ts.map +1 -0
- package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js +94 -0
- package/dist/migrations/20260429T130803_catalog_grouped_and_bundle.js.map +1 -0
- package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts +24 -0
- package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.d.ts.map +1 -0
- package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js +31 -0
- package/dist/migrations/20260501T185835_catalog_product_attribute_is_comparable.js.map +1 -0
- package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts +31 -0
- package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.d.ts.map +1 -0
- package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js +133 -0
- package/dist/migrations/20260505T060113_catalog_attribute_options_and_flags.js.map +1 -0
- package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts +16 -0
- package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.d.ts.map +1 -0
- package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js +23 -0
- package/dist/migrations/20260515T082629_catalog_attribute_mass_editable.js.map +1 -0
- package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts +10 -0
- package/dist/migrations/20260526T124736_catalog_product_status_inactive.d.ts.map +1 -0
- package/dist/migrations/20260526T124736_catalog_product_status_inactive.js +22 -0
- package/dist/migrations/20260526T124736_catalog_product_status_inactive.js.map +1 -0
- package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts +38 -0
- package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.d.ts.map +1 -0
- package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js +98 -0
- package/dist/migrations/20260611T140346_catalog_product_value_overrides_init.js.map +1 -0
- package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts +13 -0
- package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.d.ts.map +1 -0
- package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js +17 -0
- package/dist/migrations/20260611T140400_catalog_attribute_quick_searchable.js.map +1 -0
- package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts +16 -0
- package/dist/migrations/20260611T140407_catalog_bulk_operations.d.ts.map +1 -0
- package/dist/migrations/20260611T140407_catalog_bulk_operations.js +42 -0
- package/dist/migrations/20260611T140407_catalog_bulk_operations.js.map +1 -0
- package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts +14 -0
- package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.d.ts.map +1 -0
- package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js +18 -0
- package/dist/migrations/20260611T140408_catalog_bulk_operation_logs.js.map +1 -0
- package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts +11 -0
- package/dist/migrations/20260611T140412_catalog_product_packaging_units.d.ts.map +1 -0
- package/dist/migrations/20260611T140412_catalog_product_packaging_units.js +29 -0
- package/dist/migrations/20260611T140412_catalog_product_packaging_units.js.map +1 -0
- package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts +23 -0
- package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.d.ts.map +1 -0
- package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js +35 -0
- package/dist/migrations/20260718T060659_catalog_bulk_operation_revert_state.js.map +1 -0
- package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts +12 -0
- package/dist/migrations/20260718T200343_catalog_category_custom_field_values.d.ts.map +1 -0
- package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js +16 -0
- package/dist/migrations/20260718T200343_catalog_category_custom_field_values.js.map +1 -0
- package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts +32 -0
- package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.d.ts.map +1 -0
- package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js +286 -0
- package/dist/migrations/20260723T230401_catalog_attributes_on_custom_fields.js.map +1 -0
- package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts +36 -0
- package/dist/migrations/20260804T152604_catalog_widen_product_sku.d.ts.map +1 -0
- package/dist/migrations/20260804T152604_catalog_widen_product_sku.js +63 -0
- package/dist/migrations/20260804T152604_catalog_widen_product_sku.js.map +1 -0
- package/dist/migrations/20260804T160244_catalog_category_activation.d.ts +18 -0
- package/dist/migrations/20260804T160244_catalog_category_activation.d.ts.map +1 -0
- package/dist/migrations/20260804T160244_catalog_category_activation.js +22 -0
- package/dist/migrations/20260804T160244_catalog_category_activation.js.map +1 -0
- package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts +27 -0
- package/dist/migrations/20260912T094557_catalog_sales_channel_products.d.ts.map +1 -0
- package/dist/migrations/20260912T094557_catalog_sales_channel_products.js +44 -0
- package/dist/migrations/20260912T094557_catalog_sales_channel_products.js.map +1 -0
- package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts +27 -0
- package/dist/migrations/20260912T094623_catalog_sales_channel_categories.d.ts.map +1 -0
- package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js +44 -0
- package/dist/migrations/20260912T094623_catalog_sales_channel_categories.js.map +1 -0
- package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts +36 -0
- package/dist/migrations/20260925T125527_catalog_inventory_columns.d.ts.map +1 -0
- package/dist/migrations/20260925T125527_catalog_inventory_columns.js +87 -0
- package/dist/migrations/20260925T125527_catalog_inventory_columns.js.map +1 -0
- package/dist/migrations/index.d.ts +55 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +80 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +96 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/catalog/attachments.md +69 -0
- package/docs/catalog/attribute-sets.md +63 -0
- package/docs/catalog/attributes.md +244 -0
- package/docs/catalog/composite-products.md +115 -0
- package/docs/catalog/gallery-and-labels.md +73 -0
- package/docs/catalog/packaging-units.md +67 -0
- package/docs/catalog/per-channel-per-language-overrides.md +234 -0
- package/docs/catalog/product-links.md +79 -0
- package/docs/catalog.md +207 -0
- package/i18n/en.json +649 -0
- package/i18n/pl.json +649 -0
- package/package.json +109 -0
- 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 @@
|
|
|
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')).
|