Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geo6 locator API changes #35

Open
sten opened this issue May 25, 2023 · 0 comments
Open

Geo6 locator API changes #35

sten opened this issue May 25, 2023 · 0 comments

Comments

@sten
Copy link

sten commented May 25, 2023

Bpost has released a new version of the Locator API, https://pudo.bpost.be/Locator. This causes errors with the current implementation of the library. It appears they are phasing out support for HTTP POST requests.

Info API:
The package does an HTTP POST request for the info API to fetch the details of a pudo point. This no longer works since the upgrade. If I do an HTTP GET request it works.

Search API:
The package does an HTTP POST request for the search API with the search parameters as a form url-encoded body. This only works partially since the upgrade. It seems only the postal code is taken into account and the street not. If I encode the parameters as JSON in the body, it works again. A GET request with the parameters as query string works well.

I have contacted bpost to check if they are willing to make this backward compatible again. To fix this temporarily I made my own copy of the Geo6 class. For those experiencing the same issue here is my code:

<?php

namespace App\Services\Logistics\Bpost\Geo6;

use Bpost\BpostApiClient\ApiCaller\ApiCaller;
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostCurlException;
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostInvalidXmlResponseException;
use Bpost\BpostApiClient\Exception\BpostApiResponseException\BpostTaxipostLocatorException;
use Bpost\BpostApiClient\Geo6\Poi;
use Bpost\BpostApiClient\Logger;

/**
 * Copy of Bpost\BpostApiClient\Geo6 to "temporarily" fix the API call to support json encoded body instead of form query string, to fix new Bpost API
 */
class Geo6Locator
{
    // URL for the api
    const API_URL = 'https://pudo.bpost.be/Locator';

    // current version
    const VERSION = '3';

    /**
     * @see getPointType
     * @see getServicePointPageUrl
     */
    const POINT_TYPE_POST_OFFICE = 1;
    const POINT_TYPE_POST_POINT = 2;
    const POINT_TYPE_BPACK_247 = 4;
    const POINT_TYPE_CLICK_COLLECT_SHOP = 8;

    /** @var ApiCaller */
    private $apiCaller;

    /**
     * @var string
     */
    private $appId;

    /**
     * @var string
     */
    private $partner;

    /**
     * The timeout
     *
     * @var int
     */
    private $timeOut = 10;

    /**
     * The user agent
     *
     * @var string
     */
    private $userAgent;

    /**
     * Constructor
     * @param string $partner Static parameter used for protection/statistics
     * @param string $appId   Static parameter used for protection/statistics
     */
    public function __construct($partner, $appId)
    {
        $this->setPartner((string)$partner);
        $this->setAppId((string)$appId);
    }

    /**
     * @return ApiCaller
     */
    public function getApiCaller()
    {
        if ($this->apiCaller === null) {
            $this->apiCaller = new ApiCaller(new Logger());
        }
        return $this->apiCaller;
    }

    /**
     * @param ApiCaller $apiCaller
     */
    public function setApiCaller(ApiCaller $apiCaller)
    {
        $this->apiCaller = $apiCaller;
    }

    /**
     * Build the url to be called
     *
     * @param string $method
     * @param array  $parameters
     * @return string
     */
    private function buildUrl($method, array $parameters = array())
    {
        return self::API_URL . '?' . $this->buildParameters($method, $parameters);
    }

    /**
     * Build the parameters to send (URL-encoded string)
     *
     * @param  string $method
     * @param  array  $parameters
     * @return string
     */
    private function buildParameters($method, array $parameters = array())
    {
        // add credentials
        $parameters = $this->addExtraParameters($method, $parameters);

        return http_build_query($parameters);
    }

    private function addExtraParameters($method, array $parameters = array()): array {
        $parameters['Function'] = $method;
        $parameters['Partner'] = $this->getPartner();
        $parameters['AppId'] = $this->getAppId();
        $parameters['Format'] = 'xml';

        return $parameters;
    }

    /**
     * Make the HTTP POST call
     *
     * @param  string $method
     * @param  array  $parameters
     * @return \SimpleXMLElement
     * @throws BpostCurlException
     * @throws BpostInvalidXmlResponseException
     * @throws BpostTaxipostLocatorException
     */
    private function doCall($method, array $parameters = array())
    {
        $parameters = $this->addExtraParameters($method, $parameters);

        $options = array(
            CURLOPT_URL => self::API_URL,
            CURLOPT_USERAGENT => $this->getUserAgent(),
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => (int)$this->getTimeOut(),

            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($parameters),
        );

        $this->getApiCaller()->doCall($options);

        return $this->parseResponse();
    }

    /**
     * Make the HTTP GET call
     *
     * @param  string $method
     * @param  array  $parameters
     * @return \SimpleXMLElement
     * @throws BpostCurlException
     * @throws BpostInvalidXmlResponseException
     * @throws BpostTaxipostLocatorException
     */
    private function doGetCall($method, array $parameters = array())
    {
        $options = array(
            CURLOPT_URL => $this->buildUrl($method, $parameters),
            CURLOPT_USERAGENT => $this->getUserAgent(),
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => (int)$this->getTimeOut(),

            CURLOPT_POST => false,
        );

        $this->getApiCaller()->doCall($options);

        return $this->parseResponse();
    }

    /**
     * @return \SimpleXMLElement
     * @throws BpostInvalidXmlResponseException
     * @throws BpostTaxipostLocatorException
     */
    private function parseResponse(): \SimpleXMLElement {
        // we expect XML so decode it
        $xml = @simplexml_load_string($this->getApiCaller()->getResponseBody());

        // validate xml
        if ($xml === false || (isset($xml->head) && isset($xml->body))) {
            throw new BpostInvalidXmlResponseException();
        }

        // catch generic errors
        if (isset($xml['type']) && (string)$xml['type'] == 'TaxipostLocatorError') {
            throw new BpostTaxipostLocatorException((string)$xml->txt, (int)$xml->status);
        }

        // return
        return $xml;
    }

    /**
     * @param string $appId
     */
    public function setAppId($appId)
    {
        $this->appId = $appId;
    }

    /**
     * @return string
     */
    public function getAppId()
    {
        return $this->appId;
    }

    /**
     * @param string $partner
     */
    public function setPartner($partner)
    {
        $this->partner = $partner;
    }

    /**
     * @return string
     */
    public function getPartner()
    {
        return $this->partner;
    }

    /**
     * Set the timeout
     * After this time the request will stop. You should handle any errors triggered by this.
     *
     * @param int $seconds The timeout in seconds.
     */
    public function setTimeOut($seconds)
    {
        $this->timeOut = (int)$seconds;
    }

    /**
     * Get the timeout that will be used
     *
     * @return int
     */
    public function getTimeOut()
    {
        return (int)$this->timeOut;
    }

    /**
     * Get the useragent that will be used.
     * Our version will be prepended to yours.
     * It will look like: "PHP Bpost/<version> <your-user-agent>"
     *
     * @return string
     */
    public function getUserAgent()
    {
        return (string)'PHP Bpost Geo6/' . self::VERSION . ' ' . $this->userAgent;
    }

    /**
     * Set the user-agent for you application
     * It will be appended to ours, the result will look like: "PHP Bpost/<version> <your-user-agent>"
     *
     * @param string $userAgent Your user-agent, it should look like <app-name>/<app-version>.
     */
    public function setUserAgent($userAgent)
    {
        $this->userAgent = (string)$userAgent;
    }

    // webservice methods
    /**
     * The GetNearestServicePoints web service delivers the nearest bpost pick-up points to a location
     *
     * @param string $street   Street name
     * @param string $number   Street number
     * @param string $zone     Postal code and/or city
     * @param string $language Language, possible values are: nl, fr
     * @param int    $type     Requested point type, possible values are:
     *                         - 1: Post Office
     *                         - 2: Post Point
     *                         - 3: (1+2, Post Office + Post Point)
     *                         - 4: bpack 24/7
     *                         - 7: (1+2+4, Post Office + Post Point + bpack 24/7)
     * @param int    $limit
     * @param string $country  Country: "BE", "FR"...
     * @return array
     * @throws BpostCurlException
     * @throws BpostInvalidXmlResponseException
     * @throws BpostTaxipostLocatorException
     */
    //public function getNearestServicePoint($street, $number, $zone, $country = 'BE', $language = 'nl', $type = 3, $limit = 10)
    public function getNearestServicePoint($street, $number, $zone, $language = 'nl', $type = 3, $limit = 10, $country = 'BE')
    {
        $parameters = array(
            'Street' => (string)$street,
            'Number' => (string)$number,
            'Zone' => (string)$zone,
            'Country' => (string)$country,
            'Language' => (string)$language,
            'Type' => (int)$type,
            'Limit' => (int)$limit
        );

        $xml = $this->doCall('search', $parameters);

        if (!isset($xml->PoiList->Poi)) {
            throw new BpostInvalidXmlResponseException();
        }

        $pois = array();
        foreach ($xml->PoiList->Poi as $poi) {
            $pois[] = array(
                'poi' => Poi::createFromXML($poi),
                'distance' => (float)$poi->Distance,
            );
        }

        return $pois;
    }

    /**
     * The GetServicePointDetails web service delivers the details for a bpost
     * pick up point referred to by its identifier.
     *
     * @param string $id       Requested point identifier
     * @param string $language Language, possible values: nl, fr
     * @param int    $type     Requested point type, possible values are:
     *                         - 1: Post Office
     *                         - 2: Post Point
     *                         - 4: bpack 24/7
     * @param string $country  Country: "BE", "FR"...
     *
     * @return Poi
     * @throws BpostCurlException
     * @throws BpostInvalidXmlResponseException
     * @throws BpostTaxipostLocatorException
     */
    public function getServicePointDetails($id, $language = 'nl', $type = 3, $country = 'BE')
    {
        $parameters = array(
            'Id' => (string)$id,
            'Language' => (string)$language,
            'Type' => (int)$type,
            'Country' => (string)$country,
        );

        $xml = $this->doGetCall('info', $parameters);

        if (!isset($xml->Poi)) {
            throw new BpostInvalidXmlResponseException();
        }

        return Poi::createFromXML($xml->Poi);
    }

    /**
     * @param int    $id
     * @param string $language
     * @param int    $type
     * @param string $country
     * @return string
     *
     * @see getPointType to feed the param $type
     */
    public function getServicePointPageUrl($id, $language = 'nl', $type = 3, $country = 'BE')
    {
        $parameters = array(
            'Id' => (string)$id,
            'Language' => (string)$language,
            'Type' => (int)$type,
            'Country' => (string)$country,
        );

        return $this->buildUrl('page', $parameters);
    }

    /**
     * @param int    $id
     * @param string $language
     * @param int    $type
     * @param string $country
     * @return string
     *
     * @deprecated Renamed
     * @see        getServicePointPageUrl
     */
    public function getServicePointPage($id, $language = 'nl', $type = 3, $country = 'BE')
    {
        return $this->getServicePointPageUrl($id, $language, $type, $country);
    }

    /**
     * @param bool $withPostOffice
     * @param bool $withPostPoint
     * @param bool $withBpack247
     * @param bool $withClickAndCollectShop
     * @return int
     */
    public function getPointType(
        $withPostOffice = true,
        $withPostPoint = true,
        $withBpack247 = false,
        $withClickAndCollectShop = false
    ) {
        return
            ($withPostOffice ? self::POINT_TYPE_POST_OFFICE : 0)
            + ($withPostPoint ? self::POINT_TYPE_POST_POINT : 0)
            + ($withBpack247 ? self::POINT_TYPE_BPACK_247 : 0)
            + ($withClickAndCollectShop ? self::POINT_TYPE_CLICK_COLLECT_SHOP : 0);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant