From 5ddbad1857d9d272cfabb88cc3f3d2dd5509b925 Mon Sep 17 00:00:00 2001 From: Alexis POUPELIN Date: Fri, 19 Jan 2024 15:20:01 +0100 Subject: [PATCH 1/6] Fixed service-points api url --- src/Client.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index 67874a7..c51a107 100644 --- a/src/Client.php +++ b/src/Client.php @@ -26,6 +26,7 @@ class Client { protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v2/'; + protected const SERVICE_POINTS_BASE_URL = 'https://servicepoints.sendcloud.sc/api/v2/'; protected \GuzzleHttp\Client $guzzleClient; @@ -614,7 +615,7 @@ public function searchServicePoints( } // Send request - $response = $this->guzzleClient->get('service-point', [ + $response = $this->guzzleClient->get('service-points', [ 'query' => $query, ]); @@ -644,7 +645,7 @@ public function getServicePoint(ServicePoint|int $servicePoint): ServicePoint $servicePointId = $servicePoint instanceof ServicePoint ? $servicePoint->getId() : $servicePoint; try { - $response = $this->guzzleClient->get('service-point/' . $servicePointId); + $response = $this->guzzleClient->get('service-points/' . $servicePointId); return ServicePoint::fromData(json_decode((string)$response->getBody(), true)); } catch (TransferException $exception) { throw $this->parseGuzzleException($exception, 'Could not retrieve service point.'); From bc4fa94ea5fcfe06102cd2e7747266c5887339e8 Mon Sep 17 00:00:00 2001 From: Alexis POUPELIN Date: Fri, 19 Jan 2024 15:26:10 +0100 Subject: [PATCH 2/6] Fixed country parameter not named correctly --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index c51a107..dadb080 100644 --- a/src/Client.php +++ b/src/Client.php @@ -566,7 +566,7 @@ public function searchServicePoints( try { // Construct query array $query = []; - $query['country_id'] = $country; + $query['country'] = $country; if (isset($address)) { $query['address'] = $address; From 391b740f9671616957ca7d0588eb1d11342e0e99 Mon Sep 17 00:00:00 2001 From: Alexis POUPELIN Date: Fri, 19 Jan 2024 15:27:19 +0100 Subject: [PATCH 3/6] Fixed distance parameter is not being treated as optional + test and docs --- src/Model/ServicePoint.php | 10 +++++----- test/ClientTest.php | 23 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Model/ServicePoint.php b/src/Model/ServicePoint.php index 7227c83..a8273e2 100644 --- a/src/Model/ServicePoint.php +++ b/src/Model/ServicePoint.php @@ -32,14 +32,14 @@ public static function fromData(array $data): self (array)$data['formatted_opening_times'], (bool)$data['open_tomorrow'], (bool)$data['open_upcoming_week'], - (int)$data['distance'] + isset($data['distance']) ? (int) $data['distance'] : null ); } /** * @param array $extraData Can contain carrier specific data * @param array $formattedOpeningTimes - * @param int $distance Distance between the reference point and the service point in meters. + * @param ?int $distance Distance in meters OR null if latitude and longitude are not provided in the request */ public function __construct( protected int $id, @@ -62,7 +62,7 @@ public function __construct( protected array $formattedOpeningTimes, protected bool $openTomorrow, protected bool $openUpcomingWeek, - protected int $distance, + protected ?int $distance, ) { } @@ -175,9 +175,9 @@ public function isOpenUpcomingWeek(): bool } /** - * Distance between the reference point and the service point in meters. + * @return ?int Distance in meters OR null if latitude and longitude are not provided in the request */ - public function getDistance(): int + public function getDistance(): ?int { return $this->distance; } diff --git a/test/ClientTest.php b/test/ClientTest.php index 6b2a34f..3e83ba9 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -485,18 +485,35 @@ public function testSearchServicePoint(): void 200, [], '[ - {"id":1,"code":"217165","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"Media Markt Eindhoven Centrum B.V.","street":"Boschdijktunnel","house_number":"1","postal_code":"5611AG","city":"EINDHOVEN","latitude":"51.441444","longitude":"5.475185","email":"","phone":"","homepage":"","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["10:00 - 20:00"],"1":["10:00 - 20:00"],"2":["10:00 - 20:00"],"3":["10:00 - 20:00"],"4":["10:00 - 20:00"],"5":["10:00 - 18:00"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true,"distance":381}, - {"id":2,"code":"217165","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"Media Markt Eindhoven Centrum B.V.","street":"Boschdijktunnel","house_number":"1","postal_code":"5611AG","city":"EINDHOVEN","latitude":"51.441444","longitude":"5.475185","email":"","phone":"","homepage":"","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["10:00 - 20:00"],"1":["10:00 - 20:00"],"2":["10:00 - 20:00"],"3":["10:00 - 20:00"],"4":["10:00 - 20:00"],"5":["10:00 - 18:00"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true,"distance":381} + {"id":1,"code":"217165","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"Media Markt Eindhoven Centrum B.V.","street":"Boschdijktunnel","house_number":"1","postal_code":"5611AG","city":"EINDHOVEN","latitude":"51.441444","longitude":"5.475185","email":"","phone":"","homepage":"","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["10:00 - 20:00"],"1":["10:00 - 20:00"],"2":["10:00 - 20:00"],"3":["10:00 - 20:00"],"4":["10:00 - 20:00"],"5":["10:00 - 18:00"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true}, + {"id":2,"code":"217165","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"Media Markt Eindhoven Centrum B.V.","street":"Boschdijktunnel","house_number":"1","postal_code":"5611AG","city":"EINDHOVEN","latitude":"51.441444","longitude":"5.475185","email":"","phone":"","homepage":"","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["10:00 - 20:00"],"1":["10:00 - 20:00"],"2":["10:00 - 20:00"],"3":["10:00 - 20:00"],"4":["10:00 - 20:00"],"5":["10:00 - 18:00"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true} ]' )); - $servicePoints = $this->client->searchServicePoints('NL'); + $servicePoints = $this->client->searchServicePoints(country: 'NL'); $this->assertCount(2, $servicePoints); $this->assertEquals(1, $servicePoints[0]->getID()); $this->assertEquals(2, $servicePoints[1]->getID()); } + public function testSearchServicePointWithDistance(): void + { + $this->guzzleClientMock->expects($this->once())->method('request')->willReturn(new Response( + 200, + [], + '[ + {"id":1,"code":"217165","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"Media Markt Eindhoven Centrum B.V.","street":"Boschdijktunnel","house_number":"1","postal_code":"5611AG","city":"EINDHOVEN","latitude":"51.441444","longitude":"5.475185","email":"","phone":"","homepage":"","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["10:00 - 20:00"],"1":["10:00 - 20:00"],"2":["10:00 - 20:00"],"3":["10:00 - 20:00"],"4":["10:00 - 20:00"],"5":["10:00 - 18:00"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true,"distance":381} + ]' + )); + + $servicePoints = $this->client->searchServicePoints(country: 'NL', latitude: 0, longitude: 0); + + $this->assertCount(1, $servicePoints); + $this->assertEquals(1, $servicePoints[0]->getID()); + $this->assertNotNull($servicePoints[0]->getDistance()); + } + public function testGetServicePoint(): void { $this->guzzleClientMock->expects($this->once())->method('request')->willReturn(new Response( From 6f2dd06ef226e3839c305b23800da1d1e899ad37 Mon Sep 17 00:00:00 2001 From: Alexis POUPELIN Date: Fri, 19 Jan 2024 15:41:48 +0100 Subject: [PATCH 4/6] Updated README with service points usage --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index eeca600..3ef87d1 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,32 @@ if ($webhookEvent->getType() === WebhookEvent::TYPE_PARCEL_STATUS_CHANGED) { } ``` +### Retieve a list of service points + +```php +use JouwWeb\Sendcloud\Client; +use JouwWeb\Sendcloud\Exception\SendcloudRequestException; + +// Sendcloud uses another api url for service points +// if you use the default one (i.e https://panel.sendcloud.sc/api/v2/), the API will return a 404 error +$client = new Client('your_public_key', 'your_secret_key', null, Client::SERVICE_POINTS_BASE_URL); + +try { + // Search for service points in Netherlands + $service_points = $client->searchServicePoints('NL'); + + // If we want sendcloud to calculate the distance between us and each service points, we need to give the latitude and longitude + $service_points_with_distance = $client->searchServicePoints(country: 'NL', latitude: 51.4350511, longitude: 5.4746339); + + // $service_points[0]->getDistance() == null + // $service_points_with_distance[1]->getDistance() != null + + // Search for a specific service point using his sendcloud ID + $service_point = $client->getServicePoint(1); +} catch (SendcloudRequestException $exception) { + echo $exception->getMessage(); +} +``` + ## Installation `composer require jouwweb/sendcloud` From f670262dcecda05e09a99513d76f5bb5cc1b7705 Mon Sep 17 00:00:00 2001 From: Alexis POUPELIN Date: Fri, 19 Jan 2024 15:48:31 +0100 Subject: [PATCH 5/6] Made service points base url constant public --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index dadb080..e8efcfc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -26,7 +26,7 @@ class Client { protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v2/'; - protected const SERVICE_POINTS_BASE_URL = 'https://servicepoints.sendcloud.sc/api/v2/'; + public const SERVICE_POINTS_BASE_URL = 'https://servicepoints.sendcloud.sc/api/v2/'; protected \GuzzleHttp\Client $guzzleClient; From 1d2c3099f3edeba87cbaf0cd0e6129140e401504 Mon Sep 17 00:00:00 2001 From: Villermen Date: Wed, 7 Feb 2024 22:47:17 +0100 Subject: [PATCH 6/6] Split service point logic into separate client Closes #34. --- README.md | 34 ++++--- src/Client.php | 193 +++--------------------------------- src/ServicePointsClient.php | 171 ++++++++++++++++++++++++++++++++ src/Utility.php | 41 ++++++++ test/ClientTest.php | 22 ++-- 5 files changed, 260 insertions(+), 201 deletions(-) create mode 100644 src/ServicePointsClient.php diff --git a/README.md b/README.md index 3ef87d1..3cf5bbe 100644 --- a/README.md +++ b/README.md @@ -76,25 +76,33 @@ if ($webhookEvent->getType() === WebhookEvent::TYPE_PARCEL_STATUS_CHANGED) { ### Retieve a list of service points ```php -use JouwWeb\Sendcloud\Client; +use JouwWeb\Sendcloud\ServicePointsClient; use JouwWeb\Sendcloud\Exception\SendcloudRequestException; -// Sendcloud uses another api url for service points -// if you use the default one (i.e https://panel.sendcloud.sc/api/v2/), the API will return a 404 error -$client = new Client('your_public_key', 'your_secret_key', null, Client::SERVICE_POINTS_BASE_URL); +$client = new ServicePointsClient('your_public_key', 'your_secret_key'); try { - // Search for service points in Netherlands - $service_points = $client->searchServicePoints('NL'); - - // If we want sendcloud to calculate the distance between us and each service points, we need to give the latitude and longitude - $service_points_with_distance = $client->searchServicePoints(country: 'NL', latitude: 51.4350511, longitude: 5.4746339); + // Search for service points in the Netherlands. + $servicePoints = $client->searchServicePoints('NL'); + + var_dump($servicePoints[0]->isActive()); // bool(true) + var_dump($servicePoints[0]->getName()); // string(7) "Primera" + var_dump($servicePoints[0]->getCarrier()); // string(6) "postnl" + var_dump($servicePoints[0]->getDistance()); // NULL ↓ + + // If we want Sendcloud to calculate the distance between us and each service point, we need to supply latitude and + // longitude. + $servicePointsWithDistance = $client->searchServicePoints( + country: 'NL', + latitude: 51.4350511, + longitude: 5.4746339 + ); - // $service_points[0]->getDistance() == null - // $service_points_with_distance[1]->getDistance() != null + var_dump($servicePointsWithDistance[0]->getName()); // string(14) "Pakketautomaat" + var_dump($servicePointsWithDistance[0]->getDistance()); // int(553) - // Search for a specific service point using his sendcloud ID - $service_point = $client->getServicePoint(1); + // Obtain a specific service point by ID. + $servicePoint = $client->getServicePoint(1); } catch (SendcloudRequestException $exception) { echo $exception->getMessage(); } diff --git a/src/Client.php b/src/Client.php index e8efcfc..8c45798 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,7 +2,6 @@ namespace JouwWeb\Sendcloud; -use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Utils; @@ -17,7 +16,6 @@ use JouwWeb\Sendcloud\Model\ShippingMethod; use JouwWeb\Sendcloud\Model\User; use JouwWeb\Sendcloud\Model\WebhookEvent; -use JouwWeb\Sendcloud\Model\ServicePoint; use Psr\Http\Message\RequestInterface; /** @@ -26,7 +24,6 @@ class Client { protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v2/'; - public const SERVICE_POINTS_BASE_URL = 'https://servicepoints.sendcloud.sc/api/v2/'; protected \GuzzleHttp\Client $guzzleClient; @@ -67,7 +64,7 @@ public function getUser(): User try { return User::fromData(json_decode((string)$this->guzzleClient->get('user')->getBody(), true)['user']); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'An error occurred while fetching the Sendcloud user.'); + throw Utility::parseGuzzleException($exception, 'An error occurred while fetching the Sendcloud user.'); } } @@ -131,7 +128,7 @@ public function getShippingMethods( return $shippingMethods; } catch (TransferException $exception) { - throw $this->parseGuzzleException( + throw Utility::parseGuzzleException( $exception, 'An error occurred while fetching shipping methods from the Sendcloud API.' ); @@ -197,7 +194,7 @@ public function createParcel( return Parcel::fromData(json_decode((string)$response->getBody(), true)['parcel']); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not create parcel in Sendcloud.'); + throw Utility::parseGuzzleException($exception, 'Could not create parcel in Sendcloud.'); } } @@ -277,7 +274,7 @@ public function createMultiParcel( return $parcels; } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not create parcel in Sendcloud.'); + throw Utility::parseGuzzleException($exception, 'Could not create parcel in Sendcloud.'); } } @@ -312,7 +309,7 @@ public function updateParcel(Parcel|int $parcel, Address $shippingAddress): Parc return Parcel::fromData(json_decode((string)$response->getBody(), true)['parcel']); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not update parcel in Sendcloud.'); + throw Utility::parseGuzzleException($exception, 'Could not update parcel in Sendcloud.'); } } @@ -348,7 +345,7 @@ public function createLabel(Parcel|int $parcel, ShippingMethod|int $shippingMeth return Parcel::fromData(json_decode((string)$response->getBody(), true)['parcel']); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not create parcel with Sendcloud.'); + throw Utility::parseGuzzleException($exception, 'Could not create parcel with Sendcloud.'); } } @@ -373,7 +370,7 @@ public function cancelParcel(Parcel|int $parcel): bool return false; } - throw $this->parseGuzzleException($exception, 'An error occurred while cancelling the parcel.'); + throw Utility::parseGuzzleException($exception, 'An error occurred while cancelling the parcel.'); } } @@ -405,7 +402,7 @@ public function getLabelPdf(Parcel|int $parcel, int $format): string try { return (string)$this->guzzleClient->get($labelUrl)->getBody(); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve label.'); + throw Utility::parseGuzzleException($exception, 'Could not retrieve label.'); } } @@ -440,7 +437,7 @@ public function getBulkLabelPdf(array $parcels, int $format): string ], ]); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve label information.'); + throw Utility::parseGuzzleException($exception, 'Could not retrieve label information.'); } $labelData = json_decode((string)$response->getBody(), true); @@ -452,7 +449,7 @@ public function getBulkLabelPdf(array $parcels, int $format): string try { return (string)$this->guzzleClient->get($labelUrl)->getBody(); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve label PDF data.'); + throw Utility::parseGuzzleException($exception, 'Could not retrieve label PDF data.'); } } @@ -472,7 +469,7 @@ public function getSenderAddresses(): array return SenderAddress::fromData($senderAddressData); }, $senderAddressesData); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve sender addresses.'); + throw Utility::parseGuzzleException($exception, 'Could not retrieve sender addresses.'); } } @@ -487,7 +484,7 @@ public function getParcel(Parcel|int $parcel): Parcel $response = $this->guzzleClient->get('parcels/' . $this->parseParcelArgument($parcel)); return Parcel::fromData(json_decode((string)$response->getBody(), true)['parcel']); } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve parcel.'); + throw Utility::parseGuzzleException($exception, 'Could not retrieve parcel.'); } } @@ -523,135 +520,6 @@ public function getReturnPortalUrl(Parcel|int $parcel): ?string } } - /** - * Summary of searchServicePoints - * - * @see https://api.sendcloud.dev/docs/sendcloud-public-api/service-points%2Foperations%2Flist-service-points - * @param string $country A country ISO 2 code (Example : 'NL') - * @param string|null $address Address of the destination address. Can accept postal code instead of the street and the house number. (Example : 'Stadhuisplein 10') - * @param string|null $carrier A comma-separated list of carrier codes (stringified) (Example : 'postnl,dpd') - * @param string|null $city City of the destination address. (Example : 'Eindhoven') - * @param string|null $houseNumber House number of the destination address. (Example : '10') - * @param string|null $latitude Used as a reference point to calculate the distance of the service point to the provided location. - * @param string|null $longitude Used as a reference point to calculate the distance of the service point to the provided location. - * @param string|null $neLatitude Latitude of the northeast corner of the bounding box. - * @param string|null $neLongitude Longitude of the northeast corner of the bounding box. - * @param string|null $postalCode Postal code of the destination address. Using postal_code will return you service points located around that particular postal code. (Example : '5611 EM') - * @param string|null $pudoId DPD-specific query parameter. (<= 7 characters) - * @param int|null $radius Radius (in meter) of a bounding circle. Can be used instead of the ne_latitude, ne_longitude, sw_latitude, and sw_longitude parameters to define a bounding box. By default, it’s 100 meters. Minimum value: 100 meters. Maximum value: 50 000 meters. - * @param string|null $shopType Filters results by their shop type. - * @param string|null $swLatitude Latitude of the southwest corner of the bounding box. - * @param string|null $swLongitude Longitude of the southwest corner of the bounding box. - * @param float|null $weight Weight (in kg.) of the parcel to be shipped to the service points. Certain carriers impose limits for certain service points that cannot accept parcels above a certain weight limit. - * @return ServicePoint[] - */ - public function searchServicePoints( - string $country, - ?string $address = null, - ?string $carrier = null, - ?string $city = null, - ?string $houseNumber = null, - ?string $latitude = null, - ?string $longitude = null, - ?string $neLatitude = null, - ?string $neLongitude = null, - ?string $postalCode = null, - ?string $pudoId = null, - ?int $radius = null, - ?string $shopType = null, - ?string $swLatitude = null, - ?string $swLongitude = null, - ?float $weight = null - ): array { - try { - // Construct query array - $query = []; - $query['country'] = $country; - - if (isset($address)) { - $query['address'] = $address; - } - if (isset($carrier)) { - $query['carrier'] = $carrier; - } - if (isset($city)) { - $query['city'] = $city; - } - if (isset($houseNumber)) { - $query['house_number'] = $houseNumber; - } - if (isset($latitude)) { - $query['latitude'] = $latitude; - } - if (isset($longitude)) { - $query['longitude'] = $longitude; - } - if (isset($neLatitude)) { - $query['ne_latitude'] = $neLatitude; - } - if (isset($neLongitude)) { - $query['ne_longitude'] = $neLongitude; - } - if (isset($postalCode)) { - $query['postal_code'] = $postalCode; - } - if (isset($pudoId)) { - $query['pudo_id'] = $pudoId; - } - if (isset($radius)) { - $query['radius'] = $radius; - } - if (isset($shopType)) { - $query['shop_type'] = $shopType; - } - if (isset($swLatitude)) { - $query['sw_latitude'] = $swLatitude; - } - if (isset($swLongitude)) { - $query['sw_longitude'] = $swLongitude; - } - if (isset($weight)) { - $query['weight'] = $weight; - } - - // Send request - $response = $this->guzzleClient->get('service-points', [ - 'query' => $query, - ]); - - // Decode and create ServicePoint objects - $json = json_decode((string)$response->getBody(), true); - - $servicePoints = []; - foreach ($json as $obj) { - $servicePoints[] = ServicePoint::fromData($obj); - } - - return $servicePoints; - } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve service point.'); - } - } - - /** - * Returns service point by ID. - * - * @see https://api.sendcloud.dev/docs/sendcloud-public-api/service-points%2Foperations%2Fget-a-service-point - * @return ServicePoint - * @throws SendcloudRequestException - */ - public function getServicePoint(ServicePoint|int $servicePoint): ServicePoint - { - $servicePointId = $servicePoint instanceof ServicePoint ? $servicePoint->getId() : $servicePoint; - - try { - $response = $this->guzzleClient->get('service-points/' . $servicePointId); - return ServicePoint::fromData(json_decode((string)$response->getBody(), true)); - } catch (TransferException $exception) { - throw $this->parseGuzzleException($exception, 'Could not retrieve service point.'); - } - } - /** * Returns the given arguments as data in Sendcloud parcel format. * @@ -813,43 +681,6 @@ protected function getParcelData( return $parcelData; } - protected function parseGuzzleException( - TransferException $exception, - string $defaultMessage - ): SendcloudRequestException { - $message = $defaultMessage; - $code = SendcloudRequestException::CODE_UNKNOWN; - - $responseCode = null; - $responseMessage = null; - if ($exception instanceof RequestException && $exception->hasResponse()) { - $responseData = json_decode((string)$exception->getResponse()->getBody(), true); - $responseCode = $responseData['error']['code'] ?? null; - $responseMessage = $responseData['error']['message'] ?? null; - } - - if ($exception instanceof ConnectException) { - $message = 'Could not contact Sendcloud API.'; - $code = SendcloudRequestException::CODE_CONNECTION_FAILED; - } - - // Precondition failed, parse response message to determine code of exception - if ($exception->getCode() === 401) { - $message = 'Invalid public/secret key combination.'; - $code = SendcloudRequestException::CODE_UNAUTHORIZED; - } elseif ($exception->getCode() === 412) { - $message = 'Sendcloud account is not fully configured yet.'; - - if (stripos($responseMessage, 'no address data') !== false) { - $code = SendcloudRequestException::CODE_NO_ADDRESS_DATA; - } elseif (stripos($responseMessage, 'not allowed to announce') !== false) { - $code = SendcloudRequestException::CODE_NOT_ALLOWED_TO_ANNOUNCE; - } - } - - return new SendcloudRequestException($message, $code, $exception, $responseCode, $responseMessage); - } - // TODO: Remove parseParcelArgument() now we use native unions. protected function parseParcelArgument(Parcel|int $parcel): int { diff --git a/src/ServicePointsClient.php b/src/ServicePointsClient.php new file mode 100644 index 0000000..8d78d3d --- /dev/null +++ b/src/ServicePointsClient.php @@ -0,0 +1,171 @@ + $apiBaseUrl ?: self::API_BASE_URL, + 'timeout' => 10, + 'auth' => [ + $publicKey, + $secretKey, + ], + 'headers' => [ + 'User-Agent' => 'jouwweb/sendcloud ' . Utils::defaultUserAgent(), + ], + ]; + + if ($this->partnerId) { + $clientConfig['headers']['Sendcloud-Partner-Id'] = $this->partnerId; + } + + $this->guzzleClient = new \GuzzleHttp\Client($clientConfig); + } + + /** + * Summary of searchServicePoints + * + * @param string $country A country ISO 2 code (Example : 'NL') + * @param string|null $address Address of the destination address. Can accept postal code instead of the street and the house number. (Example : 'Stadhuisplein 10') + * @param string|null $carrier A comma-separated list of carrier codes (stringified) (Example : 'postnl,dpd') + * @param string|null $city City of the destination address. (Example : 'Eindhoven') + * @param string|null $houseNumber House number of the destination address. (Example : '10') + * @param string|null $latitude Used as a reference point to calculate the distance of the service point to the provided location. + * @param string|null $longitude Used as a reference point to calculate the distance of the service point to the provided location. + * @param string|null $neLatitude Latitude of the northeast corner of the bounding box. + * @param string|null $neLongitude Longitude of the northeast corner of the bounding box. + * @param string|null $postalCode Postal code of the destination address. Using postal_code will return you service points located around that particular postal code. (Example : '5611 EM') + * @param string|null $pudoId DPD-specific query parameter. (<= 7 characters) + * @param int|null $radius Radius (in meter) of a bounding circle. Can be used instead of the ne_latitude, ne_longitude, sw_latitude, and sw_longitude parameters to define a bounding box. By default, it’s 100 meters. Minimum value: 100 meters. Maximum value: 50 000 meters. + * @param string|null $shopType Filters results by their shop type. + * @param string|null $swLatitude Latitude of the southwest corner of the bounding box. + * @param string|null $swLongitude Longitude of the southwest corner of the bounding box. + * @param float|null $weight Weight (in kg.) of the parcel to be shipped to the service points. Certain carriers impose limits for certain service points that cannot accept parcels above a certain weight limit. + * @return ServicePoint[] + * @see https://api.sendcloud.dev/docs/sendcloud-public-api/service-points%2Foperations%2Flist-service-points + */ + public function searchServicePoints( + string $country, + ?string $address = null, + ?string $carrier = null, + ?string $city = null, + ?string $houseNumber = null, + ?string $latitude = null, + ?string $longitude = null, + ?string $neLatitude = null, + ?string $neLongitude = null, + ?string $postalCode = null, + ?string $pudoId = null, + ?int $radius = null, + ?string $shopType = null, + ?string $swLatitude = null, + ?string $swLongitude = null, + ?float $weight = null + ): array { + try { + // Construct query array + $query = []; + $query['country'] = $country; + + if (isset($address)) { + $query['address'] = $address; + } + if (isset($carrier)) { + $query['carrier'] = $carrier; + } + if (isset($city)) { + $query['city'] = $city; + } + if (isset($houseNumber)) { + $query['house_number'] = $houseNumber; + } + if (isset($latitude)) { + $query['latitude'] = $latitude; + } + if (isset($longitude)) { + $query['longitude'] = $longitude; + } + if (isset($neLatitude)) { + $query['ne_latitude'] = $neLatitude; + } + if (isset($neLongitude)) { + $query['ne_longitude'] = $neLongitude; + } + if (isset($postalCode)) { + $query['postal_code'] = $postalCode; + } + if (isset($pudoId)) { + $query['pudo_id'] = $pudoId; + } + if (isset($radius)) { + $query['radius'] = $radius; + } + if (isset($shopType)) { + $query['shop_type'] = $shopType; + } + if (isset($swLatitude)) { + $query['sw_latitude'] = $swLatitude; + } + if (isset($swLongitude)) { + $query['sw_longitude'] = $swLongitude; + } + if (isset($weight)) { + $query['weight'] = $weight; + } + + // Send request + $response = $this->guzzleClient->get('service-points', [ + 'query' => $query, + ]); + + // Decode and create ServicePoint objects + $json = json_decode((string)$response->getBody(), true); + + $servicePoints = []; + foreach ($json as $obj) { + $servicePoints[] = ServicePoint::fromData($obj); + } + + return $servicePoints; + } catch (TransferException $exception) { + throw Utility::parseGuzzleException($exception, 'Could not retrieve service point.'); + } + } + + /** + * Returns service point by ID. + * + * @throws SendcloudRequestException + * @see https://api.sendcloud.dev/docs/sendcloud-public-api/service-points%2Foperations%2Fget-a-service-point + */ + public function getServicePoint(ServicePoint|int $servicePoint): ServicePoint + { + $servicePointId = $servicePoint instanceof ServicePoint ? $servicePoint->getId() : $servicePoint; + + try { + $response = $this->guzzleClient->get('service-points/' . $servicePointId); + return ServicePoint::fromData(json_decode((string)$response->getBody(), true)); + } catch (TransferException $exception) { + throw Utility::parseGuzzleException($exception, 'Could not retrieve service point.'); + } + } +} diff --git a/src/Utility.php b/src/Utility.php index 33d0ecf..1ce93b7 100644 --- a/src/Utility.php +++ b/src/Utility.php @@ -2,6 +2,10 @@ namespace JouwWeb\Sendcloud; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; +use JouwWeb\Sendcloud\Exception\SendcloudRequestException; use JouwWeb\Sendcloud\Exception\SendcloudWebhookException; use JouwWeb\Sendcloud\Model\Parcel; use JouwWeb\Sendcloud\Model\WebhookEvent; @@ -80,4 +84,41 @@ public static function getLabelUrlFromData(array $data, int $format): ?string return ($labelUrl ? (string)$labelUrl : null); } + + public static function parseGuzzleException( + TransferException $exception, + string $defaultMessage + ): SendcloudRequestException { + $message = $defaultMessage; + $code = SendcloudRequestException::CODE_UNKNOWN; + + $responseCode = null; + $responseMessage = null; + if ($exception instanceof RequestException && $exception->hasResponse()) { + $responseData = json_decode((string)$exception->getResponse()->getBody(), true); + $responseCode = $responseData['error']['code'] ?? null; + $responseMessage = $responseData['error']['message'] ?? null; + } + + if ($exception instanceof ConnectException) { + $message = 'Could not contact Sendcloud API.'; + $code = SendcloudRequestException::CODE_CONNECTION_FAILED; + } + + // Precondition failed, parse response message to determine code of exception + if ($exception->getCode() === 401) { + $message = 'Invalid public/secret key combination.'; + $code = SendcloudRequestException::CODE_UNAUTHORIZED; + } elseif ($exception->getCode() === 412) { + $message = 'Sendcloud account is not fully configured yet.'; + + if (stripos($responseMessage, 'no address data') !== false) { + $code = SendcloudRequestException::CODE_NO_ADDRESS_DATA; + } elseif (stripos($responseMessage, 'not allowed to announce') !== false) { + $code = SendcloudRequestException::CODE_NOT_ALLOWED_TO_ANNOUNCE; + } + } + + return new SendcloudRequestException($message, $code, $exception, $responseCode, $responseMessage); + } } diff --git a/test/ClientTest.php b/test/ClientTest.php index 3e83ba9..0dcc285 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -12,6 +12,7 @@ use JouwWeb\Sendcloud\Model\Parcel; use JouwWeb\Sendcloud\Model\ParcelItem; use JouwWeb\Sendcloud\Model\ShippingMethod; +use JouwWeb\Sendcloud\ServicePointsClient; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -19,12 +20,15 @@ class ClientTest extends TestCase { protected Client $client; + protected ServicePointsClient $servicePointsClient; + /** @var \GuzzleHttp\Client&MockObject */ protected \GuzzleHttp\Client $guzzleClientMock; public function setUp(): void { $this->client = new Client('handsome public key', 'gorgeous secret key', 'aPartnerId'); + $this->servicePointsClient = new ServicePointsClient('handsome public key', 'gorgeous secret key', 'aPartnerId'); $this->guzzleClientMock = $this->createPartialMock(\GuzzleHttp\Client::class, ['request']); @@ -33,6 +37,10 @@ public function setUp(): void $clientProperty = new \ReflectionProperty(Client::class, 'guzzleClient'); $clientProperty->setAccessible(true); $clientProperty->setValue($this->client, $this->guzzleClientMock); + + $clientProperty = new \ReflectionProperty(ServicePointsClient::class, 'guzzleClient'); + $clientProperty->setAccessible(true); + $clientProperty->setValue($this->servicePointsClient, $this->guzzleClientMock); } public function testGetUser(): void @@ -479,7 +487,7 @@ function ($method, $url, $data) use (&$requestNumber) { $this->assertEquals('pdfdata', $pdf); } - public function testSearchServicePoint(): void + public function testSearchServicePoints(): void { $this->guzzleClientMock->expects($this->once())->method('request')->willReturn(new Response( 200, @@ -490,11 +498,11 @@ public function testSearchServicePoint(): void ]' )); - $servicePoints = $this->client->searchServicePoints(country: 'NL'); + $servicePoints = $this->servicePointsClient->searchServicePoints(country: 'NL'); $this->assertCount(2, $servicePoints); - $this->assertEquals(1, $servicePoints[0]->getID()); - $this->assertEquals(2, $servicePoints[1]->getID()); + $this->assertEquals(1, $servicePoints[0]->getId()); + $this->assertEquals(2, $servicePoints[1]->getId()); } public function testSearchServicePointWithDistance(): void @@ -507,10 +515,10 @@ public function testSearchServicePointWithDistance(): void ]' )); - $servicePoints = $this->client->searchServicePoints(country: 'NL', latitude: 0, longitude: 0); + $servicePoints = $this->servicePointsClient->searchServicePoints(country: 'NL', latitude: 0, longitude: 0); $this->assertCount(1, $servicePoints); - $this->assertEquals(1, $servicePoints[0]->getID()); + $this->assertEquals(1, $servicePoints[0]->getId()); $this->assertNotNull($servicePoints[0]->getDistance()); } @@ -553,7 +561,7 @@ public function testGetServicePoint(): void '6' => [], ]; - $servicePoint = $this->client->getServicePoint(26); + $servicePoint = $this->servicePointsClient->getServicePoint(26); $this->assertEquals(26, $servicePoint->getId()); $this->assertEquals('4c8181feec8f49fdbe67d9c9f6aaaf6f', $servicePoint->getCode());