From c52b2a53a7e93723aae019030025162431e3cd57 Mon Sep 17 00:00:00 2001 From: Sebas Date: Mon, 24 Nov 2025 12:22:19 +0100 Subject: [PATCH 01/10] Add combinations endpoint --- config/admin/services.yml | 4 + .../ProductCombinationListNormalizer.php | 54 +++++++++ .../Resources/Product/ProductCombination.php | 103 ++++++++++++++++++ .../Product/ProductCombinationList.php | 73 +++++++++++++ .../ProductCombinationEndpointTest.php | 64 +++++++++++ .../ProductCombinationListEndpointTest.php | 51 +++++++++ 6 files changed, 349 insertions(+) create mode 100644 src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php create mode 100644 src/ApiPlatform/Resources/Product/ProductCombination.php create mode 100644 src/ApiPlatform/Resources/Product/ProductCombinationList.php create mode 100644 tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php create mode 100644 tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php diff --git a/config/admin/services.yml b/config/admin/services.yml index ed59ebbf..51fc6b65 100644 --- a/config/admin/services.yml +++ b/config/admin/services.yml @@ -57,6 +57,10 @@ services: tags: - { name: 'serializer.normalizer', priority: 300 } + PrestaShop\Module\APIResources\ApiPlatform\Normalizer\ProductCombinationListNormalizer: + tags: + - { name: 'serializer.normalizer', priority: 100 } + PrestaShopBundle\ApiPlatform\Serializer\CQRSApiSerializer: class: PrestaShop\Module\APIResources\Serializer\QueryParameterTypeCastSerializer decorates: 'api_platform.serializer' diff --git a/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php b/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php new file mode 100644 index 00000000..d68a8a83 --- /dev/null +++ b/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php @@ -0,0 +1,54 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +namespace PrestaShop\Module\APIResources\ApiPlatform\Normalizer; + +use PrestaShop\Module\APIResources\ApiPlatform\Resources\Product\ProductCombinationList; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Custom normalizer for ProductCombinationList to return only the combinationId + */ +class ProductCombinationListNormalizer implements NormalizerInterface +{ + /** + * @param ProductCombinationList $object + * @param string|null $format + * @param array $context + * + * @return int + */ + public function normalize(mixed $object, ?string $format = null, array $context = []): int + { + return $object->combinationId; + } + + public function supportsNormalization(mixed $data, ?string $format = null): bool + { + return $data instanceof ProductCombinationList; + } + + public function getSupportedTypes(?string $format): array + { + return [ + ProductCombinationList::class => true, + ]; + } +} diff --git a/src/ApiPlatform/Resources/Product/ProductCombination.php b/src/ApiPlatform/Resources/Product/ProductCombination.php new file mode 100644 index 00000000..08d90f18 --- /dev/null +++ b/src/ApiPlatform/Resources/Product/ProductCombination.php @@ -0,0 +1,103 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Exception\CombinationNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetCombinationForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/products/combinations/{combinationId}', + CQRSQuery: GetCombinationForEditing::class, + scopes: [ + 'product_read', + ], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + ], + exceptionToStatus: [ + CombinationNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class ProductCombination +{ + #[ApiProperty(identifier: true)] + public int $combinationId; + + public int $productId; + + public bool $isDefault; // I cannot see this atribute, It works? + + public string $name; + + public int $quantity; + + public array $imageIds; + + public string $coverThumbnailUrl; + + public string $gtin; + public string $isbn; + public string $mpn; + public string $reference; + public string $upc; + public DecimalNumber $impactOnWeight; + + public DecimalNumber $impactOnPrice; + public DecimalNumber $impactOnPriceTaxIncluded; + public DecimalNumber $impactOnUnitPrice; + public DecimalNumber $impactOnUnitPriceTaxIncluded; + public DecimalNumber $ecotax; + public DecimalNumber $ecotaxTaxIncluded; + public DecimalNumber $wholesalePrice; + public DecimalNumber $productTaxRate; + public DecimalNumber $productPrice; + public DecimalNumber $productEcotax; + + public const QUERY_MAPPING = [ + '[_context][shopConstraint]' => '[shopConstraint]', + '[details][gtin]' => '[gtin]', + '[details][isbn]' => '[isbn]', + '[details][mpn]' => '[mpn]', + '[details][reference]' => '[reference]', + '[details][upc]' => '[upc]', + '[details][impactOnWeight]' => '[impactOnWeight]', + '[prices][impactOnPrice]' => '[impactOnPrice]', + '[prices][impactOnPriceTaxIncluded]' => '[impactOnPriceTaxIncluded]', + '[prices][impactOnUnitPrice]' => '[impactOnUnitPrice]', + '[prices][impactOnUnitPriceTaxIncluded]' => '[impactOnUnitPriceTaxIncluded]', + '[prices][ecotax]' => '[ecotax]', + '[prices][ecotaxTaxIncluded]' => '[ecotaxTaxIncluded]', + '[prices][wholesalePrice]' => '[wholesalePrice]', + '[prices][productTaxRate]' => '[productTaxRate]', + '[prices][productPrice]' => '[productPrice]', + '[prices][productEcotax]' => '[productEcotax]', + '[stock][quantity]' => '[quantity]', + ]; +} diff --git a/src/ApiPlatform/Resources/Product/ProductCombinationList.php b/src/ApiPlatform/Resources/Product/ProductCombinationList.php new file mode 100644 index 00000000..d75d9e49 --- /dev/null +++ b/src/ApiPlatform/Resources/Product/ProductCombinationList.php @@ -0,0 +1,73 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; + +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetCombinationIds; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetEditableCombinationsList; +use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGetCollection; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSGetCollection( + uriTemplate: '/products/{productId}/combinations', + CQRSQuery: GetCombinationIds::class, + scopes: [ + 'product_read', + ], + CQRSQueryMapping: [ + '[_context][shopConstraint]' => '[shopConstraint]', + // '[@index][combinationId]' => '[@index]' TODO Test in PS 9.0.2 + ], + ApiResourceMapping: [ + '[localizedLegends]' => '[legends]', + ], + ), + // TODO: we would like to implement this resource but we need to improve the core + // new CQRSGetCollection( + // uriTemplate: '/product/{productId}/combinations', + // CQRSQuery: GetEditableCombinationsList::class, + // scopes: [ + // 'product_read', + // ], + // CQRSQueryMapping: [ + // '[_context][shopConstraint]' => '[shopConstraint]', + // '[_context][langId]' => '[languageId]', + // '[combinations]' => '[]', + // ], + // ), + ], + exceptionToStatus: [ + ProductNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class ProductCombinationList +{ + public int $productId; + public int $combinationId; + public array $shopIds; +} + +// CombinationListForEditing diff --git a/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php new file mode 100644 index 00000000..d74fb8f4 --- /dev/null +++ b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php @@ -0,0 +1,64 @@ + [ + 'GET', + '/products/combinations/1', + ]; + } + + /** + * @return int + */ + public function testGetProductCombination(): int + { + $combinationId = 1; + $productCombination = $this->getItem('/products/combinations/' . $combinationId, ['product_read']); + $combinationIdExpected = 1; + + // TODO: Add create, updat before doing this asset + // $this->assertEquals([ + // 'attributeGroupId' => $attributeGroupId, + // 'names' => [ + // 'en-US' => 'name en', + // 'fr-FR' => 'name fr', + // ], + // 'publicNames' => [ + // 'en-US' => 'public name en', + // 'fr-FR' => 'public name fr', + // ], + // 'type' => 'select', + // 'shopIds' => [1], + // ], $productCombination); + + return $combinationId; + } +} diff --git a/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php b/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php new file mode 100644 index 00000000..f898c45e --- /dev/null +++ b/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php @@ -0,0 +1,51 @@ + [ + 'GET', + '/products/1/combinations', + ]; + } + + /** + * @return int + */ + public function testGetProductCombinationList(): int + { + $productId = 1; + $productCombinationList = $this->getItem('/products/' . $productId . '/combinations', ['product_read']); + $combinationIdExpected = 1; + $this->assertIsArray($productCombinationList); + $this->assertContains($combinationIdExpected, $productCombinationList); + return $productId; + } + +} From d84967e949c891f278d421ca25fef8cff7875301 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 16:34:31 +0100 Subject: [PATCH 02/10] Add endpoint to generate combinations --- config/admin/services.yml | 12 +-- .../GenerateCombinationsSerializer.php | 70 ++++++++++++++ .../ProductCombinationListNormalizer.php | 54 ----------- .../Product/GenerateCombinations.php | 92 +++++++++++++++++++ 4 files changed, 168 insertions(+), 60 deletions(-) create mode 100644 src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php delete mode 100644 src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php create mode 100644 src/ApiPlatform/Resources/Product/GenerateCombinations.php diff --git a/config/admin/services.yml b/config/admin/services.yml index 51fc6b65..39d2646c 100644 --- a/config/admin/services.yml +++ b/config/admin/services.yml @@ -33,6 +33,12 @@ services: tags: - { name: 'console.command' } + # Custom normalizers for commands/queries that are very specific and cannot rely on generic solutions from the core + PrestaShop\Module\APIResources\ApiPlatform\Normalizer\GenerateCombinationsSerializer: + autowire: true + autoconfigure: true + public: false + PrestaShop\Module\APIResources\ApiPlatform\Normalizer\StateIdInterfaceNormalizer: tags: - { name: 'serializer.normalizer', priority: 100 } @@ -54,12 +60,6 @@ services: public: false arguments: $decorated: '@serializer.normalizer.object' - tags: - - { name: 'serializer.normalizer', priority: 300 } - - PrestaShop\Module\APIResources\ApiPlatform\Normalizer\ProductCombinationListNormalizer: - tags: - - { name: 'serializer.normalizer', priority: 100 } PrestaShopBundle\ApiPlatform\Serializer\CQRSApiSerializer: class: PrestaShop\Module\APIResources\Serializer\QueryParameterTypeCastSerializer diff --git a/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php b/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php new file mode 100644 index 00000000..e3a1c229 --- /dev/null +++ b/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php @@ -0,0 +1,70 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\Module\APIResources\ApiPlatform\Normalizer; + +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Command\GenerateProductCombinationsCommand; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; +use PrestaShopBundle\ApiPlatform\Normalizer\ShopConstraintNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +class GenerateCombinationsSerializer implements DenormalizerInterface +{ + public function __construct( + private readonly ShopConstraintNormalizer $shopConstraintNormalizer, + ) { + } + + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []) + { + $groupedAttributes = []; + foreach ($data['groupedAttributes'] as $attributeGroup) { + $groupedAttributes[$attributeGroup['attributeGroupId']] = array_map(static function ($attributeId): int { + return (int) $attributeId; + }, $attributeGroup['attributeIds']); + } + + return new GenerateProductCombinationsCommand( + $data['productId'], + $groupedAttributes, + $this->shopConstraintNormalizer->denormalize($data['_context']['shopConstraint'], ShopConstraint::class), + ); + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null) + { + return $type === GenerateProductCombinationsCommand::class; + } + + public function getSupportedTypes(?string $format): array + { + return [ + GenerateProductCombinationsCommand::class => true, + 'object' => null, + '*' => null, + ]; + } +} diff --git a/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php b/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php deleted file mode 100644 index d68a8a83..00000000 --- a/src/ApiPlatform/Normalizer/ProductCombinationListNormalizer.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 - */ - -namespace PrestaShop\Module\APIResources\ApiPlatform\Normalizer; - -use PrestaShop\Module\APIResources\ApiPlatform\Resources\Product\ProductCombinationList; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Custom normalizer for ProductCombinationList to return only the combinationId - */ -class ProductCombinationListNormalizer implements NormalizerInterface -{ - /** - * @param ProductCombinationList $object - * @param string|null $format - * @param array $context - * - * @return int - */ - public function normalize(mixed $object, ?string $format = null, array $context = []): int - { - return $object->combinationId; - } - - public function supportsNormalization(mixed $data, ?string $format = null): bool - { - return $data instanceof ProductCombinationList; - } - - public function getSupportedTypes(?string $format): array - { - return [ - ProductCombinationList::class => true, - ]; - } -} diff --git a/src/ApiPlatform/Resources/Product/GenerateCombinations.php b/src/ApiPlatform/Resources/Product/GenerateCombinations.php new file mode 100644 index 00000000..30358175 --- /dev/null +++ b/src/ApiPlatform/Resources/Product/GenerateCombinations.php @@ -0,0 +1,92 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Command\GenerateProductCombinationsCommand; +use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSCreate; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSCreate( + uriTemplate: '/products/{productId}/generate-combinations', + CQRSCommand: GenerateProductCombinationsCommand::class, + scopes: [ + 'product_write', + ], + ApiResourceMapping: [ + // Used to denormalize the command result + '[@index][combinationId]' => '[newCombinationIds][@index]', + ], + ), + ], + exceptionToStatus: [ + ProductNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class GenerateCombinations +{ + public int $productId; + #[ApiProperty( + openapiContext: [ + 'type' => 'array', + 'description' => 'List of new generated combination IDs', + 'items' => [ + 'type' => 'integer', + 'description' => 'Combination ID', + ], + ] + )] + public array $newCombinationIds = []; + + #[ApiProperty( + openapiContext: [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'description' => 'List of attributes grouped by their attribute group', + 'properties' => [ + 'attributeGroupId' => [ + 'type' => 'number', + 'description' => 'Attribute group ID', + ], + 'attributeIds' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'integer', + 'description' => 'Attribute ID', + ], + ], + ], + ], + ], + )] + public array $groupedAttributes; +} From 812a2be1f3b8dc6c51fe3e44f5d939499c3b7164 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 16:35:34 +0100 Subject: [PATCH 03/10] Handle endpoints to list combination IDs --- ...binationList.php => CombinationIdList.php} | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) rename src/ApiPlatform/Resources/Product/{ProductCombinationList.php => CombinationIdList.php} (58%) diff --git a/src/ApiPlatform/Resources/Product/ProductCombinationList.php b/src/ApiPlatform/Resources/Product/CombinationIdList.php similarity index 58% rename from src/ApiPlatform/Resources/Product/ProductCombinationList.php rename to src/ApiPlatform/Resources/Product/CombinationIdList.php index d75d9e49..11c3ca73 100644 --- a/src/ApiPlatform/Resources/Product/ProductCombinationList.php +++ b/src/ApiPlatform/Resources/Product/CombinationIdList.php @@ -24,50 +24,30 @@ use ApiPlatform\Metadata\ApiResource; use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetCombinationIds; -use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetEditableCombinationsList; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; -use PrestaShopBundle\ApiPlatform\Metadata\CQRSGetCollection; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; use Symfony\Component\HttpFoundation\Response; #[ApiResource( operations: [ - new CQRSGetCollection( - uriTemplate: '/products/{productId}/combinations', + new CQRSGet( + uriTemplate: '/products/{productId}/combination-ids', CQRSQuery: GetCombinationIds::class, scopes: [ 'product_read', ], CQRSQueryMapping: [ '[_context][shopConstraint]' => '[shopConstraint]', - // '[@index][combinationId]' => '[@index]' TODO Test in PS 9.0.2 - ], - ApiResourceMapping: [ - '[localizedLegends]' => '[legends]', + '[@index][combinationId]' => '[combinationIds][@index]', ], ), - // TODO: we would like to implement this resource but we need to improve the core - // new CQRSGetCollection( - // uriTemplate: '/product/{productId}/combinations', - // CQRSQuery: GetEditableCombinationsList::class, - // scopes: [ - // 'product_read', - // ], - // CQRSQueryMapping: [ - // '[_context][shopConstraint]' => '[shopConstraint]', - // '[_context][langId]' => '[languageId]', - // '[combinations]' => '[]', - // ], - // ), ], exceptionToStatus: [ ProductNotFoundException::class => Response::HTTP_NOT_FOUND, ], )] -class ProductCombinationList +class CombinationIdList { public int $productId; - public int $combinationId; - public array $shopIds; + public array $combinationIds; } - -// CombinationListForEditing From 0a1513a948468e95ed6f7854689591918a8f2f90 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 19:00:22 +0100 Subject: [PATCH 04/10] Add endpoint that list product combinations with details --- .../Resources/Product/CombinationList.php | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/ApiPlatform/Resources/Product/CombinationList.php diff --git a/src/ApiPlatform/Resources/Product/CombinationList.php b/src/ApiPlatform/Resources/Product/CombinationList.php new file mode 100644 index 00000000..a9142342 --- /dev/null +++ b/src/ApiPlatform/Resources/Product/CombinationList.php @@ -0,0 +1,94 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetEditableCombinationsList; +use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; +use PrestaShop\PrestaShop\Core\Search\Filters\ProductCombinationFilters; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSPaginate; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSPaginate( + uriTemplate: '/products/{productId}/combinations', + CQRSQuery: GetEditableCombinationsList::class, + scopes: [ + 'product_read', + ], + CQRSQueryMapping: [ + '[_context][langId]' => '[languageId]', + '[_context][shopConstraint]' => '[shopConstraint]', + ], + ApiResourceMapping: [ + '[combinationName]' => '[name]', + '[attributesInformation]' => '[attributes]', + '[impactOnPrice]' => '[impactOnPriceTaxExcluded]', + ], + filtersClass: ProductCombinationFilters::class, + filtersMapping: [ + '[_context][shopId]' => '[shopId]', + ], + itemsField: 'combinations', + countField: 'totalCombinationsCount', + ), + ], + exceptionToStatus: [ + ProductNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class CombinationList +{ + public int $productId; + public int $combinationId; + public string $name; + public bool $default; + public string $reference; + public DecimalNumber $impactOnPriceTaxExcluded; + public DecimalNumber $ecoTax; + public int $quantity; + public string $imageUrl; + #[ApiProperty( + openapiContext: [ + 'type' => 'array', + 'description' => 'Combination attributes', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'attributeGroupId' => ['type' => 'integer'], + 'attributeGroupName' => ['type' => 'string'], + 'attributeId' => ['type' => 'integer'], + 'attributeName' => ['type' => 'string'], + ], + ], + ] + )] + public array $attributes; +} From 9439ad33cc65579d3d690e7cbde492ccfd85b276 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 19:26:22 +0100 Subject: [PATCH 05/10] Implement GET endpoint for single combination --- ...ProductCombination.php => Combination.php} | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) rename src/ApiPlatform/Resources/Product/{ProductCombination.php => Combination.php} (84%) diff --git a/src/ApiPlatform/Resources/Product/ProductCombination.php b/src/ApiPlatform/Resources/Product/Combination.php similarity index 84% rename from src/ApiPlatform/Resources/Product/ProductCombination.php rename to src/ApiPlatform/Resources/Product/Combination.php index 08d90f18..1fcb1b73 100644 --- a/src/ApiPlatform/Resources/Product/ProductCombination.php +++ b/src/ApiPlatform/Resources/Product/Combination.php @@ -45,40 +45,37 @@ CombinationNotFoundException::class => Response::HTTP_NOT_FOUND, ], )] -class ProductCombination +class Combination { - #[ApiProperty(identifier: true)] - public int $combinationId; - public int $productId; - public bool $isDefault; // I cannot see this atribute, It works? - + #[ApiProperty(identifier: true)] + public int $combinationId; public string $name; - - public int $quantity; - - public array $imageIds; - - public string $coverThumbnailUrl; + public bool $default; public string $gtin; public string $isbn; public string $mpn; public string $reference; public string $upc; - public DecimalNumber $impactOnWeight; - public DecimalNumber $impactOnPrice; + public string $coverThumbnailUrl; + public array $imageIds; + + public DecimalNumber $impactOnPriceTaxExcluded; public DecimalNumber $impactOnPriceTaxIncluded; public DecimalNumber $impactOnUnitPrice; public DecimalNumber $impactOnUnitPriceTaxIncluded; - public DecimalNumber $ecotax; + public DecimalNumber $ecotaxTaxExcluded; public DecimalNumber $ecotaxTaxIncluded; + public DecimalNumber $impactOnWeight; public DecimalNumber $wholesalePrice; public DecimalNumber $productTaxRate; - public DecimalNumber $productPrice; - public DecimalNumber $productEcotax; + public DecimalNumber $productPriceTaxExcluded; + public DecimalNumber $productEcotaxTaxExcluded; + + public int $quantity; public const QUERY_MAPPING = [ '[_context][shopConstraint]' => '[shopConstraint]', @@ -88,16 +85,16 @@ class ProductCombination '[details][reference]' => '[reference]', '[details][upc]' => '[upc]', '[details][impactOnWeight]' => '[impactOnWeight]', - '[prices][impactOnPrice]' => '[impactOnPrice]', + '[prices][impactOnPrice]' => '[impactOnPriceTaxExcluded]', '[prices][impactOnPriceTaxIncluded]' => '[impactOnPriceTaxIncluded]', - '[prices][impactOnUnitPrice]' => '[impactOnUnitPrice]', + '[prices][impactOnUnitPrice]' => '[impactOnUnitPriceTaxExcluded]', '[prices][impactOnUnitPriceTaxIncluded]' => '[impactOnUnitPriceTaxIncluded]', - '[prices][ecotax]' => '[ecotax]', + '[prices][ecotax]' => '[ecotaxTaxExcluded]', '[prices][ecotaxTaxIncluded]' => '[ecotaxTaxIncluded]', '[prices][wholesalePrice]' => '[wholesalePrice]', '[prices][productTaxRate]' => '[productTaxRate]', - '[prices][productPrice]' => '[productPrice]', - '[prices][productEcotax]' => '[productEcotax]', + '[prices][productPrice]' => '[productPriceTaxExcluded]', + '[prices][productEcotax]' => '[productEcotaxTaxExcluded]', '[stock][quantity]' => '[quantity]', ]; } From ae642208b98fb4d35f96ed34df43d4390e33e4de Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 19:26:40 +0100 Subject: [PATCH 06/10] Update integrations tests for all new combination endpoints --- .../ProductCombinationEndpointTest.php | 317 ++++++++++++++++-- .../ProductCombinationListEndpointTest.php | 51 --- 2 files changed, 295 insertions(+), 73 deletions(-) delete mode 100644 tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php diff --git a/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php index d74fb8f4..19b174fe 100644 --- a/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php +++ b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php @@ -4,20 +4,56 @@ namespace PsApiResourcesTest\Integration\ApiPlatform; +use PrestaShop\PrestaShop\Adapter\Attribute\Repository\AttributeRepository; +use PrestaShop\PrestaShop\Adapter\AttributeGroup\Repository\AttributeGroupRepository; +use PrestaShop\PrestaShop\Core\Domain\AttributeGroup\ValueObject\AttributeGroupId; +use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType; +use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint; +use Tests\Resources\Resetter\LanguageResetter; use Tests\Resources\Resetter\ProductResetter; use Tests\Resources\ResourceResetter; class ProductCombinationEndpointTest extends ApiTestCase { + /** + * @var array + */ + private static array $attributeGroupData = []; + /** + * @var array + */ + private static array $attributeData = []; + public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - // Add the fr-FR language to test multi lang values accurately - LanguageResetter::resetLanguages(); + (new ResourceResetter())->backupTestModules(); ProductResetter::resetProducts(); + LanguageResetter::resetLanguages(); self::addLanguageByLocale('fr-FR'); // Pre-create the API Client with the needed scopes, this way we reduce the number of created API Clients - self::createApiClient(['product_read']); + self::createApiClient(['product_write', 'product_read']); + + // Fetch data for attributes to use them more easily in the following tests + /** @var AttributeGroupRepository $attributeGroupRepository */ + $attributeGroupRepository = self::getContainer()->get(AttributeGroupRepository::class); + $attributeGroups = $attributeGroupRepository->getAttributeGroups(ShopConstraint::allShops()); + foreach ($attributeGroups as $attributeGroup) { + // Store english name as the key + self::$attributeGroupData[$attributeGroup->name[1]] = (int) $attributeGroup->id; + } + + /** @var AttributeRepository $attributeRepository */ + $attributeRepository = self::getContainer()->get(AttributeRepository::class); + $attributeGroupIds = array_map(static function (int $attributeGroupId) { + return new AttributeGroupId($attributeGroupId); + }, array_values(self::$attributeGroupData)); + $groupedAttributes = $attributeRepository->getGroupedAttributes(ShopConstraint::allShops(), $attributeGroupIds); + foreach ($groupedAttributes as $attributeGroupId => $groupAttributes) { + foreach ($groupAttributes as $attribute) { + self::$attributeData[$attribute->name[1]] = (int) $attribute->id; + } + } } public static function tearDownAfterClass(): void @@ -25,39 +61,276 @@ public static function tearDownAfterClass(): void parent::tearDownAfterClass(); ProductResetter::resetProducts(); LanguageResetter::resetLanguages(); + // Reset modules folder that are removed with the FR language + (new ResourceResetter())->resetTestModules(); } public static function getProtectedEndpoints(): iterable { + yield 'generate combinations endpoint' => [ + 'POST', + '/products/1/generate-combinations', + ]; + + yield 'list combination IDs' => [ + 'GET', + '/products/1/combination-ids', + ]; + yield 'get endpoint' => [ 'GET', '/products/combinations/1', ]; } + public function testAddProductWithCombinations(): int + { + $addedProduct = $this->createItem('/products', [ + 'type' => ProductType::TYPE_COMBINATIONS, + 'names' => [ + 'en-US' => 'product with combinations', + 'fr-FR' => 'produit avec combinaisons', + ], + ], ['product_write']); + + return $addedProduct['productId']; + } + + /** + * @depends testAddProductWithCombinations + */ + public function testCreateProductCombinations(int $productId): array + { + $postData = [ + 'groupedAttributes' => [ + [ + 'attributeGroupId' => self::$attributeGroupData['Color'], + 'attributeIds' => [ + self::$attributeData['Red'], + self::$attributeData['Black'], + self::$attributeData['Yellow'], + ], + ], + [ + 'attributeGroupId' => self::$attributeGroupData['Size'], + 'attributeIds' => [ + self::$attributeData['S'], + self::$attributeData['M'], + self::$attributeData['L'], + ], + ], + ], + ]; + $createdCombinations = $this->createItem(sprintf('/products/%d/generate-combinations', $productId), $postData, ['product_write']); + // 9 combinations should have been created (three sizes for each three colors) + $this->assertCount(9, $createdCombinations['newCombinationIds']); + $newCombinationIds = $createdCombinations['newCombinationIds']; + foreach ($newCombinationIds as $combinationId) { + $this->assertIsInt($combinationId); + } + $expectedResult = [ + 'productId' => $productId, + // We fill this value dynamically because we can't guess their values + 'newCombinationIds' => $newCombinationIds, + ]; + $this->assertEquals($expectedResult, $createdCombinations); + + // Now we call the same endpoint with the same attributes + $createdCombinations = $this->createItem(sprintf('/products/%d/generate-combinations', $productId), $postData, ['product_write']); + $this->assertEquals([ + 'productId' => $productId, + // Since the combinations already exist no new combination is created + 'newCombinationIds' => [], + ], $createdCombinations); + + // Now we call with extra attributes, only the missing combinations are created + $postData['groupedAttributes'][0]['attributeIds'][] = self::$attributeData['White']; + $postData['groupedAttributes'][1]['attributeIds'][] = self::$attributeData['XL']; + + // In total 16 combinations should be created, 9 have already been so 7 new combinations should be returned + $createdCombinations = $this->createItem(sprintf('/products/%d/generate-combinations', $productId), $postData, ['product_write']); + $this->assertCount(7, $createdCombinations['newCombinationIds']); + $newCombinationIds = array_merge($newCombinationIds, $createdCombinations['newCombinationIds']); + $this->assertCount(16, $newCombinationIds); + + // Now create new combinations from other attributes (we don't merge with previous postData) + $postData = [ + 'groupedAttributes' => [ + [ + 'attributeGroupId' => self::$attributeGroupData['Color'], + 'attributeIds' => [ + self::$attributeData['Blue'], + ], + ], + [ + 'attributeGroupId' => self::$attributeGroupData['Size'], + 'attributeIds' => [ + self::$attributeData['M'], + self::$attributeData['L'], + ], + ], + ], + ]; + $createdCombinations = $this->createItem(sprintf('/products/%d/generate-combinations', $productId), $postData, ['product_write']); + // Only two new combinations should have been created + $this->assertCount(2, $createdCombinations['newCombinationIds']); + $newCombinationIds = array_merge($newCombinationIds, $createdCombinations['newCombinationIds']); + $this->assertCount(18, $newCombinationIds); + + return $newCombinationIds; + } + + /** + * @depends testAddProductWithCombinations + * @depends testCreateProductCombinations + */ + public function testListCombinationsIds(int $productId, array $newCombinationIds): array + { + $combinations = $this->getItem(sprintf('/products/%d/combination-ids', $productId), ['product_read']); + $this->assertEquals([ + 'productId' => $productId, + 'combinationIds' => $newCombinationIds, + ], $combinations); + + // Now test pagination + $resultsPerPage = 5; + $pagesNumber = ceil(count($newCombinationIds) / $resultsPerPage); + for ($page = 1; $page <= $pagesNumber; ++$page) { + $offset = ($page - 1) * $resultsPerPage; + $paginatedCombinations = $this->getItem(sprintf( + '/products/%d/combination-ids?offset=%d&limit=%d', + $productId, + $offset, + $resultsPerPage), + ['product_read'] + ); + $expectedCombinationIds = array_slice($newCombinationIds, $offset, $resultsPerPage); + $this->assertEquals([ + 'productId' => $productId, + 'combinationIds' => $expectedCombinationIds, + ], $paginatedCombinations); + } + + return $newCombinationIds; + } + + /** + * @depends testAddProductWithCombinations + * @depends testCreateProductCombinations + */ + public function testCombinationList(int $productId, array $newCombinationIds): array + { + $paginatedCombinations = $this->listItems(sprintf('/products/%d/combinations', $productId), ['product_read']); + $this->assertEquals(count($newCombinationIds), $paginatedCombinations['totalItems']); + + // Now check the expected format at least for the first two + $this->assertEquals([ + 'productId' => $productId, + 'combinationId' => $newCombinationIds[0], + 'name' => 'Size - S, Color - Red', + 'default' => true, + 'reference' => '', + 'impactOnPriceTaxExcluded' => 0.0, + 'ecoTax' => 0.0, + 'quantity' => 0, + 'imageUrl' => 'http://myshop.com/img/p/en-default-small_default.jpg', + 'attributes' => [ + [ + 'attributeGroupId' => self::$attributeGroupData['Size'], + 'attributeGroupName' => 'Size', + 'attributeId' => self::$attributeData['S'], + 'attributeName' => 'S', + ], + [ + 'attributeGroupId' => self::$attributeGroupData['Color'], + 'attributeGroupName' => 'Color', + 'attributeId' => self::$attributeData['Red'], + 'attributeName' => 'Red', + ], + ], + ], $paginatedCombinations['items'][0]); + $this->assertEquals([ + 'productId' => $productId, + 'combinationId' => $newCombinationIds[1], + 'name' => 'Size - M, Color - Red', + 'default' => false, + 'reference' => '', + 'impactOnPriceTaxExcluded' => 0.0, + 'ecoTax' => 0.0, + 'quantity' => 0, + 'imageUrl' => 'http://myshop.com/img/p/en-default-small_default.jpg', + 'attributes' => [ + [ + 'attributeGroupId' => self::$attributeGroupData['Size'], + 'attributeGroupName' => 'Size', + 'attributeId' => self::$attributeData['M'], + 'attributeName' => 'M', + ], + [ + 'attributeGroupId' => self::$attributeGroupData['Color'], + 'attributeGroupName' => 'Color', + 'attributeId' => self::$attributeData['Red'], + 'attributeName' => 'Red', + ], + ], + ], $paginatedCombinations['items'][1]); + + // Now test pagination + $resultsPerPage = 5; + $pagesNumber = ceil(count($newCombinationIds) / $resultsPerPage); + for ($page = 1; $page <= $pagesNumber; ++$page) { + $offset = ($page - 1) * $resultsPerPage; + $paginatedCombinations = $this->getItem(sprintf( + '/products/%d/combinations?offset=%d&limit=%d', + $productId, + $offset, + $resultsPerPage), + ['product_read'] + ); + $expectedCombinationIds = array_slice($newCombinationIds, $offset, $resultsPerPage); + $paginatedCombinationIds = array_map(static function (array $combination): int { + return $combination['combinationId']; + }, $paginatedCombinations['items']); + $this->assertEquals($expectedCombinationIds, $paginatedCombinationIds); + } + + return $newCombinationIds; + } + /** + * @depends testAddProductWithCombinations + * @depends testListCombinationsIds + * * @return int */ - public function testGetProductCombination(): int + public function testGetProductCombination(int $productId, array $newCombinationIds): int { - $combinationId = 1; - $productCombination = $this->getItem('/products/combinations/' . $combinationId, ['product_read']); - $combinationIdExpected = 1; - - // TODO: Add create, updat before doing this asset - // $this->assertEquals([ - // 'attributeGroupId' => $attributeGroupId, - // 'names' => [ - // 'en-US' => 'name en', - // 'fr-FR' => 'name fr', - // ], - // 'publicNames' => [ - // 'en-US' => 'public name en', - // 'fr-FR' => 'public name fr', - // ], - // 'type' => 'select', - // 'shopIds' => [1], - // ], $productCombination); + $combinationId = $newCombinationIds[0]; + $combination = $this->getItem('/products/combinations/' . $combinationId, ['product_read']); + $this->assertEquals([ + 'productId' => $productId, + 'combinationId' => $combinationId, + 'name' => 'Size - S, Color - Red', + 'default' => true, + 'gtin' => '', + 'isbn' => '', + 'mpn' => '', + 'reference' => '', + 'upc' => '', + 'coverThumbnailUrl' => 'http://myshop.com/img/p/en-default-cart_default.jpg', + 'imageIds' => [], + 'impactOnPriceTaxExcluded' => 0.0, + 'impactOnPriceTaxIncluded' => 0.0, + 'impactOnUnitPriceTaxIncluded' => 0.0, + 'ecotaxTaxExcluded' => 0.0, + 'ecotaxTaxIncluded' => 0.0, + 'impactOnWeight' => 0.0, + 'wholesalePrice' => 0.0, + 'productTaxRate' => 6.0, + 'productPriceTaxExcluded' => 0.0, + 'productEcotaxTaxExcluded' => 0.0, + 'quantity' => 0, + ], $combination); return $combinationId; } diff --git a/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php b/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php deleted file mode 100644 index f898c45e..00000000 --- a/tests/Integration/ApiPlatform/ProductCombinationListEndpointTest.php +++ /dev/null @@ -1,51 +0,0 @@ - [ - 'GET', - '/products/1/combinations', - ]; - } - - /** - * @return int - */ - public function testGetProductCombinationList(): int - { - $productId = 1; - $productCombinationList = $this->getItem('/products/' . $productId . '/combinations', ['product_read']); - $combinationIdExpected = 1; - $this->assertIsArray($productCombinationList); - $this->assertContains($combinationIdExpected, $productCombinationList); - return $productId; - } - -} From 6517f317ce5d656b83a66d65a1160c02884adde8 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 19:29:29 +0100 Subject: [PATCH 07/10] Update PS min version --- ps_apiresources.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ps_apiresources.php b/ps_apiresources.php index 7aae2a9a..3f83ca72 100644 --- a/ps_apiresources.php +++ b/ps_apiresources.php @@ -37,7 +37,7 @@ public function __construct() $this->description = $this->trans('Includes the resources allowing using the API for the PrestaShop domain, all endpoints are based on CQRS commands/queries from the Core and we APIPlatform framework is used as a base.', [], 'Modules.Apiresources.Admin'); $this->author = 'PrestaShop'; $this->version = '0.3.0'; - $this->ps_versions_compliancy = ['min' => '9.0.2', 'max' => _PS_VERSION_]; + $this->ps_versions_compliancy = ['min' => '9.0.3', 'max' => _PS_VERSION_]; $this->need_instance = 0; $this->tab = 'administration'; parent::__construct(); From 71144bfc5b6411534cb737c7c18fd8979964be06 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 19:51:51 +0100 Subject: [PATCH 08/10] Update tests that have been impacted by recent changes --- src/ApiPlatform/Resources/Product/NewProductImage.php | 2 +- tests/Integration/ApiPlatform/AddressEndpointTest.php | 11 +++++++++++ tests/Integration/ApiPlatform/ProductEndpointTest.php | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ApiPlatform/Resources/Product/NewProductImage.php b/src/ApiPlatform/Resources/Product/NewProductImage.php index 6be8a379..fe2e4e96 100644 --- a/src/ApiPlatform/Resources/Product/NewProductImage.php +++ b/src/ApiPlatform/Resources/Product/NewProductImage.php @@ -36,6 +36,7 @@ new CQRSCreate( uriTemplate: '/products/{productId}/images', inputFormats: ['multipart' => ['multipart/form-data']], + requirements: ['productId' => '\d+'], read: false, CQRSCommand: AddProductImageCommand::class, CQRSQuery: GetProductImage::class, @@ -56,7 +57,6 @@ class NewProductImage { public int $productId; - public int $imageId; public string $imageUrl; diff --git a/tests/Integration/ApiPlatform/AddressEndpointTest.php b/tests/Integration/ApiPlatform/AddressEndpointTest.php index 7051860b..846094dd 100644 --- a/tests/Integration/ApiPlatform/AddressEndpointTest.php +++ b/tests/Integration/ApiPlatform/AddressEndpointTest.php @@ -230,6 +230,17 @@ public function testUpdateOrderAddress(): void ]; $customerAddress = $this->createItem('/addresses/customers', $addressData, ['address_write']); $addressId = $customerAddress['addressId']; + $expectedAddress = [ + 'addressId' => $addressId, + 'address2' => '', + 'dni' => '', + 'company' => '', + 'vatNumber' => '', + 'homePhone' => '', + 'mobilePhone' => '', + 'other' => '', + ] + $addressData; + $this->assertEquals($expectedAddress, $customerAddress); // Create a minimal order with this address $order = new \Order(); diff --git a/tests/Integration/ApiPlatform/ProductEndpointTest.php b/tests/Integration/ApiPlatform/ProductEndpointTest.php index c224a985..ca91efe9 100644 --- a/tests/Integration/ApiPlatform/ProductEndpointTest.php +++ b/tests/Integration/ApiPlatform/ProductEndpointTest.php @@ -507,6 +507,7 @@ public function testAddImage(int $productId): int // Check URLs format based on the newly created Image ID $expectedImage = [ + 'productId' => $productId, 'imageId' => $imageId, 'imageUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, false), 'thumbnailUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, true), @@ -635,6 +636,7 @@ public function testListImages(int $productId, int $imageId): void $this->assertEquals(2, count($productImages)); $this->assertEquals([ [ + 'productId' => $productId, 'imageId' => $imageId, 'imageUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, false), 'thumbnailUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, true), @@ -649,6 +651,7 @@ public function testListImages(int $productId, int $imageId): void ], ], [ + 'productId' => $productId, 'imageId' => $newImageId, 'imageUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($newImageId, false), 'thumbnailUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($newImageId, true), @@ -686,6 +689,7 @@ public function testListImages(int $productId, int $imageId): void // The images are sorted differently (since they are automatically order by position) and the cover has been updated $this->assertEquals([ [ + 'productId' => $productId, 'imageId' => $newImageId, 'imageUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($newImageId, false), 'thumbnailUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($newImageId, true), @@ -700,6 +704,7 @@ public function testListImages(int $productId, int $imageId): void ], ], [ + 'productId' => $productId, 'imageId' => $imageId, 'imageUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, false), 'thumbnailUrl' => 'http://myshop.com/img/p/' . $this->getImagePath($imageId, true), From 86e47474aa9004dfeaf7b726bfb192e66ab15779 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Tue, 20 Jan 2026 20:15:20 +0100 Subject: [PATCH 09/10] Add mising openapi details --- src/ApiPlatform/Resources/Product/Combination.php | 1 + src/ApiPlatform/Resources/Product/CombinationIdList.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/ApiPlatform/Resources/Product/Combination.php b/src/ApiPlatform/Resources/Product/Combination.php index 1fcb1b73..07435b82 100644 --- a/src/ApiPlatform/Resources/Product/Combination.php +++ b/src/ApiPlatform/Resources/Product/Combination.php @@ -61,6 +61,7 @@ class Combination public string $upc; public string $coverThumbnailUrl; + #[ApiProperty(openapiContext: ['type' => 'array', 'description' => 'List of image IDs', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] public array $imageIds; public DecimalNumber $impactOnPriceTaxExcluded; diff --git a/src/ApiPlatform/Resources/Product/CombinationIdList.php b/src/ApiPlatform/Resources/Product/CombinationIdList.php index 11c3ca73..b703f289 100644 --- a/src/ApiPlatform/Resources/Product/CombinationIdList.php +++ b/src/ApiPlatform/Resources/Product/CombinationIdList.php @@ -22,6 +22,7 @@ namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Query\GetCombinationIds; use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException; @@ -49,5 +50,6 @@ class CombinationIdList { public int $productId; + #[ApiProperty(openapiContext: ['type' => 'array', 'description' => 'List of combination IDs', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] public array $combinationIds; } From ea599e29b511a9e9660e763dedf793a18caecd7f Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Thu, 22 Jan 2026 08:28:35 +0100 Subject: [PATCH 10/10] Fix license headers --- .../GenerateCombinationsSerializer.php | 12 +++--------- .../Resources/Product/CombinationList.php | 12 +++--------- .../Resources/Product/GenerateCombinations.php | 12 +++--------- .../ProductCombinationEndpointTest.php | 18 ++++++++++++++++++ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php b/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php index e3a1c229..785b2f01 100644 --- a/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php +++ b/src/ApiPlatform/Normalizer/GenerateCombinationsSerializer.php @@ -5,23 +5,17 @@ * * NOTICE OF LICENSE * - * This source file is subject to the Open Software License (OSL 3.0) + * This source file is subject to the Academic Free License version 3.0 * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: - * https://opensource.org/licenses/OSL-3.0 + * https://opensource.org/licenses/AFL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - * versions in the future. If you wish to customize PrestaShop for your - * needs please refer to https://devdocs.prestashop.com/ for more information. - * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 */ namespace PrestaShop\Module\APIResources\ApiPlatform\Normalizer; diff --git a/src/ApiPlatform/Resources/Product/CombinationList.php b/src/ApiPlatform/Resources/Product/CombinationList.php index a9142342..8b3604f5 100644 --- a/src/ApiPlatform/Resources/Product/CombinationList.php +++ b/src/ApiPlatform/Resources/Product/CombinationList.php @@ -5,23 +5,17 @@ * * NOTICE OF LICENSE * - * This source file is subject to the Open Software License (OSL 3.0) + * This source file is subject to the Academic Free License version 3.0 * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: - * https://opensource.org/licenses/OSL-3.0 + * https://opensource.org/licenses/AFL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - * versions in the future. If you wish to customize PrestaShop for your - * needs please refer to https://devdocs.prestashop.com/ for more information. - * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 */ namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; diff --git a/src/ApiPlatform/Resources/Product/GenerateCombinations.php b/src/ApiPlatform/Resources/Product/GenerateCombinations.php index 30358175..0e9ac387 100644 --- a/src/ApiPlatform/Resources/Product/GenerateCombinations.php +++ b/src/ApiPlatform/Resources/Product/GenerateCombinations.php @@ -5,23 +5,17 @@ * * NOTICE OF LICENSE * - * This source file is subject to the Open Software License (OSL 3.0) + * This source file is subject to the Academic Free License version 3.0 * that is bundled with this package in the file LICENSE.md. * It is also available through the world-wide-web at this URL: - * https://opensource.org/licenses/OSL-3.0 + * https://opensource.org/licenses/AFL-3.0 * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@prestashop.com so we can send you a copy immediately. * - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade PrestaShop to newer - * versions in the future. If you wish to customize PrestaShop for your - * needs please refer to https://devdocs.prestashop.com/ for more information. - * * @author PrestaShop SA and Contributors * @copyright Since 2007 PrestaShop SA and Contributors - * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 */ namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Product; diff --git a/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php index 19b174fe..8b290ce9 100644 --- a/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php +++ b/tests/Integration/ApiPlatform/ProductCombinationEndpointTest.php @@ -1,4 +1,22 @@ + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ declare(strict_types=1);