@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,1162 @@
|
|
|
1
|
+
import { assignAttributesRequestSchema, batchByIdProductsRequestSchema, bulkUpdateProductsRequestSchema, resolveProductIdsRequestSchema, createAttributeRequestSchema, createAttributeSetRequestSchema, createCategoryRequestSchema, createAttachmentRequestSchema, createPackagingUnitRequestSchema, updatePackagingUnitRequestSchema, reorderPackagingUnitsRequestSchema, createAttachmentTypeRequestSchema, createGalleryItemRequestSchema, createProductRequestSchema, createVariantRequestSchema, reorderGalleryRequestSchema, bulkCreateLinksRequestSchema, reorderLinksRequestSchema, productLinkKindSchema, createGroupedItemRequestSchema, updateGroupedItemRequestSchema, createBundleSlotRequestSchema, updateBundleSlotRequestSchema, createBundleSlotOptionRequestSchema, updateAttachmentRequestSchema, updateAttachmentTypeRequestSchema, updateAttributeRequestSchema, updateAttributeSetRequestSchema, updateCategoryRequestSchema, updateGalleryItemRequestSchema, updateProductRequestSchema, updateVariantRequestSchema, ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { dbToApiAttributeType } from './services/catalog-admin.service.js';
|
|
4
|
+
import { productValueOverridesPatchRequestSchema } from '@endora-commerce/contracts';
|
|
5
|
+
/**
|
|
6
|
+
* Admin write surface for the catalog. Every route is gated by an admin session
|
|
7
|
+
* with the `catalog:write` permission — wiring for permission enforcement is
|
|
8
|
+
* finalised in US4 (T188). Until then the plugin accepts any authenticated admin.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Selections larger than this are delegated to a background bulk
|
|
12
|
+
* operation rather than applied inline (when the queue is wired). Kept
|
|
13
|
+
* comfortably below the synchronous MAX_BATCH_SIZE so the inline path is
|
|
14
|
+
* always fast.
|
|
15
|
+
*/
|
|
16
|
+
const BULK_ASYNC_THRESHOLD = 50;
|
|
17
|
+
const BULK_OPERATION_STATUSES = [
|
|
18
|
+
'pending',
|
|
19
|
+
'running',
|
|
20
|
+
'completed',
|
|
21
|
+
'failed',
|
|
22
|
+
];
|
|
23
|
+
function isBulkOperationStatus(value) {
|
|
24
|
+
return BULK_OPERATION_STATUSES.includes(value);
|
|
25
|
+
}
|
|
26
|
+
export async function registerCatalogAdminRoutes(app, deps) {
|
|
27
|
+
const { adminService, requireAdmin } = deps;
|
|
28
|
+
/**
|
|
29
|
+
* Feature 002 T155 — fire-and-forget audit log helper used by the
|
|
30
|
+
* sub-resource handlers (AttributeSet, Gallery, Attachment,
|
|
31
|
+
* ProductLink, GroupedItem, BundleSlot CRUD). The product CRUD path
|
|
32
|
+
* (T060/T065-era) does its own audit emission inside `CatalogAdminService`;
|
|
33
|
+
* this helper covers everything else without a service-layer rewrite.
|
|
34
|
+
*
|
|
35
|
+
* Silently no-ops when either `auditLogService` or
|
|
36
|
+
* `resolveAdminAuditContext` is missing (dev/test setups), so the
|
|
37
|
+
* call site is safe to drop in everywhere.
|
|
38
|
+
*/
|
|
39
|
+
const auditEmit = async (request, opts) => {
|
|
40
|
+
if (!deps.auditLogService)
|
|
41
|
+
return;
|
|
42
|
+
const ctx = deps.resolveAdminAuditContext?.(request);
|
|
43
|
+
await deps.auditLogService.record({
|
|
44
|
+
action: opts.action,
|
|
45
|
+
objectType: opts.objectType,
|
|
46
|
+
objectId: opts.objectId,
|
|
47
|
+
...(ctx?.actorAdminUserId !== undefined
|
|
48
|
+
? { actorAdminUserId: ctx.actorAdminUserId }
|
|
49
|
+
: {}),
|
|
50
|
+
...(ctx?.impersonatedCustomerAccountId !== undefined
|
|
51
|
+
? { impersonatedCustomerAccountId: ctx.impersonatedCustomerAccountId }
|
|
52
|
+
: {}),
|
|
53
|
+
...(opts.stateAfter !== undefined ? { stateAfter: opts.stateAfter } : {}),
|
|
54
|
+
...(request.ip !== undefined ? { ipAddress: request.ip } : {}),
|
|
55
|
+
...(typeof request.headers['user-agent'] === 'string'
|
|
56
|
+
? { userAgent: request.headers['user-agent'] }
|
|
57
|
+
: {}),
|
|
58
|
+
requestId: request.id,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
app.post('/api/v1/admin/catalog/products', {
|
|
62
|
+
preHandler: requireAdmin('catalog:write'),
|
|
63
|
+
schema: { body: createProductRequestSchema },
|
|
64
|
+
}, async (request, reply) => {
|
|
65
|
+
const body = createProductRequestSchema.parse(request.body);
|
|
66
|
+
const auditCtx = deps.resolveAdminAuditContext?.(request);
|
|
67
|
+
const product = await adminService.createProduct(body, auditCtx
|
|
68
|
+
? {
|
|
69
|
+
actorAdminUserId: auditCtx.actorAdminUserId,
|
|
70
|
+
impersonatedCustomerAccountId: auditCtx.impersonatedCustomerAccountId ?? null,
|
|
71
|
+
ipAddress: request.ip ?? null,
|
|
72
|
+
userAgent: typeof request.headers['user-agent'] === 'string'
|
|
73
|
+
? request.headers['user-agent']
|
|
74
|
+
: null,
|
|
75
|
+
requestId: request.id,
|
|
76
|
+
}
|
|
77
|
+
: undefined);
|
|
78
|
+
reply.status(201);
|
|
79
|
+
return { data: serializeAdminProduct(product) };
|
|
80
|
+
});
|
|
81
|
+
app.patch('/api/v1/admin/catalog/products/:id', {
|
|
82
|
+
preHandler: requireAdmin('catalog:write'),
|
|
83
|
+
schema: { body: updateProductRequestSchema },
|
|
84
|
+
}, async (request) => {
|
|
85
|
+
const body = updateProductRequestSchema.parse(request.body);
|
|
86
|
+
// Feature 054 — audited co-transactionally via the Command Bus; the actor
|
|
87
|
+
// is derived from the ambient TenantContext (not from the request body).
|
|
88
|
+
const product = await adminService.updateProductAudited(request.params.id, body);
|
|
89
|
+
return { data: serializeAdminProduct(product) };
|
|
90
|
+
});
|
|
91
|
+
app.delete('/api/v1/admin/catalog/products/:id', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
92
|
+
const auditCtx = deps.resolveAdminAuditContext?.(request);
|
|
93
|
+
await adminService.deleteProduct(request.params.id, auditCtx
|
|
94
|
+
? {
|
|
95
|
+
actorAdminUserId: auditCtx.actorAdminUserId,
|
|
96
|
+
impersonatedCustomerAccountId: auditCtx.impersonatedCustomerAccountId ?? null,
|
|
97
|
+
ipAddress: request.ip ?? null,
|
|
98
|
+
userAgent: typeof request.headers['user-agent'] === 'string'
|
|
99
|
+
? request.headers['user-agent']
|
|
100
|
+
: null,
|
|
101
|
+
requestId: request.id,
|
|
102
|
+
}
|
|
103
|
+
: undefined);
|
|
104
|
+
return reply.status(204).send();
|
|
105
|
+
});
|
|
106
|
+
app.post('/api/v1/admin/catalog/products/:id/duplicate', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
107
|
+
const dup = await adminService.duplicateProduct(request.params.id);
|
|
108
|
+
await auditEmit(request, {
|
|
109
|
+
action: 'product.duplicate',
|
|
110
|
+
objectType: 'product',
|
|
111
|
+
objectId: dup.id,
|
|
112
|
+
stateAfter: { sourceProductId: request.params.id, sku: dup.sku },
|
|
113
|
+
});
|
|
114
|
+
reply.status(201);
|
|
115
|
+
return { data: serializeAdminProduct(dup) };
|
|
116
|
+
});
|
|
117
|
+
// Feature 022 — Products Bulk Edit. Applies a sparse field-patch to a
|
|
118
|
+
// selection of products in one call; returns a per-product outcome.
|
|
119
|
+
//
|
|
120
|
+
// Above BULK_ASYNC_THRESHOLD products the request is delegated to a
|
|
121
|
+
// background bulk operation (when the queue service is wired): the
|
|
122
|
+
// handler returns a 202 ack immediately and the work is finished
|
|
123
|
+
// off-thread, with the requester notified by bell + email on
|
|
124
|
+
// completion. The synchronous path keeps the inline per-product result.
|
|
125
|
+
if (deps.bulkUpdateService) {
|
|
126
|
+
const bulkUpdateService = deps.bulkUpdateService;
|
|
127
|
+
const bulkOperationService = deps.bulkOperationService;
|
|
128
|
+
app.post('/api/v1/admin/catalog/products/bulk-update', {
|
|
129
|
+
preHandler: requireAdmin('catalog:write'),
|
|
130
|
+
schema: { body: bulkUpdateProductsRequestSchema },
|
|
131
|
+
}, async (request, reply) => {
|
|
132
|
+
const body = bulkUpdateProductsRequestSchema.parse(request.body);
|
|
133
|
+
const auditCtx = deps.resolveAdminAuditContext?.(request);
|
|
134
|
+
if (bulkOperationService && body.productIds.length > BULK_ASYNC_THRESHOLD) {
|
|
135
|
+
const op = await bulkOperationService.create({
|
|
136
|
+
requestedByAdminUserId: auditCtx?.actorAdminUserId ?? '00000000-0000-0000-0000-000000000000',
|
|
137
|
+
payload: { productIds: body.productIds, fields: body.fields },
|
|
138
|
+
});
|
|
139
|
+
reply.status(202);
|
|
140
|
+
return { data: { queued: true, bulkOperationId: op.id, total: op.total } };
|
|
141
|
+
}
|
|
142
|
+
const ctx = auditCtx
|
|
143
|
+
? {
|
|
144
|
+
actorAdminUserId: auditCtx.actorAdminUserId,
|
|
145
|
+
impersonatedCustomerAccountId: auditCtx.impersonatedCustomerAccountId ?? null,
|
|
146
|
+
ipAddress: request.ip ?? null,
|
|
147
|
+
userAgent: typeof request.headers['user-agent'] === 'string'
|
|
148
|
+
? request.headers['user-agent']
|
|
149
|
+
: null,
|
|
150
|
+
requestId: request.id,
|
|
151
|
+
}
|
|
152
|
+
: undefined;
|
|
153
|
+
const result = await bulkUpdateService.bulkUpdate(body, ctx);
|
|
154
|
+
return { data: result };
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Bulk operations history — list + detail backing the "Bulk actions"
|
|
158
|
+
// admin page (completed / running / pending operations + summaries).
|
|
159
|
+
if (deps.bulkOperationService) {
|
|
160
|
+
const bulkOperationService = deps.bulkOperationService;
|
|
161
|
+
app.get('/api/v1/admin/catalog/bulk-operations', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
162
|
+
const q = request.query;
|
|
163
|
+
const rawStatuses = q.status
|
|
164
|
+
? Array.isArray(q.status)
|
|
165
|
+
? q.status
|
|
166
|
+
: [q.status]
|
|
167
|
+
: [];
|
|
168
|
+
const status = rawStatuses.filter(isBulkOperationStatus);
|
|
169
|
+
const limit = q.limit ? Number.parseInt(q.limit, 10) : 25;
|
|
170
|
+
const offset = q.offset ? Number.parseInt(q.offset, 10) : 0;
|
|
171
|
+
const res = await bulkOperationService.list({
|
|
172
|
+
...(status.length ? { status } : {}),
|
|
173
|
+
limit: Number.isFinite(limit) ? limit : 25,
|
|
174
|
+
offset: Number.isFinite(offset) ? offset : 0,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
data: res.items,
|
|
178
|
+
pagination: {
|
|
179
|
+
total: res.total,
|
|
180
|
+
limit: Number.isFinite(limit) ? limit : 25,
|
|
181
|
+
offset: Number.isFinite(offset) ? offset : 0,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
app.get('/api/v1/admin/catalog/bulk-operations/:id', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
186
|
+
const { id } = request.params;
|
|
187
|
+
const op = await bulkOperationService.get(id);
|
|
188
|
+
if (!op) {
|
|
189
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `bulk operation ${id} not found`);
|
|
190
|
+
}
|
|
191
|
+
return { data: op };
|
|
192
|
+
});
|
|
193
|
+
// Feature 054 (US2) — undo a reversible bulk edit. Restores every affected
|
|
194
|
+
// product whose current state still matches the operation, reports the rest
|
|
195
|
+
// as conflicts (never clobbered), and audits the undo. 404 when unknown,
|
|
196
|
+
// 409 when not reversible / already reverted.
|
|
197
|
+
app.post('/api/v1/admin/catalog/bulk-operations/:id/undo', { preHandler: requireAdmin('catalog:write') }, async (request) => {
|
|
198
|
+
const { id } = request.params;
|
|
199
|
+
const outcome = await bulkOperationService.undo(id);
|
|
200
|
+
if (!outcome.ok) {
|
|
201
|
+
if (outcome.code === 'NOT_FOUND') {
|
|
202
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `bulk operation ${id} not found`);
|
|
203
|
+
}
|
|
204
|
+
throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, outcome.code === 'ALREADY_REVERTED'
|
|
205
|
+
? 'This bulk operation has already been undone.'
|
|
206
|
+
: 'This bulk operation cannot be undone.');
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
data: {
|
|
210
|
+
undoStatus: outcome.undoStatus,
|
|
211
|
+
reverted: outcome.reverted.length,
|
|
212
|
+
conflicts: outcome.conflicts,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
app.post('/api/v1/admin/catalog/attributes', {
|
|
218
|
+
preHandler: requireAdmin('catalog:write'),
|
|
219
|
+
schema: { body: createAttributeRequestSchema },
|
|
220
|
+
}, async (request, reply) => {
|
|
221
|
+
const body = createAttributeRequestSchema.parse(request.body);
|
|
222
|
+
const attr = await adminService.createAttribute(body);
|
|
223
|
+
reply.status(201);
|
|
224
|
+
const optionValues = await adminService.getAttributeOptionValues(attr.id);
|
|
225
|
+
return { data: serializeAdminAttribute(attr, optionValues) };
|
|
226
|
+
});
|
|
227
|
+
app.patch('/api/v1/admin/catalog/attributes/:idOrKey', {
|
|
228
|
+
preHandler: requireAdmin('catalog:write'),
|
|
229
|
+
schema: { body: updateAttributeRequestSchema },
|
|
230
|
+
}, async (request) => {
|
|
231
|
+
const body = updateAttributeRequestSchema.parse(request.body);
|
|
232
|
+
const auditCtx = deps.resolveAdminAuditContext?.(request);
|
|
233
|
+
// Accept either UUID (id) or snake_case key — admin UI consumes
|
|
234
|
+
// the API by key, but contract tests round-trip through the id
|
|
235
|
+
// returned on create.
|
|
236
|
+
const attr = await adminService.updateAttributeByIdOrKey(request.params.idOrKey, body, auditCtx);
|
|
237
|
+
const optionValues = await adminService.getAttributeOptionValues(attr.id);
|
|
238
|
+
return { data: serializeAdminAttribute(attr, optionValues) };
|
|
239
|
+
});
|
|
240
|
+
// --- Read endpoints feeding the admin UI ---------------------------------
|
|
241
|
+
app.get('/api/v1/admin/catalog/products', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
242
|
+
const q = (request.query ?? {});
|
|
243
|
+
const includeArchived = q['includeArchived'] === '1' || q['includeArchived'] === 'true';
|
|
244
|
+
const statusRaw = q['status'];
|
|
245
|
+
const status = statusRaw === 'active' || statusRaw === 'draft' || statusRaw === 'inactive'
|
|
246
|
+
? statusRaw
|
|
247
|
+
: statusRaw === 'archived'
|
|
248
|
+
? 'inactive'
|
|
249
|
+
: undefined;
|
|
250
|
+
const typeRaw = q['type'];
|
|
251
|
+
const type = typeRaw === 'simple' ||
|
|
252
|
+
typeRaw === 'configurable' ||
|
|
253
|
+
typeRaw === 'grouped' ||
|
|
254
|
+
typeRaw === 'bundle' ||
|
|
255
|
+
typeRaw === 'virtual'
|
|
256
|
+
? typeRaw
|
|
257
|
+
: undefined;
|
|
258
|
+
const search = q['q']?.trim();
|
|
259
|
+
const categorySlug = q['categorySlug']?.trim();
|
|
260
|
+
const result = await adminService.listProducts({
|
|
261
|
+
includeArchived,
|
|
262
|
+
...(status ? { status } : {}),
|
|
263
|
+
...(type ? { type } : {}),
|
|
264
|
+
...(search ? { q: search } : {}),
|
|
265
|
+
...(categorySlug ? { categorySlug } : {}),
|
|
266
|
+
...(q['page'] ? { page: Number.parseInt(q['page'], 10) } : {}),
|
|
267
|
+
...(q['pageSize'] ? { pageSize: Number.parseInt(q['pageSize'], 10) } : {}),
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
data: result.items.map(serializeAdminProduct),
|
|
271
|
+
pagination: {
|
|
272
|
+
page: result.page,
|
|
273
|
+
pageSize: result.pageSize,
|
|
274
|
+
total: result.total,
|
|
275
|
+
// Legacy fields kept for any back-compat consumer that read them.
|
|
276
|
+
cursor: null,
|
|
277
|
+
hasMore: (result.page + 1) * result.pageSize < result.total,
|
|
278
|
+
limit: result.items.length,
|
|
279
|
+
},
|
|
280
|
+
counts: result.counts,
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
app.post('/api/v1/admin/catalog/products/resolve-ids', {
|
|
284
|
+
preHandler: requireAdmin('catalog:read'),
|
|
285
|
+
schema: { body: resolveProductIdsRequestSchema },
|
|
286
|
+
}, async (request) => {
|
|
287
|
+
const body = resolveProductIdsRequestSchema.parse(request.body);
|
|
288
|
+
const includeArchived = body.includeArchived ?? true;
|
|
289
|
+
const result = await adminService.resolveProductIds({
|
|
290
|
+
includeArchived,
|
|
291
|
+
...(body.status ? { status: body.status } : {}),
|
|
292
|
+
...(body.type ? { type: body.type } : {}),
|
|
293
|
+
...(body.q ? { q: body.q } : {}),
|
|
294
|
+
});
|
|
295
|
+
return { data: result };
|
|
296
|
+
});
|
|
297
|
+
app.get('/api/v1/admin/catalog/products/:id', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
298
|
+
const product = await adminService.getProductById(request.params.id);
|
|
299
|
+
const categoryIds = await adminService.getProductCategoryIds(request.params.id);
|
|
300
|
+
const base = { ...serializeAdminProduct(product), categoryIds };
|
|
301
|
+
const channelIdRaw = request.query.channelId;
|
|
302
|
+
const languageCodeRaw = request.query.languageCode;
|
|
303
|
+
const includeOverridesMap = request.query.includeOverridesMap === true ||
|
|
304
|
+
request.query.includeOverridesMap === 'true';
|
|
305
|
+
// Feature 022 — attach resolved + overrides blocks when the caller
|
|
306
|
+
// asked for a specific (channel, language) context, or for the
|
|
307
|
+
// full override map. Both pieces are gated on the resolver service
|
|
308
|
+
// being wired (test/dev setups may omit it).
|
|
309
|
+
if (deps.productValueResolverService &&
|
|
310
|
+
(channelIdRaw !== undefined || languageCodeRaw !== undefined || includeOverridesMap)) {
|
|
311
|
+
const resolverSvc = deps.productValueResolverService;
|
|
312
|
+
const channelId = channelIdRaw ?? null;
|
|
313
|
+
const languageCode = languageCodeRaw ?? null;
|
|
314
|
+
const ctx = await resolverSvc.makeContext(channelId, languageCode);
|
|
315
|
+
const overrides = await resolverSvc.loadOverrides(product.id);
|
|
316
|
+
const resolved = await resolverSvc.resolveForProduct(product, ctx);
|
|
317
|
+
const decorated = { ...base };
|
|
318
|
+
if (includeOverridesMap) {
|
|
319
|
+
decorated['overrides'] = overrides.map((o) => ({
|
|
320
|
+
attributeKey: o.attributeKey,
|
|
321
|
+
channelId: o.channelId,
|
|
322
|
+
languageCode: o.languageCode,
|
|
323
|
+
value: o.value,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
if (channelIdRaw !== undefined || languageCodeRaw !== undefined) {
|
|
327
|
+
decorated['resolved'] = {
|
|
328
|
+
context: { channelId, languageCode },
|
|
329
|
+
name: resolved.values['name'] ?? null,
|
|
330
|
+
description: resolved.values['description'] ?? null,
|
|
331
|
+
attributeValues: Object.fromEntries(Object.entries(resolved.values).filter(([k]) => k !== 'name' && k !== 'description')),
|
|
332
|
+
sources: resolved.sources,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return { data: decorated };
|
|
336
|
+
}
|
|
337
|
+
return { data: base };
|
|
338
|
+
});
|
|
339
|
+
// -------------------------------------------------------------------------
|
|
340
|
+
// Feature 022 — Product Scope Editor: per Sales Channel + per Language.
|
|
341
|
+
// -------------------------------------------------------------------------
|
|
342
|
+
if (deps.productScopeContextService) {
|
|
343
|
+
const scopeContextSvc = deps.productScopeContextService;
|
|
344
|
+
app.get('/api/v1/admin/catalog/products/:id/scope-context', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
345
|
+
const ctx = deps.resolveAdminAuditContext?.(request);
|
|
346
|
+
// Editor identity is required for the per-(user, product) prefs;
|
|
347
|
+
// when the composition root cannot resolve an admin actor (e.g.,
|
|
348
|
+
// dev mode without auth), fall back to a fixed UUID — the
|
|
349
|
+
// returned preference block is just `null` in that path.
|
|
350
|
+
const adminUserId = ctx?.actorAdminUserId ?? '00000000-0000-0000-0000-000000000000';
|
|
351
|
+
const data = await scopeContextSvc.getContext(request.params.id, adminUserId);
|
|
352
|
+
return { data };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (deps.productValueResolverService) {
|
|
356
|
+
const resolverSvc = deps.productValueResolverService;
|
|
357
|
+
app.get('/api/v1/admin/catalog/products/:id/value-overrides', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
358
|
+
// For a missing product the resolver service does not 404 on
|
|
359
|
+
// its own (it just returns an empty list). Run the lookup
|
|
360
|
+
// through adminService.getProductById so the 404 path stays
|
|
361
|
+
// consistent with every other product-scoped endpoint.
|
|
362
|
+
const product = await adminService.getProductById(request.params.id);
|
|
363
|
+
const overrides = await resolverSvc.loadOverrides(product.id);
|
|
364
|
+
return {
|
|
365
|
+
data: {
|
|
366
|
+
productId: product.id,
|
|
367
|
+
overrides: overrides.map((o) => ({
|
|
368
|
+
attributeKey: o.attributeKey,
|
|
369
|
+
channelId: o.channelId,
|
|
370
|
+
languageCode: o.languageCode,
|
|
371
|
+
value: o.value,
|
|
372
|
+
})),
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (deps.productOverridesService) {
|
|
378
|
+
const overridesSvc = deps.productOverridesService;
|
|
379
|
+
app.patch('/api/v1/admin/catalog/products/:id/value-overrides', { preHandler: requireAdmin('catalog:write') }, async (request) => {
|
|
380
|
+
const body = productValueOverridesPatchRequestSchema.parse(request.body);
|
|
381
|
+
const result = await overridesSvc.applyBulk(request.params.id, {
|
|
382
|
+
upserts: body.upserts,
|
|
383
|
+
deletes: body.deletes,
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
data: {
|
|
387
|
+
productId: result.productId,
|
|
388
|
+
applied: result.applied,
|
|
389
|
+
overrides: result.overrides.map((o) => ({
|
|
390
|
+
id: o.id,
|
|
391
|
+
attributeKey: o.attributeKey,
|
|
392
|
+
channelId: o.channelId,
|
|
393
|
+
languageCode: o.languageCode ?? null,
|
|
394
|
+
value: o.value,
|
|
395
|
+
updatedAt: o.updatedAt.toISOString(),
|
|
396
|
+
})),
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (deps.productEditorPreferencesService && deps.productScopeContextService) {
|
|
402
|
+
const prefSvc = deps.productEditorPreferencesService;
|
|
403
|
+
const scopeContextSvc = deps.productScopeContextService;
|
|
404
|
+
app.put('/api/v1/admin/catalog/products/:id/editor-preference', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
405
|
+
// Validate the product exists + channel (if any) is assigned to it.
|
|
406
|
+
await adminService.getProductById(request.params.id);
|
|
407
|
+
const ctx = deps.resolveAdminAuditContext?.(request);
|
|
408
|
+
const adminUserId = ctx?.actorAdminUserId ?? '00000000-0000-0000-0000-000000000000';
|
|
409
|
+
const body = request.body;
|
|
410
|
+
if (body.lastChannelId) {
|
|
411
|
+
await scopeContextSvc.assertChannelAssignedToProduct(request.params.id, body.lastChannelId);
|
|
412
|
+
}
|
|
413
|
+
const row = await prefSvc.upsert(adminUserId, request.params.id, {
|
|
414
|
+
lastChannelId: body.lastChannelId,
|
|
415
|
+
lastLanguageCode: body.lastLanguageCode,
|
|
416
|
+
});
|
|
417
|
+
return {
|
|
418
|
+
data: {
|
|
419
|
+
productId: row.productId,
|
|
420
|
+
adminUserId: row.adminUserId,
|
|
421
|
+
lastChannelId: row.lastChannelId ?? null,
|
|
422
|
+
lastLanguageCode: row.lastLanguageCode ?? null,
|
|
423
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
app.post('/api/v1/admin/catalog/products/batch-by-id', {
|
|
429
|
+
preHandler: requireAdmin('catalog:read'),
|
|
430
|
+
schema: { body: batchByIdProductsRequestSchema },
|
|
431
|
+
}, async (request) => {
|
|
432
|
+
const body = batchByIdProductsRequestSchema.parse(request.body);
|
|
433
|
+
const result = await adminService.listProductsByIds({
|
|
434
|
+
ids: body.ids,
|
|
435
|
+
...(body.page !== undefined ? { page: body.page } : {}),
|
|
436
|
+
...(body.pageSize !== undefined ? { pageSize: body.pageSize } : {}),
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
data: result.items.map(serializeAdminProduct),
|
|
440
|
+
pagination: {
|
|
441
|
+
page: result.page,
|
|
442
|
+
pageSize: result.pageSize,
|
|
443
|
+
total: result.total,
|
|
444
|
+
hasMore: (result.page + 1) * result.pageSize < result.total,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
app.get('/api/v1/admin/catalog/attributes', { preHandler: requireAdmin('catalog:read') }, async () => {
|
|
449
|
+
const rows = await adminService.listAttributes();
|
|
450
|
+
const optionsByAttr = await adminService.getAttributeOptionValuesByIds(rows.map((r) => r.id));
|
|
451
|
+
return {
|
|
452
|
+
data: rows.map((r) => serializeAdminAttribute(r, optionsByAttr.get(r.id) ?? null)),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
// Feature 012 — read by flag for picker consumers (Promotion Rule editor,
|
|
456
|
+
// Compare-page column picker, etc.). Single boolean filter.
|
|
457
|
+
app.get('/api/v1/admin/catalog/attributes/by-flag', { preHandler: requireAdmin('catalog:read') }, async (request, reply) => {
|
|
458
|
+
const flag = request.query.flag;
|
|
459
|
+
const allowed = [
|
|
460
|
+
'isSearchable',
|
|
461
|
+
'isFilterable',
|
|
462
|
+
'isComparable',
|
|
463
|
+
'isVariantAxis',
|
|
464
|
+
'isPromoRule',
|
|
465
|
+
'isVisibleOnProductPage',
|
|
466
|
+
'isRequired',
|
|
467
|
+
'isMassEditable',
|
|
468
|
+
];
|
|
469
|
+
if (!flag || !allowed.includes(flag)) {
|
|
470
|
+
reply.status(400);
|
|
471
|
+
return {
|
|
472
|
+
error: {
|
|
473
|
+
code: 'VALIDATION_FAILED',
|
|
474
|
+
message: `flag must be one of ${allowed.join(', ')}`,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const rows = await adminService.listAttributesByFlag(flag);
|
|
479
|
+
return {
|
|
480
|
+
data: {
|
|
481
|
+
items: rows.map((r) => ({
|
|
482
|
+
id: r.id,
|
|
483
|
+
key: r.key,
|
|
484
|
+
label: r.label,
|
|
485
|
+
labelDefault: r.labelDefault,
|
|
486
|
+
valueType: r.valueType,
|
|
487
|
+
})),
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
// Feature 012 — single attribute read by id or key.
|
|
492
|
+
app.get('/api/v1/admin/catalog/attributes/:idOrKey', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
493
|
+
const attr = await adminService.getAttributeByIdOrKey(request.params.idOrKey);
|
|
494
|
+
const optionValues = await adminService.getAttributeOptionValues(attr.id);
|
|
495
|
+
return { data: serializeAdminAttribute(attr, optionValues) };
|
|
496
|
+
});
|
|
497
|
+
// Feature 012 — delete an attribute. Refused while any Attribute Set or
|
|
498
|
+
// product still references it (FR-006).
|
|
499
|
+
app.delete('/api/v1/admin/catalog/attributes/:idOrKey', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
500
|
+
await adminService.deleteAttribute(request.params.idOrKey);
|
|
501
|
+
return reply.status(204).send();
|
|
502
|
+
});
|
|
503
|
+
// Feature 012 / US2 — preview a Set swap on a Product. Pure read.
|
|
504
|
+
app.post('/api/v1/admin/catalog/products/:id/attribute-set-preview', { preHandler: requireAdmin('catalog:write') }, async (request) => {
|
|
505
|
+
const body = (request.body ?? { targetSetId: null });
|
|
506
|
+
const preview = await adminService.previewAttributeSetSwap(request.params.id, body.targetSetId ?? null);
|
|
507
|
+
return { data: preview };
|
|
508
|
+
});
|
|
509
|
+
// Feature 012 / US4 — option-list CRUD per contracts/attribute-options.contract.md.
|
|
510
|
+
// Backed by `custom_field_options` since feature 061; `attributeId` stays the
|
|
511
|
+
// attribute (extension) id the admin API has always exposed.
|
|
512
|
+
function serializeOption(o) {
|
|
513
|
+
return {
|
|
514
|
+
id: o.id,
|
|
515
|
+
attributeId: o.attributeId,
|
|
516
|
+
value: o.value,
|
|
517
|
+
label: o.label,
|
|
518
|
+
labelDefault: o.labelDefault,
|
|
519
|
+
isDefault: o.isDefault,
|
|
520
|
+
sortOrder: o.sortOrder,
|
|
521
|
+
createdAt: o.createdAt.toISOString(),
|
|
522
|
+
updatedAt: o.updatedAt.toISOString(),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
app.get('/api/v1/admin/catalog/attributes/:attributeId/options', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
526
|
+
const attr = await adminService.getAttributeByIdOrKey(request.params.attributeId);
|
|
527
|
+
const options = await adminService.listAttributeOptions(attr.id);
|
|
528
|
+
return { data: { items: options.map(serializeOption) } };
|
|
529
|
+
});
|
|
530
|
+
app.post('/api/v1/admin/catalog/attributes/:attributeId/options', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
531
|
+
const row = await adminService.addAttributeOption(request.params.attributeId, request.body);
|
|
532
|
+
reply.status(201);
|
|
533
|
+
return { data: serializeOption(row) };
|
|
534
|
+
});
|
|
535
|
+
app.patch('/api/v1/admin/catalog/attributes/:attributeId/options/:optionId', { preHandler: requireAdmin('catalog:write') }, async (request) => {
|
|
536
|
+
const row = await adminService.patchAttributeOption(request.params.optionId, request.body);
|
|
537
|
+
return { data: serializeOption(row) };
|
|
538
|
+
});
|
|
539
|
+
app.delete('/api/v1/admin/catalog/attributes/:attributeId/options/:optionId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
540
|
+
await adminService.removeAttributeOption(request.params.optionId);
|
|
541
|
+
return reply.status(204).send();
|
|
542
|
+
});
|
|
543
|
+
// --- Categories CRUD -----------------------------------------------------
|
|
544
|
+
if (deps.categoryAdminService) {
|
|
545
|
+
const categoryService = deps.categoryAdminService;
|
|
546
|
+
app.get('/api/v1/admin/catalog/categories', { preHandler: requireAdmin('catalog:read') }, async () => {
|
|
547
|
+
const rows = await categoryService.listAll();
|
|
548
|
+
return { data: rows.map(serializeAdminCategory) };
|
|
549
|
+
});
|
|
550
|
+
app.post('/api/v1/admin/catalog/categories', {
|
|
551
|
+
preHandler: requireAdmin('catalog:write'),
|
|
552
|
+
schema: { body: createCategoryRequestSchema },
|
|
553
|
+
}, async (request, reply) => {
|
|
554
|
+
const body = createCategoryRequestSchema.parse(request.body);
|
|
555
|
+
const cat = await categoryService.create(body);
|
|
556
|
+
reply.status(201);
|
|
557
|
+
return { data: serializeAdminCategory(cat) };
|
|
558
|
+
});
|
|
559
|
+
app.patch('/api/v1/admin/catalog/categories/:id', {
|
|
560
|
+
preHandler: requireAdmin('catalog:write'),
|
|
561
|
+
schema: { body: updateCategoryRequestSchema },
|
|
562
|
+
}, async (request) => {
|
|
563
|
+
const body = updateCategoryRequestSchema.parse(request.body);
|
|
564
|
+
const cat = await categoryService.update(request.params.id, {
|
|
565
|
+
...(body.parentCategoryId !== undefined
|
|
566
|
+
? { parentCategoryId: body.parentCategoryId }
|
|
567
|
+
: {}),
|
|
568
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
569
|
+
...(body.slug !== undefined ? { slug: body.slug } : {}),
|
|
570
|
+
...(body.sortOrder !== undefined ? { sortOrder: body.sortOrder } : {}),
|
|
571
|
+
...(body.isActive !== undefined ? { isActive: body.isActive } : {}),
|
|
572
|
+
...(body.mainImageAssetId !== undefined
|
|
573
|
+
? { mainImageAssetId: body.mainImageAssetId }
|
|
574
|
+
: {}),
|
|
575
|
+
...(body.customFieldValues !== undefined
|
|
576
|
+
? { customFieldValues: body.customFieldValues }
|
|
577
|
+
: {}),
|
|
578
|
+
});
|
|
579
|
+
return { data: serializeAdminCategory(cat) };
|
|
580
|
+
});
|
|
581
|
+
app.delete('/api/v1/admin/catalog/categories/:id', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
582
|
+
await categoryService.softDelete(request.params.id);
|
|
583
|
+
return reply.status(204).send();
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
// ===== Feature 002 — Attribute Sets =========================================
|
|
587
|
+
// contracts/catalog-002.contract.md → 7 endpoints. Wired only when the
|
|
588
|
+
// composition root supplies `attributeSetService` (test-server.ts +
|
|
589
|
+
// production composition root both do; dev shims may opt out).
|
|
590
|
+
if (deps.attributeSetService) {
|
|
591
|
+
const attrSetService = deps.attributeSetService;
|
|
592
|
+
app.get('/api/v1/admin/catalog/attribute-sets', { preHandler: requireAdmin('catalog:read') }, async () => {
|
|
593
|
+
const sets = await attrSetService.listSets();
|
|
594
|
+
return { data: sets };
|
|
595
|
+
});
|
|
596
|
+
app.get('/api/v1/admin/catalog/attribute-sets/:id', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
597
|
+
const detail = await attrSetService.getSetDetail(request.params.id);
|
|
598
|
+
return { data: detail };
|
|
599
|
+
});
|
|
600
|
+
app.post('/api/v1/admin/catalog/attribute-sets', {
|
|
601
|
+
preHandler: requireAdmin('catalog:write'),
|
|
602
|
+
schema: { body: createAttributeSetRequestSchema },
|
|
603
|
+
}, async (request, reply) => {
|
|
604
|
+
const body = createAttributeSetRequestSchema.parse(request.body);
|
|
605
|
+
// Feature 054/061 — audited co-transactionally inside the service's
|
|
606
|
+
// `attribute_set.create` Command (no hand audit here, Principle XIII).
|
|
607
|
+
const detail = await attrSetService.createSet(body);
|
|
608
|
+
reply.status(201);
|
|
609
|
+
return { data: detail };
|
|
610
|
+
});
|
|
611
|
+
app.patch('/api/v1/admin/catalog/attribute-sets/:id', {
|
|
612
|
+
preHandler: requireAdmin('catalog:write'),
|
|
613
|
+
schema: { body: updateAttributeSetRequestSchema },
|
|
614
|
+
}, async (request) => {
|
|
615
|
+
const body = updateAttributeSetRequestSchema.parse(request.body);
|
|
616
|
+
// Audited co-transactionally via the `attribute_set.update` Command.
|
|
617
|
+
const set = await attrSetService.updateSet(request.params.id, body);
|
|
618
|
+
return { data: set };
|
|
619
|
+
});
|
|
620
|
+
app.delete('/api/v1/admin/catalog/attribute-sets/:id', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
621
|
+
// Audited co-transactionally via the `attribute_set.delete` Command.
|
|
622
|
+
await attrSetService.deleteSet(request.params.id);
|
|
623
|
+
return reply.status(204).send();
|
|
624
|
+
});
|
|
625
|
+
app.post('/api/v1/admin/catalog/attribute-sets/:id/attributes', {
|
|
626
|
+
preHandler: requireAdmin('catalog:write'),
|
|
627
|
+
schema: { body: assignAttributesRequestSchema },
|
|
628
|
+
}, async (request) => {
|
|
629
|
+
const body = assignAttributesRequestSchema.parse(request.body);
|
|
630
|
+
// Audited co-transactionally via the `attribute_set.assign_attributes`
|
|
631
|
+
// Command (feature 061 — the bridge write and audit share one tx).
|
|
632
|
+
const detail = await attrSetService.assignAttributes(request.params.id, body);
|
|
633
|
+
return { data: detail };
|
|
634
|
+
});
|
|
635
|
+
app.delete('/api/v1/admin/catalog/attribute-sets/:id/attributes/:attributeId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
636
|
+
// Audited co-transactionally via the `attribute_set.unassign_attribute`
|
|
637
|
+
// Command (no-op deletes stay 204 and are not double-audited).
|
|
638
|
+
await attrSetService.unassignAttribute(request.params.id, request.params.attributeId);
|
|
639
|
+
return reply.status(204).send();
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
// ===== Feature 002 — Gallery admin CRUD (US3) ==============================
|
|
643
|
+
if (deps.galleryService) {
|
|
644
|
+
const gallery = deps.galleryService;
|
|
645
|
+
app.get('/api/v1/admin/catalog/products/:productId/gallery', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
646
|
+
const items = await gallery.list(request.params.productId);
|
|
647
|
+
return { data: items };
|
|
648
|
+
});
|
|
649
|
+
app.post('/api/v1/admin/catalog/products/:productId/gallery', {
|
|
650
|
+
preHandler: requireAdmin('catalog:write'),
|
|
651
|
+
schema: { body: createGalleryItemRequestSchema },
|
|
652
|
+
}, async (request, reply) => {
|
|
653
|
+
const body = createGalleryItemRequestSchema.parse(request.body);
|
|
654
|
+
const item = await gallery.create(request.params.productId, body, {
|
|
655
|
+
replaceConflictingLabels: request.query.replace === 'true',
|
|
656
|
+
});
|
|
657
|
+
reply.status(201);
|
|
658
|
+
await auditEmit(request, {
|
|
659
|
+
action: 'gallery.create',
|
|
660
|
+
objectType: 'gallery_item',
|
|
661
|
+
objectId: item.id,
|
|
662
|
+
});
|
|
663
|
+
return { data: item };
|
|
664
|
+
});
|
|
665
|
+
app.patch('/api/v1/admin/catalog/products/:productId/gallery/:itemId', {
|
|
666
|
+
preHandler: requireAdmin('catalog:write'),
|
|
667
|
+
schema: { body: updateGalleryItemRequestSchema },
|
|
668
|
+
}, async (request) => {
|
|
669
|
+
const body = updateGalleryItemRequestSchema.parse(request.body);
|
|
670
|
+
const item = await gallery.update(request.params.productId, request.params.itemId, body, { replaceConflictingLabels: request.query.replace === 'true' });
|
|
671
|
+
await auditEmit(request, {
|
|
672
|
+
action: 'gallery.update',
|
|
673
|
+
objectType: 'gallery_item',
|
|
674
|
+
objectId: request.params.itemId,
|
|
675
|
+
});
|
|
676
|
+
return { data: item };
|
|
677
|
+
});
|
|
678
|
+
app.delete('/api/v1/admin/catalog/products/:productId/gallery/:itemId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
679
|
+
await gallery.delete(request.params.productId, request.params.itemId);
|
|
680
|
+
await auditEmit(request, {
|
|
681
|
+
action: 'gallery.delete',
|
|
682
|
+
objectType: 'gallery_item',
|
|
683
|
+
objectId: request.params.itemId,
|
|
684
|
+
});
|
|
685
|
+
return reply.status(204).send();
|
|
686
|
+
});
|
|
687
|
+
app.put('/api/v1/admin/catalog/products/:productId/gallery/order', {
|
|
688
|
+
preHandler: requireAdmin('catalog:write'),
|
|
689
|
+
schema: { body: reorderGalleryRequestSchema },
|
|
690
|
+
}, async (request, reply) => {
|
|
691
|
+
const body = reorderGalleryRequestSchema.parse(request.body);
|
|
692
|
+
await gallery.reorder(request.params.productId, body.orderedGalleryItemIds);
|
|
693
|
+
await auditEmit(request, {
|
|
694
|
+
action: 'gallery.reorder',
|
|
695
|
+
objectType: 'product',
|
|
696
|
+
objectId: request.params.productId,
|
|
697
|
+
});
|
|
698
|
+
return reply.status(204).send();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// ===== Feature 002 — Attachments admin CRUD (US3) ==========================
|
|
702
|
+
if (deps.attachmentService) {
|
|
703
|
+
const att = deps.attachmentService;
|
|
704
|
+
app.get('/api/v1/admin/catalog/attachment-types', { preHandler: requireAdmin('catalog:read') }, async () => ({ data: await att.listTypes() }));
|
|
705
|
+
app.post('/api/v1/admin/catalog/attachment-types', {
|
|
706
|
+
preHandler: requireAdmin('catalog:write'),
|
|
707
|
+
schema: { body: createAttachmentTypeRequestSchema },
|
|
708
|
+
}, async (request, reply) => {
|
|
709
|
+
const body = createAttachmentTypeRequestSchema.parse(request.body);
|
|
710
|
+
const type = await att.createType(body);
|
|
711
|
+
reply.status(201);
|
|
712
|
+
await auditEmit(request, {
|
|
713
|
+
action: 'attachment_type.create',
|
|
714
|
+
objectType: 'attachment_type',
|
|
715
|
+
objectId: type.id,
|
|
716
|
+
});
|
|
717
|
+
return { data: type };
|
|
718
|
+
});
|
|
719
|
+
app.patch('/api/v1/admin/catalog/attachment-types/:id', {
|
|
720
|
+
preHandler: requireAdmin('catalog:write'),
|
|
721
|
+
schema: { body: updateAttachmentTypeRequestSchema },
|
|
722
|
+
}, async (request) => {
|
|
723
|
+
const body = updateAttachmentTypeRequestSchema.parse(request.body);
|
|
724
|
+
const type = await att.updateType(request.params.id, body);
|
|
725
|
+
await auditEmit(request, {
|
|
726
|
+
action: 'attachment_type.update',
|
|
727
|
+
objectType: 'attachment_type',
|
|
728
|
+
objectId: request.params.id,
|
|
729
|
+
});
|
|
730
|
+
return { data: type };
|
|
731
|
+
});
|
|
732
|
+
app.delete('/api/v1/admin/catalog/attachment-types/:id', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
733
|
+
await att.deleteType(request.params.id);
|
|
734
|
+
await auditEmit(request, {
|
|
735
|
+
action: 'attachment_type.delete',
|
|
736
|
+
objectType: 'attachment_type',
|
|
737
|
+
objectId: request.params.id,
|
|
738
|
+
});
|
|
739
|
+
return reply.status(204).send();
|
|
740
|
+
});
|
|
741
|
+
app.get('/api/v1/admin/catalog/products/:productId/attachments', { preHandler: requireAdmin('catalog:read') }, async (request) => ({
|
|
742
|
+
data: await att.listAttachments(request.params.productId),
|
|
743
|
+
}));
|
|
744
|
+
app.post('/api/v1/admin/catalog/products/:productId/attachments', {
|
|
745
|
+
preHandler: requireAdmin('catalog:write'),
|
|
746
|
+
schema: { body: createAttachmentRequestSchema },
|
|
747
|
+
}, async (request, reply) => {
|
|
748
|
+
const body = createAttachmentRequestSchema.parse(request.body);
|
|
749
|
+
const a = await att.createAttachment(request.params.productId, body);
|
|
750
|
+
reply.status(201);
|
|
751
|
+
await auditEmit(request, {
|
|
752
|
+
action: 'attachment.create',
|
|
753
|
+
objectType: 'product_attachment',
|
|
754
|
+
objectId: a.id,
|
|
755
|
+
});
|
|
756
|
+
return { data: a };
|
|
757
|
+
});
|
|
758
|
+
app.patch('/api/v1/admin/catalog/products/:productId/attachments/:attachmentId', {
|
|
759
|
+
preHandler: requireAdmin('catalog:write'),
|
|
760
|
+
schema: { body: updateAttachmentRequestSchema },
|
|
761
|
+
}, async (request) => {
|
|
762
|
+
const body = updateAttachmentRequestSchema.parse(request.body);
|
|
763
|
+
const a = await att.updateAttachment(request.params.productId, request.params.attachmentId, body);
|
|
764
|
+
await auditEmit(request, {
|
|
765
|
+
action: 'attachment.update',
|
|
766
|
+
objectType: 'product_attachment',
|
|
767
|
+
objectId: request.params.attachmentId,
|
|
768
|
+
});
|
|
769
|
+
return { data: a };
|
|
770
|
+
});
|
|
771
|
+
app.delete('/api/v1/admin/catalog/products/:productId/attachments/:attachmentId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
772
|
+
await att.deleteAttachment(request.params.productId, request.params.attachmentId);
|
|
773
|
+
await auditEmit(request, {
|
|
774
|
+
action: 'attachment.delete',
|
|
775
|
+
objectType: 'product_attachment',
|
|
776
|
+
objectId: request.params.attachmentId,
|
|
777
|
+
});
|
|
778
|
+
return reply.status(204).send();
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
// ===== Feature 043 — Packaging units admin CRUD ===========================
|
|
782
|
+
if (deps.packagingUnitService) {
|
|
783
|
+
const pkg = deps.packagingUnitService;
|
|
784
|
+
app.get('/api/v1/admin/catalog/products/:productId/packaging-units', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
785
|
+
const data = await pkg.list(request.params.productId);
|
|
786
|
+
return { data };
|
|
787
|
+
});
|
|
788
|
+
app.post('/api/v1/admin/catalog/products/:productId/packaging-units', {
|
|
789
|
+
preHandler: requireAdmin('catalog:write'),
|
|
790
|
+
schema: { body: createPackagingUnitRequestSchema },
|
|
791
|
+
}, async (request, reply) => {
|
|
792
|
+
const body = createPackagingUnitRequestSchema.parse(request.body);
|
|
793
|
+
const unit = await pkg.create(request.params.productId, body);
|
|
794
|
+
reply.status(201);
|
|
795
|
+
await auditEmit(request, {
|
|
796
|
+
action: 'packaging_unit.create',
|
|
797
|
+
objectType: 'product_packaging_unit',
|
|
798
|
+
objectId: unit.id,
|
|
799
|
+
});
|
|
800
|
+
return { data: unit };
|
|
801
|
+
});
|
|
802
|
+
app.patch('/api/v1/admin/catalog/products/:productId/packaging-units/:unitId', {
|
|
803
|
+
preHandler: requireAdmin('catalog:write'),
|
|
804
|
+
schema: { body: updatePackagingUnitRequestSchema },
|
|
805
|
+
}, async (request) => {
|
|
806
|
+
const body = updatePackagingUnitRequestSchema.parse(request.body);
|
|
807
|
+
const unit = await pkg.update(request.params.productId, request.params.unitId, body);
|
|
808
|
+
await auditEmit(request, {
|
|
809
|
+
action: 'packaging_unit.update',
|
|
810
|
+
objectType: 'product_packaging_unit',
|
|
811
|
+
objectId: request.params.unitId,
|
|
812
|
+
});
|
|
813
|
+
return { data: unit };
|
|
814
|
+
});
|
|
815
|
+
app.delete('/api/v1/admin/catalog/products/:productId/packaging-units/:unitId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
816
|
+
await pkg.delete(request.params.productId, request.params.unitId);
|
|
817
|
+
await auditEmit(request, {
|
|
818
|
+
action: 'packaging_unit.delete',
|
|
819
|
+
objectType: 'product_packaging_unit',
|
|
820
|
+
objectId: request.params.unitId,
|
|
821
|
+
});
|
|
822
|
+
return reply.status(204).send();
|
|
823
|
+
});
|
|
824
|
+
app.patch('/api/v1/admin/catalog/products/:productId/packaging-units/reorder', {
|
|
825
|
+
preHandler: requireAdmin('catalog:write'),
|
|
826
|
+
schema: { body: reorderPackagingUnitsRequestSchema },
|
|
827
|
+
}, async (request) => {
|
|
828
|
+
const body = reorderPackagingUnitsRequestSchema.parse(request.body);
|
|
829
|
+
const data = await pkg.reorder(request.params.productId, body);
|
|
830
|
+
await auditEmit(request, {
|
|
831
|
+
action: 'packaging_unit.reorder',
|
|
832
|
+
objectType: 'product',
|
|
833
|
+
objectId: request.params.productId,
|
|
834
|
+
});
|
|
835
|
+
return { data };
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
// ===== Feature 002 — Variants admin CRUD ===================================
|
|
839
|
+
// contracts/catalog-002.contract.md / spec.md US2: configurable Products
|
|
840
|
+
// need an admin write surface for Variants. Foundation 001 shipped the
|
|
841
|
+
// ProductVariant entity but no routes.
|
|
842
|
+
app.post('/api/v1/admin/catalog/products/:productId/variants', {
|
|
843
|
+
preHandler: requireAdmin('catalog:write'),
|
|
844
|
+
schema: { body: createVariantRequestSchema },
|
|
845
|
+
}, async (request, reply) => {
|
|
846
|
+
const body = createVariantRequestSchema.parse(request.body);
|
|
847
|
+
const variant = await adminService.createVariant(request.params.productId, body);
|
|
848
|
+
reply.status(201);
|
|
849
|
+
return { data: serializeAdminVariant(variant) };
|
|
850
|
+
});
|
|
851
|
+
app.patch('/api/v1/admin/catalog/products/:productId/variants/:variantId', {
|
|
852
|
+
preHandler: requireAdmin('catalog:write'),
|
|
853
|
+
schema: { body: updateVariantRequestSchema },
|
|
854
|
+
}, async (request) => {
|
|
855
|
+
const body = updateVariantRequestSchema.parse(request.body);
|
|
856
|
+
const variant = await adminService.updateVariant(request.params.productId, request.params.variantId, body);
|
|
857
|
+
return { data: serializeAdminVariant(variant) };
|
|
858
|
+
});
|
|
859
|
+
app.delete('/api/v1/admin/catalog/products/:productId/variants/:variantId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
860
|
+
await adminService.deleteVariant(request.params.productId, request.params.variantId);
|
|
861
|
+
return reply.status(204).send();
|
|
862
|
+
});
|
|
863
|
+
// --- Product Links (Feature 002 US4) ----------------------------------
|
|
864
|
+
if (deps.productLinkService) {
|
|
865
|
+
const links = deps.productLinkService;
|
|
866
|
+
app.get('/api/v1/admin/catalog/products/:id/links', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
867
|
+
const kind = request.query.kind
|
|
868
|
+
? productLinkKindSchema.parse(request.query.kind)
|
|
869
|
+
: undefined;
|
|
870
|
+
const rows = await links.listForAdmin(request.params.id, kind);
|
|
871
|
+
return { data: rows };
|
|
872
|
+
});
|
|
873
|
+
app.post('/api/v1/admin/catalog/products/:id/links', {
|
|
874
|
+
preHandler: requireAdmin('catalog:write'),
|
|
875
|
+
schema: { body: bulkCreateLinksRequestSchema },
|
|
876
|
+
}, async (request, reply) => {
|
|
877
|
+
const body = bulkCreateLinksRequestSchema.parse(request.body);
|
|
878
|
+
const rows = await links.bulkCreate(request.params.id, body.links);
|
|
879
|
+
reply.status(201);
|
|
880
|
+
for (const row of rows) {
|
|
881
|
+
await auditEmit(request, {
|
|
882
|
+
action: 'product_link.create',
|
|
883
|
+
objectType: 'product_link',
|
|
884
|
+
objectId: row.id,
|
|
885
|
+
stateAfter: {
|
|
886
|
+
sourceProductId: row.sourceProductId,
|
|
887
|
+
targetProductId: row.targetProductId,
|
|
888
|
+
kind: row.kind,
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return { data: rows };
|
|
893
|
+
});
|
|
894
|
+
app.delete('/api/v1/admin/catalog/products/:id/links/:linkId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
895
|
+
await links.removeLink(request.params.id, request.params.linkId);
|
|
896
|
+
await auditEmit(request, {
|
|
897
|
+
action: 'product_link.delete',
|
|
898
|
+
objectType: 'product_link',
|
|
899
|
+
objectId: request.params.linkId,
|
|
900
|
+
});
|
|
901
|
+
return reply.status(204).send();
|
|
902
|
+
});
|
|
903
|
+
app.put('/api/v1/admin/catalog/products/:id/links/:kind/order', {
|
|
904
|
+
preHandler: requireAdmin('catalog:write'),
|
|
905
|
+
schema: { body: reorderLinksRequestSchema },
|
|
906
|
+
}, async (request) => {
|
|
907
|
+
const kind = productLinkKindSchema.parse(request.params.kind);
|
|
908
|
+
const body = reorderLinksRequestSchema.parse(request.body);
|
|
909
|
+
await links.reorderForKind(request.params.id, kind, body.linkIds);
|
|
910
|
+
await auditEmit(request, {
|
|
911
|
+
action: 'product_link.reorder',
|
|
912
|
+
objectType: 'product',
|
|
913
|
+
objectId: request.params.id,
|
|
914
|
+
stateAfter: { kind, linkIds: body.linkIds },
|
|
915
|
+
});
|
|
916
|
+
return { data: { ok: true } };
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
// --- Grouped items (Feature 002 US5) ----------------------------------
|
|
920
|
+
if (deps.groupedService) {
|
|
921
|
+
const grouped = deps.groupedService;
|
|
922
|
+
app.get('/api/v1/admin/catalog/products/:id/grouped-items', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
923
|
+
const rows = await grouped.list(request.params.id);
|
|
924
|
+
return { data: rows };
|
|
925
|
+
});
|
|
926
|
+
app.post('/api/v1/admin/catalog/products/:id/grouped-items', {
|
|
927
|
+
preHandler: requireAdmin('catalog:write'),
|
|
928
|
+
schema: { body: createGroupedItemRequestSchema },
|
|
929
|
+
}, async (request, reply) => {
|
|
930
|
+
const body = createGroupedItemRequestSchema.parse(request.body);
|
|
931
|
+
const row = await grouped.addItem(request.params.id, {
|
|
932
|
+
childProductId: body.childProductId,
|
|
933
|
+
quantity: body.quantity,
|
|
934
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
935
|
+
});
|
|
936
|
+
reply.status(201);
|
|
937
|
+
await auditEmit(request, {
|
|
938
|
+
action: 'grouped_item.create',
|
|
939
|
+
objectType: 'grouped_item',
|
|
940
|
+
objectId: row.id,
|
|
941
|
+
stateAfter: {
|
|
942
|
+
parentProductId: row.parentProductId,
|
|
943
|
+
childProductId: row.childProductId,
|
|
944
|
+
quantity: row.quantity,
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
return { data: row };
|
|
948
|
+
});
|
|
949
|
+
app.patch('/api/v1/admin/catalog/products/:id/grouped-items/:itemId', {
|
|
950
|
+
preHandler: requireAdmin('catalog:write'),
|
|
951
|
+
schema: { body: updateGroupedItemRequestSchema },
|
|
952
|
+
}, async (request) => {
|
|
953
|
+
const body = updateGroupedItemRequestSchema.parse(request.body);
|
|
954
|
+
const row = await grouped.updateItem(request.params.id, request.params.itemId, {
|
|
955
|
+
...(body.quantity !== undefined ? { quantity: body.quantity } : {}),
|
|
956
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
957
|
+
});
|
|
958
|
+
await auditEmit(request, {
|
|
959
|
+
action: 'grouped_item.update',
|
|
960
|
+
objectType: 'grouped_item',
|
|
961
|
+
objectId: request.params.itemId,
|
|
962
|
+
});
|
|
963
|
+
return { data: row };
|
|
964
|
+
});
|
|
965
|
+
app.delete('/api/v1/admin/catalog/products/:id/grouped-items/:itemId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
966
|
+
await grouped.removeItem(request.params.id, request.params.itemId);
|
|
967
|
+
await auditEmit(request, {
|
|
968
|
+
action: 'grouped_item.delete',
|
|
969
|
+
objectType: 'grouped_item',
|
|
970
|
+
objectId: request.params.itemId,
|
|
971
|
+
});
|
|
972
|
+
return reply.status(204).send();
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
// --- Bundle slots + options (Feature 002 US5) -------------------------
|
|
976
|
+
if (deps.bundleService) {
|
|
977
|
+
const bundle = deps.bundleService;
|
|
978
|
+
app.get('/api/v1/admin/catalog/products/:id/bundle-slots', { preHandler: requireAdmin('catalog:read') }, async (request) => {
|
|
979
|
+
const rows = await bundle.listSlots(request.params.id);
|
|
980
|
+
return { data: rows };
|
|
981
|
+
});
|
|
982
|
+
app.post('/api/v1/admin/catalog/products/:id/bundle-slots', {
|
|
983
|
+
preHandler: requireAdmin('catalog:write'),
|
|
984
|
+
schema: { body: createBundleSlotRequestSchema },
|
|
985
|
+
}, async (request, reply) => {
|
|
986
|
+
const body = createBundleSlotRequestSchema.parse(request.body);
|
|
987
|
+
const row = await bundle.createSlot(request.params.id, {
|
|
988
|
+
name: body.name,
|
|
989
|
+
...(body.minQuantity !== undefined ? { minQuantity: body.minQuantity } : {}),
|
|
990
|
+
maxQuantity: body.maxQuantity,
|
|
991
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
992
|
+
});
|
|
993
|
+
reply.status(201);
|
|
994
|
+
await auditEmit(request, {
|
|
995
|
+
action: 'bundle_slot.create',
|
|
996
|
+
objectType: 'bundle_slot',
|
|
997
|
+
objectId: row.id,
|
|
998
|
+
});
|
|
999
|
+
return { data: row };
|
|
1000
|
+
});
|
|
1001
|
+
app.patch('/api/v1/admin/catalog/products/:id/bundle-slots/:slotId', {
|
|
1002
|
+
preHandler: requireAdmin('catalog:write'),
|
|
1003
|
+
schema: { body: updateBundleSlotRequestSchema },
|
|
1004
|
+
}, async (request) => {
|
|
1005
|
+
const body = updateBundleSlotRequestSchema.parse(request.body);
|
|
1006
|
+
const row = await bundle.updateSlot(request.params.id, request.params.slotId, {
|
|
1007
|
+
...(body.name !== undefined ? { name: body.name } : {}),
|
|
1008
|
+
...(body.minQuantity !== undefined ? { minQuantity: body.minQuantity } : {}),
|
|
1009
|
+
...(body.maxQuantity !== undefined ? { maxQuantity: body.maxQuantity } : {}),
|
|
1010
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
1011
|
+
});
|
|
1012
|
+
await auditEmit(request, {
|
|
1013
|
+
action: 'bundle_slot.update',
|
|
1014
|
+
objectType: 'bundle_slot',
|
|
1015
|
+
objectId: request.params.slotId,
|
|
1016
|
+
});
|
|
1017
|
+
return { data: row };
|
|
1018
|
+
});
|
|
1019
|
+
app.delete('/api/v1/admin/catalog/products/:id/bundle-slots/:slotId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
1020
|
+
await bundle.deleteSlot(request.params.id, request.params.slotId);
|
|
1021
|
+
await auditEmit(request, {
|
|
1022
|
+
action: 'bundle_slot.delete',
|
|
1023
|
+
objectType: 'bundle_slot',
|
|
1024
|
+
objectId: request.params.slotId,
|
|
1025
|
+
});
|
|
1026
|
+
return reply.status(204).send();
|
|
1027
|
+
});
|
|
1028
|
+
app.post('/api/v1/admin/catalog/products/:id/bundle-slots/:slotId/options', {
|
|
1029
|
+
preHandler: requireAdmin('catalog:write'),
|
|
1030
|
+
schema: { body: createBundleSlotOptionRequestSchema },
|
|
1031
|
+
}, async (request, reply) => {
|
|
1032
|
+
const body = createBundleSlotOptionRequestSchema.parse(request.body);
|
|
1033
|
+
const row = await bundle.addOption(request.params.id, request.params.slotId, {
|
|
1034
|
+
optionProductId: body.optionProductId,
|
|
1035
|
+
...(body.defaultQuantity !== undefined
|
|
1036
|
+
? { defaultQuantity: body.defaultQuantity }
|
|
1037
|
+
: {}),
|
|
1038
|
+
...(body.position !== undefined ? { position: body.position } : {}),
|
|
1039
|
+
});
|
|
1040
|
+
reply.status(201);
|
|
1041
|
+
await auditEmit(request, {
|
|
1042
|
+
action: 'bundle_slot_option.create',
|
|
1043
|
+
objectType: 'bundle_slot_option',
|
|
1044
|
+
objectId: row.id,
|
|
1045
|
+
stateAfter: {
|
|
1046
|
+
slotId: row.slotId,
|
|
1047
|
+
optionProductId: row.optionProductId,
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
return { data: row };
|
|
1051
|
+
});
|
|
1052
|
+
app.delete('/api/v1/admin/catalog/products/:id/bundle-slots/:slotId/options/:optionId', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
|
|
1053
|
+
await bundle.removeOption(request.params.id, request.params.slotId, request.params.optionId);
|
|
1054
|
+
await auditEmit(request, {
|
|
1055
|
+
action: 'bundle_slot_option.delete',
|
|
1056
|
+
objectType: 'bundle_slot_option',
|
|
1057
|
+
objectId: request.params.optionId,
|
|
1058
|
+
});
|
|
1059
|
+
return reply.status(204).send();
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function serializeAdminProduct(p) {
|
|
1064
|
+
return {
|
|
1065
|
+
id: p.id,
|
|
1066
|
+
sku: p.sku,
|
|
1067
|
+
slug: p.slug,
|
|
1068
|
+
type: p.type,
|
|
1069
|
+
status: p.status,
|
|
1070
|
+
name: p.name,
|
|
1071
|
+
description: p.description,
|
|
1072
|
+
visibility: p.visibility,
|
|
1073
|
+
stockMode: p.stockMode ?? null,
|
|
1074
|
+
attributeValues: p.attributeValues,
|
|
1075
|
+
allowedOrganizationIds: p.allowedOrganizationIds ?? [],
|
|
1076
|
+
// Feature 002 (T034) — surface the wired Attribute Set so the
|
|
1077
|
+
// admin Product editor can render the selector with the right
|
|
1078
|
+
// initial value.
|
|
1079
|
+
attributeSetId: p.attributeSetId,
|
|
1080
|
+
// Feature 002 — virtual product download fields.
|
|
1081
|
+
downloadAssetId: p.downloadAssetId ?? null,
|
|
1082
|
+
downloadUrl: p.downloadUrl ?? null,
|
|
1083
|
+
// Feature 010 — inventory module per-product stock flags.
|
|
1084
|
+
manageStock: p.manageStock,
|
|
1085
|
+
backorderEnabled: p.backorderEnabled,
|
|
1086
|
+
lowStockThreshold: p.lowStockThreshold ?? null,
|
|
1087
|
+
lowStockThresholdMode: p.lowStockThresholdMode,
|
|
1088
|
+
fulfilmentStrategy: p.fulfilmentStrategy ?? null,
|
|
1089
|
+
fulfilmentStrategyWarehouseOrder: p.fulfilmentStrategyWarehouseOrder ?? null,
|
|
1090
|
+
// Feature 022 — surface so the Bulk Edit summary and the single-
|
|
1091
|
+
// product editor can render the archived state.
|
|
1092
|
+
archivedAt: p.archivedAt ? p.archivedAt.toISOString() : null,
|
|
1093
|
+
createdAt: p.createdAt.toISOString(),
|
|
1094
|
+
updatedAt: p.updatedAt.toISOString(),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function serializeAdminAttribute(a, optionValues = null) {
|
|
1098
|
+
const api = dbToApiAttributeType(a.valueType, a.displayAsSlider);
|
|
1099
|
+
return {
|
|
1100
|
+
id: a.id,
|
|
1101
|
+
key: a.key,
|
|
1102
|
+
label: a.label,
|
|
1103
|
+
labelDefault: a.labelDefault,
|
|
1104
|
+
// API-form (feature 002 T013/T021/T022): admin UI can read either
|
|
1105
|
+
// `type` or the legacy `valueType` — both are emitted.
|
|
1106
|
+
type: api.type,
|
|
1107
|
+
numericKind: api.numericKind,
|
|
1108
|
+
valueType: a.valueType,
|
|
1109
|
+
// Feature 012 — legacy projection of the option rows (callers that
|
|
1110
|
+
// need the rich shape use the dedicated /attributes/:id/options
|
|
1111
|
+
// endpoints). Null when the attribute has no options or the caller
|
|
1112
|
+
// didn't fetch them.
|
|
1113
|
+
enumValues: optionValues,
|
|
1114
|
+
isSearchable: a.isSearchable,
|
|
1115
|
+
isFilterable: a.isFilterable,
|
|
1116
|
+
isVariantAxis: a.isVariantAxis,
|
|
1117
|
+
displayAsSlider: a.displayAsSlider,
|
|
1118
|
+
isComparable: a.isComparable,
|
|
1119
|
+
isRequired: a.isRequired,
|
|
1120
|
+
isPromoRule: a.isPromoRule,
|
|
1121
|
+
filterPosition: a.filterPosition,
|
|
1122
|
+
isVisibleOnProductPage: a.isVisibleOnProductPage,
|
|
1123
|
+
// Feature 022 — gates appearance in the Products Bulk Edit dialog.
|
|
1124
|
+
massEditable: a.massEditable,
|
|
1125
|
+
// Feature 039 — gates participation in Quick Order search.
|
|
1126
|
+
quickSearchable: a.quickSearchable,
|
|
1127
|
+
// Feature 061 (additive) — id of the backing product-host Custom Field
|
|
1128
|
+
// definition (adminAttributeResponseSchema.customFieldDefinitionId).
|
|
1129
|
+
customFieldDefinitionId: a.customFieldDefinitionId,
|
|
1130
|
+
createdAt: a.createdAt.toISOString(),
|
|
1131
|
+
updatedAt: a.updatedAt.toISOString(),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function serializeAdminVariant(v) {
|
|
1135
|
+
return {
|
|
1136
|
+
id: v.id,
|
|
1137
|
+
parentProductId: v.parentProductId,
|
|
1138
|
+
sku: v.sku,
|
|
1139
|
+
variantAttributeValues: v.variantAttributeValues,
|
|
1140
|
+
priceOverride: v.priceOverride != null ? Number(v.priceOverride) : null,
|
|
1141
|
+
stockLevel: v.stockLevel ?? null,
|
|
1142
|
+
createdAt: v.createdAt.toISOString(),
|
|
1143
|
+
updatedAt: v.updatedAt.toISOString(),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function serializeAdminCategory(c) {
|
|
1147
|
+
return {
|
|
1148
|
+
id: c.id,
|
|
1149
|
+
parentCategoryId: c.parentCategoryId ?? null,
|
|
1150
|
+
name: c.name,
|
|
1151
|
+
slug: c.slug,
|
|
1152
|
+
sortOrder: c.sortOrder,
|
|
1153
|
+
// Feature 068 — the admin tree lists inactive categories too, so the
|
|
1154
|
+
// operator can see (and undo) what is hidden from customers.
|
|
1155
|
+
isActive: c.isActive,
|
|
1156
|
+
mainImageAssetId: c.mainImageAssetId ?? null,
|
|
1157
|
+
customFieldValues: c.customFieldValues ?? {},
|
|
1158
|
+
createdAt: c.createdAt.toISOString(),
|
|
1159
|
+
updatedAt: c.updatedAt.toISOString(),
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
//# sourceMappingURL=routes.admin.js.map
|