diff --git a/composer.json b/composer.json
index 262880a..463458c 100644
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
"require": {
"php": ">=7.4",
"ext-mbstring": "*",
+ "ext-simplexml": "*",
"behat/transliterator": "^1.3",
"doctrine/event-manager": "^1.1",
"doctrine/orm": "^2.7",
diff --git a/src/Client/Bpost/Client.php b/src/Client/Bpost/Client.php
new file mode 100644
index 0000000..1d1aed8
--- /dev/null
+++ b/src/Client/Bpost/Client.php
@@ -0,0 +1,92 @@
+httpClient = $httpClient;
+ $this->requestFactory = $requestFactory;
+ $this->streamFactory = $streamFactory;
+ $this->baseUrl = $baseUrl;
+ }
+
+ /**
+ * @throws ClientExceptionInterface
+ * @throws JsonException
+ */
+ private function get(string $endpoint, array $params = []): array
+ {
+ return $this->sendRequest('GET', $endpoint, $params);
+ }
+
+ /**
+ * @throws ClientExceptionInterface|JsonException
+ */
+ private function sendRequest(string $method, string $endpoint, array $params = [], array $body = []): array
+ {
+ $url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
+
+ $request = $this->requestFactory->createRequest($method, $url);
+
+ if (count($body) > 0) {
+ $request = $request->withBody($this->streamFactory->createStream(json_encode($body)));
+ }
+
+ $response = $this->httpClient->sendRequest($request);
+
+
+ if (200 !== $response->getStatusCode()) {
+ throw new RequestFailedException($request, $response, $response->getStatusCode());
+ }
+
+ $xml = simplexml_load_string($response->getBody()->getContents());
+
+ $data = [];
+ $poiList = $xml->PoiList->Poi ?? $xml->PickupPointList->Point ?? $xml->Poi;
+
+ if ($poiList !== null) {
+ foreach ($poiList as $poi) {
+ $poiData = json_decode(json_encode($poi), true);
+ $data[] = $poiData['Record'] ?? $poiData;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @throws ClientExceptionInterface|JsonException
+ */
+ public function locate(ServicePointQueryInterface $servicePointQuery): iterable
+ {
+ return $this->get($servicePointQuery->getEndPoint(), $servicePointQuery->toArray());
+ }
+}
diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php
new file mode 100644
index 0000000..517de91
--- /dev/null
+++ b/src/Client/ClientInterface.php
@@ -0,0 +1,12 @@
+httpClient = $httpClient;
+ $this->requestFactory = $requestFactory;
+ $this->streamFactory = $streamFactory;
+ $this->baseUrl = $baseUrl;
+ $this->apiKey = $apiKey;
+ }
+
+ /**
+ * @throws ClientExceptionInterface
+ * @throws JsonException
+ */
+ private function get(string $endpoint, array $params = []): array
+ {
+ return $this->sendRequest('GET', $endpoint, $params);
+ }
+
+ /**
+ * @throws ClientExceptionInterface|JsonException
+ */
+ private function sendRequest(string $method, string $endpoint, array $params = [], array $body = []): array
+ {
+ $url = $this->baseUrl . '/' . ltrim($endpoint, '/') . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
+
+ $request = $this->requestFactory->createRequest($method, $url);
+
+ if (count($body) > 0) {
+ $request = $request->withBody($this->streamFactory->createStream(json_encode($body)));
+ }
+
+ $request = $request->withHeader('apikey', $this->apiKey);
+ $request = $request->withHeader('accept', 'application/json');
+
+ $response = $this->httpClient->sendRequest($request);
+
+ if (200 !== $response->getStatusCode()) {
+ throw new RequestFailedException($request, $response, $response->getStatusCode());
+ }
+
+ $data = json_decode($response->getBody()->getContents(), true);
+
+ if (!isset($data['GetLocationsResult']['ResponseLocation']))
+ {
+ throw new \UnexpectedValueException(
+ "Expected field '['GetLocationsResult']['ResponseLocation']' to be set."
+ );
+ }
+
+ return $data['GetLocationsResult']['ResponseLocation'];
+ }
+
+ /**
+ * @throws ClientExceptionInterface|JsonException
+ */
+ public function locate(ServicePointQueryInterface $servicePointQuery): iterable
+ {
+ return $this->get($servicePointQuery->getEndPoint(), $servicePointQuery->toArray());
+ }
+}
diff --git a/src/DependencyInjection/Compiler/RegisterFactoriesPass.php b/src/DependencyInjection/Compiler/RegisterFactoriesPass.php
new file mode 100644
index 0000000..3849fb9
--- /dev/null
+++ b/src/DependencyInjection/Compiler/RegisterFactoriesPass.php
@@ -0,0 +1,74 @@
+register(sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::PSR17_FACTORY_SUFFIX), Psr17Factory::class);
+ }
+
+ $factoryId = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::PSR17_FACTORY_SUFFIX);
+ $requestFactoryAlias = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::REQUEST_FACTORY_SUFFIX);
+ $this->registerFactory(
+ $container,
+ $requestFactoryAlias,
+ $requestFactoryAlias,
+ $factoryId,
+ RequestFactoryInterface::class
+ );
+
+ $streamFactoryAlias = sprintf(self::SERVICE_ID_FORMAT, $PROVIDER, self::STREAM_FACTORY_SUFFIX);
+ $this->registerFactory($container,
+ $streamFactoryAlias,
+ $streamFactoryAlias,
+ $factoryId,
+ StreamFactoryInterface::class
+ );
+ }
+ }
+
+ private function registerFactory(ContainerBuilder $container, string $parameter, string $service, string $factoryId, string $factoryInterface): void
+ {
+ if ($container->hasParameter($parameter)) {
+ if (!$container->has($container->getParameter($parameter))) {
+ throw new ServiceNotFoundException($container->getParameter($parameter));
+ }
+
+ $container->setAlias($service, $container->getParameter($parameter));
+ } elseif ($container->has($factoryInterface)) {
+ $container->setAlias($service, $factoryInterface);
+ } elseif ($container->has('nyholm.psr7.psr17_factory')) {
+ $container->setAlias($service, 'nyholm.psr7.psr17_factory');
+ } elseif (class_exists(Psr17Factory::class)) {
+ $container->setAlias($service, $factoryId);
+ }
+ }
+}
diff --git a/src/DependencyInjection/Compiler/RegisterHttpClientPass.php b/src/DependencyInjection/Compiler/RegisterHttpClientPass.php
new file mode 100644
index 0000000..f70bd01
--- /dev/null
+++ b/src/DependencyInjection/Compiler/RegisterHttpClientPass.php
@@ -0,0 +1,33 @@
+ 'setono_bpost.http_client',
+ 'setono_postnl.http_client' => 'setono_postnl.http_client',
+ ];
+
+ public function process(ContainerBuilder $container): void
+ {
+ foreach (self::HTTP_CLIENT_PARAMETER_SERVICE_IDS as $PARAMETER => $SERVICE_ID) {
+ if ($container->hasParameter($PARAMETER)) {
+ if (!$container->has($container->getParameter($PARAMETER))) {
+ throw new ServiceNotFoundException($container->getParameter($PARAMETER));
+ }
+
+ $container->setAlias($SERVICE_ID, $container->getParameter($SERVICE_ID));
+ } elseif ($container->has(BuzzClientInterface::class)) {
+ $container->setAlias($SERVICE_ID, BuzzClientInterface::class);
+ }
+ }
+ }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 2c33354..6ae03d6 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -12,6 +12,7 @@
use Sylius\Bundle\ResourceBundle\Controller\ResourceController;
use Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType;
use Sylius\Bundle\ResourceBundle\SyliusResourceBundle;
+use Sylius\Component\Core\Model\Address;
use Sylius\Component\Resource\Factory\Factory;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
@@ -66,9 +67,76 @@ public function getConfigTreeBuilder(): TreeBuilder
->info('Whether to enable the PostNord provider')
->defaultValue(class_exists(SetonoPostNordBundle::class))
->end()
+ ->arrayNode('bpost')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('enabled')
+ ->example(true)
+ ->info('Whether to enable the Bpost provider')
+ ->defaultValue(false)
+ ->end()
+ ->scalarNode('base_url')
+ ->cannotBeEmpty()
+ ->defaultValue('https://pudo.bpost.be')
+ ->info('The base URL of the bpost API')
+ ->end()
+ ->scalarNode('partner_id')
+ ->cannotBeEmpty()
+ ->info('Bpost partner id required for making api calls')
+ ->end()
+ ->scalarNode('http_client')
+ ->cannotBeEmpty()
+ ->info('PSR18 HTTP client that is injected into the client service')
+ ->end()
+ ->scalarNode('request_factory')
+ ->cannotBeEmpty()
+ ->info('Is injected into the client service')
+ ->end()
+ ->scalarNode('stream_factory')
+ ->cannotBeEmpty()
+ ->info('Is injected into the client service')
+ ->end()
+ ->end()
+ ->end()
+ ->arrayNode('postnl')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->booleanNode('enabled')
+ ->example(true)
+ ->info('Whether to enable the PostNL provider')
+ ->defaultValue(false)
+ ->end()
+ ->scalarNode('base_url')
+ ->cannotBeEmpty()
+ ->defaultValue('https://api-sandbox.postnl.nl')
+ ->info('The base URL of the PostNL API')
+ ->end()
+ ->scalarNode('address_class')
+ ->cannotBeEmpty()
+ ->defaultValue(Address::class)
+ ->info('The address class to use for querying all points based on encountered postcodes')
+ ->end()
+ ->scalarNode('api_key')
+ ->cannotBeEmpty()
+ ->info('PostNL api key required for making api calls')
+ ->end()
+ ->scalarNode('http_client')
+ ->cannotBeEmpty()
+ ->info('PSR18 HTTP client that is injected into the client service')
+ ->end()
+ ->scalarNode('request_factory')
+ ->cannotBeEmpty()
+ ->info('Is injected into the client service')
+ ->end()
+ ->scalarNode('stream_factory')
+ ->cannotBeEmpty()
+ ->info('Is injected into the client service')
+ ->end()
+ ->end()
->end()
->end()
->end()
+ ->end()
;
$this->addResourcesSection($rootNode);
diff --git a/src/DependencyInjection/SetonoSyliusPickupPointExtension.php b/src/DependencyInjection/SetonoSyliusPickupPointExtension.php
index dfae998..9afb53f 100644
--- a/src/DependencyInjection/SetonoSyliusPickupPointExtension.php
+++ b/src/DependencyInjection/SetonoSyliusPickupPointExtension.php
@@ -71,5 +71,60 @@ public function load(array $configs, ContainerBuilder $container): void
$loader->load('services/providers/post_nord.xml');
}
+
+ if ($config['providers']['bpost'] && $config['providers']['bpost']['enabled'] === true) {
+ $bpostConfig = $config['providers']['bpost'];
+ $container->setParameter('setono_bpost.base_url', $bpostConfig['base_url']);
+
+ if (isset($bpostConfig['http_client'])) {
+ $container->setParameter('setono_bpost.http_client', $bpostConfig['http_client']);
+ }
+
+ if (isset($bpostConfig['request_factory'])) {
+ $container->setParameter('setono_bpost.request_factory', $bpostConfig['request_factory']);
+ }
+
+ if (isset($bpostConfig['stream_factory'])) {
+ $container->setParameter('setono_bpost.stream_factory', $bpostConfig['stream_factory']);
+ }
+
+ if (isset($bpostConfig['partner_id'])) {
+ $container->setParameter('setono_bpost.partner_id', $bpostConfig['partner_id']);
+ }
+
+ $loader->load('services/providers/bpost.xml');
+ $loader->load('services/clients/bpost.xml');
+ $loader->load('services/transformers/bpost.xml');
+ }
+
+ if ($config['providers']['postnl'] && $config['providers']['postnl']['enabled'] === true) {
+ $postnlConfig = $config['providers']['postnl'];
+
+ $container->setParameter('setono_postnl.base_url', $postnlConfig['base_url']);
+
+ if (isset($postnlConfig['http_client'])) {
+ $container->setParameter('setono_postnl.http_client', $postnlConfig['http_client']);
+ }
+
+ if (isset($postnlConfig['request_factory'])) {
+ $container->setParameter('setono_postnl.request_factory', $postnlConfig['request_factory']);
+ }
+
+ if (isset($postnlConfig['stream_factory'])) {
+ $container->setParameter('setono_postnl.stream_factory', $postnlConfig['stream_factory']);
+ }
+
+ if (isset($postnlConfig['api_key'])) {
+ $container->setParameter('setono_postnl.api_key', $postnlConfig['api_key']);
+ }
+
+ if (isset($postnlConfig['address_class'])) {
+ $container->setParameter('setono_postnl.address_class', $postnlConfig['address_class']);
+ }
+
+ $loader->load('services/providers/postnl.xml');
+ $loader->load('services/clients/postnl.xml');
+ $loader->load('services/transformers/postnl.xml');
+ }
}
}
diff --git a/src/Exception/RequestFailedException.php b/src/Exception/RequestFailedException.php
new file mode 100644
index 0000000..a452d94
--- /dev/null
+++ b/src/Exception/RequestFailedException.php
@@ -0,0 +1,55 @@
+request = $request;
+ $this->response = $response;
+ $this->statusCode = $statusCode;
+
+ $uri = self::resolveUri($this->request->getUri());
+
+ parent::__construct(sprintf('Request failed with status code %d. Request URI was: %s', $this->statusCode, $uri));
+ }
+
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+
+ public function getResponse(): ResponseInterface
+ {
+ return $this->response;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ private static function resolveUri(UriInterface $uri): string
+ {
+ /** @var string $query */
+ $query = preg_replace('/apikey=[^&]+/i', 'apikey=******', $uri->getQuery()); // this will mask the API key in logs etc
+
+ return (string) $uri->withQuery($query);
+ }
+}
diff --git a/src/Factory/Bpost/ServicePointQueryFactory.php b/src/Factory/Bpost/ServicePointQueryFactory.php
new file mode 100644
index 0000000..ba54e4b
--- /dev/null
+++ b/src/Factory/Bpost/ServicePointQueryFactory.php
@@ -0,0 +1,73 @@
+partnerId = $partnerId;
+ }
+
+ public function createServicePointQueryForOrder(OrderInterface $order): ServicePointQueryInterface
+ {
+ $servicePointQuery = new ServicePointQuery($this->partnerId);
+ $servicePointQuery->setFunction(ServicePointQueryInterface::FUNCTION_SEARCH);
+ $servicePointQuery->setLimit(20);
+
+ $shippingAddress = $order->getShippingAddress();
+ if (null === $shippingAddress) {
+ return $servicePointQuery;
+ }
+
+ $street = $shippingAddress->getStreet();
+ $postCode = $shippingAddress->getPostcode();
+ $countryCode = $shippingAddress->getCountryCode();
+
+ if ($street !== null) {
+ $servicePointQuery->setStreet($street);
+ }
+
+ if ($countryCode !== null) {
+ $servicePointQuery->setCountry($countryCode);
+ }
+
+ if ($postCode !== null) {
+ $servicePointQuery->setZone($postCode);
+ }
+
+ return $servicePointQuery;
+ }
+
+ public function createServicePointQueryForPickupPoint(PickupPointCode $pickupPointCode): ServicePointQueryInterface
+ {
+ $servicePointQuery = new ServicePointQuery($this->partnerId);
+ $servicePointQuery->setFunction(ServicePointQueryInterface::FUNCTION_INFO);
+ $servicePointQuery->setId($pickupPointCode->getIdPart());
+ $servicePointQuery->setCountry($pickupPointCode->getCountryPart());
+
+ return $servicePointQuery;
+ }
+
+ public function createServicePointQueryForAllPickupPoints(string $countryCode, ?string $postalCode = null): ServicePointQueryInterface
+ {
+ $servicePointQuery = new ServicePointQuery($this->partnerId);
+ $servicePointQuery->setFunction(ServicePointQueryInterface::FUNCTION_GET_ALL_SERVICE_POINTS);
+ $servicePointQuery->setAccount($this->partnerId);
+ $servicePointQuery->setCountry($countryCode);
+
+ if ($postalCode !== null) {
+ $servicePointQuery->setZone($postalCode);
+ }
+
+ return $servicePointQuery;
+ }
+}
diff --git a/src/Factory/PostNL/ServicePointQueryFactory.php b/src/Factory/PostNL/ServicePointQueryFactory.php
new file mode 100644
index 0000000..70b7058
--- /dev/null
+++ b/src/Factory/PostNL/ServicePointQueryFactory.php
@@ -0,0 +1,67 @@
+getShippingAddress();
+ if (null === $shippingAddress) {
+ return $servicePointQuery;
+ }
+
+ $street = $shippingAddress->getStreet();
+ $postCode = $shippingAddress->getPostcode();
+ $countryCode = $shippingAddress->getCountryCode();
+ $city = $shippingAddress->getCity();
+
+ if ($street !== null) {
+ $servicePointQuery->setStreet($street);
+ }
+
+ if ($countryCode !== null) {
+ $servicePointQuery->setCountryCode($countryCode);
+ }
+
+ if ($postCode !== null) {
+ $servicePointQuery->setPostalCode($postCode);
+ }
+
+ if ($city !== null) {
+ $servicePointQuery->setCity($city);
+ }
+
+ return $servicePointQuery;
+ }
+
+ public function createServicePointQueryForPickupPoint(PickupPointCode $pickupPointCode): ServicePointQueryInterface
+ {
+ return new ServicePointLookup(
+ $pickupPointCode->getIdPart(),
+ ServicePointLookupInterface::RETAIL_NETWORK_IDS[$pickupPointCode->getCountryPart()]
+ );
+ }
+
+ public function createServicePointQueryForAllPickupPoints(string $countryCode, ?string $postalCode = null): ServicePointQueryInterface
+ {
+ $servicePointQuery = new ServicePointQuery();
+ $servicePointQuery->setCountryCode($countryCode);
+
+ if ($postalCode !== null) {
+ $servicePointQuery->setPostalCode($postalCode);
+ }
+
+ return $servicePointQuery;
+ }
+}
diff --git a/src/Factory/ServicePointQueryFactoryInterface.php b/src/Factory/ServicePointQueryFactoryInterface.php
new file mode 100644
index 0000000..0fcd4ba
--- /dev/null
+++ b/src/Factory/ServicePointQueryFactoryInterface.php
@@ -0,0 +1,16 @@
+checkData = 1;
+ $this->checkOpen = 1;
+ $this->checkList = 1;
+ $this->info = 1;
+ $this->limit = 20;
+
+ $this->partner = $partner;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): void
+ {
+ $this->id = $id;
+ }
+
+ public function getFunction(): string
+ {
+ return $this->function;
+ }
+
+ public function setFunction(string $function): void
+ {
+ if (!in_array($function, self::FUNCTIONS, true)) {
+ throw new \LogicException(
+ sprintf(
+ 'Function: %s is not known, supported functions are: %s',
+ $function,
+ implode(', ', self::FUNCTIONS)
+ )
+ );
+ }
+ $this->function = $function;
+ }
+
+ public function getLanguage(): string
+ {
+ return $this->language;
+ }
+
+ public function setLanguage(string $language): void
+ {
+ $this->language = $language;
+ }
+
+ public function getStreet(): string
+ {
+ return $this->street;
+ }
+
+ public function setStreet(string $street): void
+ {
+ $this->street = $street;
+ }
+
+ public function getNumber(): string
+ {
+ return $this->number;
+ }
+
+ public function setNumber(string $number): void
+ {
+ $this->number = $number;
+ }
+
+ public function getZone(): string
+ {
+ return $this->zone;
+ }
+
+ public function setZone(string $zone): void
+ {
+ $this->zone = $zone;
+ }
+
+ public function getType(): int
+ {
+ return $this->type;
+ }
+
+ public function setType(int $type): void
+ {
+ if (!in_array($type, self::TYPES, true)) {
+ throw new \LogicException(
+ sprintf(
+ 'Type: %s is not known, supported types are: %s',
+ $type,
+ implode(', ', self::TYPES)
+ )
+ );
+ }
+ $this->type = $type;
+ }
+
+ public function getLimit(): int
+ {
+ return $this->limit;
+ }
+
+ public function setLimit(int $limit): void
+ {
+ $this->limit = $limit;
+ }
+
+ public function getPartner(): string
+ {
+ return $this->partner;
+ }
+
+ public function getAccount(): string
+ {
+ return $this->account;
+ }
+
+ public function setAccount(string $account): void
+ {
+ $this->account = $account;
+ }
+
+ public function getCountry(): string
+ {
+ return $this->country;
+ }
+
+ public function setCountry(string $country): void
+ {
+ $this->country = $country;
+ }
+
+ public function getEndPoint(): string {
+ return self::ENDPOINT;
+ }
+
+ public function getCheckData(): int
+ {
+ return (int) $this->checkData;
+ }
+
+ public function setCheckData(bool $checkData): void
+ {
+ $this->checkData = $checkData;
+ }
+
+ public function getCheckList(): int
+ {
+ return (int) $this->checkList;
+ }
+
+ public function setCheckList(bool $checkList): void
+ {
+ $this->checkList = $checkList;
+ }
+
+ public function getCheckOpen(): int
+ {
+ return (int) $this->checkOpen;
+ }
+
+ public function setCheckOpen(bool $checkOpen): void
+ {
+ $this->checkOpen = $checkOpen;
+ }
+
+ public function getInfo(): int
+ {
+ return (int) $this->info;
+ }
+
+ public function setInfo(bool $info): void
+ {
+ $this->info = $info;
+ }
+
+ public function toArray(): array
+ {
+ $arrayValue = [];
+ foreach(get_object_vars($this) as $key => $value) {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ $arrayValue[ucfirst($key)] = $value;
+ }
+ return $arrayValue;
+ }
+}
diff --git a/src/Model/Query/Bpost/ServicePointQueryInterface.php b/src/Model/Query/Bpost/ServicePointQueryInterface.php
new file mode 100644
index 0000000..31d0fa1
--- /dev/null
+++ b/src/Model/Query/Bpost/ServicePointQueryInterface.php
@@ -0,0 +1,32 @@
+ 1,
+ self::TYPE_POST_POINT => 2,
+ self::TYPE_PACK_STATION => 4,
+ self::TYPE_SHOP => 8,
+ self::TYPE_KARIBOO => 16,
+ ];
+}
diff --git a/src/Model/Query/PostNL/ServicePointLookup.php b/src/Model/Query/PostNL/ServicePointLookup.php
new file mode 100644
index 0000000..a9b0b59
--- /dev/null
+++ b/src/Model/Query/PostNL/ServicePointLookup.php
@@ -0,0 +1,31 @@
+locationCode = $locationCode;
+ $this->retailNetworkId = $retailNetworkId;
+ }
+
+ public function getEndPoint(): string
+ {
+ return self::ENDPOINT;
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'RetailNetorkID' => $this->retailNetworkId,
+ 'LocationCode' => $this->locationCode,
+ ];
+ }
+}
diff --git a/src/Model/Query/PostNL/ServicePointLookupInterface.php b/src/Model/Query/PostNL/ServicePointLookupInterface.php
new file mode 100644
index 0000000..e3a1dc6
--- /dev/null
+++ b/src/Model/Query/PostNL/ServicePointLookupInterface.php
@@ -0,0 +1,16 @@
+ self::RETAIL_NETWORK_ID_BE,
+ 'NL' => self::RETAIL_NETWORK_ID_NL,
+ ];
+
+ public const RETAIL_NETWORK_ID_BE = 'PNPBE-01';
+ public const RETAIL_NETWORK_ID_NL = 'PNPNL-01';
+}
diff --git a/src/Model/Query/PostNL/ServicePointQuery.php b/src/Model/Query/PostNL/ServicePointQuery.php
new file mode 100644
index 0000000..2559ddf
--- /dev/null
+++ b/src/Model/Query/PostNL/ServicePointQuery.php
@@ -0,0 +1,134 @@
+id;
+ }
+
+ public function setId(string $id): void
+ {
+ $this->id = $id;
+ }
+
+ public function getStreet(): string
+ {
+ return $this->street;
+ }
+
+ public function setStreet(string $street): void
+ {
+ $this->street = $street;
+ }
+
+ public function getCountryCode(): string
+ {
+ return $this->countryCode;
+ }
+
+ public function setCountryCode(string $countryCode): void
+ {
+ $this->countryCode = $countryCode;
+ }
+
+ public function getPostalCode(): string
+ {
+ return $this->postalCode;
+ }
+
+ public function setPostalCode(string $postalCode): void
+ {
+ $this->postalCode = $postalCode;
+ }
+
+ public function getCity(): string
+ {
+ return $this->city;
+ }
+
+ public function setCity(string $city): void
+ {
+ $this->city = $city;
+ }
+
+ public function getHouseNumber(): string
+ {
+ return $this->houseNumber;
+ }
+
+ public function setHouseNumber(string $houseNumber): void
+ {
+ $this->houseNumber = $houseNumber;
+ }
+
+ public function getDeliveryDate(): \DateTime
+ {
+ return $this->deliveryDate;
+ }
+
+ public function setDeliveryDate(\DateTime $deliveryDate): void
+ {
+ $this->deliveryDate = $deliveryDate;
+ }
+
+ public function getOpeningTime(): \DateTime
+ {
+ return $this->openingTime;
+ }
+
+ public function setOpeningTime(\DateTime $openingTime): void
+ {
+ $this->openingTime = $openingTime;
+ }
+
+ public function getDeliveryOptions(): array
+ {
+ return $this->deliveryOptions;
+ }
+
+ public function setDeliveryOptions(array $deliveryOptions): void
+ {
+ $this->deliveryOptions = $deliveryOptions;
+ }
+
+ public function getEndPoint(): string {
+ return self::ENDPOINT;
+ }
+
+ public function toArray(): array
+ {
+ $arrayValue = [];
+ foreach(get_object_vars($this) as $key => $value) {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ $arrayValue[ucfirst($key)] = $value;
+ }
+ return $arrayValue;
+ }
+}
diff --git a/src/Model/Query/ServicePointQueryInterface.php b/src/Model/Query/ServicePointQueryInterface.php
new file mode 100644
index 0000000..8e1ce5f
--- /dev/null
+++ b/src/Model/Query/ServicePointQueryInterface.php
@@ -0,0 +1,10 @@
+client = $client;
+ $this->countryCodes = $countryCodes;
+ $this->servicePointQueryFactory = $servicePointQueryFactory;
+ $this->partnerId = $partnerId;
+ $this->pickupPointTransformer = $pickupPointTransformer;
+ }
+
+ public function findPickupPoints(OrderInterface $order): iterable
+ {
+ $shippingAddress = $order->getShippingAddress();
+ if (null === $shippingAddress) {
+ return [];
+ }
+
+ $countryCode = $shippingAddress->getCountryCode();
+ if (null === $countryCode) {
+ return [];
+ }
+
+ $servicePointQuery = $this->getServicePointQueryFactory()->createServicePointQueryForOrder($order);
+ $servicePoints = $this->client->locate($servicePointQuery);
+ foreach ($servicePoints as $item) {
+ $item['country'] = $countryCode;
+ yield $this->transform($item);
+ }
+ }
+
+ public function findPickupPoint(PickupPointCode $code): ?PickupPointInterface
+ {
+ $servicePoints = [];
+ try {
+ $servicePointQuery = $this->getServicePointQueryFactory()->createServicePointQueryForPickupPoint($code);
+ foreach (ServicePointQueryInterface::TYPES as $type) {
+ $servicePointQuery->setType($type);
+ $servicePoints = $this->client->locate($servicePointQuery);
+ if (count($servicePoints) > 0) {
+ break;
+ }
+ }
+ } catch (NetworkExceptionInterface $e) {
+ throw new TimeoutException($e);
+ }
+
+ if (count($servicePoints) < 1) {
+ return null;
+ }
+
+ $servicePoint = $servicePoints[0];
+ $servicePoint['country'] = $code->getCountryPart();
+ return $this->transform($servicePoint);
+ }
+
+ public function findAllPickupPoints(): iterable
+ {
+ try {
+ foreach ($this->countryCodes as $countryCode) {
+ $servicePointQuery = $this->getServicePointQueryFactory()->createServicePointQueryForAllPickupPoints($countryCode);
+
+ $servicePointQuery->setCountry($countryCode);
+ $servicePoints = $this->client->locate($servicePointQuery);
+ foreach ($servicePoints as $item) {
+ $item['country'] = $countryCode;
+ yield $this->transform($item);
+ }
+ }
+ } catch (NetworkExceptionInterface $e) {
+ throw new TimeoutException($e);
+ }
+ }
+
+ public function getCode(): string
+ {
+ return self::CODE;
+ }
+
+ public function getName(): string
+ {
+ return self::NAME;
+ }
+
+ private function getServicePointQueryFactory(): ServicePointQueryFactoryInterface
+ {
+ if ($this->servicePointQueryFactory === null) {
+ $this->servicePointQueryFactory = new ServicePointQueryFactory($this->partnerId);
+ }
+ return $this->servicePointQueryFactory;
+ }
+
+ private function transform(array $servicePoint): PickupPointInterface
+ {
+ return $this->pickupPointTransformer->transform($servicePoint, $this->getCode());
+ }
+}
diff --git a/src/Provider/PostNLProvider.php b/src/Provider/PostNLProvider.php
new file mode 100644
index 0000000..8989490
--- /dev/null
+++ b/src/Provider/PostNLProvider.php
@@ -0,0 +1,154 @@
+client = $client;
+ $this->countryCodes = $countryCodes;
+ $this->servicePointQueryFactory = $servicePointQueryFactory;
+ $this->pickupPointTransformer = $pickupPointTransformer;
+ $this->managerRegistry = $managerRegistry;
+ $this->addressClassString = $addressClassString;
+ }
+
+ public function findPickupPoints(OrderInterface $order): iterable
+ {
+ $shippingAddress = $order->getShippingAddress();
+ if (null === $shippingAddress) {
+ return [];
+ }
+
+ $countryCode = $shippingAddress->getCountryCode();
+ if (null === $countryCode) {
+ return [];
+ }
+
+ $servicePointQuery = $this->getServicePointQueryFactory()->createServicePointQueryForOrder($order);
+ $servicePoints = $this->client->locate($servicePointQuery);
+ foreach ($servicePoints as $item) {
+ $item['country'] = $countryCode;
+ yield $this->transform($item);
+ }
+ }
+
+ public function findPickupPoint(PickupPointCode $code): ?PickupPointInterface
+ {
+ try {
+ $servicePointQuery = $this->getServicePointQueryFactory()->createServicePointQueryForPickupPoint($code);
+ $servicePoint = $this->client->locate($servicePointQuery);
+ } catch (NetworkExceptionInterface $e) {
+ throw new TimeoutException($e);
+ }
+
+ $servicePoints = [];
+ foreach ($servicePoint as $index => $point) {
+ $servicePoints[$index] = $point;
+ }
+
+ if (\count($servicePoints) < 1) {
+ return null;
+ }
+
+ return $this->transform($servicePoints);
+ }
+
+ /**
+ * As there is currently no good alternative to query all pickup points for a given country, this will be provided
+ * as an alternative.
+ */
+ public function findAllPickupPoints(): iterable
+ {
+ if (!class_exists($this->addressClassString)) {
+ throw new \InvalidArgumentException(sprintf("Class '%s' does not exist", $this->addressClassString));
+ }
+
+ $manager = $this->managerRegistry->getManagerForClass($this->addressClassString);
+ Assert::notNull($manager);
+
+ /** @var EntityRepository $repository */
+ $repository = $manager->getRepository($this->addressClassString);
+ try {
+ foreach ($this->countryCodes as $countryCode) {
+ $qb = $repository->createQueryBuilder('sa');
+ $postalCodes = $qb->distinct()->select('sa.postcode')
+ ->where('sa.countryCode = :countryCode')
+ ->setParameter('countryCode', $countryCode)
+ ->getQuery()->getResult();
+
+ $postalCodes = array_map(static function (array $code) {
+ return $code['postcode'];
+ }, $postalCodes);
+
+ foreach ($postalCodes as $postalCode) {
+ $servicePointQuery = $this->getServicePointQueryFactory()
+ ->createServicePointQueryForAllPickupPoints($countryCode, $postalCode);
+ $servicePoints = $this->client->locate($servicePointQuery);
+ foreach ($servicePoints as $item) {
+ $item['country'] = $countryCode;
+ yield $this->transform($item);
+ }
+ }
+ }
+ } catch (NetworkExceptionInterface $e) {
+ throw new TimeoutException($e);
+ }
+ }
+ public function getCode(): string
+ {
+ return self::CODE;
+ }
+
+ public function getName(): string
+ {
+ return self::NAME;
+ }
+
+ private function getServicePointQueryFactory(): ServicePointQueryFactoryInterface
+ {
+ if ($this->servicePointQueryFactory === null) {
+ $this->servicePointQueryFactory = new ServicePointQueryFactory();
+ }
+ return $this->servicePointQueryFactory;
+ }
+
+ private function transform(array $servicePoint): PickupPointInterface
+ {
+ return $this->pickupPointTransformer->transform($servicePoint, $this->getCode());
+ }
+}
diff --git a/src/Resources/config/app/fixtures.yaml b/src/Resources/config/app/fixtures.yaml
index 96f6670..5cc22ce 100644
--- a/src/Resources/config/app/fixtures.yaml
+++ b/src/Resources/config/app/fixtures.yaml
@@ -21,6 +21,8 @@ sylius_fixtures:
- 'ES'
- 'CN'
- 'UK'
+ - 'NL'
+ - 'BE'
zones:
WORLD:
name: 'World'
@@ -39,6 +41,8 @@ sylius_fixtures:
- 'ES'
- 'CN'
- 'UK'
+ - 'NL'
+ - 'BE'
DAO_PP_COUNTRIES:
name: 'Denmark'
countries:
@@ -61,6 +65,16 @@ sylius_fixtures:
- 'PT'
- 'ES'
- 'UK'
+ BPOST_PP_COUNTRIES:
+ name: 'Bpost countries'
+ countries:
+ - 'NL'
+ - 'BE'
+ POSTNL_PP_COUNTRIES:
+ name: 'PostNL countries'
+ countries:
+ - 'NL'
+ - 'BE'
setono_sylius_pickup_point_shipping_method:
options:
@@ -103,6 +117,22 @@ sylius_fixtures:
zone: "WORLD"
channels:
- "FASHION_WEB"
+ postnl_pickup_point:
+ code: "postnl_pickup_point"
+ name: "PostNL with pickup points"
+ enabled: true
+ zone: "POSTNL_PP_COUNTRIES"
+ pickup_point_provider: postnl
+ channels:
+ - "FASHION_WEB"
+ bpost_pickup_point:
+ code: "bpost_pickup_point"
+ name: "Bpost with pickup points"
+ enabled: true
+ zone: "BPOST_PP_COUNTRIES"
+ pickup_point_provider: bpost
+ channels:
+ - "FASHION_WEB"
faker:
code: "faker"
name: "Fake delivery"
diff --git a/src/Resources/config/services/clients/bpost.xml b/src/Resources/config/services/clients/bpost.xml
new file mode 100644
index 0000000..3d1437c
--- /dev/null
+++ b/src/Resources/config/services/clients/bpost.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+ %setono_bpost.base_url%
+
+
+
diff --git a/src/Resources/config/services/clients/postnl.xml b/src/Resources/config/services/clients/postnl.xml
new file mode 100644
index 0000000..3bc83e4
--- /dev/null
+++ b/src/Resources/config/services/clients/postnl.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+ %setono_postnl.api_key%
+ %setono_postnl.base_url%
+
+
+
diff --git a/src/Resources/config/services/providers/bpost.xml b/src/Resources/config/services/providers/bpost.xml
new file mode 100644
index 0000000..a0d0019
--- /dev/null
+++ b/src/Resources/config/services/providers/bpost.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ %setono_bpost.partner_id%
+
+
+
+
diff --git a/src/Resources/config/services/providers/postnl.xml b/src/Resources/config/services/providers/postnl.xml
new file mode 100644
index 0000000..485ca30
--- /dev/null
+++ b/src/Resources/config/services/providers/postnl.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+ %setono_postnl.address_class%
+
+
+
+
diff --git a/src/Resources/config/services/transformers/bpost.xml b/src/Resources/config/services/transformers/bpost.xml
new file mode 100644
index 0000000..e85098c
--- /dev/null
+++ b/src/Resources/config/services/transformers/bpost.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/config/services/transformers/postnl.xml b/src/Resources/config/services/transformers/postnl.xml
new file mode 100644
index 0000000..d20cd10
--- /dev/null
+++ b/src/Resources/config/services/transformers/postnl.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/translations/messages.da.yml b/src/Resources/translations/messages.da.yml
index 099cc90..370741f 100644
--- a/src/Resources/translations/messages.da.yml
+++ b/src/Resources/translations/messages.da.yml
@@ -13,3 +13,5 @@ setono_sylius_pickup_point:
dao: DAO
gls: GLS
post_nord: PostNord
+ postnl: Post NL
+ bpost: Bpost
diff --git a/src/Resources/translations/messages.en.yml b/src/Resources/translations/messages.en.yml
index 2d26668..5fb0d3c 100644
--- a/src/Resources/translations/messages.en.yml
+++ b/src/Resources/translations/messages.en.yml
@@ -13,3 +13,5 @@ setono_sylius_pickup_point:
dao: DAO
gls: GLS
post_nord: PostNord
+ postnl: Post NL
+ bpost: Bpost
diff --git a/src/SetonoSyliusPickupPointPlugin.php b/src/SetonoSyliusPickupPointPlugin.php
index 1844b60..85a3912 100644
--- a/src/SetonoSyliusPickupPointPlugin.php
+++ b/src/SetonoSyliusPickupPointPlugin.php
@@ -5,6 +5,8 @@
namespace Setono\SyliusPickupPointPlugin;
use Setono\SyliusPickupPointPlugin\DependencyInjection\Compiler\RegisterProvidersPass;
+use Setono\SyliusPickupPointPlugin\DependencyInjection\Compiler\RegisterHttpClientPass;
+use Setono\SyliusPickupPointPlugin\DependencyInjection\Compiler\RegisterFactoriesPass;
use Sylius\Bundle\CoreBundle\Application\SyliusPluginTrait;
use Sylius\Bundle\ResourceBundle\AbstractResourceBundle;
use Sylius\Bundle\ResourceBundle\SyliusResourceBundle;
@@ -19,6 +21,8 @@ public function build(ContainerBuilder $container): void
parent::build($container);
$container->addCompilerPass(new RegisterProvidersPass());
+ $container->addCompilerPass(new RegisterHttpClientPass());
+ $container->addCompilerPass(new RegisterFactoriesPass());
}
public function getSupportedDrivers(): array
diff --git a/src/Transformer/Bpost/PickupPointTransformer.php b/src/Transformer/Bpost/PickupPointTransformer.php
new file mode 100644
index 0000000..41936ef
--- /dev/null
+++ b/src/Transformer/Bpost/PickupPointTransformer.php
@@ -0,0 +1,63 @@
+pickupPointFactory = $pickupPointFactory;
+ }
+
+ public function transform(array $servicePoint, string $providerCode): PickupPointInterface
+ {
+ $servicePointSanitized = $this->arrayChangeKeyCaseRecursive($servicePoint);
+ $id = new PickupPointCode(
+ $servicePointSanitized['id'],
+ $providerCode,
+ $servicePointSanitized['country'] ?? $servicePointSanitized['city']
+ );
+
+ $address = trim(
+ sprintf(
+ '%s %s',
+ $servicePointSanitized['street'] ?? '',
+ $servicePointSanitized['number'] ?? $servicePointSanitized['nr'] ?? '',
+ )
+ );
+ $latitude = $servicePointSanitized['latitude'] ? (float) $servicePointSanitized['latitude'] : null;
+ $longitude = $servicePointSanitized['longitude'] ? (float) $servicePointSanitized['longitude'] : null;
+
+ /** @var PickupPointInterface|object $pickupPoint */
+ $pickupPoint = $this->pickupPointFactory->createNew();
+ Assert::isInstanceOf($pickupPoint, PickupPointInterface::class);
+ $pickupPoint->setCode($id);
+ $pickupPoint->setName($servicePointSanitized['name'] ?? $servicePointSanitized['office']);
+ $pickupPoint->setAddress($address);
+ $pickupPoint->setZipCode((string) $servicePointSanitized['zip']);
+ $pickupPoint->setCity($servicePointSanitized['city']);
+ $pickupPoint->setCountry($servicePointSanitized['country']);
+ $pickupPoint->setLatitude($latitude);
+ $pickupPoint->setLongitude($longitude);
+
+ return $pickupPoint;
+ }
+
+ private function arrayChangeKeyCaseRecursive($arr, $case = CASE_LOWER): array
+ {
+ return array_map(function($item) use($case) {
+ if(is_array($item)) {
+ $item = $this->arrayChangeKeyCaseRecursive($item, $case);
+ }
+ return $item;
+ },array_change_key_case($arr, $case));
+ }
+}
diff --git a/src/Transformer/PickupPointTransformerInterface.php b/src/Transformer/PickupPointTransformerInterface.php
new file mode 100644
index 0000000..92bb1f1
--- /dev/null
+++ b/src/Transformer/PickupPointTransformerInterface.php
@@ -0,0 +1,10 @@
+pickupPointFactory = $pickupPointFactory;
+ }
+
+ public function transform(array $servicePoint, string $providerCode): PickupPointInterface
+ {
+ $servicePointSanitized = $this->arrayChangeKeyCaseRecursive($servicePoint);
+ $servicePointAddress = $servicePointSanitized['address'];
+
+ $id = new PickupPointCode(
+ $servicePointSanitized['locationcode'],
+ $providerCode,
+ $servicePointAddress['countrycode']
+ );
+
+ $address = trim(
+ sprintf(
+ '%s %s',
+ $servicePointAddress['street'] ?? '',
+ $servicePointAddress['housenr'] ?? '',
+ )
+ );
+
+ $latitude = $servicePointSanitized['latitude'] ? (float) $servicePointSanitized['latitude'] : null;
+ $longitude = $servicePointSanitized['longitude'] ? (float) $servicePointSanitized['longitude'] : null;
+
+ /** @var PickupPointInterface|object $pickupPoint */
+ $pickupPoint = $this->pickupPointFactory->createNew();
+ Assert::isInstanceOf($pickupPoint, PickupPointInterface::class);
+
+ $pickupPoint->setCode($id);
+ $pickupPoint->setName($servicePointSanitized['name']);
+ $pickupPoint->setAddress($address);
+ $pickupPoint->setZipCode((string) $servicePointAddress['zipcode']);
+ $pickupPoint->setCity($servicePointAddress['city']);
+ $pickupPoint->setCountry($servicePointAddress['countrycode']);
+ $pickupPoint->setLatitude($latitude);
+ $pickupPoint->setLongitude($longitude);
+
+ return $pickupPoint;
+ }
+
+ private function arrayChangeKeyCaseRecursive($arr, $case = CASE_LOWER): array
+ {
+ return array_map(function($item) use($case) {
+ if(is_array($item)) {
+ $item = $this->arrayChangeKeyCaseRecursive($item, $case);
+ }
+ return $item;
+ },array_change_key_case($arr, $case));
+ }
+}
diff --git a/tests/Application/.env b/tests/Application/.env
index 85229f7..d76d139 100644
--- a/tests/Application/.env
+++ b/tests/Application/.env
@@ -30,3 +30,11 @@ DAO_PASSWORD=
###> setono/post-nord-bundle ###
POST_NORD_API_KEY=
###< setono/post-nord-bundle ###
+
+###> setono/postnl-bundle ###
+POSTNL_API_KEY=
+###< setono/postnl-bundle ###
+
+###> setono/bpost-bundle ###
+BPOST_PARTNER_ID=
+###< setono/bpost-bundle ###
diff --git a/tests/Application/config/packages/setono_sylius_pickup_point.yaml b/tests/Application/config/packages/setono_sylius_pickup_point.yaml
index 0265651..81bbe7d 100644
--- a/tests/Application/config/packages/setono_sylius_pickup_point.yaml
+++ b/tests/Application/config/packages/setono_sylius_pickup_point.yaml
@@ -18,3 +18,9 @@ setono_sylius_pickup_point:
gls: true
post_nord: true
dao: true
+ bpost:
+ enabled: true
+ partner_id: '%env(BPOST_PARTNER_ID)%'
+ postnl:
+ enabled: true
+ api_key: '%env(POSTNL_API_KEY)%'