From 0a5d9a24b1d398e5d4ea866efd59740e329282c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Salo=C5=88?= Date: Fri, 11 Feb 2022 11:52:59 +0100 Subject: [PATCH] Added forceToken support for pricing endpoints --- CHANGELOG.md | 8 +++ README.md | 8 +-- doc/Article.md | 8 ++- doc/Exception.md | 23 ++++++-- example/Article.php | 22 ++++++++ src/Article/ArticleClient.php | 16 ++++-- .../Interfaces/ArticleClientInterface.php | 4 +- src/Exception/BadResponseException.php | 56 +++++++++++++------ src/Exception/ExceptionFactory.php | 2 +- src/Exception/PriceProtectionException.php | 24 ++++++++ src/MpApiClient.php | 2 +- 11 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 src/Exception/PriceProtectionException.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca4428..d9141db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.2.0 - 2022-02-10 + +### Added + +- support for `forceToken` parameter for `updateProductPricing` and `updateVariantPricing` methods of `Article` client +- new `PriceProtectionException` thrown when API returns error with `PRICE_PROTECTION_ERROR` code, used to obtain force token using `getForceToken` method +- `getBody` method to `BadResponseException`, which returns API response as an array + ## 4.1.0 - 2021-12-16 ### Added diff --git a/README.md b/README.md index 3af3964..61a3b6b 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,17 @@ Client consists of one main client and multiple, separate, domain clients. The main client groups all domain clients under one object, for easier implementation, but every domain client can be initialized and used by itself. -Every client provides an interface that SHOULD be used as parameter types in code, instead of client classes themselves (i.e., use `MpApiClientInterface $client` +Every client provides an interface that SHOULD be used as parameter types in code, instead of client classes themselves (e.g., use `MpApiClientInterface $client` or `BrandsClientInterface $client` instead of `MpApiClient $client` or `BrandsClient $client`). When initializing the client, you MUST provide 1. an authenticator implementing [AuthMiddlewareInterface](src/Common/Interfaces/AuthMiddlewareInterface.php) - currently, only [ClientIdAuthenticator](src/Common/Authenticators/ClientIdAuthenticator.php), which accepts `my-client-id`, is provided - - in the future, new authenticators will be released (i.e., OAuth) + - in the future, new authenticators will be released (e.g., OAuth) 2. name of the app using the API - it is sent with every request to Mall API for easier request identification and debugging of reported issues - - please provide a simple, yet meaningful name, i.e., `MyAppName CRM` or `MyAppName Order sync` instead of a random string + - please provide a simple, yet meaningful name (e.g., `MyAppName CRM` or `MyAppName Order sync`), instead of a random string ### Examples @@ -131,7 +131,7 @@ List of custom Exceptions thrown in this client can be found [here](doc/Exceptio ## ⚠ Warning -- client does not include support for deprecated endpoints that will be changed, replaced or removed in the future (i.e., `/v1/deliveries` or `/v1/gifts`) +- client does not include support for deprecated endpoints that will be changed, replaced or removed in the future (e.g., `/v1/deliveries` or `/v1/gifts`) ## ℹ Known missing or incomplete features diff --git a/doc/Article.md b/doc/Article.md index bf85a18..3247cec 100644 --- a/doc/Article.md +++ b/doc/Article.md @@ -317,7 +317,7 @@ Example above prints out ## Update product pricing -Method expects `product` ID and [Pricing](../src/Article/Entity/Common/Pricing.php) entity and does not return anything. +Method expects `product` ID, [Pricing](../src/Article/Entity/Common/Pricing.php) entity and an optional `forceToken` and does not return anything. ```php /** @var MpApiClient\Common\Interfaces\ArticleClientInterface $articleClient */ @@ -638,14 +638,16 @@ Example above prints out ## Update variant pricing -Method expects `product` and `variant` IDs and [Pricing](../src/Article/Entity/Common/Pricing.php) entity and does not return anything. +Method expects `product` and `variant` IDs, [Pricing](../src/Article/Entity/Common/Pricing.php) entity and an optional `forceToken` and does not return anything. ```php /** @var MpApiClient\Common\Interfaces\ArticleClientInterface $articleClient */ +/** @var MpApiClient\Exception\PriceProtectionException $e */ $articleClient->updateVariantPricing( 'my-product-id', 'my-variant-id', - new Pricing(99.9, 112.0, 80.5) + new Pricing(99.9, 112.0, 80.5), + $e->getForceToken() ); ``` diff --git a/doc/Exception.md b/doc/Exception.md index 1f134a2..7376de2 100644 --- a/doc/Exception.md +++ b/doc/Exception.md @@ -4,7 +4,7 @@ Client contains custom exceptions, for easier error handling in your application All custom exceptions extend generic `MpApiException`. -Some methods might throw other native PHP exceptions (i.e., `InvalidArgumentException`), which do not extend `MpApiException`. Such methods always have +Some methods might throw other native PHP exceptions (e.g., `InvalidArgumentException`, `LogicException`), which do not extend `MpApiException`. Such methods always have appropriate `@throws` tags. ## [MpApiException](../src/Exception/MpApiException.php) @@ -13,25 +13,36 @@ appropriate `@throws` tags. ## [IncorrectDataTypeException](../src/Exception/IncorrectDataTypeException.php) +- Extends [MpApiException](#mpapiexception) - Thrown when method called expects data of a specific type, but received a different one ## [BadResponseException](../src/Exception/BadResponseException.php) +- Extends [MpApiException](#mpapiexception) - Thrown when API responded with `4xx` or `5xx` status code that could not be translated to more specific exceptions -- Usually indicates an unknown/unexpected error occurred +- Usually indicates, that an unknown/unexpected error occurred + +## [PriceProtectionException](../src/Exception/PriceProtectionException.php) + +- Extends [BadResponseException](#badresponseexception) +- Thrown when price protection mechanism is tripped during an article price update +- Contains `getForceToken` method, that may be used to force the price change using a provided token ## [ForbiddenException](../src/Exception/ForbiddenException.php) +- Extends [BadResponseException](#badresponseexception) - Thrown when API returns `403` status code - Usually returned on endpoints your account does not have access to ## [NotFoundException](../src/Exception/NotFoundException.php) +- Extends [BadResponseException](#badresponseexception) - Thrown when API returns `404` status code -- This exception should never be thrown for endpoints with static url (i.e., enums/lists) +- This exception should never be thrown for endpoints with static url (e.g., enums/lists) ## [UnauthorizedException](../src/Exception/UnauthorizedException.php) +- Extends [BadResponseException](#badresponseexception) - Thrown when API returns `401` status code - Reasons might include - you forgot to provide authenticator middleware @@ -40,16 +51,18 @@ appropriate `@throws` tags. ## [TooManyRequestsException](../src/Exception/TooManyRequestsException.php) +- Extends [BadResponseException](#badresponseexception) - Thrown when API returns `429` status code - In that case, you have been rate limited by the API and should slow down your request rate -- API returns rate limit information as part of response headers, which you can easily check +- API returns rate limit information as part of response headers, which can be easily checked ### Example of handling some errors ```php article()->updateProductPricing( + 'my-product-id', + new Pricing(99.9, 112.0, 80.5), + $e->getForceToken() + ); + } catch (MpApiException $e) { + echo 'Unexpected error occurred during product pricing update with force token: ' . $e->getMessage(); + } } catch (MpApiException $e) { echo 'Unexpected error occurred during product pricing update: ' . $e->getMessage(); } @@ -429,6 +440,17 @@ 'my-variant-id', new Pricing(99.9, 112.0, 80.5) ); +} catch (PriceProtectionException $e) { + try { + $client->article()->updateVariantPricing( + 'my-product-id', + 'my-variant-id', + new Pricing(99.9, 112.0, 80.5), + $e->getForceToken() + ); + } catch (MpApiException $e) { + echo 'Unexpected error occurred during variant pricing update with force token: ' . $e->getMessage(); + } } catch (MpApiException $e) { echo 'Unexpected error occurred during variant pricing update: ' . $e->getMessage(); } diff --git a/src/Article/ArticleClient.php b/src/Article/ArticleClient.php index fa9a985..c964fe8 100644 --- a/src/Article/ArticleClient.php +++ b/src/Article/ArticleClient.php @@ -87,9 +87,13 @@ public function getProductPricing(string $productId): Pricing ); } - public function updateProductPricing(string $productId, Pricing $pricing): void + public function updateProductPricing(string $productId, Pricing $pricing, ?string $forceToken = null): void { - $this->sendJson('PUT', sprintf(self::PRODUCT_PRICING, $productId), $pricing->getArrayForApi()); + $url = sprintf(self::PRODUCT_PRICING, $productId); + if ($forceToken !== null) { + $url = sprintf('%s?force_token=%s', $url, $forceToken); + } + $this->sendJson('PUT', $url, $pricing->getArrayForApi()); } public function listProductVariants(string $productId, ?Filter $filter): BasicVariantList @@ -144,9 +148,13 @@ public function getVariantPricing(string $productId, string $variantId): Pricing ); } - public function updateVariantPricing(string $productId, string $variantId, Pricing $pricing): void + public function updateVariantPricing(string $productId, string $variantId, Pricing $pricing, ?string $forceToken = null): void { - $this->sendJson('PUT', sprintf(self::VARIANT_PRICING, $productId, $variantId), $pricing->getArrayForApi()); + $url = sprintf(self::VARIANT_PRICING, $productId, $variantId); + if ($forceToken !== null) { + $url = sprintf('%s?force_token=%s', $url, $forceToken); + } + $this->sendJson('PUT', $url, $pricing->getArrayForApi()); } public function updateBatchAvailability(BatchAvailabilityIterator $availability): void diff --git a/src/Common/Interfaces/ArticleClientInterface.php b/src/Common/Interfaces/ArticleClientInterface.php index 74201e5..e567e99 100644 --- a/src/Common/Interfaces/ArticleClientInterface.php +++ b/src/Common/Interfaces/ArticleClientInterface.php @@ -59,7 +59,7 @@ public function getProductPricing(string $productId): Pricing; * @throws MpApiException * @deprecated Will be replaced with batch endpoint, similar to BatchAvailability */ - public function updateProductPricing(string $productId, Pricing $pricing): void; + public function updateProductPricing(string $productId, Pricing $pricing, ?string $forceToken = null): void; /** * @throws MpApiException @@ -102,7 +102,7 @@ public function getVariantPricing(string $productId, string $variantId): Pricing * @throws MpApiException * @deprecated Will be replaced with batch endpoint, similar to BatchAvailability */ - public function updateVariantPricing(string $productId, string $variantId, Pricing $pricing): void; + public function updateVariantPricing(string $productId, string $variantId, Pricing $pricing, ?string $forceToken = null): void; /** * @throws MpApiException diff --git a/src/Exception/BadResponseException.php b/src/Exception/BadResponseException.php index ad4939f..69c9448 100644 --- a/src/Exception/BadResponseException.php +++ b/src/Exception/BadResponseException.php @@ -16,32 +16,54 @@ class BadResponseException extends MpApiException private array $errorCodes; /** - * @param string $message - * @param int $code - * @param Throwable|null $previous - * @param ErrorCode[] $errorCodes + * @var array */ - final private function __construct(string $message = '', int $code = 0, Throwable $previous = null, array $errorCodes = []) + private array $body; + + /** + * @param string $message + * @param int $code + * @param Throwable|null $previous + * @param array $body + * @param ErrorCode[] $errorCodes + */ + final private function __construct(string $message = '', int $code = 0, Throwable $previous = null, array $body = [], array $errorCodes = []) { parent::__construct($message, $code, $previous); + $this->body = $body; $this->errorCodes = $errorCodes; } /** - * @param string $message - * @param int $code - * @param GuzzleBadResponseException $previous - * @param array> $errorCodes - * @return static + * @param string $message + * @param int $code + * @param GuzzleBadResponseException $previous + * @param array $body + * @return BadResponseException + * + * @internal + */ + public static function createFromGuzzle(string $message, int $code, GuzzleBadResponseException $previous, array $body = []): BadResponseException + { + $errorCodes = array_map(fn(array $item): ErrorCode => ErrorCode::createFromApi($item), $body['errorCodes'] ?? []); + if ($errorCodes === []) { + return new static($message, $code, $previous, $body); + } + + switch ($errorCodes[0]->getCode()) { + case PriceProtectionException::ERROR_CODE: + return new PriceProtectionException($message, $code, $previous, $body, $errorCodes); + default: + return new static($message, $code, $previous, $body, $errorCodes); + } + } + + /** + * @return array */ - public static function createFromGuzzle(string $message, int $code, GuzzleBadResponseException $previous, array $errorCodes = []): BadResponseException + public function getBody(): array { - return new static( - $message, - $code, - $previous, - array_map(fn(array $item): ErrorCode => ErrorCode::createFromApi($item), $errorCodes), - ); + return $this->body; } /** diff --git a/src/Exception/ExceptionFactory.php b/src/Exception/ExceptionFactory.php index f40ee8a..380f751 100644 --- a/src/Exception/ExceptionFactory.php +++ b/src/Exception/ExceptionFactory.php @@ -39,7 +39,7 @@ public static function fromGuzzleBadResponse(GuzzleBadResponseException $e): MpA return TooManyRequestsException::createFromGuzzle($message, $code, $e); default: /** @psalm-suppress PossiblyInvalidArgument - https://github.com/vimeo/psalm/issues/4295 */ - return BadResponseException::createFromGuzzle($message, $code, $e, $body['errorCodes'] ?? []); + return BadResponseException::createFromGuzzle($message, $code, $e, $body); } } diff --git a/src/Exception/PriceProtectionException.php b/src/Exception/PriceProtectionException.php new file mode 100644 index 0000000..52ef024 --- /dev/null +++ b/src/Exception/PriceProtectionException.php @@ -0,0 +1,24 @@ +getBody()['data']['data']['forceToken'])) { + throw new LogicException('forceToken not present in response'); + } + + return (string) $this->getBody()['data']['data']['forceToken']; + } + +} diff --git a/src/MpApiClient.php b/src/MpApiClient.php index 2eaa9da..07dfeb6 100644 --- a/src/MpApiClient.php +++ b/src/MpApiClient.php @@ -29,7 +29,7 @@ final class MpApiClient implements ClientInterface, MpApiClientInterface { const APP_NAME = 'mp-api-client'; - const APP_VERSION = '4.1.0'; + const APP_VERSION = '4.2.0'; private BrandClientInterface $brandClient; private CategoryClientInterface $categoryClient;