diff --git a/src/Client.php b/src/Client.php index bdefc64..67874a7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -17,6 +17,7 @@ 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; /** @@ -226,7 +227,7 @@ public function createMultiParcel( ?ShippingMethod $shippingMethod = null, ?string $errors = null, int $quantity = 1 - ) : array { + ): array { $parcelData = $this->getParcelData( null, $shippingAddress, @@ -521,6 +522,135 @@ 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_id'] = $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-point', [ + '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-point/' . $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. * @@ -719,6 +849,7 @@ protected function parseGuzzleException( return new SendcloudRequestException($message, $code, $exception, $responseCode, $responseMessage); } + // TODO: Remove parseParcelArgument() now we use native unions. protected function parseParcelArgument(Parcel|int $parcel): int { if (is_int($parcel)) { diff --git a/src/Model/ServicePoint.php b/src/Model/ServicePoint.php new file mode 100644 index 0000000..7227c83 --- /dev/null +++ b/src/Model/ServicePoint.php @@ -0,0 +1,184 @@ + $extraData Can contain carrier specific data + * @param array $formattedOpeningTimes + * @param int $distance Distance between the reference point and the service point in meters. + */ + public function __construct( + protected int $id, + protected string $code, + protected bool $isActive, + protected ?string $shopType, + protected array $extraData, + protected string $name, + protected string $street, + protected string $houseNumber, + protected string $postalCode, + protected string $city, + protected string $latitude, + protected string $longitude, + protected string $email, + protected string $phone, + protected string $homepage, + protected string $carrier, + protected string $country, + protected array $formattedOpeningTimes, + protected bool $openTomorrow, + protected bool $openUpcomingWeek, + protected int $distance, + ) { + } + + public function getId(): int + { + return $this->id; + } + + public function getCode(): string + { + return $this->code; + } + + public function isActive(): bool + { + return $this->isActive; + } + + public function getShopType(): ?string + { + return $this->shopType; + } + + /** + * Can contain carrier specific data. + * + * @return array + */ + public function getExtraData(): array + { + return $this->extraData; + } + + public function getName(): string + { + return $this->name; + } + + public function getStreet(): string + { + return $this->street; + } + + public function getHouseNumber(): string + { + return $this->houseNumber; + } + + public function getPostalCode(): string + { + return $this->postalCode; + } + + public function getCity(): string + { + return $this->city; + } + + public function getLatitude(): string + { + return $this->latitude; + } + + public function getLongitude(): string + { + return $this->longitude; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getPhone(): string + { + return $this->phone; + } + + public function getHomepage(): string + { + return $this->homepage; + } + + public function getCarrier(): string + { + return $this->carrier; + } + + public function getCountry(): string + { + return $this->country; + } + + /** + * @return array + */ + public function getFormattedOpeningTimes(): array + { + return $this->formattedOpeningTimes; + } + + public function isOpenTomorrow(): bool + { + return $this->openTomorrow; + } + + public function isOpenUpcomingWeek(): bool + { + return $this->openUpcomingWeek; + } + + /** + * Distance between the reference point and the service point in meters. + */ + public function getDistance(): int + { + return $this->distance; + } +} diff --git a/test/ClientTest.php b/test/ClientTest.php index 23954aa..6b2a34f 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -217,7 +217,7 @@ public function testCreateMultiParcel(): void $this->assertCount(2, $parcels); - foreach($parcels as $key => $parcel) { + foreach ($parcels as $key => $parcel) { $id = $key == 0 ? 8293794 : 8293795; $this->assertEquals($id, $parcel->getId()); @@ -478,4 +478,86 @@ function ($method, $url, $data) use (&$requestNumber) { $pdf = $this->client->getBulkLabelPdf([1234, $parcelMock], Parcel::LABEL_FORMAT_A4_TOP_LEFT); $this->assertEquals('pdfdata', $pdf); } + + public function testSearchServicePoint(): 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}, + {"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} + ]' + )); + + $servicePoints = $this->client->searchServicePoints('NL'); + + $this->assertCount(2, $servicePoints); + $this->assertEquals(1, $servicePoints[0]->getID()); + $this->assertEquals(2, $servicePoints[1]->getID()); + } + + public function testGetServicePoint(): void + { + $this->guzzleClientMock->expects($this->once())->method('request')->willReturn(new Response( + 200, + [], + '{"id":26,"code":"4c8181feec8f49fdbe67d9c9f6aaaf6f","is_active":true,"shop_type":null,"extra_data":{"partner_name":"PostNL","sales_channel":"AFHAALPUNT","terminal_type":"NRS","retail_network_id":"PNPNL-01"},"name":"DUMMY-3f1d6384391f45ce","street":"Sesamstraat","house_number":"40","postal_code":"5699YE","city":"Eindhoven","latitude":"51.440400","longitude":"5.475800","email":"devnull@sendcloud.nl","phone":"+31401234567","homepage":"https://www.sendcloud.nl","carrier":"postnl","country":"NL","formatted_opening_times":{"0":["13:30 - 17:15"],"1":["09:00 - 12:00","13:30 - 17:15"],"2":["09:00 - 12:00","13:30 - 17:15"],"3":[],"4":["09:00 - 12:00","13:30 - 17:15"],"5":["09:00 - 12:00","13:30 - 17:15"],"6":[]},"open_tomorrow":true,"open_upcoming_week":true,"distance":361}' + )); + + $extraData = [ + 'partner_name' => 'PostNL', + 'sales_channel' => 'AFHAALPUNT', + 'terminal_type' => 'NRS', + 'retail_network_id' => 'PNPNL-01', + ]; + + $formattedOpeningTimes = [ + '0' => [ + '13:30 - 17:15', + ], + '1' => [ + '09:00 - 12:00', + '13:30 - 17:15', + ], + '2' => [ + '09:00 - 12:00', + '13:30 - 17:15', + ], + '3' => [], + '4' => [ + '09:00 - 12:00', + '13:30 - 17:15', + ], + '5' => [ + '09:00 - 12:00', + '13:30 - 17:15', + ], + '6' => [], + ]; + + $servicePoint = $this->client->getServicePoint(26); + + $this->assertEquals(26, $servicePoint->getId()); + $this->assertEquals('4c8181feec8f49fdbe67d9c9f6aaaf6f', $servicePoint->getCode()); + $this->assertTrue($servicePoint->isActive()); + $this->assertNull($servicePoint->getShopType()); + $this->assertEquals($extraData, $servicePoint->getExtraData()); + $this->assertEquals('DUMMY-3f1d6384391f45ce', $servicePoint->getName()); + $this->assertEquals('Sesamstraat', $servicePoint->getStreet()); + $this->assertEquals('40', $servicePoint->getHouseNumber()); + $this->assertEquals('5699YE', $servicePoint->getPostalCode()); + $this->assertEquals('Eindhoven', $servicePoint->getCity()); + $this->assertEquals('51.440400', $servicePoint->getLatitude()); + $this->assertEquals('5.475800', $servicePoint->getLongitude()); + $this->assertEquals('devnull@sendcloud.nl', $servicePoint->getEmail()); + $this->assertEquals('+31401234567', $servicePoint->getPhone()); + $this->assertEquals('https://www.sendcloud.nl', $servicePoint->getHomepage()); + $this->assertEquals('postnl', $servicePoint->getCarrier()); + $this->assertEquals('NL', $servicePoint->getCountry()); + $this->assertEquals($formattedOpeningTimes, $servicePoint->getFormattedOpeningTimes()); + $this->assertTrue($servicePoint->isOpenTomorrow()); + $this->assertTrue($servicePoint->isOpenUpcomingWeek()); + $this->assertEquals(361, $servicePoint->getDistance()); + } }