From bb9ebd5f0ae2b4c0e5a8f808fd74bfed1f785cd2 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Thu, 5 Mar 2026 17:20:03 +0100 Subject: [PATCH 01/19] Add GET /addresses/required-fields API endpoint | Questions | Answers | ------------- | ------------------------------------------- | Description? | Add GetRequiredFieldsForAddress CQRS endpoint | Type? | new feature | BC breaks? | no | Deprecations? | no | Fixed ticket? | N/A | Sponsor company | @PrestaShopCorp | How to test? | Ask a dev --- src/ApiPlatform/Resources/Address/Address.php | 5 ++ .../Address/AddressRequiredFields.php | 54 +++++++++++++++++++ .../ApiPlatform/AddressEndpointTest.php | 19 +++++++ 3 files changed, 78 insertions(+) create mode 100644 src/ApiPlatform/Resources/Address/AddressRequiredFields.php diff --git a/src/ApiPlatform/Resources/Address/Address.php b/src/ApiPlatform/Resources/Address/Address.php index a5f8cfb6..9f0de179 100644 --- a/src/ApiPlatform/Resources/Address/Address.php +++ b/src/ApiPlatform/Resources/Address/Address.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\NotExposed; use PrestaShop\PrestaShop\Core\Domain\Address\Command\DeleteAddressCommand; use PrestaShop\PrestaShop\Core\Domain\Address\Exception\AddressConstraintException; use PrestaShop\PrestaShop\Core\Domain\Address\Exception\AddressNotFoundException; @@ -38,6 +39,10 @@ 'address_write', ], ), + new NotExposed( + uriTemplate: '/addresses/{addressId}', + requirements: ['addressId' => '\d+'], + ), ], exceptionToStatus: [ AddressConstraintException::class => Response::HTTP_UNPROCESSABLE_ENTITY, diff --git a/src/ApiPlatform/Resources/Address/AddressRequiredFields.php b/src/ApiPlatform/Resources/Address/AddressRequiredFields.php new file mode 100644 index 00000000..d99c20da --- /dev/null +++ b/src/ApiPlatform/Resources/Address/AddressRequiredFields.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 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\APIResources\ApiPlatform\Resources\Address; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Address\Query\GetRequiredFieldsForAddress; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/addresses/required-fields', + CQRSQuery: GetRequiredFieldsForAddress::class, + scopes: ['address_read'], + CQRSQueryMapping: ['[@index]' => '[requiredFields][@index]'], + ), + ], + normalizationContext: ['skip_null_values' => false], +)] +class AddressRequiredFields +{ + /** + * @var string[] + */ + #[ApiProperty( + openapiContext: [ + 'type' => 'array', + 'items' => ['type' => 'string'], + 'example' => ['phone', 'company'], + ] + )] + public array $requiredFields = []; +} diff --git a/tests/Integration/ApiPlatform/AddressEndpointTest.php b/tests/Integration/ApiPlatform/AddressEndpointTest.php index 87745b6a..fed6a314 100644 --- a/tests/Integration/ApiPlatform/AddressEndpointTest.php +++ b/tests/Integration/ApiPlatform/AddressEndpointTest.php @@ -48,6 +48,11 @@ public static function tearDownAfterClass(): void public static function getProtectedEndpoints(): iterable { + yield 'get address required fields endpoint' => [ + 'GET', + '/addresses/required-fields', + ]; + yield 'get customer address endpoint' => [ 'GET', '/addresses/customers/1', @@ -498,4 +503,18 @@ public function testAddressValidation(): void // Use the createItem method but expect it to fail with validation error $this->createItem('/addresses/customers', $invalidData, ['address_write'], 422); } + + public function testGetAddressRequiredFields(): void + { + $result = $this->getItem('/addresses/required-fields', ['address_read']); + + $this->assertArrayHasKey('requiredFields', $result); + $this->assertIsArray($result['requiredFields']); + + // Assert some common required fields are present + if (!empty($result['requiredFields'])) { + // Just verifying it's an array of strings + $this->assertIsString($result['requiredFields'][0]); + } + } } From 645d208921fdad9241e11920486f4e8591b31faf Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 17:53:38 +0100 Subject: [PATCH 02/19] cs fix --- tests/Integration/ApiPlatform/AddressEndpointTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/ApiPlatform/AddressEndpointTest.php b/tests/Integration/ApiPlatform/AddressEndpointTest.php index fed6a314..6827ef55 100644 --- a/tests/Integration/ApiPlatform/AddressEndpointTest.php +++ b/tests/Integration/ApiPlatform/AddressEndpointTest.php @@ -507,7 +507,6 @@ public function testAddressValidation(): void public function testGetAddressRequiredFields(): void { $result = $this->getItem('/addresses/required-fields', ['address_read']); - $this->assertArrayHasKey('requiredFields', $result); $this->assertIsArray($result['requiredFields']); From 60e3ac8888b1ead39ef615c14c4fd4fd69e55318 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:17:02 +0100 Subject: [PATCH 03/19] Add GET /carriers API endpoint --- .../Resources/Carrier/CarrierList.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/ApiPlatform/Resources/Carrier/CarrierList.php diff --git a/src/ApiPlatform/Resources/Carrier/CarrierList.php b/src/ApiPlatform/Resources/Carrier/CarrierList.php new file mode 100644 index 00000000..a2e9f9e5 --- /dev/null +++ b/src/ApiPlatform/Resources/Carrier/CarrierList.php @@ -0,0 +1,67 @@ + + * @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\Carrier; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; +use PrestaShop\PrestaShop\Core\Search\Filters\CarrierFilters; +use PrestaShopBundle\ApiPlatform\Metadata\PaginatedList; +use PrestaShopBundle\ApiPlatform\Provider\QueryListProvider; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new PaginatedList( + uriTemplate: '/carriers', + provider: QueryListProvider::class, + scopes: ['carrier_read'], + ApiResourceMapping: [ + '[id_carrier]' => '[carrierId]', + '[is_free]' => '[isFree]', + ], + gridDataFactory: 'prestashop.core.grid.data.factory.carrier_decorator', + filtersClass: CarrierFilters::class, + ), + ], + exceptionToStatus: [ + CarrierNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class CarrierList +{ + #[ApiProperty(identifier: true)] + public int $carrierId; + + public string $name; + + public ?string $delay; + + public bool $active; + + public bool $isFree; + + public int $position; + + public ?string $logo; +} From e7a182963e95ac4ee22f16a883e6a2ca04c6b73c Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:19:01 +0100 Subject: [PATCH 04/19] Implement GET /carriers API endpoint --- src/ApiPlatform/Resources/Carrier/CarrierList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiPlatform/Resources/Carrier/CarrierList.php b/src/ApiPlatform/Resources/Carrier/CarrierList.php index a2e9f9e5..94b697bd 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierList.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierList.php @@ -40,7 +40,7 @@ '[id_carrier]' => '[carrierId]', '[is_free]' => '[isFree]', ], - gridDataFactory: 'prestashop.core.grid.data.factory.carrier_decorator', + gridDataFactory: 'prestashop.core.grid.data.factory.carrier', filtersClass: CarrierFilters::class, ), ], From 1c1c252c5f7c54fca8c908a7ff152bae0552e11c Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:20:09 +0100 Subject: [PATCH 05/19] Implement GET /carriers/{carrierId} API endpoint --- src/ApiPlatform/Resources/Carrier/Carrier.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/ApiPlatform/Resources/Carrier/Carrier.php diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php new file mode 100644 index 00000000..17001abd --- /dev/null +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -0,0 +1,121 @@ + + * @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\Carrier; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\Decimal\DecimalNumber; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/carriers/{carrierId}', + requirements: ['carrierId' => '\d+'], + CQRSQuery: GetCarrierForEditing::class, + scopes: ['carrier_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + ], + normalizationContext: ['skip_null_values' => false], + exceptionToStatus: [ + CarrierNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class Carrier +{ + #[ApiProperty(identifier: true)] + public int $carrierId; + + public string $name; + + public int $grade; + + public string $trackingUrl; + + public int $position; + + public bool $active; + + #[LocalizedValue] + public array $delay; + + public ?string $logoPath; + + public int $maxWidth; + + public int $maxHeight; + + public int $maxDepth; + + public DecimalNumber $maxWeight; + + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + public array $associatedGroupIds; + + public bool $hasAdditionalHandlingFee; + + public bool $isFree; + + public int $shippingMethod; + + public int $idTaxRuleGroup; + + public int $rangeBehavior; + + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + public array $associatedShopIds; + + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'object']])] + public array $zones; + + public int $ordersCount; + + public const QUERY_MAPPING = [ + '[carrierId]' => '[getCarrierId]', + '[name]' => '[getName]', + '[grade]' => '[getGrade]', + '[trackingUrl]' => '[getTrackingUrl]', + '[position]' => '[getPosition]', + '[active]' => '[isActive]', + '[delay]' => '[getLocalizedDelay]', + '[logoPath]' => '[getLogoPath]', + '[maxWidth]' => '[getMaxWidth]', + '[maxHeight]' => '[getMaxHeight]', + '[maxDepth]' => '[getMaxDepth]', + '[maxWeight]' => '[getMaxWeight]', + '[associatedGroupIds]' => '[getAssociatedGroupIds]', + '[hasAdditionalHandlingFee]' => '[hasAdditionalHandlingFee]', + '[isFree]' => '[isFree]', + '[shippingMethod]' => '[getShippingMethod]', + '[idTaxRuleGroup]' => '[getIdTaxRuleGroup]', + '[rangeBehavior]' => '[getRangeBehavior]', + '[associatedShopIds]' => '[getAssociatedShopIds]', + '[zones]' => '[getZones]', + '[ordersCount]' => '[getOrdersCount]', + ]; +} From e742901869ceb5ee99715be954e8c11936416ab1 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:21:19 +0100 Subject: [PATCH 06/19] Implement GET /carriers/{carrierId}/ranges API endpoint --- .../Resources/Carrier/CarrierRanges.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/ApiPlatform/Resources/Carrier/CarrierRanges.php diff --git a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php new file mode 100644 index 00000000..672d1407 --- /dev/null +++ b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php @@ -0,0 +1,80 @@ + + * @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\Carrier; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierRanges; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use Symfony\Component\HttpFoundation\Response; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/carriers/{carrierId}/ranges', + requirements: ['carrierId' => '\d+'], + CQRSQuery: GetCarrierRanges::class, + scopes: ['carrier_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + ], + normalizationContext: ['skip_null_values' => false], + exceptionToStatus: [ + CarrierNotFoundException::class => Response::HTTP_NOT_FOUND, + ], +)] +class CarrierRanges +{ + #[ApiProperty(identifier: true)] + public int $carrierId; + + #[ApiProperty( + openapiContext: [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'zoneId' => ['type' => 'integer'], + 'ranges' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'from' => ['type' => 'string'], + 'to' => ['type' => 'string'], + 'price' => ['type' => 'string'], + ], + ], + ], + ], + ], + ] + )] + public array $zones = []; + + public const QUERY_MAPPING = [ + '[carrierId]' => '[getCarrierId]', + '[zones]' => '[getZones]', + ]; +} From e19bb04a9f76140b18429df467fced4157b220e6 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:23:35 +0100 Subject: [PATCH 07/19] Implement GET /carriers/{carrierId}/ranges API endpoint --- src/ApiPlatform/Resources/Carrier/CarrierRanges.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php index 672d1407..c522b862 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php @@ -74,7 +74,7 @@ class CarrierRanges public array $zones = []; public const QUERY_MAPPING = [ - '[carrierId]' => '[getCarrierId]', + '[carrierId]' => '[getCarrierId][getValue]', '[zones]' => '[getZones]', ]; } From 3980395bb95f32c00de02543d8887151d1ad6e17 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:33:00 +0100 Subject: [PATCH 08/19] Fix ShopConstraint mapping and ValueObject getValue() in Carrier queries --- src/ApiPlatform/Resources/Carrier/Carrier.php | 10 +++++----- src/ApiPlatform/Resources/Carrier/CarrierRanges.php | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php index 17001abd..29ec99e9 100644 --- a/src/ApiPlatform/Resources/Carrier/Carrier.php +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -24,7 +24,6 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; -use PrestaShop\Decimal\DecimalNumber; use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierForEditing; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; @@ -72,7 +71,7 @@ class Carrier public int $maxDepth; - public DecimalNumber $maxWeight; + public float $maxWeight; #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] public array $associatedGroupIds; @@ -96,7 +95,8 @@ class Carrier public int $ordersCount; public const QUERY_MAPPING = [ - '[carrierId]' => '[getCarrierId]', + '[_context][shopConstraint]' => '[shopConstraint]', + '[carrierId]' => '[getCarrierId][getValue]', '[name]' => '[getName]', '[grade]' => '[getGrade]', '[trackingUrl]' => '[getTrackingUrl]', @@ -111,9 +111,9 @@ class Carrier '[associatedGroupIds]' => '[getAssociatedGroupIds]', '[hasAdditionalHandlingFee]' => '[hasAdditionalHandlingFee]', '[isFree]' => '[isFree]', - '[shippingMethod]' => '[getShippingMethod]', + '[shippingMethod]' => '[getShippingMethod][getValue]', '[idTaxRuleGroup]' => '[getIdTaxRuleGroup]', - '[rangeBehavior]' => '[getRangeBehavior]', + '[rangeBehavior]' => '[getRangeBehavior][getValue]', '[associatedShopIds]' => '[getAssociatedShopIds]', '[zones]' => '[getZones]', '[ordersCount]' => '[getOrdersCount]', diff --git a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php index c522b862..f5e01eae 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php @@ -74,6 +74,7 @@ class CarrierRanges public array $zones = []; public const QUERY_MAPPING = [ + '[_context][shopConstraint]' => '[shopConstraint]', '[carrierId]' => '[getCarrierId][getValue]', '[zones]' => '[getZones]', ]; From 77e7701444e7ac09de1ab828b5bb325c97297e80 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:35:47 +0100 Subject: [PATCH 09/19] Map CarrierConstraintException to 422 in CarrierRanges endpoint --- src/ApiPlatform/Resources/Carrier/CarrierRanges.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php index f5e01eae..18d37b26 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php @@ -24,6 +24,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierConstraintException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierRanges; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; @@ -42,6 +43,8 @@ normalizationContext: ['skip_null_values' => false], exceptionToStatus: [ CarrierNotFoundException::class => Response::HTTP_NOT_FOUND, + // GetCarrierRanges only supports ShopConstraint::allShops() - core limitation (TODO in CarrierRangeRepository) + CarrierConstraintException::class => Response::HTTP_UNPROCESSABLE_ENTITY, ], )] class CarrierRanges From 831c8fa4bee21936f62550224989f5c6482f38d3 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 9 Mar 2026 22:40:42 +0100 Subject: [PATCH 10/19] Add TODO.md with known limitation for GET /carriers/{carrierId}/ranges --- TODO.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..42319b08 --- /dev/null +++ b/TODO.md @@ -0,0 +1,22 @@ +# TODO + +## GET /carriers/{carrierId}/ranges — Core limitation + +**Stato:** Endpoint implementato ma non funzionante (restituisce 422) + +**Causa:** `CarrierRangeRepository::assertShopConstraint()` nel core PrestaShop accetta +solo `ShopConstraint::allShops()`, ma il framework API passa sempre un constraint +shop-specifico (es. `shopId=1`). La funzione `applyShopConstraint()` ha un TODO +irrisolto nel core. + +**File coinvolti:** +- Modulo: `src/ApiPlatform/Resources/Carrier/CarrierRanges.php` +- Core PS: `src/Adapter/Carrier/Repository/CarrierRangeRepository.php` (righe 220-237) + +**Fix richiesto:** +1. Aprire una issue/PR nel repo `PrestaShop/PrestaShop` per implementare + `CarrierRangeRepository::applyShopConstraint()` con supporto ai constraint shop-specifici +2. Una volta fixato il core, verificare che l'endpoint funzioni correttamente +3. Valutare se rimuovere o mantenere il mapping `CarrierConstraintException → 422` + +**Workaround attuale:** `CarrierConstraintException` mappata a HTTP 422 invece di 500. From fdb1627feabf39388ec1f7373de84c952e224137 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Tue, 10 Mar 2026 07:12:35 +0100 Subject: [PATCH 11/19] Add toggle-status and toggle-is-free endpoints for carriers Expose ToggleCarrierStatusCommand and ToggleCarrierIsFreeCommand via PUT /carriers/{carrierId}/toggle-status and PUT /carriers/{carrierId}/toggle-is-free endpoints with carrier_write scope. Add integration tests for both toggle endpoints. Co-Authored-By: Claude Sonnet 4.6 --- src/ApiPlatform/Resources/Carrier/Carrier.php | 19 +++ .../ApiPlatform/CarrierEndpointTest.php | 134 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 tests/Integration/ApiPlatform/CarrierEndpointTest.php diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php index 29ec99e9..79fe2d46 100644 --- a/src/ApiPlatform/Resources/Carrier/Carrier.php +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -24,9 +24,12 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\ToggleCarrierIsFreeCommand; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\ToggleCarrierStatusCommand; use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierForEditing; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; use Symfony\Component\HttpFoundation\Response; @@ -39,6 +42,22 @@ scopes: ['carrier_read'], CQRSQueryMapping: self::QUERY_MAPPING, ), + new CQRSUpdate( + uriTemplate: '/carriers/{carrierId}/toggle-status', + requirements: ['carrierId' => '\d+'], + output: false, + allowEmptyBody: true, + CQRSCommand: ToggleCarrierStatusCommand::class, + scopes: ['carrier_write'], + ), + new CQRSUpdate( + uriTemplate: '/carriers/{carrierId}/toggle-is-free', + requirements: ['carrierId' => '\d+'], + output: false, + allowEmptyBody: true, + CQRSCommand: ToggleCarrierIsFreeCommand::class, + scopes: ['carrier_write'], + ), ], normalizationContext: ['skip_null_values' => false], exceptionToStatus: [ diff --git a/tests/Integration/ApiPlatform/CarrierEndpointTest.php b/tests/Integration/ApiPlatform/CarrierEndpointTest.php new file mode 100644 index 00000000..6f98f302 --- /dev/null +++ b/tests/Integration/ApiPlatform/CarrierEndpointTest.php @@ -0,0 +1,134 @@ + + * @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 PsApiResourcesTest\Integration\ApiPlatform; + +use Symfony\Component\HttpFoundation\Response; +use Tests\Resources\DatabaseDump; + +class CarrierEndpointTest extends ApiTestCase +{ + public static \Carrier $carrier1; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + self::createApiClient(['carrier_read', 'carrier_write']); + + self::$carrier1 = new \Carrier(); + self::$carrier1->name = 'Test Carrier 1'; + self::$carrier1->delay = [1 => 'Delivery in 2-3 days']; + self::$carrier1->active = true; + self::$carrier1->is_free = false; + self::$carrier1->shipping_handling = false; + self::$carrier1->range_behavior = 0; + self::$carrier1->is_module = false; + self::$carrier1->save(); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + DatabaseDump::restoreTables(['carrier', 'carrier_lang', 'carrier_shop', 'carrier_group', 'carrier_zone', 'carrier_tax_rules_group_shop']); + } + + public static function getProtectedEndpoints(): iterable + { + yield 'get endpoint' => [ + 'GET', + '/carriers/1', + ]; + + yield 'toggle status endpoint' => [ + 'PUT', + '/carriers/1/toggle-status', + ]; + + yield 'toggle is free endpoint' => [ + 'PUT', + '/carriers/1/toggle-is-free', + ]; + } + + public function testGetCarrier(): int + { + $carrierId = (int) self::$carrier1->id; + + $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); + $this->assertEquals($carrierId, $carrier['carrierId']); + $this->assertTrue($carrier['active']); + $this->assertFalse($carrier['isFree']); + + return $carrierId; + } + + /** + * @depends testGetCarrier + */ + public function testToggleCarrierStatus(int $carrierId): int + { + $this->updateItem('/carriers/' . $carrierId . '/toggle-status', [], ['carrier_write'], Response::HTTP_NO_CONTENT); + $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); + $this->assertFalse($carrier['active']); + + // Toggle back + $this->updateItem('/carriers/' . $carrierId . '/toggle-status', [], ['carrier_write'], Response::HTTP_NO_CONTENT); + $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); + $this->assertTrue($carrier['active']); + + return $carrierId; + } + + /** + * @depends testToggleCarrierStatus + */ + public function testToggleCarrierIsFree(int $carrierId): int + { + $this->updateItem('/carriers/' . $carrierId . '/toggle-is-free', [], ['carrier_write'], Response::HTTP_NO_CONTENT); + $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); + $this->assertTrue($carrier['isFree']); + + // Toggle back + $this->updateItem('/carriers/' . $carrierId . '/toggle-is-free', [], ['carrier_write'], Response::HTTP_NO_CONTENT); + $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); + $this->assertFalse($carrier['isFree']); + + return $carrierId; + } + + /** + * @depends testToggleCarrierIsFree + */ + public function testToggleCarrierStatusNotFound(int $carrierId): void + { + $this->updateItem('/carriers/99999/toggle-status', [], ['carrier_write'], Response::HTTP_NOT_FOUND); + } + + /** + * @depends testToggleCarrierStatusNotFound + */ + public function testToggleCarrierIsFreeNotFound(): void + { + $this->updateItem('/carriers/99999/toggle-is-free', [], ['carrier_write'], Response::HTTP_NOT_FOUND); + } +} From 9c84ea8b945648d4e2bb9af95fde1e42530855d8 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Sun, 15 Mar 2026 21:21:24 +0100 Subject: [PATCH 12/19] Add BulkToggleCarrierStatusCommand endpoint for carriers Add BulkCarriers API resource with PUT /carriers/bulk-set-status endpoint that maps to BulkToggleCarrierStatusCommand, following the same pattern used for taxes and zones bulk status toggle. Co-Authored-By: Claude Sonnet 4.6 --- .../Resources/Carrier/BulkCarriers.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/ApiPlatform/Resources/Carrier/BulkCarriers.php diff --git a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php new file mode 100644 index 00000000..a9fc916d --- /dev/null +++ b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php @@ -0,0 +1,61 @@ + + * @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\Carrier; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\BulkToggleCarrierStatusCommand; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierConstraintException; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSUpdate( + uriTemplate: '/carriers/bulk-set-status', + output: false, + CQRSCommand: BulkToggleCarrierStatusCommand::class, + CQRSCommandMapping: [ + '[enabled]' => '[expectedStatus]', + ], + scopes: [ + 'carrier_write', + ], + ), + ], + exceptionToStatus: [ + CarrierConstraintException::class => Response::HTTP_UNPROCESSABLE_ENTITY, + ], +)] +class BulkCarriers +{ + /** + * @var int[] + */ + #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] + #[Assert\NotBlank] + public array $carrierIds; + + public bool $enabled; +} From 8ba2ce2e9476cdef12029f11e15a0a043455b0af Mon Sep 17 00:00:00 2001 From: luigi massa Date: Sun, 15 Mar 2026 21:39:03 +0100 Subject: [PATCH 13/19] Add Assert\Positive validation on carrierIds to prevent invalid IDs Without this constraint, passing carrierId=0 causes a CarrierConstraintException thrown during command construction (before the command bus), which bypasses the exceptionToStatus mapping and returns 500 instead of a proper error. The Assert\All([Assert\Positive()]) constraint catches invalid IDs during Symfony validation phase and returns a 400 Bad Request with details. Co-Authored-By: Claude Sonnet 4.6 --- src/ApiPlatform/Resources/Carrier/BulkCarriers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php index a9fc916d..1068c336 100644 --- a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php +++ b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php @@ -55,6 +55,7 @@ class BulkCarriers */ #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] #[Assert\NotBlank] + #[Assert\All([new Assert\Positive()])] public array $carrierIds; public bool $enabled; From 1419281ec6c65d95fa22325685b70743a5790cf6 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Sun, 15 Mar 2026 21:54:26 +0100 Subject: [PATCH 14/19] Fix deserialization: set explicit input class to prevent direct command instantiation CQRSCommand/CQRSUpdate automatically sets input to the CQRS command class, causing CQRSApiNormalizer to instantiate BulkToggleCarrierStatusCommand directly during the DeserializeListener phase. This bypasses the DTO and passes raw string values from JSON to the command constructor, where (int) cast on non-numeric strings produces 0, triggering CarrierConstraintException. Setting input: BulkCarriers::class forces the deserialization to use the DTO class, letting the CommandProcessor handle command instantiation with proper type handling. Co-Authored-By: Claude Opus 4.6 --- src/ApiPlatform/Resources/Carrier/BulkCarriers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php index 1068c336..1e204816 100644 --- a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php +++ b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php @@ -34,6 +34,7 @@ operations: [ new CQRSUpdate( uriTemplate: '/carriers/bulk-set-status', + input: BulkCarriers::class, output: false, CQRSCommand: BulkToggleCarrierStatusCommand::class, CQRSCommandMapping: [ From 98dc5737ff871102d5f55e8d486bf03e7373de0c Mon Sep 17 00:00:00 2001 From: luigi massa Date: Mon, 16 Mar 2026 22:23:06 +0100 Subject: [PATCH 15/19] Fix BulkCarriers: remove explicit input override to restore CQRSApiNormalizer flow Removing input: BulkCarriers::class lets CQRSCommand auto-set input to BulkToggleCarrierStatusCommand, so CQRSApiNormalizer validates BulkCarriers constraints (Assert\Positive on carrierIds) before applying CQRSCommandMapping ([enabled] -> [expectedStatus]) and instantiating the command. Co-Authored-By: Claude Sonnet 4.6 --- src/ApiPlatform/Resources/Carrier/BulkCarriers.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php index 1e204816..1068c336 100644 --- a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php +++ b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php @@ -34,7 +34,6 @@ operations: [ new CQRSUpdate( uriTemplate: '/carriers/bulk-set-status', - input: BulkCarriers::class, output: false, CQRSCommand: BulkToggleCarrierStatusCommand::class, CQRSCommandMapping: [ From 2b45287abf06cb75e19546f533fa520be4a78bb3 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Tue, 17 Mar 2026 12:19:49 +0100 Subject: [PATCH 16/19] Add Assert\Type numeric validation on carrierIds to prevent 500 errors Non-numeric values (e.g. strings) in carrierIds array were bypassing Assert\Positive and causing CarrierConstraintException during command instantiation, resulting in HTTP 500 instead of 422. Co-Authored-By: Claude Sonnet 4.6 --- src/ApiPlatform/Resources/Carrier/BulkCarriers.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php index 1068c336..80b5fa06 100644 --- a/src/ApiPlatform/Resources/Carrier/BulkCarriers.php +++ b/src/ApiPlatform/Resources/Carrier/BulkCarriers.php @@ -55,7 +55,10 @@ class BulkCarriers */ #[ApiProperty(openapiContext: ['type' => 'array', 'items' => ['type' => 'integer'], 'example' => [1, 3]])] #[Assert\NotBlank] - #[Assert\All([new Assert\Positive()])] + #[Assert\All([ + new Assert\Type('numeric'), + new Assert\Positive(), + ])] public array $carrierIds; public bool $enabled; From 093c206251e05585db41b65375bd34d97a217eb0 Mon Sep 17 00:00:00 2001 From: luigi massa Date: Tue, 17 Mar 2026 12:51:05 +0100 Subject: [PATCH 17/19] Add DELETE /carriers/{carrierId} endpoint and CarrierList filters mapping Co-Authored-By: Claude Sonnet 4.6 --- src/ApiPlatform/Resources/Carrier/Carrier.php | 10 ++++++++++ src/ApiPlatform/Resources/Carrier/CarrierList.php | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php index 79fe2d46..28885766 100644 --- a/src/ApiPlatform/Resources/Carrier/Carrier.php +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -24,10 +24,13 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\DeleteCarrierCommand; use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\ToggleCarrierIsFreeCommand; use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\ToggleCarrierStatusCommand; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierConstraintException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Exception\CarrierNotFoundException; use PrestaShop\PrestaShop\Core\Domain\Carrier\Query\GetCarrierForEditing; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; use PrestaShopBundle\ApiPlatform\Metadata\CQRSUpdate; use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; @@ -58,10 +61,17 @@ CQRSCommand: ToggleCarrierIsFreeCommand::class, scopes: ['carrier_write'], ), + new CQRSDelete( + uriTemplate: '/carriers/{carrierId}', + requirements: ['carrierId' => '\d+'], + CQRSCommand: DeleteCarrierCommand::class, + scopes: ['carrier_write'], + ), ], normalizationContext: ['skip_null_values' => false], exceptionToStatus: [ CarrierNotFoundException::class => Response::HTTP_NOT_FOUND, + CarrierConstraintException::class => Response::HTTP_UNPROCESSABLE_ENTITY, ], )] class Carrier diff --git a/src/ApiPlatform/Resources/Carrier/CarrierList.php b/src/ApiPlatform/Resources/Carrier/CarrierList.php index 94b697bd..72d3ee61 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierList.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierList.php @@ -42,6 +42,10 @@ ], gridDataFactory: 'prestashop.core.grid.data.factory.carrier', filtersClass: CarrierFilters::class, + filtersMapping: [ + '[carrierId]' => '[id_carrier]', + '[isFree]' => '[is_free]', + ], ), ], exceptionToStatus: [ From d0583917a3f7e1e30ddd6347b0175b613cd8b2eb Mon Sep 17 00:00:00 2001 From: bwlab Date: Mon, 30 Mar 2026 07:40:18 +0200 Subject: [PATCH 18/19] Refactor testGetCarrier to compare full response object Replace individual property assertions with a single assertEquals on the entire carrier array, following the project test conventions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ApiPlatform/CarrierEndpointTest.php | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/Integration/ApiPlatform/CarrierEndpointTest.php b/tests/Integration/ApiPlatform/CarrierEndpointTest.php index 6f98f302..7428c530 100644 --- a/tests/Integration/ApiPlatform/CarrierEndpointTest.php +++ b/tests/Integration/ApiPlatform/CarrierEndpointTest.php @@ -75,9 +75,34 @@ public function testGetCarrier(): int $carrierId = (int) self::$carrier1->id; $carrier = $this->getItem('/carriers/' . $carrierId, ['carrier_read']); - $this->assertEquals($carrierId, $carrier['carrierId']); - $this->assertTrue($carrier['active']); - $this->assertFalse($carrier['isFree']); + $this->assertEquals( + [ + 'carrierId' => $carrierId, + 'name' => 'Test Carrier 1', + 'grade' => 0, + 'trackingUrl' => '', + 'position' => $carrier['position'], + 'active' => true, + 'delay' => [ + 'en-US' => 'Delivery in 2-3 days', + ], + 'logoPath' => null, + 'maxWidth' => 0, + 'maxHeight' => 0, + 'maxDepth' => 0, + 'maxWeight' => 0.0, + 'associatedGroupIds' => $carrier['associatedGroupIds'], + 'hasAdditionalHandlingFee' => false, + 'isFree' => false, + 'shippingMethod' => 0, + 'idTaxRuleGroup' => 0, + 'rangeBehavior' => 0, + 'associatedShopIds' => [1], + 'zones' => [], + 'ordersCount' => 0, + ], + $carrier + ); return $carrierId; } From 36d9979db936919dd5ee7f01d5501fac0df32474 Mon Sep 17 00:00:00 2001 From: bwlab Date: Mon, 30 Mar 2026 07:57:41 +0200 Subject: [PATCH 19/19] Fix QUERY_MAPPING conventions and remove TODO.md Use property names instead of getter method names in QUERY_MAPPING, and only map properties where names differ between QueryResult and API. Remove TODO.md as it should not be committed. Co-Authored-By: Claude Opus 4.6 (1M context) --- TODO.md | 22 ------------------- src/ApiPlatform/Resources/Carrier/Carrier.php | 22 +------------------ .../Resources/Carrier/CarrierRanges.php | 2 -- 3 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 42319b08..00000000 --- a/TODO.md +++ /dev/null @@ -1,22 +0,0 @@ -# TODO - -## GET /carriers/{carrierId}/ranges — Core limitation - -**Stato:** Endpoint implementato ma non funzionante (restituisce 422) - -**Causa:** `CarrierRangeRepository::assertShopConstraint()` nel core PrestaShop accetta -solo `ShopConstraint::allShops()`, ma il framework API passa sempre un constraint -shop-specifico (es. `shopId=1`). La funzione `applyShopConstraint()` ha un TODO -irrisolto nel core. - -**File coinvolti:** -- Modulo: `src/ApiPlatform/Resources/Carrier/CarrierRanges.php` -- Core PS: `src/Adapter/Carrier/Repository/CarrierRangeRepository.php` (righe 220-237) - -**Fix richiesto:** -1. Aprire una issue/PR nel repo `PrestaShop/PrestaShop` per implementare - `CarrierRangeRepository::applyShopConstraint()` con supporto ai constraint shop-specifici -2. Una volta fixato il core, verificare che l'endpoint funzioni correttamente -3. Valutare se rimuovere o mantenere il mapping `CarrierConstraintException → 422` - -**Workaround attuale:** `CarrierConstraintException` mappata a HTTP 422 invece di 500. diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php index 28885766..7982f9bb 100644 --- a/src/ApiPlatform/Resources/Carrier/Carrier.php +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -125,26 +125,6 @@ class Carrier public const QUERY_MAPPING = [ '[_context][shopConstraint]' => '[shopConstraint]', - '[carrierId]' => '[getCarrierId][getValue]', - '[name]' => '[getName]', - '[grade]' => '[getGrade]', - '[trackingUrl]' => '[getTrackingUrl]', - '[position]' => '[getPosition]', - '[active]' => '[isActive]', - '[delay]' => '[getLocalizedDelay]', - '[logoPath]' => '[getLogoPath]', - '[maxWidth]' => '[getMaxWidth]', - '[maxHeight]' => '[getMaxHeight]', - '[maxDepth]' => '[getMaxDepth]', - '[maxWeight]' => '[getMaxWeight]', - '[associatedGroupIds]' => '[getAssociatedGroupIds]', - '[hasAdditionalHandlingFee]' => '[hasAdditionalHandlingFee]', - '[isFree]' => '[isFree]', - '[shippingMethod]' => '[getShippingMethod][getValue]', - '[idTaxRuleGroup]' => '[getIdTaxRuleGroup]', - '[rangeBehavior]' => '[getRangeBehavior][getValue]', - '[associatedShopIds]' => '[getAssociatedShopIds]', - '[zones]' => '[getZones]', - '[ordersCount]' => '[getOrdersCount]', + '[localizedDelay]' => '[delay]', ]; } diff --git a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php index 18d37b26..1581964c 100644 --- a/src/ApiPlatform/Resources/Carrier/CarrierRanges.php +++ b/src/ApiPlatform/Resources/Carrier/CarrierRanges.php @@ -78,7 +78,5 @@ class CarrierRanges public const QUERY_MAPPING = [ '[_context][shopConstraint]' => '[shopConstraint]', - '[carrierId]' => '[getCarrierId][getValue]', - '[zones]' => '[getZones]', ]; }