Jak nawiązać połączenie z API firmy kurierskiej DHL

 In Technicznie

W tym artykule chciałbym przedstawić przykładowy projekt służący do komunikacji z API DHL. Nawiążę połączenie oraz przebrnę przez proces zamawiania kuriera.

 

Jednym z naszych klientów, któremu oferujemy wsparcie i rozwój systemu jest BrokerSystem, aplikacja ta skierowana jest do brokerów kurierskich obsługujących dużą liczbę przesyłek.

Jednym z głównych naszych zadań i wyzwań są integracje z API (Application Programming Interface) firm kurierskich (DHL, DPD, Geis, FedEx itp.).  W tym wpisie chciałbym zaprezentować sposób obsługi procesu zamawiania kuriera DHL. Zadanie to wymaga wykonania kolejnych kroków:

  • dodanie przesyłki do systemu DHL
  • wygenerowanie listu przewozowego lub etykiet termalnych
  • zamówienie kuriera po odbiór przesyłki

Dokumentacja WebAPI DHL dostępna jest pod adresem https://dhl24.com.pl/webapi2/doc/index.html. Aby nawiązać połączenie wymagane też są dane dostępowe, które można uzyskać kontaktując się z obsługą klienta tejże firmy.

Usługa została stworzona w technologii SOAP, w której do opisu struktury wykorzystywany jest plik WSDL, a zapytania wysyłane są w formie języka znaczników XML. Posiada ona wiele metod ale ja opiszę tylko te, które umożliwią wygenerowanie listu przewozowego, etykiet oraz zamówienie kuriera. Integrację utworzę w języku PHP.

Nawiązanie połączenia

W celu nawiązania połączenia należy skorzystać z wbudowanego klienta SOAP. DHL umożliwia połączenie w trybie produkcyjnym oraz testowym. Aby zmienić tryb połączenia należy wskazać odpowiedni adres pliku WSDL:

namespace DHL;
class DHL24_webapi_client extends SoapClient
{
    const WSDL = 'https://dhl24.com.pl/webapi2';
 
    public function __construct()
    {
        parent::__construct( self::WSDL );
    }
}

Dodatkowo tworzę nową klasę, która będzie zawierała zdefiniowane metody służące do przesyłania danych do serwera DHL.

include 'DHL24_webapi_client.php';
class Dhl24 {
	private $__client;

	public function __construct() {
		$this->__client =  new DHL24_webapi_client();
	}
}

Aby zobrazować poprawność połączenia oraz format zwracanych danych wykorzystam metodę getVersion(), która nie przyjmuje parametrów i do wykonania której nie są wymagane dane dostępowe.

public function getVersion() {
	$result = $this->__client->getVersion();
	print_r($result);
}

W odpowiedzi otrzymujemy:

 [getVersionResult] => 2.0 - serwer testowy

W ten sposób uzyskaliśmy połączenie z API firmy kurierskiej DHL i pobraliśmy informacje o jego aktualnej wersji.

Utworzenie pliku konfiguracyjnego

Dla metod, które zostaną opisane od tej części wpisu wymagane będą dane uwierzytelniające.

class Config {
	const
		AD_USERNAME = '',
		AD_PASSWORD = '',
		AD_SAP = '',
		C_SHIPMENT_ID = '',
		C_SHIPMENT_DATE = 'YYYY-MM_DD';
}

Klasa Config zawiera definicję pięciu stałych, które odpowiadają kolejno za nazwę użytkownika, hasło, numer klienta SAP, numer przesyłki, datę nadanie. Dwa ostatnie parametry ułatwią pracę z plikami zaprezentowanymi w tym wpisie.

Dodanie przesyłki do systemu – definicje struktur

W pierwszej kolejności postanowiłem utworzyć definicje struktur opisanych w dokumentacji. Są to najmniejsze części, które będą kolejno dołączane do zapytania. Dodatkowo sensowne jest dodanie pliku konfiguracyjnego aby później w łatwy sposób można było zmienić dane dostępowe.

namespace DHL\Structures;

class AuthData {
	private $__authData = [
		'username' => \Config::AD_USERNAME,
		'password' => \Config::AD_PASSWORD
	];

	public function getAuthData() {
		return $this->__authData;
	}
}

W tej klasie tworzymy tablicę zawierającą dane uwierzytelniające oraz metodę, która pozwoli nam dodać je w wymaganych miejscach.

namespace DHL\Structures;
class Address {
	private $__senderAddressData = [
		'country' => 'PL',
		'name' => 'Jan Kowalski',
		'postalCode' => '10001',
		'city' => 'Olsztyn',
		'street' => 'Test',
		'houseNumber' => '99',
		'contactPerson' => 'Jan Kowalski',
		'contactEmail' => 'jkowalski@example.net',
	];

	private $__receiverAddressData = [
		'country' => 'PL',
		'name' => 'Janina Nowak',
		'postalCode' => '00999',
		'city' => 'Warszawa',
		'street' => 'Test',
		'houseNumber' => '99',
		'contactPerson' => 'Janina Nowak',
		'contactEmail' => 'jnowak@example.net',
	];

	public function getReceiverAddressData(): array {
		return $this->__receiverAddressData;
	}

	public function getSenderAddressData(): array {
		return $this->__senderAddressData;
	}
}

W kolejnej metodzie tworzymy dane nadawcy oraz odbiorcy przesyłki.

namespace DHL\Structures;
class Piece {
	private $__pieceDefinition = [
		'type' => 'PACKAGE',
		'width' => 80,
		'height' => 40,
		'length' => 40,
		'weight' => 15,
		'quantity' => 1,
		'nonStandard' => false
	];

	public function getPieceDefinition() {
		return $this->__pieceDefinition;
	}
}

Klasa Piece zajmuje się definicją każdej z paczek w przesyłce. W tym przykładzie tylko dla jednej paczki.

namespace DHL\Structures;

class PaymentData {
	private $__paymentData = [
		'paymentMethod' => 'BANK_TRANSFER',
		'payerType' => 'SHIPPER',
		'accountNumber' => \Config::AD_SAP
	];

	public function getPaymentData(): array {
		return $this->__paymentData;
	}
}

PaymentData definiuje sposób płatności za zamówienie. Warto zwrócić uwagę na to, że to tutaj pojawia się zainicjowana w pliku konfiguracyjnym stała AD_SAP.

namespace DHL\Structures;
class ServiceDefinition {
	private $__serviceDefinition = [
		'product' => 'AH',
		'deliveryEvening' => false,
		'insurance' => true,
		'insuranceValue' => 150
	];

	public function getServiceDefinition(): array {
		return $this->__serviceDefinition;
	}
}

W tej klasie ustaliliśmy usługi jakie będzie zawierała przesyłka. Jedynym wymaganym parametrem w tej metodzie jest product (o ile nie ustalimy na wartość true parametrów insurance lub collectOnDelivery). Służy on do wyboru usługi kurierskiej. W tym przykładzie jest to AH, czyli przesyłka krajowa.

Tworzenie struktury danych, które mają być przesłane do API

Teraz można zgrupować dane, które są zawarte w wyżej przedstawionych metodach. Do tego celu utworzę kolejną klasę, która stworzy większą część zapytania wykorzystując mniejsze części zawarte w klasach omówionych wcześniej.

namespace DHL\Structures;

include 'Address.php';
include 'Piece.php';
include 'PaymentData.php';
include 'ServiceDefinition.php';
class ShipmentFullData {
	private $__address;
	private $__piece;
	private $__paymentData;
	private $__serviceDefinition;

	public function __construct() {
		$this->__address = new Address();
		$this->__piece = new Piece();
		$this->__paymentData = new PaymentData();
		$this->__serviceDefinition = new ServiceDefinition();
	}

	public function getShipmentFullData($shipmentDate) {
		$data['item'] = [
			'shipper' => $this->__address->getSenderAddressData(),
			'receiver' => $this->__address->getReceiverAddressData(),
			'pieceList' => [
				'item' => $this->__piece->getPieceDefinition()
			],
			'payment' => $this->__paymentData->getPaymentData(),
			'service' => $this->__serviceDefinition->getServiceDefinition(),
			'shipmentDate' => $shipmentDate,
			'content' => 'Something',
			'skipRestrictionCheck' => true
		];

		return $data;
	}
}

Zapytanie do API w celu utworzenia przesyłki w systemie DHL

W tej sekcji dodam do systemu DHL informacje o przesyłce przy pomocy metody createShipments(). Umożliwia ona dodanie wielu przesyłek do systemu ale aby uprościć proces dodam tylko jedną paczkę i ustawię wszystkie parametry “na sztywno”. To zapytanie jest największym pod względem przyjmowania liczby parametrów w systemie DHL24.

Pozostało już tylko dodać dane uwierzytelniające i odpytać API. W tym celu dodać należy kolejną metodę w klasie DHL24.

public function createShipments($shipmentDate) {
	$shipments = new ShipmentFullData();
	$params = [
		'authData' => $this->__authData->getAuthData(),
		'shipments' => $shipments->getShipmentFullData($shipmentDate)
	];
	$this->__client->createShipments($params);
	$this->__saveFiles();
}

Pojawiła się tu nowa metoda saveFiles(). Zajmuje się ona zapisywaniem do plików w formacie .xml wysyłanych i odbieranych danych.

Jeżeli wysłane dane nie zawierają błędów w odpowiedzi zostanie zwrócony obiekt XML. Zawiera on informacje o nadawcy i odbiorcy ale przede wszystkim jest tu zwracany numer przesyłki (shipmentId), który będzie wymagany do dalszych działań na przesyłce.

<SOAP-ENV:Envelope>
	<SOAP-ENV:Body>
		<ns1:createShipmentsResponse>
			<createShipmentsResult>
				<item>
					<shipmentId>90000760658</shipmentId>
					<created/>
					<shipper>
						<name>Jan Kowalski</name>
						<postalCode>10-001</postalCode>
						<city>Olsztyn</city>
						<street>Test</street>
						<houseNumber>99</houseNumber>
						<apartmentNumber/>
						<contactPerson>Jan Kowalski</contactPerson>
						<contactPhone/>
						<contactEmail>jkowalski@example.net</contactEmail>
					</shipper>
					<receiver>
						<name>Janina Nowak</name>
						<postalCode>00-999</postalCode>
						<city>Warszawa</city>
						<street>Test</street>
						<houseNumber>99</houseNumber>
						<apartmentNumber/>
						<contactPerson>Janina Nowak</contactPerson>
						<contactPhone/>
						<contactEmail>jnowak@example.net</contactEmail>
					</receiver>
					<orderStatus xsi:nil="true"/>
				</item>
			</createShipmentsResult>
		</ns1:createShipmentsResponse>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

W tej sekcji został utworzony szkic przesyłki w systemie DHL. Nie wiąże się to jeszcze z nadaniem przesyłki, które następuje po zamówieniu kuriera.

Pobieranie listu przewozowego i etykiet

Kolejnym etapem będzie pobranie wymaganych do nadania przesyłki dokumentów. W tym celu będzie potrzebny nam numer przesyłki uzyskany w poprzedniej sekcji wpisu. Wystarczy go (w tym przypadku) dodać ręcznie w pliku konfiguracyjnym.

Do wykonania tego zapytania będzie potrzebna kolejna ze struktur.

namespace DHL\Structures;
class ItemToPrint {
	private $__labels = [
		'BLP',
		'LBLP',
		'ZBLP'
	];

	private $__itemToPrint = [
		'item' => []
	];

	public function getItemToPrint($shipmentId, $type = 'protocol') {
		switch($type) {
			case 'protocol': {
				$item = [
					'labelType' => 'LP',
					'shipmentId' => $shipmentId
				];
				array_push($this->__itemToPrint['item'], $item);
				break;
			}
			case 'labels': {
				foreach ($this->__labels as $label) {
					$item = [
							'labelType' => $label,
							'shipmentId' => $shipmentId
					];
					array_push($this->__itemToPrint['item'], $item);
				}
			}
		}

		return $this->__itemToPrint;
	}
}

Tutaj generowane są dane do zapytania pobierającego list przewozowy i etykiety. W metodzie getItemToPrint()  dla parametru $type o wartości protocol zwracamy dane tylko dla listu przewozowego, a dla wartości labels w pętli dodajemy kolejne typy etykiet do zapytania. Zwrócić należy uwagę na to, że nie ma możliwości jednoczesnego pobrania listu przewozowego i etykiet dla przesyłki. Jest to zmiana, która została wprowadzona w nowej wersji API DHL (v2).

Pozostaje już tylko dodać dane uwierzytelniające i odebrać list przewozowy lub etykiety z API. W tym celu należy dodać kolejną metodę w klasie DHL24.

public function getLabels($shipmentId, $type) {
	$itemToPrint = new ItemToPrint();
	$params = [
		'authData' => $this->__authData->getAuthData(),
		'itemsToPrint' => $itemToPrint->getItemToPrint($shipmentId, $type)
	];
	$result = $this->__client->getLabels($params);

	$this->__saveLabels($result, $type);
}

Dane zostaną zwrócone jako struktura (lub tablica struktur) zawierająca numer przesyłki, typ etykiety, etykietę zakodowaną w metodą base64 oraz typ mime.

Zapis etykiety do pliku

Ostatnim krokiem będzie zapis etykiet do pliku co pozwoli na ich przechowanie i wydruk. Tutaj zajmuje się tym metoda saveLabels().

private function __saveLabels($data, $type) {
		if($type == 'protocol')
			$labels = $data->getLabelsResult;
		else
			$labels = $data->getLabelsResult->item;

		foreach ($labels as $label) {
			file_put_contents('labels/' . $label->labelName, base64_decode($label->labelData));
		}
	}

Znów należy rozróżnić typ etykiety, gdyż dla typu labels struktura będzie zawierała tablicę po której trzeba iterować. List przewozowy dla danych z wpisu będzie wyglądał następująco:

List przewozowy DHLZawiera on podstawowe informacje o przesyłce, które służą do jej identyfikacji i skierowania w odpowiednie miejsce.

Zamawianie kuriera

Ostatnim etapem będzie zamówienie kuriera po odbiór przesyłki. Do tego zapytania będą potrzebne już tylko dane uwierzytelniające, numer przesyłki, data nadania (taka sama jak na liście przewozowym) oraz preferowany przedział godzinowy przyjazdu kuriera.

public function bookCourier($shipmentId, $shipmentDate) {
	$params = [
		'authData' => $this->__authData->getAuthData(),
		'pickupDate' => $shipmentDate,
		'pickupTimeFrom' => '10:00',
		'pickupTimeTo' => '16:00',
		'shipmentIdList' => [
			$shipmentId
		]
	];
	$this->__client->bookCourier($params);

	$this->__saveFiles();
}

W odpowiedzi zostaną zwrócone następujące dane zawierające numer zamówienia kuriera. Może on służyć do anulowania zamówienia kuriera.

<SOAP-ENV:Envelope>
	<SOAP-ENV:Body>
		<ns1:bookCourierResponse>
			<bookCourierResult>
				<item>3650817WWW</item>
			</bookCourierResult>
		</ns1:bookCourierResponse>
	</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Obsługa błędów

Łącząc się z API zawsze należy oczekiwać jako odpowiedzi błędu lub brak odpowiedzi gdy serwer nie jest osiągalny. Warto więc każde wywołanie połączenie opatrzyć w bloki try-catch, które umożliwią zabezpieczenie aplikacji przed niespodziewanym zachowaniem po otrzymaniu błędu. Błąd w połączeniu za pomocą SOAP wygląda tak jak poniżej.

Fatal error: Uncaught SoapFault exception: [100] Nieprawidlowe dane autoryzacyjne

Podsumowanie

WebAPI DHL posiada dobrą dokumentację dostępną publicznie. Komunikaty o błędach są czytelne i często zawierają sposób rozwiązania problemu, np. podając datę nadania, w której kurier nie może odebrać przesyłki otrzymamy komunikat o błędzie z proponowanymi terminami kiedy jest to możliwe. Zdarzają się jednak błędy, które dotyczą części biznesowej.

Kod wykorzystywany w tym wpisie jest dostępny pod adresem https://github.com/Ermlab/establishing-connection-with-dhl-api.

Nazwy struktur oraz metod wykorzystanych we wpisie pokrywają się z ich opisem w dokumentacji API DHL.

Zdjęcie użyte w nagłówku pochodzi z: https://www.flickr.com/photos/viewpix69/15331721455/

Recommended Posts