diff --git a/src/ApiPlatform/Resources/Carrier/Carrier.php b/src/ApiPlatform/Resources/Carrier/Carrier.php new file mode 100644 index 000000000..5c69df4ed --- /dev/null +++ b/src/ApiPlatform/Resources/Carrier/Carrier.php @@ -0,0 +1,165 @@ + + * @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\AddCarrierCommand; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\DeleteCarrierCommand; +use PrestaShop\PrestaShop\Core\Domain\Carrier\Command\EditCarrierCommand; +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\CQRSCreate; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSDelete; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSGet; +use PrestaShopBundle\ApiPlatform\Metadata\CQRSPartialUpdate; +use PrestaShopBundle\ApiPlatform\Metadata\LocalizedValue; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + operations: [ + new CQRSGet( + uriTemplate: '/carriers/{carrierId}', + requirements: ['carrierId' => '\d+'], + CQRSQuery: GetCarrierForEditing::class, + scopes: ['carrier_read'], + CQRSQueryMapping: self::QUERY_MAPPING, + ), + new CQRSCreate( + uriTemplate: '/carriers', + validationContext: ['groups' => ['Default', 'Create']], + CQRSCommand: AddCarrierCommand::class, + CQRSQuery: GetCarrierForEditing::class, + scopes: ['carrier_write'], + inputFormats: ['multipart' => ['multipart/form-data']], + denormalizationContext: [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::CREATE_COMMAND_MAPPING, + ), + new CQRSPartialUpdate( + uriTemplate: '/carriers/{carrierId}', + requirements: ['carrierId' => '\d+'], + read: false, + CQRSCommand: EditCarrierCommand::class, + CQRSQuery: GetCarrierForEditing::class, + scopes: ['carrier_write'], + inputFormats: ['multipart' => ['multipart/form-data']], + denormalizationContext: [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true], + CQRSQueryMapping: self::QUERY_MAPPING, + CQRSCommandMapping: self::UPDATE_COMMAND_MAPPING, + ), + 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 +{ + #[ApiProperty(identifier: true)] + public int $carrierId; + + #[Assert\NotBlank(groups: ['Create'])] + public string $name; + + #[LocalizedValue] + #[Assert\NotBlank(groups: ['Create'])] + public array $delay; + + public int $grade; + + public string $trackingUrl; + + public bool $active; + + public array $associatedGroupIds; + + public bool $hasAdditionalHandlingFee; + + public bool $isFree; + + public int $shippingMethod; + + public int $rangeBehavior; + + #[Assert\Count(min: 1, groups: ['Create'])] + public array $zones; + + public array $associatedShopIds; + + public int $maxWidth; + + public int $maxHeight; + + public int $maxDepth; + + public float $maxWeight; + + public int $taxRuleGroupId; + + public ?File $logoFile; + + public ?string $logoPath; + + public int $position; + + public const QUERY_MAPPING = [ + '[_context][shopConstraint]' => '[shopConstraint]', + '[localizedDelay]' => '[delay]', + '[idTaxRuleGroup]' => '[taxRuleGroupId]', + ]; + + // AddCarrierCommand takes most fields as constructor arguments matching these property names; + // only the renamed/uploaded ones need an explicit mapping. + public const CREATE_COMMAND_MAPPING = [ + '[delay]' => '[localizedDelay]', + '[maxWidth]' => '[max_width]', + '[maxHeight]' => '[max_height]', + '[maxDepth]' => '[max_depth]', + '[maxWeight]' => '[max_weight]', + '[logoFile].pathName' => '[logoPathName]', + ]; + + // EditCarrierCommand exposes the fields through setters (setMaxWidth, setLocalizedDelay, + // setLogoPathName, setTaxRuleGroupId, setAdditionalHandlingFee...). + public const UPDATE_COMMAND_MAPPING = [ + '[delay]' => '[localizedDelay]', + '[maxWidth]' => '[max_width]', + '[maxHeight]' => '[max_height]', + '[maxDepth]' => '[max_depth]', + '[maxWeight]' => '[max_weight]', + '[hasAdditionalHandlingFee]' => '[additionalHandlingFee]', + '[logoFile].pathName' => '[logoPathName]', + ]; +} diff --git a/tests/Integration/ApiPlatform/CarrierEndpointTest.php b/tests/Integration/ApiPlatform/CarrierEndpointTest.php new file mode 100644 index 000000000..d218408a2 --- /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 function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + DatabaseDump::restoreTables(['carrier', 'carrier_lang', 'carrier_group', 'carrier_zone', 'carrier_shop']); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + DatabaseDump::restoreTables(['carrier', 'carrier_lang', 'carrier_group', 'carrier_zone', 'carrier_shop']); + } + + public static function getProtectedEndpoints(): iterable + { + yield 'create endpoint' => ['POST', '/carriers', 'multipart/form-data']; + yield 'get endpoint' => ['GET', '/carriers/1']; + yield 'update endpoint' => ['PATCH', '/carriers/1', 'multipart/form-data']; + yield 'delete endpoint' => ['DELETE', '/carriers/1']; + } + + public function testGetCarrier(): void + { + $carrier = $this->getItem('/carriers/2', ['carrier_read']); + + $this->assertSame(2, $carrier['carrierId']); + $this->assertSame('My carrier', $carrier['name']); + $this->assertArrayHasKey('delay', $carrier); + $this->assertArrayHasKey('grade', $carrier); + $this->assertArrayHasKey('zones', $carrier); + $this->assertArrayHasKey('taxRuleGroupId', $carrier); + } + + public function testAddCarrier(): int + { + $logo = $this->prepareUploadedFile(__DIR__ . '/../../Resources/assets/image/Hummingbird_cushion.jpg'); + + $carrier = $this->requestApi('POST', '/carriers', null, ['carrier_write'], Response::HTTP_CREATED, [ + 'headers' => [ + 'content-type' => 'multipart/form-data', + ], + 'extra' => [ + 'files' => [ + 'logoFile' => $logo, + ], + 'parameters' => [ + 'name' => 'API carrier', + 'delay' => ['en-US' => 'Delivered fast'], + 'grade' => '5', + 'trackingUrl' => 'https://tracking.example.com/@', + 'active' => '1', + 'associatedGroupIds' => ['3'], + 'hasAdditionalHandlingFee' => '0', + 'isFree' => '1', + 'shippingMethod' => '2', + 'rangeBehavior' => '0', + 'zones' => ['1'], + 'associatedShopIds' => ['1'], + 'maxWidth' => '0', + 'maxHeight' => '0', + 'maxDepth' => '0', + 'maxWeight' => '0', + ], + ], + ]); + + $this->assertArrayHasKey('carrierId', $carrier); + $this->assertIsInt($carrier['carrierId']); + $this->assertGreaterThan(0, $carrier['carrierId']); + $this->assertSame('API carrier', $carrier['name']); + + return $carrier['carrierId']; + } + + /** + * @depends testAddCarrier + */ + public function testUpdateCarrier(int $carrierId): int + { + $carrier = $this->requestApi('PATCH', '/carriers/' . $carrierId, null, ['carrier_write'], Response::HTTP_OK, [ + 'headers' => [ + 'content-type' => 'multipart/form-data', + ], + 'extra' => [ + 'parameters' => [ + 'name' => 'API carrier updated', + 'grade' => '7', + ], + ], + ]); + + $this->assertSame('API carrier updated', $carrier['name']); + $this->assertSame(7, $carrier['grade']); + + return $carrierId; + } + + /** + * @depends testUpdateCarrier + */ + public function testDeleteCarrier(int $carrierId): void + { + $this->requestApi('DELETE', '/carriers/' . $carrierId, null, ['carrier_write'], Response::HTTP_NO_CONTENT); + $this->getItem('/carriers/' . $carrierId, ['carrier_read'], Response::HTTP_NOT_FOUND); + } +}