├── tests
├── InvalidTracker.php
├── mock
│ ├── Fedex
│ │ └── FedexMock.php
│ ├── GLS
│ │ ├── in_transit.txt
│ │ ├── delivered.txt
│ │ └── pick_up.txt
│ ├── DHL
│ │ ├── delivered.txt
│ │ ├── warning.txt
│ │ ├── in_transit.txt
│ │ └── pickup.txt
│ ├── DHLExpress
│ │ ├── in_transit.txt
│ │ ├── delivered.txt
│ │ └── delivered_with_unordered_statuses.txt
│ └── PostCH
│ │ └── unknown.txt
├── ValidTracker.php
├── TestCase.php
├── Trackers
│ ├── FedexTest.php
│ ├── USPSTest.php
│ ├── PostCHTest.php
│ ├── PostATTest.php
│ ├── GLSTest.php
│ ├── UPSTest.php
│ ├── DHLExpressTest.php
│ └── DHLTest.php
├── Utils
│ ├── UtilsTest.php
│ └── AdditionalDetailsTest.php
├── FileMapperDataProvider.php
├── EventTest.php
├── TrackTest.php
└── ShipmentTrackerTest.php
├── .travis.yml
├── src
├── DataProviders
│ ├── DataProviderInterface.php
│ ├── PhpClient.php
│ ├── GuzzleClient.php
│ └── Registry.php
├── Utils
│ ├── Utils.php
│ ├── AdditionalDetails.php
│ └── XmlHelpers.php
├── Event.php
├── ShipmentTracker.php
├── Track.php
└── Trackers
│ ├── PostAT.php
│ ├── Fedex.php
│ ├── AbstractTracker.php
│ ├── USPS.php
│ ├── GLS.php
│ ├── DHLExpress.php
│ ├── PostCH.php
│ ├── UPS.php
│ └── DHL.php
├── .gitignore
├── phpunit.xml
├── composer.json
└── readme.md
/tests/InvalidTracker.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('fedex');
19 | }
20 |
21 | public function test_it_resolves_a_delivered_shipment()
22 | {
23 |
24 | $track = $this->tracker->track(746965179400);
25 |
26 | $this->assertTrue($track->delivered());
27 | $this->assertCount(9, $track->events());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/DataProviders/GuzzleClient.php:
--------------------------------------------------------------------------------
1 | client = new Client();
21 | }
22 |
23 |
24 | /**
25 | * Request the given url.
26 | *
27 | * @param $url
28 | * @param array $options
29 | *
30 | * @return string
31 | */
32 | public function get($url, $options = [])
33 | {
34 | return $this->client->get($url, $options)->getBody()->getContents();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sauladam/shipment-tracker",
3 | "description": "Parses tracking information for several carriers, like UPS, USPS, DHL and GLS by simply scraping the data. No need for any kind of API access.",
4 | "license": "MIT",
5 | "keywords": [
6 | "shipment",
7 | "parcel",
8 | "tracking",
9 | "ups",
10 | "dhl",
11 | "gls",
12 | "usps",
13 | "swiss post"
14 | ],
15 | "authors": [
16 | {
17 | "name": "Saulius Adamonis"
18 | }
19 | ],
20 | "require": {
21 | "php": ">=5.5.0",
22 | "guzzlehttp/guzzle": ">=3.9",
23 | "nesbot/carbon": ">=1.20",
24 | "ext-json": "*",
25 | "ext-dom": "*"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "~4.7"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "Sauladam\\ShipmentTracker\\": "src"
33 | }
34 | },
35 | "autoload-dev": {
36 | "files": [
37 | "tests/TestCase.php",
38 | "tests/FileMapperDataProvider.php"
39 | ]
40 | }
41 | }
--------------------------------------------------------------------------------
/src/DataProviders/Registry.php:
--------------------------------------------------------------------------------
1 | providers;
21 | }
22 |
23 |
24 | /**
25 | * Register a provider under the given name.
26 | *
27 | * @param string $name
28 | * @param DataProviderInterface $provider
29 | */
30 | public function register($name, DataProviderInterface $provider)
31 | {
32 | $this->providers[$name] = $provider;
33 | }
34 |
35 |
36 | /**
37 | * Get the provider with the given name.
38 | *
39 | * @param $name
40 | *
41 | * @return DataProviderInterface
42 | */
43 | public function get($name)
44 | {
45 | return $this->providers[$name];
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Utils/UtilsTest.php:
--------------------------------------------------------------------------------
1 | assertFalse(mb_check_encoding($string, 'utf-8'));
11 |
12 | $string = \Sauladam\ShipmentTracker\Utils\Utils::ensureUtf8($string);
13 |
14 | $this->assertTrue(mb_check_encoding($string, 'utf-8'));
15 | }
16 |
17 |
18 | /** @test */
19 | public function it_encodes_an_array_of_strings()
20 | {
21 | $strings = [
22 | utf8_decode('motörheád-1'),
23 | utf8_decode('motörheád-2'),
24 | utf8_decode('motörheád-2'),
25 | ];
26 |
27 | foreach ($strings as $string) {
28 | $this->assertFalse(mb_check_encoding($string, 'utf-8'));
29 | }
30 |
31 | $strings = \Sauladam\ShipmentTracker\Utils\Utils::ensureUtf8($strings);
32 |
33 | foreach ($strings as $string) {
34 | $this->assertTrue(mb_check_encoding($string, 'utf-8'));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Utils/AdditionalDetails.php:
--------------------------------------------------------------------------------
1 | additional[$key] = $data;
24 |
25 | return $this;
26 | }
27 |
28 |
29 | /**
30 | * Get additional information.
31 | *
32 | * @param string|null $key
33 | * @param null $default
34 | *
35 | * @return array
36 | */
37 | public function getAdditionalDetails($key = null, $default = null)
38 | {
39 | if (!$key) {
40 | return $this->additional;
41 | }
42 |
43 | return array_key_exists($key, $this->additional)
44 | ? $this->additional[$key]
45 | : $default;
46 | }
47 |
48 |
49 | /**
50 | * Check if there is additional information.
51 | *
52 | * @return bool
53 | */
54 | public function hasAdditionalDetails()
55 | {
56 | return !empty($this->additional);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Utils/AdditionalDetailsTest.php:
--------------------------------------------------------------------------------
1 | addAdditionalDetails('key', 'data');
11 |
12 | $this->assertTrue($testClass->hasAdditionalDetails());
13 | }
14 |
15 |
16 | /** @test */
17 | public function it_retrieves_additional_information_by_key()
18 | {
19 | $testClass = new AdditionalTestClass;
20 |
21 | $testClass->addAdditionalDetails('foo', 'bar');
22 |
23 | $this->assertSame('bar', $testClass->getAdditionalDetails('foo'));
24 | }
25 |
26 |
27 | /** @test */
28 | public function it_gets_all_additional_information_if_no_key_is_provided()
29 | {
30 | $testClass = new AdditionalTestClass;
31 |
32 | $testClass->addAdditionalDetails('foo', 'bar');
33 | $testClass->addAdditionalDetails('bar', 'baz');
34 |
35 | $this->assertCount(2, $testClass->getAdditionalDetails());
36 | }
37 |
38 |
39 | /** @test */
40 | public function it_returns_the_default_value_if_the_given_key_does_not_exist()
41 | {
42 | $testClass = new AdditionalTestClass;
43 |
44 | $this->assertSame('foo', $testClass->getAdditionalDetails('non-existent', 'foo'));
45 | }
46 | }
47 |
48 |
49 | class AdditionalTestClass
50 | {
51 | use \Sauladam\ShipmentTracker\Utils\AdditionalDetails;
52 | }
53 |
--------------------------------------------------------------------------------
/tests/FileMapperDataProvider.php:
--------------------------------------------------------------------------------
1 | carrier = $carrier;
32 |
33 | if (is_array($fileName)) {
34 | $this->lookup = $fileName;
35 | } else {
36 | $this->fileName = $fileName;
37 | }
38 | }
39 |
40 |
41 | /**
42 | * Request the given url.
43 | *
44 | * @param string $url
45 | *
46 | * @return string
47 | */
48 | public function get($url)
49 | {
50 | return file_get_contents($this->mapToFile($url));
51 | }
52 |
53 |
54 | /**
55 | * Map the url to a path.
56 | *
57 | * @param string $url
58 | *
59 | * @return string
60 | * @throws Exception
61 | */
62 | protected function mapToFile($url)
63 | {
64 | $basePath = __DIR__ . '/mock/' . $this->carrier . '/';
65 |
66 | if ($this->fileName) {
67 | return $basePath . $this->fileName;
68 | }
69 |
70 | if (!array_key_exists($url, $this->lookup)) {
71 | throw new Exception("Could not resolve URL [{$url}] to a path.");
72 | }
73 |
74 | return $basePath . $this->lookup[$url];
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/Trackers/USPSTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('USPS');
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_extends_the_abstract_tracker()
25 | {
26 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
27 | }
28 |
29 |
30 | /** @test */
31 | public function it_builds_the_tracking_url()
32 | {
33 | $url = $this->tracker->trackingUrl('RT654906222DE');
34 |
35 | $this->assertSame('https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=RT654906222DE', $url);
36 | }
37 |
38 |
39 | /** @test */
40 | public function it_accepts_additional_url_params()
41 | {
42 | $url = $this->tracker->trackingUrl('RT654906222DE', null, ['foo' => 'bar']);
43 |
44 | $this->assertSame('https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=RT654906222DE&foo=bar', $url);
45 | }
46 |
47 |
48 | /** @test */
49 | public function it_resolves_a_delivered_shipment()
50 | {
51 | $tracker = $this->getTracker('delivered.txt');
52 |
53 | $track = $tracker->track('RT654906222DE');
54 |
55 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
56 | $this->assertTrue($track->delivered());
57 | $this->assertNull($track->getRecipient());
58 | $this->assertCount(3, $track->events());
59 | }
60 |
61 |
62 | /** @test */
63 | public function it_resolves_an_an_in_transit_status_if_the_shipment_is_on_its_way()
64 | {
65 | $this->markTestSkipped("No real world data available at the moment.");
66 | return;
67 |
68 | $tracker = $this->getTracker('in_transit.txt');
69 |
70 | $track = $tracker->track('RT654907846DE');
71 |
72 | $this->assertSame(Track::STATUS_IN_TRANSIT, $track->currentStatus());
73 | $this->assertFalse($track->delivered());
74 | }
75 |
76 |
77 | /**
78 | * Build the tracker with a custom test client.
79 | *
80 | * @param $fileName
81 | *
82 | * @return AbstractTracker
83 | */
84 | protected function getTracker($fileName)
85 | {
86 | return $this->getTrackerMock('USPS', $fileName);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/EventTest.php:
--------------------------------------------------------------------------------
1 | event = new Event;
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_can_access_the_location()
25 | {
26 | $this->event->setLocation('some location');
27 |
28 | $this->assertSame('some location', $this->event->getLocation());
29 | }
30 |
31 |
32 | /** @test */
33 | public function it_can_access_the_date()
34 | {
35 | $this->event->setDate(Carbon::parse('2016-01-31'));
36 |
37 | $this->assertSame('2016-01-31', $this->event->getDate()->toDateString());
38 | }
39 |
40 |
41 | /** @test */
42 | public function it_converts_the_date_to_carbon_if_passed_as_string()
43 | {
44 | $this->event->setDate('2016-01-31');
45 |
46 | $this->assertInstanceOf(Carbon::class, $this->event->getDate());
47 | $this->assertSame('2016-01-31', $this->event->getDate()->toDateString());
48 | }
49 |
50 |
51 | /** @test */
52 | public function it_can_access_the_description()
53 | {
54 | $this->event->setDescription('some description');
55 |
56 | $this->assertSame('some description', $this->event->getDescription());
57 | }
58 |
59 |
60 | /** @test */
61 | public function it_can_access_the_status()
62 | {
63 | $this->event->setStatus('delivered');
64 |
65 | $this->assertSame('delivered', $this->event->getStatus());
66 | }
67 |
68 |
69 | /** @test */
70 | public function it_can_access_additional_information()
71 | {
72 | $this->event->addAdditionalDetails('foo', 'additional info');
73 |
74 | $this->assertTrue($this->event->hasAdditionalDetails());
75 | $this->assertSame('additional info', $this->event->getAdditionalDetails('foo'));
76 | }
77 |
78 |
79 | /** @test */
80 | public function it_can_build_an_event_from_array()
81 | {
82 | $data = [
83 | 'location' => 'some location',
84 | 'date' => Carbon::parse('2016-01-31'),
85 | 'description' => 'some description',
86 | 'status' => 'delivered',
87 | ];
88 |
89 | $event = Event::fromArray($data);
90 |
91 | $this->assertInstanceOf(Event::class, $event);
92 | $this->assertSame('some location', $event->getLocation());
93 | $this->assertSame('2016-01-31', $event->getDate()->toDateString());
94 | $this->assertSame('some description', $event->getDescription());
95 | $this->assertSame('delivered', $event->getStatus());
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/tests/TrackTest.php:
--------------------------------------------------------------------------------
1 | track = new Track;
19 | }
20 |
21 |
22 | /** @test */
23 | public function it_can_access_the_events()
24 | {
25 | $event = $this->getEvents();
26 |
27 | $this->track->addEvent($event);
28 |
29 | $this->assertTrue($this->track->hasEvents());
30 | $this->assertCount(1, $this->track->events());
31 | }
32 |
33 |
34 | /** @test */
35 | public function it_gets_the_latest_event()
36 | {
37 | $event1 = $this->getEvents();
38 | $event2 = $this->getEvents();
39 | $event3 = $this->getEvents();
40 |
41 | $this->track->addEvent($event1);
42 | $this->track->addEvent($event2);
43 | $this->track->addEvent($event3);
44 |
45 | $this->assertCount(3, $this->track->events());
46 | $this->assertSame($event3, $this->track->latestEvent());
47 | }
48 |
49 |
50 | /** @test */
51 | public function it_sorts_the_events_by_date_in_descending_order()
52 | {
53 | $event1 = Event::fromArray(['date' => '2016-01-31 14:00:00']);
54 | $event2 = Event::fromArray(['date' => '2016-01-31 15:00:00']);
55 | $event3 = Event::fromArray(['date' => '2016-01-31 16:00:00']);
56 |
57 | $this->track->addEvent($event3);
58 | $this->track->addEvent($event1);
59 | $this->track->addEvent($event2);
60 |
61 | $this->assertSame($event3, $this->track->events()[0]);
62 | $this->assertSame($event1, $this->track->events()[1]);
63 | $this->assertSame($event2, $this->track->events()[2]);
64 |
65 | $this->track->sortEvents();
66 |
67 | $this->assertSame($event3, $this->track->events()[0]);
68 | $this->assertSame($event2, $this->track->events()[1]);
69 | $this->assertSame($event1, $this->track->events()[2]);
70 | }
71 |
72 |
73 | /**
74 | * @param int $count
75 | *
76 | * @return Event|Event[]
77 | */
78 | protected function getEvents($count = 1)
79 | {
80 | $eventData = [
81 | 'location' => 'some location',
82 | 'date' => '2016-01-31',
83 | 'description' => 'some description',
84 | 'status' => 'delivered',
85 | ];
86 |
87 | if ($count == 1) {
88 | return Event::fromArray($eventData);
89 | }
90 |
91 | $events = [];
92 |
93 | for ($x = 1; $x <= $count; $x++) {
94 | $events[] = Event::fromArray($eventData);
95 | }
96 |
97 | return $events;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/mock/GLS/in_transit.txt:
--------------------------------------------------------------------------------
1 | {"tuStatus":[{"arrivalTime":{"name":"Zustellung voraussichtlich:","value":"14:00 - 15:30"},"deliveryOwnerCode":"DE03","history":[{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"18:55:35","evtDscr":"Das Paket hat das Paketzentrum verlassen."},{"address":{"city":"Erding","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"13:56:34","evtDscr":"Das Paket wurde erfolgreich zugestellt."},{"address":{"city":"Erding","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"06:49:33","evtDscr":"Das Paket wird voraussichtlich im Laufe des Tages zugestellt."},{"address":{"city":"Erding","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"06:45:13","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"00:00:49","evtDscr":"Das Paket ist im Paketzentrum eingetroffen; das Paket wurde manuell sortiert."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"00:00:48","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Santa Perpètua de Mogoda","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-15","time":"00:56:43","evtDscr":"Das Paket hat das Paketzentrum verlassen."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"19:29:38","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"19:29:38","evtDscr":"Das Paket wurde an GLS übergeben."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"10:26:36","evtDscr":"Die Paketdaten wurden im GLS IT-System erfasst; das Paket wurde noch nicht an GLS übergeben."}],"infos":[{"type":"WEIGHT","name":"Gewicht:","value":"0.2 kg"},{"type":"PRODUCT","name":"Produkt:","value":"EuroBusinessSmallParcel"}],"owners":[{"type":"REQUEST","code":"ES01"},{"type":"DELIVERY","code":"DE03"}],"progressBar":{"retourFlag":false,"statusText":"Unterwegs","statusInfo":"INTRANSIT","evtNos":["1.0","3.0","11.0","2.0","2.106","2.0","1.0","2.29","0.0","0.100"],"colourIndex":3,"level":50,"statusBar":[{"status":"PREADVICE","imageStatus":"COMPLETE","imageText":"Datenerfassung","statusText":""},{"status":"INTRANSIT","imageStatus":"CURRENT","imageText":"Unterwegs","statusText":"Das Paket ist unterwegs zum Ziel-Paketzentrum."},{"status":"INWAREHOUSE","imageStatus":"PENDING","imageText":"Ziel-Paketzentrum","statusText":""},{"status":"INDELIVERY","imageStatus":"PENDING","imageText":"In Zustellung","statusText":""},{"status":"DELIVERED","imageStatus":"PENDING","imageText":"Zugestellt","statusText":""}]},"references":[{"type":"UNITNO","name":"Paketnummer:","value":"32631986451"},{"type":"UNIQUENO","name":"Track ID","value":"Z51UTO2B"},{"type":"GLSREF","name":"Nationale Kundenreferenz","value":"320000433"}],"signature":{"name":"Unterschrift:","value":"true","validate":true},"tuNo":"32631986451","changeDeliveryPossible":false}]}
--------------------------------------------------------------------------------
/tests/ShipmentTrackerTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(DHL::class, $tracker);
19 | }
20 |
21 |
22 | /** @test */
23 | public function it_resolves_the_gls_tracker()
24 | {
25 | $tracker = ShipmentTracker::get('GLS');
26 |
27 | $this->assertInstanceOf(GLS::class, $tracker);
28 | }
29 |
30 |
31 | /** @test */
32 | public function it_resolves_the_ups_tracker()
33 | {
34 | $tracker = ShipmentTracker::get('UPS');
35 |
36 | $this->assertInstanceOf(UPS::class, $tracker);
37 | }
38 |
39 |
40 | /** @test */
41 | public function it_resolves_the_usps_tracker()
42 | {
43 | $tracker = ShipmentTracker::get('USPS');
44 |
45 | $this->assertInstanceOf(USPS::class, $tracker);
46 | }
47 |
48 |
49 | /** @test */
50 | public function it_resolves_the_post_ch_tracker()
51 | {
52 | $tracker = ShipmentTracker::get('PostCH');
53 |
54 | $this->assertInstanceOf(PostCH::class, $tracker);
55 | }
56 |
57 | /** @test */
58 | public function it_resolves_the_fedex_tracker()
59 | {
60 | $tracker = ShipmentTracker::get('Fedex');
61 |
62 | $this->assertInstanceOf(Fedex::class, $tracker);
63 | }
64 |
65 |
66 | /**
67 | * @test
68 | * @expectedException Exception
69 | */
70 | public function it_throws_an_exception_if_the_carrier_is_unknown()
71 | {
72 | ShipmentTracker::get('some-nonexistent-tracker');
73 | }
74 |
75 | /**
76 | * @test
77 | */
78 | public function testSetCustomizeCarrier()
79 | {
80 | try {
81 | ShipmentTracker::get('foo-carrier');
82 | $this->fail();
83 | } catch (\Exception $exception) {
84 | $this->assertInstanceOf(Exception::class, $exception);
85 | ShipmentTracker::set('foo-carrier', ValidTracker::class);
86 | $tracker = ShipmentTracker::get('foo-carrier');
87 | $this->assertInstanceOf(ValidTracker::class, $tracker);
88 | }
89 | try{
90 | ShipmentTracker::set('bar-carrier', InvalidTracker::class);
91 | } catch (\Exception $exception) {
92 | $this->assertInstanceOf(\InvalidArgumentException::class, $exception);
93 | }
94 | try{
95 | ShipmentTracker::set('baz-carrier', 'NotExistingCarrier');
96 | } catch (\Exception $exception) {
97 | $this->assertInstanceOf(\InvalidArgumentException::class, $exception);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/Trackers/PostCHTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('PostCH');
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_extends_the_abstract_tracker()
25 | {
26 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
27 | }
28 |
29 |
30 | /** @test */
31 | public function it_builds_the_tracking_url()
32 | {
33 | $url = $this->tracker->trackingUrl('RB592593703DE');
34 |
35 | $this->assertSame(
36 | 'https://service.post.ch/EasyTrack/submitParcelData.do?formattedParcelCodes=RB592593703DE&lang=de',
37 | $url
38 | );
39 | }
40 |
41 |
42 | /** @test */
43 | public function it_can_override_the_language_for_the_url()
44 | {
45 | $url = $this->tracker->trackingUrl('RB592593703DE', 'en');
46 |
47 | $this->assertSame(
48 | 'https://service.post.ch/EasyTrack/submitParcelData.do?formattedParcelCodes=RB592593703DE&lang=en',
49 | $url
50 | );
51 | }
52 |
53 |
54 | /** @test */
55 | public function it_accepts_additional_url_params()
56 | {
57 | $url = $this->tracker->trackingUrl('RB592593703DE', null, ['foo' => 'bar']);
58 |
59 | $this->assertSame(
60 | 'https://service.post.ch/EasyTrack/submitParcelData.do?formattedParcelCodes=RB592593703DE&lang=de&foo=bar',
61 | $url
62 | );
63 | }
64 |
65 |
66 | /** @test */
67 | public function it_resolves_a_delivered_shipment()
68 | {
69 | $this->markTestSkipped();
70 |
71 | $tracker = $this->getTracker('delivered.txt');
72 |
73 | $track = $tracker->track('RB592593703DE');
74 |
75 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
76 | $this->assertTrue($track->delivered());
77 | $this->assertCount(8, $track->events());
78 | }
79 |
80 | /** @test */
81 | public function it_resolves_an_unknown_status_if_there_is_not_data_available_yet()
82 | {
83 | $this->markTestSkipped();
84 |
85 | $tracker = $this->getTracker('unknown.txt');
86 |
87 | $track = $tracker->track('RB592593703DE');
88 |
89 | $this->assertSame(Track::STATUS_UNKNOWN, $track->currentStatus());
90 | $this->assertFalse($track->delivered());
91 | $this->assertCount(0, $track->events());
92 | }
93 |
94 |
95 | /**
96 | * Build the tracker with a custom test client.
97 | *
98 | * @param $fileName
99 | *
100 | * @return AbstractTracker
101 | */
102 | protected function getTracker($fileName)
103 | {
104 | return $this->getTrackerMock('PostCH', $fileName);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Event.php:
--------------------------------------------------------------------------------
1 | location;
42 | }
43 |
44 |
45 | /**
46 | * Set the location.
47 | *
48 | * @param $location
49 | *
50 | * @return $this
51 | */
52 | public function setLocation($location)
53 | {
54 | $this->location = $location;
55 |
56 | return $this;
57 | }
58 |
59 |
60 | /**
61 | * Get the date.
62 | *
63 | * @return Carbon
64 | */
65 | public function getDate()
66 | {
67 | return $this->date;
68 | }
69 |
70 |
71 | /**
72 | * Set the date.
73 | *
74 | * @param string|Carbon $date
75 | *
76 | * @return $this
77 | */
78 | public function setDate($date)
79 | {
80 | $date = $date instanceof Carbon ? $date : Carbon::parse($date);
81 |
82 | $this->date = $date;
83 |
84 | return $this;
85 | }
86 |
87 |
88 | /**
89 | * Get the description.
90 | *
91 | * @return string
92 | */
93 | public function getDescription()
94 | {
95 | return $this->description;
96 | }
97 |
98 |
99 | /**
100 | * Set the description.
101 | *
102 | * @param $description
103 | *
104 | * @return $this
105 | */
106 | public function setDescription($description)
107 | {
108 | $this->description = Utils::ensureUtf8($description);
109 |
110 | return $this;
111 | }
112 |
113 |
114 | /**
115 | * Get the status during this event.
116 | *
117 | * @return string
118 | */
119 | public function getStatus()
120 | {
121 | return $this->status;
122 | }
123 |
124 |
125 | /**
126 | * Set the status.
127 | *
128 | * @param $status
129 | *
130 | * @return $this
131 | */
132 | public function setStatus($status)
133 | {
134 | $this->status = $status;
135 |
136 | return $this;
137 | }
138 |
139 |
140 | /**
141 | * Create an event from the given array.
142 | *
143 | * @param array $data
144 | *
145 | * @return Event
146 | */
147 | public static function fromArray(array $data)
148 | {
149 | $event = new self;
150 |
151 | $eligibleKeys = ['date', 'location', 'description', 'status'];
152 |
153 | $data = array_intersect_key($data, array_flip($eligibleKeys));
154 |
155 | foreach ($data as $key => $value) {
156 | $setter = 'set' . ucfirst($key);
157 |
158 | $event->{$setter}($value);
159 | }
160 |
161 | return $event;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/tests/Trackers/PostATTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('PostAT');
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_extends_the_abstract_tracker()
25 | {
26 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
27 | }
28 |
29 |
30 | /** @test */
31 | public function it_builds_the_tracking_url()
32 | {
33 | $url = $this->tracker->trackingUrl('RC320145308DE');
34 |
35 | $this->assertSame(
36 | 'https://www.post.at/sendungsverfolgung.php/details?pnum1=RC320145308DE',
37 | $url
38 | );
39 | }
40 |
41 |
42 | /** @test */
43 | public function it_can_override_the_language_for_the_url()
44 | {
45 | $url = $this->tracker->trackingUrl('RC320145308DE', 'en');
46 |
47 | $this->assertSame(
48 | 'https://www.post.at/en/track_trace.php/details?pnum1=RC320145308DE',
49 | $url
50 | );
51 | }
52 |
53 |
54 | /** @test */
55 | public function it_accepts_additional_url_params()
56 | {
57 | $url = $this->tracker->trackingUrl('RC320145308DE', null, ['foo' => 'bar']);
58 |
59 | $this->assertSame(
60 | 'https://www.post.at/sendungsverfolgung.php/details?pnum1=RC320145308DE&foo=bar',
61 | $url
62 | );
63 | }
64 |
65 |
66 | /** @test */
67 | public function it_resolves_a_delivered_shipment()
68 | {
69 | $tracker = $this->getTracker('delivered.txt');
70 |
71 | $track = $tracker->track('RC320145342DE');
72 |
73 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
74 | $this->assertTrue($track->delivered());
75 | $this->assertCount(6, $track->events());
76 | }
77 |
78 |
79 | /** @test */
80 | public function it_resolves_an_in_transit_shipment()
81 | {
82 | $tracker = $this->getTracker('in_transit.txt');
83 |
84 | $track = $tracker->track('RC320145308DE');
85 |
86 | $this->assertSame(Track::STATUS_IN_TRANSIT, $track->currentStatus());
87 | $this->assertFalse($track->delivered());
88 | $this->assertCount(4, $track->events());
89 | }
90 |
91 |
92 | /** @test */
93 | public function it_resolves_an_pick_up_shipment()
94 | {
95 | $tracker = $this->getTracker('pick_up.txt');
96 |
97 | $track = $tracker->track('RC320145223DE');
98 |
99 | $this->assertSame(Track::STATUS_PICKUP, $track->currentStatus());
100 | $this->assertFalse($track->delivered());
101 | $this->assertCount(7, $track->events());
102 | }
103 |
104 |
105 | /**
106 | * Build the tracker with a custom test client.
107 | *
108 | * @param $fileName
109 | *
110 | * @return AbstractTracker
111 | */
112 | protected function getTracker($fileName)
113 | {
114 | return $this->getTrackerMock('PostAT', $fileName);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/tests/mock/GLS/delivered.txt:
--------------------------------------------------------------------------------
1 | {"tuStatus":[{"owners":[{"type":"REQUEST","code":"DE03"},{"type":"DELIVERY","code":"DE03"}],"deliveryOwnerCode":"DE03","tuNo":"50346007538","progressBar":{"retourFlag":false,"statusText":"Zugestellt","statusInfo":"DELIVERED","evtNos":["3.0","2.124","3.124","90.132","4.40","90.132","11.0","2.0","1.0","2.0","2.0","0.0","0.100"],"colourIndex":4,"level":100},"signature":{"name":"Unterschrift:","value":"TANGER(2760236908)"},"changeDeliveryPossible":false,"infos":[{"type":"WEIGHT","name":"Gewicht:","value":"3.5 kg"},{"type":"PRODUCT","name":"Produkt:","value":"BusinessSmallParcel"}],"references":[{"type":"UNITNO","name":"Paketnummer:","value":"50346007538"},{"type":"NOTECARDID","name":"Track-ID","value":"37ETGI"},{"type":"GLSREF","name":"Nationale Kundenreferenz","value":"52285"},{"type":"CUSTREF","name":"Kundeneigene Referenznummer","value":"YH24307-2016"}],"notificationCardId":"37ETGI","parcelShop":{"psNo":2012689312,"psID":"2760236908"},"history":[{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-08","time":"18:09:21","evtDscr":"Das Paket wurde beim Empfänger zugestellt."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"14:04:10","evtDscr":"Das Paket ist im GLS PaketShop eingetroffen."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"13:41:04","evtDscr":"Das Paket wurde im GLS PaketShop zugestellt (siehe Sendungsinformationen)"},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"13:23:41","evtDscr":"Der Empfänger wurde mittels Benachrichtigungskarte über den Zustell-/Abholversuch informiert."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"13:23:41","evtDscr":"Das Paket konnte nicht zugestellt werden, da der Empfänger nicht angetroffen wurde."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"13:22:00","evtDscr":"Der Empfänger wurde mittels Benachrichtigungskarte über den Zustell-/Abholversuch informiert."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"07:24:34","evtDscr":"Das Paket wurde auf das GLS Zustellfahrzeug verladen und ist für die Zustellung im Laufe des Tages vorgesehen."},{"address":{"city":"Bernau OT Schönow","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-07","time":"07:18:57","evtDscr":"Das Paket ist im GLS Standort eingetroffen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-06","time":"19:30:41","evtDscr":"Das Paket hat den GLS Standort verlassen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-06","time":"19:30:41","evtDscr":"Das Paket ist im GLS Standort eingetroffen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-06","time":"19:28:58","evtDscr":"Das Paket ist im GLS Standort eingetroffen."},{"address":{"city":"Bornheim","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-05","time":"18:57:40","evtDscr":"Das Paket wurde an das GLS System übergeben."},{"address":{"city":"Bornheim","countryCode":"DE","countryName":"Deutschland"},"date":"2016-01-05","time":"14:19:14","evtDscr":"Die Paketdaten wurden im GLS IT-System erfasst; das Paket wurde noch nicht an GLS übergeben."}]}]}
2 |
--------------------------------------------------------------------------------
/src/ShipmentTracker.php:
--------------------------------------------------------------------------------
1 | useDataProvider('custom') : $tracker;
47 | }
48 |
49 | /**
50 | * Registers a customize carrier class
51 | * @param string $carrier
52 | * @param string $carrierClass
53 | * @throws \InvalidArgumentException
54 | */
55 | public static function set($carrier, $carrierClass)
56 | {
57 | if (!static::isValidCarrierClass($carrierClass)) {
58 | throw new \InvalidArgumentException(sprintf('The carrier class "%s" is invalid', $carrierClass));
59 | }
60 | static::$customizeTrackerClasses[$carrier] = $carrierClass;
61 | }
62 |
63 | protected static function isValidCarrierClass($carrierClass)
64 | {
65 | return class_exists($carrierClass) && is_subclass_of($carrierClass, AbstractTracker::class);
66 | }
67 |
68 | /**
69 | * Get the registry for the data providers.
70 | *
71 | * @param DataProviderInterface $customProvider
72 | *
73 | * @return Registry
74 | */
75 | protected static function getDataProviderRegistry(DataProviderInterface $customProvider = null)
76 | {
77 | $registry = new Registry;
78 |
79 | $registry->register('guzzle', new GuzzleClient);
80 | $registry->register('php', new PhpClient);
81 |
82 | if ($customProvider) {
83 | $registry->register('custom', $customProvider);
84 | }
85 |
86 | return $registry;
87 | }
88 |
89 | /**
90 | * Check if a tracker exists for the given carrier.
91 | *
92 | * @param string $carrier
93 | *
94 | * @return bool
95 | */
96 | protected static function isValidCarrier($carrier)
97 | {
98 | return class_exists(self::$carriersNamespace . '\\' . $carrier);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/Trackers/GLSTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('GLS');
19 | }
20 |
21 |
22 | /** @test */
23 | public function it_extends_the_abstract_tracker()
24 | {
25 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
26 | }
27 |
28 |
29 | /** @test */
30 | public function it_builds_the_tracking_url()
31 | {
32 | $url = $this->tracker->trackingUrl('123456789');
33 |
34 | $this->assertSame('https://gls-group.eu/DE/de/paketverfolgung?match=123456789', $url);
35 | }
36 |
37 |
38 | /** @test */
39 | public function it_can_override_the_language_for_the_url()
40 | {
41 | $url = $this->tracker->trackingUrl('123456789', 'en');
42 |
43 | $this->assertSame('https://gls-group.eu/DE/en/parcel-tracking?match=123456789', $url);
44 | }
45 |
46 |
47 | /** @test */
48 | public function it_resolves_an_in_transit_shipment()
49 | {
50 | $tracker = $this->getTracker('in_transit.txt');
51 |
52 | $track = $tracker->track('Z51UTO2B');
53 |
54 | $this->assertSame(\Sauladam\ShipmentTracker\Track::STATUS_IN_TRANSIT, $track->currentStatus());
55 | $this->assertFalse($track->delivered());
56 | $this->assertCount(10, $track->events());
57 | }
58 |
59 |
60 | /** @test */
61 | public function it_resolves_a_delivered_shipment()
62 | {
63 | $tracker = $this->getTracker('delivered.txt');
64 |
65 | $track = $tracker->track('50346007538');
66 |
67 | $this->assertSame(\Sauladam\ShipmentTracker\Track::STATUS_DELIVERED, $track->currentStatus());
68 | $this->assertTrue($track->delivered());
69 | $this->assertCount(13, $track->events());
70 | }
71 |
72 |
73 | /** @test */
74 | public function it_resolves_the_recipient_for_a_delivered_shipment()
75 | {
76 | $tracker = $this->getTracker('delivered.txt');
77 |
78 | $track = $tracker->track('50346007538');
79 |
80 | $this->assertSame('TANGER(2760236908)', $track->getRecipient());
81 | }
82 |
83 |
84 | /** @test */
85 | public function it_sets_the_parcel_shop_details_if_it_the_parcel_was_or_is_delivered_to_a_parcel_shop()
86 | {
87 | $tracker = $this->getTracker('pick_up.txt');
88 |
89 | $track = $tracker->track('50346007538');
90 |
91 | $this->assertNotEmpty($track->getAdditionalDetails('parcelShop'));
92 | }
93 |
94 |
95 | /** @test */
96 | public function it_stores_the_gls_event_number_for_each_event()
97 | {
98 | $tracker = $this->getTracker('delivered.txt');
99 |
100 | $track = $tracker->track('50346007538');
101 |
102 | foreach ($track->events() as $event) {
103 | $this->assertNotEmpty($event->getAdditionalDetails('eventNumber'));
104 | }
105 | }
106 |
107 |
108 | /**
109 | * Build the tracker with a custom test client.
110 | *
111 | * @param string|array $fileName
112 | *
113 | * @return AbstractTracker
114 | */
115 | protected function getTracker($fileName)
116 | {
117 | return $this->getTrackerMock('GLS', $fileName);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Track.php:
--------------------------------------------------------------------------------
1 | events = $events;
43 | }
44 |
45 |
46 | /**
47 | * Get all events.
48 | *
49 | * @return Event[]
50 | */
51 | public function events()
52 | {
53 | return $this->events;
54 | }
55 |
56 |
57 | /**
58 | * Add an event.
59 | *
60 | * @param Event $event
61 | *
62 | * @return $this
63 | */
64 | public function addEvent(Event $event)
65 | {
66 | $this->events[] = $event;
67 |
68 | return $this;
69 | }
70 |
71 |
72 | /**
73 | * Check if the shipment has been delivered.
74 | *
75 | * @return bool
76 | */
77 | public function delivered()
78 | {
79 | $deliveredEvents = array_filter($this->events, function (Event $event) {
80 | return $event->getStatus() == self::STATUS_DELIVERED;
81 | });
82 |
83 | return !empty($deliveredEvents);
84 | }
85 |
86 |
87 | /**
88 | * Get the current status.
89 | *
90 | * @return string
91 | */
92 | public function currentStatus()
93 | {
94 | $latestEvent = $this->latestEvent();
95 |
96 | return $latestEvent ? $latestEvent->getStatus() : self::STATUS_UNKNOWN;
97 | }
98 |
99 |
100 | /**
101 | * Get the latest event.
102 | *
103 | * @return null|Event
104 | */
105 | public function latestEvent()
106 | {
107 | if (!$this->hasEvents()) {
108 | return null;
109 | }
110 |
111 | return $this->eventsAreSorted ? reset($this->events) : end($this->events);
112 | }
113 |
114 |
115 | /**
116 | * Check if this track has any events.
117 | *
118 | * @return bool
119 | */
120 | public function hasEvents()
121 | {
122 | return !empty($this->events);
123 | }
124 |
125 |
126 | /**
127 | * Get the recipient.
128 | *
129 | * @return string
130 | */
131 | public function getRecipient()
132 | {
133 | return $this->recipient;
134 | }
135 |
136 |
137 | /**
138 | * Set the recipient.
139 | *
140 | * @param string $recipient
141 | *
142 | * @return $this
143 | */
144 | public function setRecipient($recipient)
145 | {
146 | $this->recipient = Utils::ensureUtf8($recipient);
147 |
148 | return $this;
149 | }
150 |
151 |
152 | /**
153 | * Sort the events by date in descending oder, so that the latest event is always
154 | * the first item in the array.
155 | *
156 | * @return $this
157 | */
158 | public function sortEvents()
159 | {
160 | usort($this->events, function (Event $a, Event $b) {
161 | if ($a->getDate()->toDateTimeString() == $b->getDate()->toDateTimeString()) {
162 | return 0;
163 | }
164 |
165 | return ($a->getDate()->toDateTimeString() > $b->getDate()->toDateTimeString()) ? -1 : 1;
166 | });
167 |
168 | $this->eventsAreSorted = true;
169 |
170 | return $this;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/Utils/XmlHelpers.php:
--------------------------------------------------------------------------------
1 | getNodeValueWithLineBreaks($element)
30 | : $element->nodeValue;
31 |
32 | $value = trim($value);
33 |
34 | return preg_replace(['/\s\s+/', '/\p{Zs}/u'], ' ', $value);
35 | }
36 |
37 |
38 | /**
39 | * Get the node value but mark line breaks with a '|' (pipe).
40 | *
41 | * @param DOMText|DOMNode $element
42 | * @return string
43 | */
44 | protected function getNodeValueWithLineBreaks($element)
45 | {
46 | if (!$element->hasChildNodes()) {
47 | return $element->nodeValue;
48 | }
49 |
50 | $value = '';
51 |
52 | foreach ($element->childNodes as $node) {
53 | if ($node->nodeType != XML_ELEMENT_NODE || $node->nodeName != 'br') {
54 | $value .= $this->getNodeValue($node);
55 | continue;
56 | }
57 |
58 | $value .= "|";
59 | }
60 |
61 | return rtrim($value, '|');
62 | }
63 |
64 |
65 | /**
66 | * Get the description for the given terms.
67 | *
68 | * @param $term
69 | * @param DOMXPath $xpath
70 | * @param bool $withLineBreaks
71 | * @param string $termTag
72 | * @param string $descriptionTag
73 | *
74 | * @return null|string
75 | */
76 | protected function getDescriptionForTerm(
77 | $term,
78 | DOMXPath $xpath,
79 | $withLineBreaks = false,
80 | $termTag = 'dt',
81 | $descriptionTag = 'dd'
82 | )
83 | {
84 | $terms = is_array($term) ? $term : [$term];
85 |
86 | $listNodes = $xpath->query("//dl");
87 |
88 | if (!$listNodes) {
89 | return null;
90 | }
91 |
92 | $descriptionPairBelongsToTerm = false;
93 |
94 | foreach ($listNodes as $list) {
95 | foreach ($list->childNodes as $descriptionNode) {
96 | if (get_class($descriptionNode) != DOMElement::class) {
97 | continue;
98 | }
99 |
100 | if ($descriptionNode->tagName == $descriptionTag && $descriptionPairBelongsToTerm) {
101 | return $this->getNodeValue($descriptionNode, $withLineBreaks);
102 | }
103 |
104 | if ($descriptionNode->tagName != $termTag) {
105 | continue;
106 | }
107 |
108 | $descriptionTerm = $this->getNodeValue($descriptionNode);
109 |
110 | foreach ($terms as $term) {
111 | if ($this->startsWith($term, $descriptionTerm)) {
112 | $descriptionPairBelongsToTerm = true;
113 | break;
114 | }
115 | }
116 | }
117 | }
118 |
119 | return null;
120 | }
121 |
122 |
123 | /**
124 | * Check if the subject starts with the given string.
125 | *
126 | * @param string $start
127 | * @param string $subject
128 | * @return bool
129 | */
130 | protected function startsWith($start, $subject)
131 | {
132 | return strpos($subject, $start) === 0;
133 | }
134 |
135 |
136 | /**
137 | * Create an XPath instance from the given string.
138 | *
139 | * @param $string
140 | * @return DOMXPath
141 | */
142 | public function toXpath($string)
143 | {
144 | $dom = new DOMDocument;
145 | @$dom->loadHTML($string);
146 | $dom->preserveWhiteSpace = false;
147 |
148 | return new DOMXPath($dom);
149 | }
150 |
151 |
152 | /**
153 | * Convert the DomNodeList to an array.
154 | *
155 | * @param DOMNodeList $list
156 | * @return array|DOMNodeList
157 | */
158 | public function nodeListToArray(DOMNodeList $list)
159 | {
160 | return iterator_to_array($list);
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/Trackers/PostAT.php:
--------------------------------------------------------------------------------
1 | 'https://www.post.at/sendungsverfolgung.php/details',
17 | 'en' => 'https://www.post.at/en/track_trace.php/details',
18 | ];
19 |
20 | protected $language = 'de';
21 |
22 |
23 | /**
24 | * Build the url to the user friendly tracking site. In most
25 | * cases this is also the endpoint, but sometimes the tracking
26 | * data must be retrieved from another endpoint.
27 | *
28 | * @param string $trackingNumber
29 | * @param string|null $language
30 | * @param array $params
31 | *
32 | * @return string
33 | */
34 | public function trackingUrl($trackingNumber, $language = null, $params = [])
35 | {
36 | $params = array_merge($this->trackingUrlParams, [
37 | 'pnum1' => $trackingNumber,
38 | ], $params);
39 |
40 | $language = in_array($language, array_keys($this->serviceEndpoints))
41 | ? $language
42 | : $this->language;
43 |
44 | return $this->serviceEndpoints[$language] . '?' . http_build_query($params);
45 | }
46 |
47 | /**
48 | * Build the response array.
49 | *
50 | * @param string $response
51 | *
52 | * @return Track
53 | */
54 | protected function buildResponse($response)
55 | {
56 | $rows = $this->toXpath($response)->query("//div[@class='sendungsstatus-history']//ul//li");
57 |
58 | if (!$rows) {
59 | throw new Exception("Could not parse tracking information for [{$this->parcelNumber}].");
60 | }
61 |
62 | return array_reduce($this->nodeListToArray($rows), function ($track, $row) {
63 | return $track->addEvent($this->eventFromRow($row));
64 | }, new Track)->sortEvents();
65 | }
66 |
67 |
68 | /**
69 | * Parse the event data from the given node.
70 | *
71 | * @param \DOMNode $row
72 | *
73 | * @return Event
74 | */
75 | protected function eventFromRow($row)
76 | {
77 | preg_match(
78 | '/(date|datum): ([\d.:\s]+)(.*?)(?:; (.*)|$)/i',
79 | utf8_decode($this->getNodeValue($row)),
80 | $matches
81 | );
82 |
83 | return count($matches) < 4
84 | ? new Event
85 | : Event::fromArray([
86 | 'date' => Carbon::parse($matches[2]),
87 | 'description' => $matches[3],
88 | 'location' => isset($matches[4]) ? $matches[4] : '',
89 | 'status' => $this->resolveStatus($matches[3]),
90 | ]);
91 | }
92 |
93 |
94 | /**
95 | * Match a shipping status from the given description.
96 | *
97 | * @param $statusDescription
98 | *
99 | * @return string
100 | */
101 | protected function resolveStatus($statusDescription)
102 | {
103 | $statuses = [
104 | Track::STATUS_DELIVERED => [
105 | 'Delivered',
106 | 'Zugestellt',
107 | ],
108 | Track::STATUS_IN_TRANSIT => [
109 | 'Item posted abroad',
110 | 'Postaufgabe im Ausland',
111 | 'Item ready for international transport',
112 | 'Sendung für Auslandstransport bereit',
113 | 'Item in process of delivery',
114 | 'Sendung in Zustellung',
115 | 'Item being processed in Austria',
116 | 'Sendung in Bearbeitung Österreich',
117 | 'Item arrived in Austria',
118 | 'Sendung in Österreich angekommen',
119 | 'soon ready for pick up',
120 | 'In Kürze abholbereit',
121 | ],
122 | Track::STATUS_PICKUP => [
123 | 'ready for pick up',
124 | 'Sendung abholbereit',
125 | ],
126 | ];
127 |
128 | foreach ($statuses as $status => $needles) {
129 | foreach ($needles as $needle) {
130 | if (stripos($statusDescription, $needle) !== false) {
131 | return $status;
132 | }
133 | }
134 | }
135 |
136 | return Track::STATUS_UNKNOWN;
137 | }
138 | }
--------------------------------------------------------------------------------
/tests/Trackers/UPSTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('UPS');
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_extends_the_abstract_tracker()
25 | {
26 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
27 | }
28 |
29 |
30 | /** @test */
31 | public function it_builds_the_tracking_url()
32 | {
33 | $url = $this->tracker->trackingUrl('1ZW5244V6870200569');
34 |
35 | $this->assertSame(
36 | 'https://www.ups.com/track?loc=de_DE&tracknum=1ZW5244V6870200569',
37 | $url
38 | );
39 | }
40 |
41 |
42 | /** @test */
43 | public function it_can_override_the_language_for_the_url()
44 | {
45 | $url = $this->tracker->trackingUrl('1ZW5244V6870200569', 'en');
46 |
47 | $this->assertSame(
48 | 'https://www.ups.com/track?loc=en_US&tracknum=1ZW5244V6870200569',
49 | $url
50 | );
51 | }
52 |
53 |
54 | /** @test */
55 | public function it_accepts_additional_url_params()
56 | {
57 | $url = $this->tracker->trackingUrl('1ZW5244V6870200569', null, ['foo' => 'bar']);
58 |
59 | $this->assertSame(
60 | 'https://www.ups.com/track?loc=de_DE&tracknum=1ZW5244V6870200569&foo=bar',
61 | $url
62 | );
63 | }
64 |
65 |
66 | /** @test */
67 | public function it_resolves_a_delivered_shipment()
68 | {
69 | $this->markTestSkipped("Tests coming soon.");
70 |
71 | $tracker = $this->getTracker('delivered.txt');
72 |
73 | $track = $tracker->track('1ZW5244V6870200569');
74 |
75 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
76 | $this->assertSame('PROSENITCH', $track->getRecipient());
77 | $this->assertTrue($track->delivered());
78 | $this->assertCount(13, $track->events());
79 | }
80 |
81 |
82 | /** @test */
83 | public function it_resolves_an_exception_if_there_is_a_problem()
84 | {
85 | $this->markTestSkipped("Tests coming soon.");
86 |
87 | $tracker = $this->getTracker('exception.txt');
88 |
89 | $track = $tracker->track('1ZW5244V6870129110');
90 |
91 | $this->assertSame(Track::STATUS_EXCEPTION, $track->currentStatus());
92 | $this->assertContains('The street number is incorrect.', $track->latestEvent()->getDescription());
93 | }
94 |
95 |
96 | /** @test */
97 | public function it_resolves_an_an_in_transit_status_if_the_shipment_is_on_its_way()
98 | {
99 | $this->markTestSkipped("Tests coming soon.");
100 |
101 | $tracker = $this->getTracker('in_transit.txt');
102 |
103 | $track = $tracker->track('1ZW5244V6870200470');
104 |
105 | $this->assertSame(Track::STATUS_IN_TRANSIT, $track->currentStatus());
106 | $this->assertFalse($track->delivered());
107 | }
108 |
109 |
110 | /** @test */
111 | public function it_resolves_a_shipment_that_has_to_be_picked_up()
112 | {
113 | $this->markTestSkipped("Tests coming soon.");
114 |
115 | $tracker = $this->getTracker('pickup.txt');
116 |
117 | $track = $tracker->track('1ZW5244V6870294478');
118 |
119 | $this->assertSame(Track::STATUS_PICKUP, $track->currentStatus());
120 | $this->assertFalse($track->delivered());
121 | $this->assertNull($track->getRecipient());
122 | $this->assertCount(10, $track->events());
123 | }
124 |
125 |
126 | /** @test */
127 | public function it_parses_the_access_point_details_and_the_pickup_due_date()
128 | {
129 | $this->markTestSkipped("Tests coming soon.");
130 |
131 | $tracker = $this->getTracker('pickup.txt');
132 |
133 | $track = $tracker->track('1ZW5244V6870294478');
134 |
135 | $this->assertSame('REITERSHOP|13 WEIMARER STRASSE|WIEN, 1180 AT', $track->getAdditionalDetails('accessPoint'));
136 | $this->assertInstanceOf(\Carbon\Carbon::class, $track->getAdditionalDetails('pickupDueDate'));
137 | $this->assertSame("2017-04-24", $track->getAdditionalDetails('pickupDueDate')->format('Y-m-d'));
138 | }
139 |
140 |
141 | /**
142 | * Build the tracker with a custom test client.
143 | *
144 | * @param $fileName
145 | *
146 | * @return AbstractTracker
147 | */
148 | protected function getTracker($fileName)
149 | {
150 | return $this->getTrackerMock('UPS', $fileName);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/tests/mock/DHL/delivered.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DHL Sendungsverfolgung
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Sie müssen JavaScript aktivieren um diesen Teil der Seite zu sehen.
62 |
63 |
64 |
65 | Sendungsnummer: 00340434162530583962
66 | Status: Die Sendung wurde erfolgreich zugestellt.
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/tests/mock/DHL/warning.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DHL Sendungsverfolgung
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Sie müssen JavaScript aktivieren um diesen Teil der Seite zu sehen.
62 |
63 |
64 |
65 | Sendungsnummer: 00340434162530584006
66 | Status: Die Sendung konnte nicht zugestellt werden.
67 |
68 | Nächster Schritt: Die Sendung wird in eine Filiale gebracht.
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/tests/Trackers/DHLExpressTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('DHLExpress');
21 | }
22 |
23 |
24 | /** @test */
25 | public function it_extends_the_abstract_tracker()
26 | {
27 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
28 | }
29 |
30 |
31 | /** @test */
32 | public function it_builds_the_correct_tracker_class()
33 | {
34 | $this->assertInstanceOf(DHLExpress::class, $this->tracker);
35 | }
36 |
37 |
38 | /** @test */
39 | public function it_builds_the_tracking_url()
40 | {
41 | $englishUrl = $this->tracker->trackingUrl('123456789', 'en');
42 | $germanUrl = $this->tracker->trackingUrl('123456789', 'de');
43 |
44 | $prefix = 'http://www.dhl.com/en/hidden/component_library/express/local_express/dhl_de_tracking/';
45 |
46 | $this->assertSame($prefix . 'en/tracking_dhlde.html?AWB=123456789&brand=DHL', $englishUrl);
47 | $this->assertSame($prefix . 'de/sendungsverfolgung_dhlde.html?AWB=123456789&brand=DHL', $germanUrl);
48 | }
49 |
50 |
51 | /** @test */
52 | public function it_accepts_additional_url_params()
53 | {
54 | $url = $this->tracker->trackingUrl('123456789', null, ['foo' => 'bar']);
55 |
56 | $prefix = 'http://www.dhl.com/en/hidden/component_library/express/local_express/dhl_de_tracking/';
57 |
58 | $this->assertSame(
59 | $prefix . 'de/sendungsverfolgung_dhlde.html?AWB=123456789&brand=DHL&foo=bar',
60 | $url
61 | );
62 | }
63 |
64 |
65 | /** @test */
66 | public function it_resolves_a_delivered_shipment()
67 | {
68 | $tracker = $this->getTracker('delivered.txt');
69 |
70 | $track = $tracker->track('5765159960');
71 |
72 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
73 | $this->assertTrue($track->delivered());
74 | $this->assertCount(16, $track->events());
75 | }
76 |
77 |
78 | /** @test */
79 | public function it_resolves_the_recipient_for_a_delivered_shipment()
80 | {
81 | $tracker = $this->getTracker('delivered.txt');
82 |
83 | $track = $tracker->track('5765159960');
84 |
85 | $this->assertSame('SDA', $track->getRecipient());
86 | }
87 |
88 |
89 | /** @test */
90 | public function it_resolves_a_shipment_that_is_in_transit()
91 | {
92 | $tracker = $this->getTracker('in_transit.txt');
93 |
94 | $track = $tracker->track('5765159960');
95 |
96 | $this->assertSame(Track::STATUS_IN_TRANSIT, $track->currentStatus());
97 | $this->assertFalse($track->delivered());
98 | $this->assertNull($track->getRecipient());
99 | $this->assertCount(15, $track->events());
100 | }
101 |
102 |
103 | /** @test */
104 | public function it_resolves_a_shipment_as_delivered_even_if_the_statuses_are_not_in_chronological_order()
105 | {
106 | $tracker = $this->getTracker('delivered_with_unordered_statuses.txt');
107 |
108 | $track = $tracker->track('5765159960');
109 |
110 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
111 | $this->assertTrue($track->delivered());
112 | $this->assertSame($track->getRecipient(), 'SDA');
113 | $this->assertCount(16, $track->events());
114 | }
115 |
116 |
117 | /** @test */
118 | public function it_adds_the_pieces_ids_to_the_track()
119 | {
120 | $tracker = $this->getTracker('delivered.txt');
121 |
122 | $track = $tracker->track('5765159960');
123 |
124 | $pieces = $track->getAdditionalDetails('pieces');
125 |
126 | $this->assertCount(1, $pieces);
127 | $this->assertSame('JD014600004444917061', $pieces[0]);
128 | }
129 |
130 |
131 | /** @test */
132 | public function it_adds_the_pieces_ids_to_the_events()
133 | {
134 | $tracker = $this->getTracker('delivered.txt');
135 |
136 | $track = $tracker->track('5765159960');
137 |
138 | foreach ($track->events() as $event) {
139 | if (!$event->hasAdditionalDetails()) {
140 | continue;
141 | }
142 |
143 | $pieces = $event->getAdditionalDetails('pieces');
144 |
145 | $this->assertCount(1, $pieces);
146 | $this->assertSame('JD014600004444917061', $pieces[0]);
147 | }
148 | }
149 |
150 |
151 | /**
152 | * Build the tracker with a custom test client.
153 | *
154 | * @param $fileName
155 | *
156 | * @return AbstractTracker
157 | */
158 | protected function getTracker($fileName)
159 | {
160 | return $this->getTrackerMock('DHLExpress', $fileName);
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/tests/mock/DHL/in_transit.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DHL Sendungsverfolgung
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Sie müssen JavaScript aktivieren um diesen Teil der Seite zu sehen.
62 |
63 |
64 |
65 | Sendungsnummer: 00340434162530583993
66 | Status: Die Sendung wurde in das Zustellfahrzeug geladen.
67 |
68 | Nächster Schritt: Die Sendung wird dem Empfänger voraussichtlich heute zugestellt.
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/tests/mock/GLS/pick_up.txt:
--------------------------------------------------------------------------------
1 | {"tuStatus":[{"arrivalTime":{"name":"Zugestellt am","value":"19.11.2018 um 10:22 Uhr"},"deliveryOwnerCode":"DE03","history":[{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-19","time":"10:47:21","evtDscr":"Das Paket ist im GLS PaketShop eingetroffen."},{"address":{"city":"Brechen","countryCode":"DE","countryName":"Deutschland","name":"Gläser Tankstellen GmbH"},"date":"2018-11-19","time":"10:22:03","evtDscr":"Das Paket wurde im GLS PaketShop zugestellt (siehe PaketShop-Anzeige)."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-19","time":"10:20:25","evtDscr":"Das Paket konnte nicht zugestellt werden, da der Empfänger nicht angetroffen wurde."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-19","time":"06:40:20","evtDscr":"Das Paket wird voraussichtlich im Laufe des Tages zugestellt."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-19","time":"04:21:40","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"22:38:00","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"16:25:52","evtDscr":"Das Paket befindet sich im Ziel-Paketzentrum. Es konnte nicht zugestellt werden, da der Empfänger nicht angetroffen wurde."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"12:21:47","evtDscr":"Das Paket konnte nicht zugestellt werden, da der Empfänger nicht angetroffen wurde."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"06:49:24","evtDscr":"Das Paket wird voraussichtlich im Laufe des Tages zugestellt."},{"address":{"city":"Polch","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"04:50:51","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"00:00:51","evtDscr":"Das Paket ist im Paketzentrum eingetroffen; das Paket wurde manuell sortiert."},{"address":{"city":"Neuenstein","countryCode":"DE","countryName":"Deutschland"},"date":"2018-11-16","time":"00:00:50","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"Santa Perpètua de Mogoda","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-15","time":"00:56:52","evtDscr":"Das Paket hat das Paketzentrum verlassen."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"19:29:52","evtDscr":"Das Paket ist im Paketzentrum eingetroffen."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"19:29:52","evtDscr":"Das Paket wurde an GLS übergeben."},{"address":{"city":"San Juan de Mozarrifar","countryCode":"ES","countryName":"Spanien"},"date":"2018-11-14","time":"14:09:06","evtDscr":"Die Paketdaten wurden im GLS IT-System erfasst; das Paket wurde noch nicht an GLS übergeben."}],"infos":[{"type":"WEIGHT","name":"Gewicht:","value":"0.2 kg"},{"type":"PRODUCT","name":"Produkt:","value":"EuroBusinessSmallParcel"}],"notificationCardId":"4V6YVD","owners":[{"type":"REQUEST","code":"ES01"},{"type":"DELIVERY","code":"DE03"}],"parcelShop":{"psNo":2003273262,"psID":"2760219021","address":{"postalArea":{"city":"Brechen","postalCode":"65611","postalCodeDisplay":"65611","countryCode":"DE","province":""},"phone":{"countryPrefix":"49","number":"06438836974"},"mobile":{"countryPrefix":"","number":""},"fax":{"countryPrefix":"49","number":"06438836975"},"gpsPos":{"latitude":"50.36158","longitude":"8.1707"},"name1":"Gläser Tankstellen GmbH","name2":"ARAL Tankstelle","street1":"Limburger Str.","street2":"","blockNo1":"15-19","blockNo2":""},"openingHours":[{"hours":"060000,,,210000","days":"Mo"},{"hours":"060000,,,210000","days":"Di"},{"hours":"060000,,,210000","days":"Mi"},{"hours":"060000,,,210000","days":"Do"},{"hours":"060000,,,210000","days":"Fr"},{"hours":"070000,,,210000","days":"Sa"},{"hours":"080000,,,210000","days":"So"}]},"progressBar":{"retourFlag":false,"statusText":"Zugestellt im PaketShop","statusInfo":"DELIVEREDPS","evtNos":["2.124","3.124","4.40","11.0","2.0","2.29","35.482","4.40","11.0","2.0","2.106","2.0","1.0","2.29","0.0","0.100"],"colourIndex":4,"level":75,"statusBar":[{"status":"PREADVICE","imageStatus":"COMPLETE","imageText":"Datenerfassung","statusText":""},{"status":"INTRANSIT","imageStatus":"COMPLETE","imageText":"Unterwegs","statusText":""},{"status":"INWAREHOUSE","imageStatus":"COMPLETE","imageText":"Ziel-Paketzentrum","statusText":""},{"status":"INDELIVERY","imageStatus":"COMPLETE","imageText":"In Zustellung","statusText":""},{"status":"DELIVEREDPS","imageStatus":"CURRENT","imageText":"Zugestellt","statusText":"Das Paket wurde erfolgreich im PaketShop zugestellt."}]},"references":[{"type":"UNITNO","name":"Paketnummer:","value":"32631986704"},{"type":"UNIQUENO","name":"Track ID","value":"Z51UTO9C"},{"type":"GLSREF","name":"Nationale Kundenreferenz","value":"320000433"},{"type":"NOTECARDID","name":"Track-ID","value":"4V6YVD"},{"type":"NOTECARDID","name":"Track-ID","value":"4UJRI8"}],"signature":{"name":"Unterschrift:","value":"true","validate":true},"tuNo":"32631986704","changeDeliveryPossible":false}]}
--------------------------------------------------------------------------------
/src/Trackers/Fedex.php:
--------------------------------------------------------------------------------
1 | getDataProvider()->client->post($this->serviceEndpoint, $this->buildRequest())
27 | ->getBody()
28 | ->getContents();
29 |
30 | } catch (\Exception $e) {
31 | throw new \Exception("Could not fetch tracking data for [{$this->parcelNumber}].");
32 | }
33 | }
34 |
35 | /**
36 | * @return array
37 | */
38 | protected function buildRequest()
39 | {
40 | return [
41 | 'headers' => [
42 | 'Accept' => 'application/json',
43 | ],
44 |
45 | 'form_params' => [
46 | 'data' => $this->buildDataArray(),
47 | 'action' => 'trackpackages',
48 | ],
49 | ];
50 | }
51 |
52 | /**
53 | * @return false|string
54 | */
55 | protected function buildDataArray()
56 | {
57 | $array = [
58 | 'TrackPackagesRequest' => [
59 | 'trackingInfoList' => [
60 | [
61 | 'trackNumberInfo' => [
62 | 'trackingNumber' => $this->parcelNumber,
63 | ]
64 | ]
65 | ]
66 | ]
67 | ];
68 |
69 | return json_encode($array);
70 | }
71 |
72 | /**
73 | * Build the url to the user friendly tracking site. In most
74 | * cases this is also the endpoint, but sometimes the tracking
75 | * data must be retrieved from another endpoint.
76 | *
77 | * @param string $trackingNumber
78 | * @param string|null $language
79 | * @param array $params
80 | *
81 | * @return string
82 | */
83 | public function trackingUrl($trackingNumber, $language = null, $params = [])
84 | {
85 | return $this->trackingUrl . '?tracknumbers=' . $trackingNumber;
86 | }
87 |
88 | /**
89 | * Build the response array.
90 | *
91 | * @param string $response
92 | *
93 | * @return \Sauladam\ShipmentTracker\Track
94 | */
95 | protected function buildResponse($response)
96 | {
97 | $contents = json_decode($response, true)['TrackPackagesResponse']['packageList'][0];
98 |
99 | $track = new Track;
100 |
101 | foreach ($contents['scanEventList'] as $scanEvent) {
102 | $track->addEvent(Event::fromArray([
103 | 'location' => $scanEvent['scanLocation'],
104 | 'description' => $scanEvent['status'],
105 | 'date' => $this->getDate($scanEvent),
106 | 'status' => $status = $this->resolveState($scanEvent)
107 | ]));
108 |
109 | if ($status == Track::STATUS_DELIVERED && isset($contents['receivedByNm'])) {
110 | $track->setRecipient($contents['receivedByNm']);
111 | }
112 | }
113 |
114 | if (isset($contents['totalKgsWgt'])) {
115 | $track->addAdditionalDetails('totalKgsWgt', $contents['totalKgsWgt']);
116 | }
117 |
118 | if (isset($contents['totalLbsWgt'])) {
119 | $track->addAdditionalDetails('totalLbsWgt', $contents['totalLbsWgt']);
120 | }
121 |
122 | return $track->sortEvents();
123 | }
124 |
125 | /**
126 | * Parse the date from the given strings.
127 | *
128 | * @param array $scanEvent
129 | *
130 | * @return \Carbon\Carbon
131 | */
132 | protected function getDate($scanEvent)
133 | {
134 | return Carbon::parse(
135 | $this->convert("{$scanEvent['date']}T{$scanEvent['time']}{$scanEvent['gmtOffset']}")
136 | );
137 | }
138 |
139 | /**
140 | * Convert unicode characters
141 | *
142 | * @param string $string
143 | * @return string
144 | */
145 | protected function convert($string)
146 | {
147 | if (PHP_MAJOR_VERSION >= 7)
148 | return preg_replace('/(?<=\\\u)(.{4})/', '{$1}', $string);
149 | else
150 | return str_replace('\\u002d', '-', $string);
151 | }
152 |
153 | /**
154 | * Match a shipping status from the given short code.
155 | *
156 | * @param $status
157 | *
158 | * @return string
159 | */
160 | protected function resolveState($status)
161 | {
162 | switch ($status['statusCD']) {
163 | case 'PU':
164 | case 'OC':
165 | case 'AR':
166 | case 'DP':
167 | case 'OD':
168 | return Track::STATUS_IN_TRANSIT;
169 | case 'DL':
170 | return Track::STATUS_DELIVERED;
171 | default:
172 | return Track::STATUS_UNKNOWN;
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Trackers/AbstractTracker.php:
--------------------------------------------------------------------------------
1 | dataProviderRegistry = $providers;
50 | }
51 |
52 |
53 | /**
54 | * Track the given number.
55 | *
56 | * @param string $number
57 | * @param string|null $language
58 | * @param array $params
59 | *
60 | * @return Track
61 | */
62 | public function track($number, $language = null, $params = [])
63 | {
64 | $this->parcelNumber = $number;
65 |
66 | $this->extractUrlParams($params);
67 |
68 | if ($language) {
69 | $this->setLanguage($language);
70 | }
71 |
72 | $url = $this->getEndpointUrl($number, $language, $params);
73 |
74 | $contents = $this->fetch($url);
75 |
76 | return $this->buildResponse($contents);
77 | }
78 |
79 |
80 | /**
81 | * Set the tracking-URL and endpoint-URLs params if any were given.
82 | * If non of them were specified explicitly, set the same params
83 | * for bot URLs.
84 | *
85 | * @param $params
86 | */
87 | protected function extractUrlParams($params)
88 | {
89 | if (!array_key_exists('tracking_url', $params) && !array_key_exists('endpoint_url', $params)) {
90 | $this->trackingUrlParams = $this->endpointUrlParams = $params;
91 |
92 | return;
93 | }
94 |
95 | $this->trackingUrlParams = array_key_exists('tracking_url', $params) ? $params['tracking_url'] : [];
96 | $this->endpointUrlParams = array_key_exists('endpoint_url', $params) ? $params['endpoint_url'] : [];
97 | }
98 |
99 |
100 | /**
101 | * Set the default data provider.
102 | *
103 | * @param string $name
104 | *
105 | * @return $this
106 | */
107 | public function useDataProvider($name)
108 | {
109 | $this->defaultDataProvider = $name;
110 |
111 | return $this;
112 | }
113 |
114 |
115 | /**
116 | * Get the currently set default data provider.
117 | *
118 | * @return string
119 | */
120 | public function getDefaultDataProvider()
121 | {
122 | return $this->defaultDataProvider;
123 | }
124 |
125 |
126 | /**
127 | * Set the language iso code for the results.
128 | *
129 | * @param string $lang
130 | *
131 | * @return $this
132 | */
133 | protected function setLanguage($lang)
134 | {
135 | if (strlen($lang) !== 2) {
136 | $message = "Invalid language [{$lang}].";
137 | throw new \InvalidArgumentException($message);
138 | }
139 |
140 | $this->language = strtolower($lang);
141 |
142 | return $this;
143 | }
144 |
145 |
146 | /**
147 | * Get the data provider.
148 | *
149 | * @return DataProviderInterface
150 | */
151 | protected function getDataProvider()
152 | {
153 | return $this->dataProviderRegistry->get($this->defaultDataProvider);
154 | }
155 |
156 |
157 | /**
158 | * Get the contents of the given url.
159 | *
160 | * @param string $url
161 | *
162 | * @return string
163 | */
164 | protected function fetch($url)
165 | {
166 | return $this->getDataProvider()->get($url);
167 | }
168 |
169 |
170 | /**
171 | * Build the endpoint url
172 | *
173 | * @param string $trackingNumber
174 | * @param string|null $language
175 | * @param array $params
176 | *
177 | * @return string
178 | */
179 | protected function getEndpointUrl($trackingNumber, $language = null, $params = [])
180 | {
181 | return $this->trackingUrl($trackingNumber, $language, $params);
182 | }
183 |
184 |
185 | /**
186 | * Build the url to the user friendly tracking site. In most
187 | * cases this is also the endpoint, but sometimes the tracking
188 | * data must be retrieved from another endpoint.
189 | *
190 | * @param string $trackingNumber
191 | * @param string|null $language
192 | * @param array $params
193 | *
194 | * @return string
195 | */
196 | abstract public function trackingUrl($trackingNumber, $language = null, $params = []);
197 |
198 |
199 | /**
200 | * Build the response array.
201 | *
202 | * @param string $response
203 | *
204 | * @return array
205 | */
206 | abstract protected function buildResponse($response);
207 | }
208 |
--------------------------------------------------------------------------------
/tests/Trackers/DHLTest.php:
--------------------------------------------------------------------------------
1 | tracker = ShipmentTracker::get('DHL');
20 | }
21 |
22 |
23 | /** @test */
24 | public function it_extends_the_abstract_tracker()
25 | {
26 | $this->assertInstanceOf(AbstractTracker::class, $this->tracker);
27 | }
28 |
29 |
30 | /** @test */
31 | public function it_builds_the_tracking_url()
32 | {
33 | $url = $this->tracker->trackingUrl('123456789');
34 |
35 | $this->assertSame('http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=de&idc=123456789', $url);
36 | }
37 |
38 |
39 | /** @test */
40 | public function it_can_override_the_language_for_the_url()
41 | {
42 | $url = $this->tracker->trackingUrl('123456789', 'en');
43 |
44 | $this->assertSame('http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=123456789', $url);
45 | }
46 |
47 |
48 | /** @test */
49 | public function it_accepts_additional_url_params()
50 | {
51 | $url = $this->tracker->trackingUrl('123456789', null, ['foo' => 'bar']);
52 |
53 | $this->assertSame(
54 | 'http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=de&idc=123456789&foo=bar',
55 | $url
56 | );
57 | }
58 |
59 |
60 | /** @test */
61 | public function it_resolves_a_delivered_shipment()
62 | {
63 | $tracker = $this->getTracker('delivered.txt');
64 |
65 | $track = $tracker->track('00340434162530533196');
66 |
67 | $this->assertSame(Track::STATUS_DELIVERED, $track->currentStatus());
68 | $this->assertTrue($track->delivered());
69 | $this->assertCount(5, $track->events());
70 | }
71 |
72 |
73 | /** @test */
74 | public function it_resolves_the_recipient_for_a_delivered_shipment()
75 | {
76 | $tracker = $this->getTracker('delivered.txt');
77 |
78 | $track = $tracker->track('00340434162530533196');
79 |
80 | $this->assertSame('Empfänger (orig.)', $track->getRecipient());
81 | }
82 |
83 |
84 | /** @test */
85 | public function it_resolves_a_shipment_that_has_to_be_picked_up()
86 | {
87 | $tracker = $this->getTracker('pickup.txt');
88 |
89 | $track = $tracker->track('00340434162530531062');
90 |
91 | $this->assertSame(Track::STATUS_PICKUP, $track->currentStatus());
92 | $this->assertFalse($track->delivered());
93 | $this->assertNull($track->getRecipient());
94 | $this->assertCount(6, $track->events());
95 | }
96 |
97 |
98 | /** @test */
99 | public function it_resolves_a_shipment_that_is_in_transit()
100 | {
101 | $tracker = $this->getTracker('in_transit.txt');
102 |
103 | $track = $tracker->track('00340434162530534551');
104 |
105 | $this->assertSame(Track::STATUS_IN_TRANSIT, $track->currentStatus());
106 | $this->assertFalse($track->delivered());
107 | $this->assertNull($track->getRecipient());
108 | $this->assertCount(4, $track->events());
109 | }
110 |
111 | /** @test */
112 | public function it_resolves_a_shipment_with_a_warning()
113 | {
114 | $tracker = $this->getTracker('warning.txt');
115 |
116 | $track = $tracker->track('00340434162530584006');
117 |
118 | $this->assertSame(Track::STATUS_WARNING, $track->currentStatus());
119 | $this->assertFalse($track->delivered());
120 | $this->assertNull($track->getRecipient());
121 | $this->assertCount(5, $track->events());
122 | }
123 |
124 | /** @test */
125 | public function it_resolves_a_shipment_with_an_exception()
126 | {
127 | $this->markTestSkipped("No data for testing available yet.");
128 |
129 | $tracker = $this->getTracker('exception.txt');
130 |
131 | $track = $tracker->track('00340434162530533851');
132 |
133 | $this->assertSame(Track::STATUS_EXCEPTION, $track->currentStatus());
134 | $this->assertFalse($track->delivered());
135 | $this->assertNull($track->getRecipient());
136 | $this->assertCount(5, $track->events());
137 | }
138 |
139 |
140 | /** @test */
141 | public function it_resolves_a_shipment_as_delivered_even_if_the_statuses_are_not_in_chronological_order()
142 | {
143 | $this->markTestSkipped("No data for testing available yet.");
144 |
145 | $tracker = $this->getTracker('delivered_with_unordered_statuses.txt');
146 |
147 | $track = $tracker->track('00340433924192991025');
148 |
149 | $this->assertNotSame(Track::STATUS_DELIVERED, $track->currentStatus());
150 | $this->assertTrue($track->delivered());
151 | $this->assertSame($track->getRecipient(), 'Neighbor');
152 | $this->assertCount(5, $track->events());
153 | }
154 |
155 |
156 | /**
157 | * Build the tracker with a custom test client.
158 | *
159 | * @param $fileName
160 | *
161 | * @return AbstractTracker
162 | */
163 | protected function getTracker($fileName)
164 | {
165 | return $this->getTrackerMock('DHL', $fileName);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Trackers/USPS.php:
--------------------------------------------------------------------------------
1 | trackingUrlParams;
44 |
45 | $qry = http_build_query(array_merge([
46 | 'qtc_tLabels1' => $trackingNumber,
47 | ], $additionalParams));
48 |
49 | return $this->serviceEndpoint . '?' . $qry;
50 | }
51 |
52 |
53 | /**
54 | * Build the track.
55 | *
56 | * @param string $response
57 | *
58 | * @return Track
59 | */
60 | protected function buildResponse($response)
61 | {
62 | $dom = new DOMDocument;
63 | @$dom->loadHTML($response);
64 | $dom->preserveWhiteSpace = false;
65 |
66 | $domxpath = new DOMXPath($dom);
67 |
68 | return $this->getTrack($domxpath);
69 | }
70 |
71 |
72 | /**
73 | * Get the shipment status history.
74 | *
75 | * @param DOMXPath $xpath
76 | *
77 | * @return Track
78 | * @throws \Exception
79 | */
80 | protected function getTrack(DOMXPath $xpath)
81 | {
82 | $rowsContainer = $xpath->query("//div[@id='trackingHistory_1']//div[contains(@class,'panel-actions-content')]")->item(0);
83 |
84 | $items = [];
85 | $index = 0;
86 |
87 | foreach ($rowsContainer->childNodes as $child) {
88 | if (isset($child->tagName) && $child->tagName == 'h3') {
89 | continue;
90 | }
91 |
92 | if (isset($child->tagName) && $child->tagName == 'hr') {
93 | $index++;
94 | continue;
95 | }
96 |
97 | $value = $this->getNodeValue($child);
98 |
99 | if (!empty($value)) {
100 | $items[$index][] = $value;
101 | }
102 | }
103 |
104 | $realEvents = array_filter($items, function ($eventRows) {
105 | // filter only those data-portions that are at least 3 lines long, i.e. contain the date,
106 | // the location and a description. Otherwise it's not a real event, maybe just a short
107 | // info text like "Inbound Into Customs" - not sure where to put that, so just leave it alone.
108 | return count($eventRows) >= 3;
109 | });
110 |
111 | $track = new Track;
112 |
113 | foreach ($realEvents as $eventData) {
114 | $track->addEvent(Event::fromArray([
115 | 'date' => $this->getDate($eventData[0]),
116 | 'description' => $eventData[1],
117 | 'location' => $eventData[2],
118 | 'status' => $this->resolveState($eventData[1]),
119 | ]));
120 | }
121 |
122 | return $track->sortEvents();
123 | }
124 |
125 |
126 | /**
127 | * Get the node value.
128 | *
129 | * @param DOMText|DOMNode $element
130 | * @param bool $withLineBreaks
131 | *
132 | * @return string
133 | */
134 | protected function getNodeValue($element, $withLineBreaks = false)
135 | {
136 | return $this->normalizedNodeValue($element, $withLineBreaks);
137 | }
138 |
139 |
140 | /**
141 | * Parse the date from the given string.
142 | *
143 | * @param $dateString
144 | *
145 | * @return string
146 | */
147 | protected function getDate($dateString)
148 | {
149 | // The date comes in a format like
150 | // November 9, 2015, 10:50 am
151 | return empty($dateString) ? null : Carbon::parse($dateString);
152 | }
153 |
154 |
155 | /**
156 | * Match a shipping status from the given description.
157 | *
158 | * @param $statusDescription
159 | *
160 | * @return string
161 | */
162 | protected function resolveState($statusDescription)
163 | {
164 | $statuses = [
165 | Track::STATUS_DELIVERED => [
166 | 'Delivered',
167 | ],
168 | Track::STATUS_IN_TRANSIT => [
169 | 'Notice Left',
170 | 'Arrived at Unit',
171 | 'Departed USPS Facility',
172 | 'Arrived at USPS Facility',
173 | 'Processed Through Sort Facility',
174 | 'Processed Through Facility',
175 | 'Origin Post is Preparing Shipment',
176 | 'Acceptance',
177 | 'Out for Delivery',
178 | 'Sorting Complete',
179 | 'Departed USPS Regional Facility',
180 | 'Arrived at USPS Regional Facility',
181 | ],
182 | Track::STATUS_WARNING => [],
183 | Track::STATUS_EXCEPTION => [],
184 | ];
185 |
186 | foreach ($statuses as $status => $needles) {
187 | foreach ($needles as $needle) {
188 | if (strpos($statusDescription, $needle) !== false) {
189 | return $status;
190 | }
191 | }
192 | }
193 |
194 | return Track::STATUS_UNKNOWN;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/tests/mock/DHL/pickup.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DHL Sendungsverfolgung
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Sie müssen JavaScript aktivieren um diesen Teil der Seite zu sehen.
62 |
63 |
64 |
65 | Sendungsnummer: 00340434162530583573
66 | Status: Die Sendung liegt in der <a href='https://standorte.deutschepost.de/Standortsuche?standorttyp=filialen_verkaufspunkte&ort=Darmstadt&strasse=Stresemannstr.&hausnummer=2&postleitzahl=64297&address=Darmstadt, Stresemannstr. %Fil_HN, 64297&setLng=de&lang=de' class='arrowLink' target='_blank'><span class='arrow'></span>PACKSTATION 133 Stresemannstr. 2 64297 Darmstadt</a> zur Abholung bereit.
67 |
68 | Nächster Schritt:
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/tests/mock/DHLExpress/in_transit.txt:
--------------------------------------------------------------------------------
1 | {
2 | "results" : [ {
3 | "id" : "5765159960",
4 | "label" : "Waybill",
5 | "type" : "airwaybill",
6 | "duplicate" : false,
7 | "delivery" : {
8 | "code" : "101",
9 | "status" : "not delivered status"
10 | },
11 | "origin" : {
12 | "value" : "ROTTERDAM - DEN HAAG - NETHERLANDS, THE",
13 | "label" : "Origin Service Area",
14 | "url" : "http://www.dhl.nl/en/country_profile.html"
15 | },
16 | "destination" : {
17 | "value" : "SAN FRANCISCO, CA - San Francisco - USA",
18 | "label" : "Destination Service Area",
19 | "url" : "http://www.dhl-usa.com/en/country_profile.html"
20 | },
21 | "description" : "Signed for by: SDA Friday, May 26, 2017 at 14:23",
22 | "hasDuplicateShipment" : false,
23 | "signature" : {
24 | "link" : {
25 | "url" : "https://webpod.dhl.com/webPOD/DHLePODRequest?hwb=cYCJzMUFoiC%2BdPAZG4OgTQ%3D%3D&pudate=GVM71SQC%2Fb7tWrHfRQuSGw%3D%3D&appuid=0N0K7SHOIOS83AxRjX6MPg%3D%3D&language=en&country=G0",
26 | "label" : "Get Signature Proof of Delivery"
27 | },
28 | "type" : "epod",
29 | "description" : "Friday, May 26, 2017 at 14:23",
30 | "signatory" : "SDA",
31 | "label" : "Signed for by",
32 | "help" : "help"
33 | },
34 | "pieces" : {
35 | "value" : 1,
36 | "label" : "Piece",
37 | "showSummary" : true,
38 | "pIds" : [ "JD014600004444917061" ]
39 | },
40 | "checkpoints" : [ {
41 | "counter" : 15,
42 | "description" : "With delivery courier",
43 | "time" : "10:14",
44 | "date" : "Friday, May 26, 2017 ",
45 | "location" : "SAN FRANCISCO, CA - USA",
46 | "totalPieces" : 1,
47 | "pIds" : [ "JD014600004444917061" ]
48 | }, {
49 | "counter" : 14,
50 | "description" : "Arrived at Delivery Facility in SAN FRANCISCO - USA",
51 | "time" : "07:55",
52 | "date" : "Friday, May 26, 2017 ",
53 | "location" : "SAN FRANCISCO, CA - USA",
54 | "totalPieces" : 1,
55 | "pIds" : [ "JD014600004444917061" ]
56 | }, {
57 | "counter" : 13,
58 | "description" : "Departed Facility in SAN FRANCISCO GATEWAY - USA",
59 | "time" : "07:21",
60 | "date" : "Friday, May 26, 2017 ",
61 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
62 | "totalPieces" : 1,
63 | "pIds" : [ "JD014600004444917061" ]
64 | }, {
65 | "counter" : 12,
66 | "description" : "Transferred through SAN FRANCISCO GATEWAY - USA",
67 | "time" : "07:17",
68 | "date" : "Friday, May 26, 2017 ",
69 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
70 | "totalPieces" : 1,
71 | "pIds" : [ "JD014600004444917061" ]
72 | }, {
73 | "counter" : 11,
74 | "description" : "Arrived at Sort Facility SAN FRANCISCO GATEWAY - USA",
75 | "time" : "06:30",
76 | "date" : "Friday, May 26, 2017 ",
77 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
78 | "totalPieces" : 1,
79 | "pIds" : [ "JD014600004444917061" ]
80 | }, {
81 | "counter" : 10,
82 | "description" : "Departed Facility in LOS ANGELES GATEWAY - USA",
83 | "time" : "05:21",
84 | "date" : "Friday, May 26, 2017 ",
85 | "location" : "LOS ANGELES GATEWAY, CA - USA",
86 | "totalPieces" : 1,
87 | "pIds" : [ "JD014600004444917061" ]
88 | }, {
89 | "counter" : 9,
90 | "description" : "Processed at LOS ANGELES GATEWAY - USA",
91 | "time" : "05:08",
92 | "date" : "Friday, May 26, 2017 ",
93 | "location" : "LOS ANGELES GATEWAY, CA - USA",
94 | "totalPieces" : 1,
95 | "pIds" : [ "JD014600004444917061" ]
96 | }, {
97 | "counter" : 8,
98 | "description" : "Clearance processing complete at LOS ANGELES GATEWAY - USA",
99 | "time" : "20:55",
100 | "date" : "Thursday, May 25, 2017 ",
101 | "location" : "LOS ANGELES GATEWAY, CA - USA",
102 | "totalPieces" : 1,
103 | "pIds" : [ "JD014600004444917061" ]
104 | }, {
105 | "counter" : 7,
106 | "description" : "Arrived at Sort Facility LOS ANGELES GATEWAY - USA",
107 | "time" : "20:23",
108 | "date" : "Thursday, May 25, 2017 ",
109 | "location" : "LOS ANGELES GATEWAY, CA - USA",
110 | "totalPieces" : 1,
111 | "pIds" : [ "JD014600004444917061" ]
112 | }, {
113 | "counter" : 6,
114 | "description" : "Customs status updated",
115 | "time" : "07:43",
116 | "date" : "Thursday, May 25, 2017 ",
117 | "location" : "LOS ANGELES GATEWAY, CA - USA"
118 | }, {
119 | "counter" : 5,
120 | "description" : "Departed Facility in AMSTERDAM - NETHERLANDS, THE",
121 | "time" : "08:50",
122 | "date" : "Thursday, May 25, 2017 ",
123 | "location" : "AMSTERDAM - NETHERLANDS, THE",
124 | "totalPieces" : 1,
125 | "pIds" : [ "JD014600004444917061" ]
126 | }, {
127 | "counter" : 4,
128 | "description" : "Processed at AMSTERDAM - NETHERLANDS, THE",
129 | "time" : "05:22",
130 | "date" : "Thursday, May 25, 2017 ",
131 | "location" : "AMSTERDAM - NETHERLANDS, THE",
132 | "totalPieces" : 1,
133 | "pIds" : [ "JD014600004444917061" ]
134 | }, {
135 | "counter" : 3,
136 | "description" : "Departed Facility in ROTTERDAM - NETHERLANDS, THE",
137 | "time" : "20:37",
138 | "date" : "Wednesday, May 24, 2017 ",
139 | "location" : "ROTTERDAM - NETHERLANDS, THE",
140 | "totalPieces" : 1,
141 | "pIds" : [ "JD014600004444917061" ]
142 | }, {
143 | "counter" : 2,
144 | "description" : "Processed at ROTTERDAM - NETHERLANDS, THE",
145 | "time" : "20:37",
146 | "date" : "Wednesday, May 24, 2017 ",
147 | "location" : "ROTTERDAM - NETHERLANDS, THE",
148 | "totalPieces" : 1,
149 | "pIds" : [ "JD014600004444917061" ]
150 | }, {
151 | "counter" : 1,
152 | "description" : "Shipment picked up",
153 | "time" : "17:08",
154 | "date" : "Wednesday, May 24, 2017 ",
155 | "location" : "ROTTERDAM - NETHERLANDS, THE",
156 | "totalPieces" : 1,
157 | "pIds" : [ "JD014600004444917061" ]
158 | } ],
159 | "checkpointLocationLabel" : "Location",
160 | "checkpointTimeLabel" : "Time"
161 | } ]
162 | }
163 |
--------------------------------------------------------------------------------
/tests/mock/DHLExpress/delivered.txt:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "results" : [ {
4 | "id" : "5765159960",
5 | "label" : "Waybill",
6 | "type" : "airwaybill",
7 | "duplicate" : false,
8 | "delivery" : {
9 | "code" : "101",
10 | "status" : "delivered"
11 | },
12 | "origin" : {
13 | "value" : "ROTTERDAM - DEN HAAG - NETHERLANDS, THE",
14 | "label" : "Origin Service Area",
15 | "url" : "http://www.dhl.nl/en/country_profile.html"
16 | },
17 | "destination" : {
18 | "value" : "SAN FRANCISCO, CA - San Francisco - USA",
19 | "label" : "Destination Service Area",
20 | "url" : "http://www.dhl-usa.com/en/country_profile.html"
21 | },
22 | "description" : "Signed for by: SDA Friday, May 26, 2017 at 14:23",
23 | "hasDuplicateShipment" : false,
24 | "signature" : {
25 | "link" : {
26 | "url" : "https://webpod.dhl.com/webPOD/DHLePODRequest?hwb=cYCJzMUFoiC%2BdPAZG4OgTQ%3D%3D&pudate=GVM71SQC%2Fb7tWrHfRQuSGw%3D%3D&appuid=0N0K7SHOIOS83AxRjX6MPg%3D%3D&language=en&country=G0",
27 | "label" : "Get Signature Proof of Delivery"
28 | },
29 | "type" : "epod",
30 | "description" : "Friday, May 26, 2017 at 14:23",
31 | "signatory" : "SDA",
32 | "label" : "Signed for by",
33 | "help" : "help"
34 | },
35 | "pieces" : {
36 | "value" : 1,
37 | "label" : "Piece",
38 | "showSummary" : true,
39 | "pIds" : [ "JD014600004444917061" ]
40 | },
41 | "checkpoints" : [ {
42 | "counter" : 16,
43 | "description" : "Delivered - Signed for by : SDA",
44 | "time" : "14:23",
45 | "date" : "Friday, May 26, 2017 ",
46 | "location" : "San Francisco ",
47 | "totalPieces" : 1,
48 | "pIds" : [ "JD014600004444917061" ]
49 | }, {
50 | "counter" : 15,
51 | "description" : "With delivery courier",
52 | "time" : "10:14",
53 | "date" : "Friday, May 26, 2017 ",
54 | "location" : "SAN FRANCISCO, CA - USA",
55 | "totalPieces" : 1,
56 | "pIds" : [ "JD014600004444917061" ]
57 | }, {
58 | "counter" : 14,
59 | "description" : "Arrived at Delivery Facility in SAN FRANCISCO - USA",
60 | "time" : "07:55",
61 | "date" : "Friday, May 26, 2017 ",
62 | "location" : "SAN FRANCISCO, CA - USA",
63 | "totalPieces" : 1,
64 | "pIds" : [ "JD014600004444917061" ]
65 | }, {
66 | "counter" : 13,
67 | "description" : "Departed Facility in SAN FRANCISCO GATEWAY - USA",
68 | "time" : "07:21",
69 | "date" : "Friday, May 26, 2017 ",
70 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
71 | "totalPieces" : 1,
72 | "pIds" : [ "JD014600004444917061" ]
73 | }, {
74 | "counter" : 12,
75 | "description" : "Transferred through SAN FRANCISCO GATEWAY - USA",
76 | "time" : "07:17",
77 | "date" : "Friday, May 26, 2017 ",
78 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
79 | "totalPieces" : 1,
80 | "pIds" : [ "JD014600004444917061" ]
81 | }, {
82 | "counter" : 11,
83 | "description" : "Arrived at Sort Facility SAN FRANCISCO GATEWAY - USA",
84 | "time" : "06:30",
85 | "date" : "Friday, May 26, 2017 ",
86 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
87 | "totalPieces" : 1,
88 | "pIds" : [ "JD014600004444917061" ]
89 | }, {
90 | "counter" : 10,
91 | "description" : "Departed Facility in LOS ANGELES GATEWAY - USA",
92 | "time" : "05:21",
93 | "date" : "Friday, May 26, 2017 ",
94 | "location" : "LOS ANGELES GATEWAY, CA - USA",
95 | "totalPieces" : 1,
96 | "pIds" : [ "JD014600004444917061" ]
97 | }, {
98 | "counter" : 9,
99 | "description" : "Processed at LOS ANGELES GATEWAY - USA",
100 | "time" : "05:08",
101 | "date" : "Friday, May 26, 2017 ",
102 | "location" : "LOS ANGELES GATEWAY, CA - USA",
103 | "totalPieces" : 1,
104 | "pIds" : [ "JD014600004444917061" ]
105 | }, {
106 | "counter" : 8,
107 | "description" : "Clearance processing complete at LOS ANGELES GATEWAY - USA",
108 | "time" : "20:55",
109 | "date" : "Thursday, May 25, 2017 ",
110 | "location" : "LOS ANGELES GATEWAY, CA - USA",
111 | "totalPieces" : 1,
112 | "pIds" : [ "JD014600004444917061" ]
113 | }, {
114 | "counter" : 7,
115 | "description" : "Arrived at Sort Facility LOS ANGELES GATEWAY - USA",
116 | "time" : "20:23",
117 | "date" : "Thursday, May 25, 2017 ",
118 | "location" : "LOS ANGELES GATEWAY, CA - USA",
119 | "totalPieces" : 1,
120 | "pIds" : [ "JD014600004444917061" ]
121 | }, {
122 | "counter" : 6,
123 | "description" : "Customs status updated",
124 | "time" : "07:43",
125 | "date" : "Thursday, May 25, 2017 ",
126 | "location" : "LOS ANGELES GATEWAY, CA - USA"
127 | }, {
128 | "counter" : 5,
129 | "description" : "Departed Facility in AMSTERDAM - NETHERLANDS, THE",
130 | "time" : "08:50",
131 | "date" : "Thursday, May 25, 2017 ",
132 | "location" : "AMSTERDAM - NETHERLANDS, THE",
133 | "totalPieces" : 1,
134 | "pIds" : [ "JD014600004444917061" ]
135 | }, {
136 | "counter" : 4,
137 | "description" : "Processed at AMSTERDAM - NETHERLANDS, THE",
138 | "time" : "05:22",
139 | "date" : "Thursday, May 25, 2017 ",
140 | "location" : "AMSTERDAM - NETHERLANDS, THE",
141 | "totalPieces" : 1,
142 | "pIds" : [ "JD014600004444917061" ]
143 | }, {
144 | "counter" : 3,
145 | "description" : "Departed Facility in ROTTERDAM - NETHERLANDS, THE",
146 | "time" : "20:37",
147 | "date" : "Wednesday, May 24, 2017 ",
148 | "location" : "ROTTERDAM - NETHERLANDS, THE",
149 | "totalPieces" : 1,
150 | "pIds" : [ "JD014600004444917061" ]
151 | }, {
152 | "counter" : 2,
153 | "description" : "Processed at ROTTERDAM - NETHERLANDS, THE",
154 | "time" : "20:37",
155 | "date" : "Wednesday, May 24, 2017 ",
156 | "location" : "ROTTERDAM - NETHERLANDS, THE",
157 | "totalPieces" : 1,
158 | "pIds" : [ "JD014600004444917061" ]
159 | }, {
160 | "counter" : 1,
161 | "description" : "Shipment picked up",
162 | "time" : "17:08",
163 | "date" : "Wednesday, May 24, 2017 ",
164 | "location" : "ROTTERDAM - NETHERLANDS, THE",
165 | "totalPieces" : 1,
166 | "pIds" : [ "JD014600004444917061" ]
167 | } ],
168 | "checkpointLocationLabel" : "Location",
169 | "checkpointTimeLabel" : "Time"
170 | } ]
171 | }
172 |
--------------------------------------------------------------------------------
/tests/mock/DHLExpress/delivered_with_unordered_statuses.txt:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "results" : [ {
4 | "id" : "5765159960",
5 | "label" : "Waybill",
6 | "type" : "airwaybill",
7 | "duplicate" : false,
8 | "delivery" : {
9 | "code" : "101",
10 | "status" : "delivered"
11 | },
12 | "origin" : {
13 | "value" : "ROTTERDAM - DEN HAAG - NETHERLANDS, THE",
14 | "label" : "Origin Service Area",
15 | "url" : "http://www.dhl.nl/en/country_profile.html"
16 | },
17 | "destination" : {
18 | "value" : "SAN FRANCISCO, CA - San Francisco - USA",
19 | "label" : "Destination Service Area",
20 | "url" : "http://www.dhl-usa.com/en/country_profile.html"
21 | },
22 | "description" : "Signed for by: SDA Friday, May 26, 2017 at 14:23",
23 | "hasDuplicateShipment" : false,
24 | "signature" : {
25 | "link" : {
26 | "url" : "https://webpod.dhl.com/webPOD/DHLePODRequest?hwb=cYCJzMUFoiC%2BdPAZG4OgTQ%3D%3D&pudate=GVM71SQC%2Fb7tWrHfRQuSGw%3D%3D&appuid=0N0K7SHOIOS83AxRjX6MPg%3D%3D&language=en&country=G0",
27 | "label" : "Get Signature Proof of Delivery"
28 | },
29 | "type" : "epod",
30 | "description" : "Friday, May 26, 2017 at 14:23",
31 | "signatory" : "SDA",
32 | "label" : "Signed for by",
33 | "help" : "help"
34 | },
35 | "pieces" : {
36 | "value" : 1,
37 | "label" : "Piece",
38 | "showSummary" : true,
39 | "pIds" : [ "JD014600004444917061" ]
40 | },
41 | "checkpoints" : [ {
42 | "counter" : 16,
43 | "description" : "With delivery courier",
44 | "time" : "10:14",
45 | "date" : "Friday, May 26, 2017 ",
46 | "location" : "SAN FRANCISCO, CA - USA",
47 | "totalPieces" : 1,
48 | "pIds" : [ "JD014600004444917061" ]
49 | }, {
50 | "counter" : 15,
51 | "description" : "Delivered - Signed for by : SDA",
52 | "time" : "14:23",
53 | "date" : "Friday, May 26, 2017 ",
54 | "location" : "San Francisco ",
55 | "totalPieces" : 1,
56 | "pIds" : [ "JD014600004444917061" ]
57 | }, {
58 | "counter" : 14,
59 | "description" : "Arrived at Delivery Facility in SAN FRANCISCO - USA",
60 | "time" : "07:55",
61 | "date" : "Friday, May 26, 2017 ",
62 | "location" : "SAN FRANCISCO, CA - USA",
63 | "totalPieces" : 1,
64 | "pIds" : [ "JD014600004444917061" ]
65 | }, {
66 | "counter" : 13,
67 | "description" : "Departed Facility in SAN FRANCISCO GATEWAY - USA",
68 | "time" : "07:21",
69 | "date" : "Friday, May 26, 2017 ",
70 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
71 | "totalPieces" : 1,
72 | "pIds" : [ "JD014600004444917061" ]
73 | }, {
74 | "counter" : 12,
75 | "description" : "Transferred through SAN FRANCISCO GATEWAY - USA",
76 | "time" : "07:17",
77 | "date" : "Friday, May 26, 2017 ",
78 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
79 | "totalPieces" : 1,
80 | "pIds" : [ "JD014600004444917061" ]
81 | }, {
82 | "counter" : 11,
83 | "description" : "Arrived at Sort Facility SAN FRANCISCO GATEWAY - USA",
84 | "time" : "06:30",
85 | "date" : "Friday, May 26, 2017 ",
86 | "location" : "SAN FRANCISCO GATEWAY, CA - USA",
87 | "totalPieces" : 1,
88 | "pIds" : [ "JD014600004444917061" ]
89 | }, {
90 | "counter" : 10,
91 | "description" : "Departed Facility in LOS ANGELES GATEWAY - USA",
92 | "time" : "05:21",
93 | "date" : "Friday, May 26, 2017 ",
94 | "location" : "LOS ANGELES GATEWAY, CA - USA",
95 | "totalPieces" : 1,
96 | "pIds" : [ "JD014600004444917061" ]
97 | }, {
98 | "counter" : 9,
99 | "description" : "Processed at LOS ANGELES GATEWAY - USA",
100 | "time" : "05:08",
101 | "date" : "Friday, May 26, 2017 ",
102 | "location" : "LOS ANGELES GATEWAY, CA - USA",
103 | "totalPieces" : 1,
104 | "pIds" : [ "JD014600004444917061" ]
105 | }, {
106 | "counter" : 8,
107 | "description" : "Clearance processing complete at LOS ANGELES GATEWAY - USA",
108 | "time" : "20:55",
109 | "date" : "Thursday, May 25, 2017 ",
110 | "location" : "LOS ANGELES GATEWAY, CA - USA",
111 | "totalPieces" : 1,
112 | "pIds" : [ "JD014600004444917061" ]
113 | }, {
114 | "counter" : 7,
115 | "description" : "Arrived at Sort Facility LOS ANGELES GATEWAY - USA",
116 | "time" : "20:23",
117 | "date" : "Thursday, May 25, 2017 ",
118 | "location" : "LOS ANGELES GATEWAY, CA - USA",
119 | "totalPieces" : 1,
120 | "pIds" : [ "JD014600004444917061" ]
121 | }, {
122 | "counter" : 6,
123 | "description" : "Customs status updated",
124 | "time" : "07:43",
125 | "date" : "Thursday, May 25, 2017 ",
126 | "location" : "LOS ANGELES GATEWAY, CA - USA"
127 | }, {
128 | "counter" : 5,
129 | "description" : "Departed Facility in AMSTERDAM - NETHERLANDS, THE",
130 | "time" : "08:50",
131 | "date" : "Thursday, May 25, 2017 ",
132 | "location" : "AMSTERDAM - NETHERLANDS, THE",
133 | "totalPieces" : 1,
134 | "pIds" : [ "JD014600004444917061" ]
135 | }, {
136 | "counter" : 4,
137 | "description" : "Processed at AMSTERDAM - NETHERLANDS, THE",
138 | "time" : "05:22",
139 | "date" : "Thursday, May 25, 2017 ",
140 | "location" : "AMSTERDAM - NETHERLANDS, THE",
141 | "totalPieces" : 1,
142 | "pIds" : [ "JD014600004444917061" ]
143 | }, {
144 | "counter" : 3,
145 | "description" : "Departed Facility in ROTTERDAM - NETHERLANDS, THE",
146 | "time" : "20:37",
147 | "date" : "Wednesday, May 24, 2017 ",
148 | "location" : "ROTTERDAM - NETHERLANDS, THE",
149 | "totalPieces" : 1,
150 | "pIds" : [ "JD014600004444917061" ]
151 | }, {
152 | "counter" : 2,
153 | "description" : "Processed at ROTTERDAM - NETHERLANDS, THE",
154 | "time" : "20:37",
155 | "date" : "Wednesday, May 24, 2017 ",
156 | "location" : "ROTTERDAM - NETHERLANDS, THE",
157 | "totalPieces" : 1,
158 | "pIds" : [ "JD014600004444917061" ]
159 | }, {
160 | "counter" : 1,
161 | "description" : "Shipment picked up",
162 | "time" : "17:08",
163 | "date" : "Wednesday, May 24, 2017 ",
164 | "location" : "ROTTERDAM - NETHERLANDS, THE",
165 | "totalPieces" : 1,
166 | "pIds" : [ "JD014600004444917061" ]
167 | } ],
168 | "checkpointLocationLabel" : "Location",
169 | "checkpointTimeLabel" : "Time"
170 | } ]
171 | }
172 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Shipment Tracker
2 |
3 | **A simple tool to scrape the parcel tracking data for DHL, DHL Express, GLS, UPS, FedEx USPS, Swiss Post Service and Austria Post Service**
4 |
5 | [](https://travis-ci.org/sauladam/shipment-tracker)
6 | [](https://packagist.org/packages/sauladam/shipment-tracker)
7 |
8 |
9 | Some parcel services give you a really hard time when it comes to registering some kind of merchant or developer account.
10 | All you actually want is to simply keep track of a shipment and have an eye on its status. Yes, you could keep refreshing
11 | the tracking pages, but sometimes you've just got better stuff to do.
12 |
13 | So here's a tool that does this automatically, without any of the developer-account and API mumbo jumbo. It just simply scrapes the website with the tracking information and transforms the data into an easily consumable format for humans and computers. Let me show you how it works!
14 |
15 | ## Installation
16 |
17 | Just pull this package in through composer or by adding it to your `composer.json` file:
18 |
19 | ```bash
20 | $ composer require sauladam/shipment-tracker
21 | ```
22 |
23 | Don't forget to run
24 |
25 | $ composer update
26 |
27 | after that.
28 |
29 | ## Supported Carriers
30 | The following carriers and languages are currently supported by this package:
31 |
32 | - DHL (de, en)
33 | - DHL Express (de, en) (so far only for waybill numbers, not for shipment numbers of the individual pieces)
34 | - GLS (de, en)
35 | - UPS (de, en)
36 | - Fedex (en) (hat tip to [@avrahamappel](https://github.com/avrahamappel))
37 | - USPS (en)
38 | - PostCH (Swiss Post Service) (de, en)
39 | - PostAT (Austria Post Service) (de, en)
40 |
41 | ## Basic Usage
42 |
43 | ```php
44 | require_once 'vendor/autoload.php';
45 |
46 | use Sauladam\ShipmentTracker\ShipmentTracker;
47 |
48 | $dhlTracker = ShipmentTracker::get('DHL');
49 |
50 | /* track with the standard settings */
51 | $track = $dhlTracker->track('00340434127681930812');
52 | // scrapes from http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=de&idc=00340434127681930812
53 |
54 | /* override the standard language */
55 | $track = $dhlTracker->track('00340434127681930812', 'en');
56 | // scrapes from http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=00340434127681930812
57 |
58 | /* pass additional params to the URL (or override the default ones) */
59 | $track = $dhlTracker->track('00340434127681930812', 'en', ['zip' => '12345']);
60 | // scrapes from http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=00340434127681930812&zip=12345
61 | ```
62 |
63 | And that's it. Let's check if this parcel was delivered:
64 |
65 | ```php
66 | if($track->delivered())
67 | {
68 | echo "Delivered to " . $track->getRecipient();
69 | }
70 | else
71 | {
72 | echo "Not delivered yet, The current status is " . $track->currentStatus();
73 | }
74 | ```
75 |
76 | #### Possible statuses are:
77 |
78 | - `Track::STATUS_IN_TRANSIT`
79 | - `Track::STATUS_DELIVERED`
80 | - `Track::STATUS_PICKUP`
81 | - `Track::STATUS_EXCEPTION`
82 | - `Track::STATUS_WARNING`
83 | - `Track::STATUS_UNKNOWN`
84 |
85 | #### So where is it right now and what's happening with it?
86 |
87 | ```php
88 | $latestEvent = $track->latestEvent();
89 |
90 | echo "The parcel was last seen in " . $latestEvent->getLocation() . " on " . $latestEvent->getDate()->format('Y-m-d');
91 | echo "What they did: " . $latestEvent->description();
92 | echo "The status was " . $latestEvent->getStatus();
93 | ```
94 |
95 | You can grab an array with the whole event history with `$track->events()`. The events are sorted by date in descending order. The date is a [Carbon](https://github.com/briannesbitt/Carbon) object.
96 |
97 | ## What else?
98 | You just want to build up the URL for the tracking website? No problem:
99 | ```php
100 | $url = $dhlTracker->trackingUrl('00340434127681930812');
101 | // http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=de&idc=00340434127681930812
102 | ```
103 | Oh, you need it to link to the english version? Sure thing:
104 | ```php
105 | $url = $dhlTracker->trackingUrl('00340434127681930812', 'en');
106 | // http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=00340434127681930812
107 | ```
108 | *"But wait, what if I need that URL with additional parameteres?"* - Well, just pass them:
109 | ```php
110 | $url = $dhlTracker->trackingUrl('00340434127681930812', 'en', ['zip' => '12345']);
111 | // http://nolp.dhl.de/nextt-online-public/set_identcodes.do?lang=en&idc=00340434127681930812&zip=12345
112 | ```
113 |
114 | ## Other features
115 | ### Additional details
116 | Tracks and Events both can hold additional details, accessible via e.g. `$track->getAdditionalDetails('foo')`. Currently, this is only relevant for GLS and UPS:
117 | - **GLS:**
118 | - `$track->getAdditionalDetails('parcelShop')` gets the parcel shop details and the opening hours if the parcel was delivered to one
119 |
120 | - **UPS:**
121 | - `$track->getAdditionalDetails('accessPoint')` gets the address of the access point if the parcel was delivered to one
122 | - `$track->getAdditionalDetails('pickupDueDate')` gets the pickup due date as a Carbon instance
123 |
124 | - **DHL Express (waybills):**
125 | - `$track->getAdditionalDetails('pieces')` gets the tracking numbers of the individual pieces that belong to this shipment
126 | - `$event->getAdditionalDetails('pieces')` gets the tracking numbers of the individual pieces to which this event applies
127 |
128 | - **FedEx:**
129 | - `$track->getAdditionalDetails('totalKgsWgt')` gets the weight of the shipment in kgs if it's returned
130 | - `$track->getAdditionalDetails('totalLbsWgt')` gets the weight of the shipment in lbs if it's returned
131 |
132 | ### Data Providers
133 | By default, this package uses Guzzle as well as the PHP Http client (a.k.a. `file_get_contents()`) to fetch the data. You can pass your own provider if you need to, e.g. if you have the page contents chillin' somewhere in a cache. Just make sure that it implements `Sauladam\ShipmentTracker\DataProviders\DataProviderInterface`, which only requires a `get()` method.
134 |
135 | Then, you can just pass it to the factory: `$dhlTracker = ShipmentTracker::get('DHL', new CacheDataProvider);`
136 |
137 | If you pass your data provider, it's used by default, but you can swap it out later if you want:
138 |
139 | ```php
140 | $dhlTracker->useDataProvider('guzzle');
141 | ```
142 |
143 | Currently available providers are:
144 | - guzzle
145 | - php
146 | - custom (referring to the provider that you've passed)
147 |
148 |
149 | ## Notes
150 | Please keep in mind that this is just a tool to make your life easier. I do not recommend using it in a critical environment, because, due to the way it works, it can break down as soon as the tracking website where the data is pulled from changes its structure or renames/rephrases the event descriptions. So please **use it at your own risk!**
151 |
152 | Also, there's always a chance that a status can not be resolved because the event description is not known by this package, even though the most common events should be resolved correctly.
153 |
154 | Also, the tracking data, therefore the data provided by this package, is the property of the carrier (I guess), so under no circumstances you should use it commercially (like selling it or integrating it in a commercial service). It is intended only for personal use.
155 |
--------------------------------------------------------------------------------
/src/Trackers/GLS.php:
--------------------------------------------------------------------------------
1 | 'https://gls-group.eu/DE/de/paketverfolgung',
20 | 'en' => 'https://gls-group.eu/DE/en/parcel-tracking'
21 | ];
22 |
23 | /**
24 | * @var string
25 | */
26 | protected $language = 'de';
27 |
28 |
29 | /**
30 | * Parse the response.
31 | *
32 | * @param string $contents
33 | *
34 | * @return Track
35 | * @throws \Exception
36 | */
37 | protected function buildResponse($contents)
38 | {
39 | $response = $this->jsonToArray($contents);
40 |
41 | if (isset($response['exceptionText'])) {
42 | $text = $response['exceptionText'];
43 | throw new \Exception("Unable to retrieve tracking data for [{$this->parcelNumber}]: {$text}");
44 | }
45 |
46 | return $this->getTrack($response);
47 | }
48 |
49 |
50 | /**
51 | * Get the shipment status history.
52 | *
53 | * @param array $response
54 | *
55 | * @return Track
56 | */
57 | protected function getTrack(array $response)
58 | {
59 | $track = new Track;
60 |
61 | foreach ($response['tuStatus'][0]['history'] as $index => $historyItem) {
62 | $event = new Event;
63 |
64 | $status = $this->resolveStatus($response, $index);
65 |
66 | $event->setStatus($status);
67 | $event->setLocation($this->getLocation($historyItem));
68 | $event->setDescription($historyItem['evtDscr']);
69 | $event->setDate($this->getDate($historyItem));
70 | $event->addAdditionalDetails('eventNumber', $response['tuStatus'][0]['progressBar']['evtNos'][$index]);
71 |
72 | $track->addEvent($event);
73 |
74 | if ($status == Track::STATUS_DELIVERED) {
75 | $track->setRecipient($this->getRecipient($response));
76 | }
77 |
78 | if ($status == Track::STATUS_PICKUP) {
79 | $track->addAdditionalDetails('parcelShop', $this->getParcelShopDetails($response));
80 | }
81 | }
82 |
83 | return $track->sortEvents();
84 | }
85 |
86 |
87 | /**
88 | * Get the location.
89 | *
90 | * @param array $historyItem
91 | *
92 | * @return string
93 | */
94 | protected function getLocation(array $historyItem)
95 | {
96 | return $historyItem['address']['city'] . ', ' . $historyItem['address']['countryName'];
97 | }
98 |
99 |
100 | /**
101 | * Get the formatted date.
102 | *
103 | * @param array $historyItem
104 | *
105 | * @return string
106 | */
107 | protected function getDate(array $historyItem)
108 | {
109 | return $historyItem['date'] . ' ' . $historyItem['time'];
110 | }
111 |
112 |
113 | /**
114 | * Get the recipient / person who signed the delivery.
115 | *
116 | * @param array $response
117 | *
118 | * @return string
119 | */
120 | protected function getRecipient(array $response)
121 | {
122 | return $response['tuStatus'][0]['signature']['value'];
123 | }
124 |
125 |
126 | /**
127 | * Match a shipping status from the given description.
128 | *
129 | * @param array $response
130 | * @param int $historyItemIndex
131 | *
132 | * @return string
133 | *
134 | */
135 | protected function resolveStatus(array $response, $historyItemIndex)
136 | {
137 | $statuses = [
138 | Track::STATUS_DELIVERED => [
139 | '3.120', // unconfirmed
140 | '3.121',
141 | '3.0',
142 | ],
143 | Track::STATUS_IN_TRANSIT => [
144 | '0.0',
145 | '0.100',
146 | '1.0',
147 | '11.0',
148 | '2.0',
149 | '2.106',
150 | '2.29',
151 | '4.40',
152 | '90.132',
153 | '35.40',
154 | '8.0',
155 | '6.211',
156 | ],
157 | Track::STATUS_PICKUP => [
158 | '2.124',
159 | '3.124',
160 | ],
161 | Track::STATUS_EXCEPTION => [
162 | ],
163 | ];
164 |
165 | $eventNumber = $response['tuStatus'][0]['progressBar']['evtNos'][$historyItemIndex];
166 | $progressStatusInfo = $response['tuStatus'][0]['progressBar']['statusInfo']; // DELIVERED | DELIVEREDPS | INTRANSIT
167 |
168 | foreach ($statuses as $status => $eventNumbers) {
169 | if (in_array($eventNumber, $eventNumbers)) {
170 | // if the event status is delivered but the whole shipment status is not delivered yet, override it with IN_TRANSIT
171 | if (($status === Track::STATUS_DELIVERED) && ($progressStatusInfo !== 'DELIVERED')) {
172 | return Track::STATUS_IN_TRANSIT;
173 | }
174 |
175 | return $status;
176 | }
177 | }
178 |
179 | return Track::STATUS_UNKNOWN;
180 | }
181 |
182 |
183 | /**
184 | * Get the parcel-shop details
185 | *
186 | * @param array $response
187 | *
188 | * @return array
189 | */
190 | protected function getParcelShopDetails(array $response)
191 | {
192 | return isset($response['tuStatus'][0]['parcelShop']) && isset($response['tuStatus'][0]['parcelShop']['address'])
193 | ? $response['tuStatus'][0]['parcelShop']['address']
194 | : [];
195 | }
196 |
197 |
198 | /**
199 | * Try to convert the Json string into an array.
200 | *
201 | * @param $string
202 | *
203 | * @return array
204 | * @throws \Exception
205 | */
206 | protected function jsonToArray($string)
207 | {
208 | $array = json_decode($string, true);
209 |
210 | if (!$array) {
211 | throw new \Exception("Unable to decode GLS Json string [$string] for [{$this->parcelNumber}].");
212 | }
213 |
214 | return $array;
215 | }
216 |
217 |
218 | /**
219 | * Build the user friendly url for the given tracking number.
220 | *
221 | * @param string $trackingNumber
222 | * @param null $language
223 | * @param array $params
224 | *
225 | * @return string
226 | */
227 | public function trackingUrl($trackingNumber, $language = null, $params = [])
228 | {
229 | $language = $language ?: $this->language;
230 |
231 | $url = array_key_exists(
232 | $language,
233 | $this->trackingUrls
234 | ) ? $this->trackingUrls[$language] : $this->trackingUrls['de'];
235 |
236 | $additionalParams = !empty($params) ? $params : $this->trackingUrlParams;
237 |
238 | $qry = http_build_query(array_merge([
239 | 'match' => $trackingNumber,
240 | ], $additionalParams));
241 |
242 | return $url . '?' . $qry;
243 | }
244 |
245 |
246 | /**
247 | * Get the endpoint url.
248 | *
249 | * @param string $trackingNumber
250 | * @param null $language
251 | * @param array $params
252 | *
253 | * @return string
254 | */
255 | protected function getEndpointUrl($trackingNumber, $language = null, $params = [])
256 | {
257 | $language = $language ?: $this->language;
258 |
259 | $url = str_replace('{language}', $language, $this->endpointUrl);
260 |
261 | $additionalParams = !empty($params) ? $params : $this->endpointUrlParams;
262 |
263 | $qry = http_build_query(array_merge([
264 | 'match' => $trackingNumber,
265 | ], $additionalParams));
266 |
267 | return $url . '?' . $qry;
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/src/Trackers/DHLExpress.php:
--------------------------------------------------------------------------------
1 | 'http://www.dhl.com/en/hidden/component_library/express/local_express/dhl_de_tracking/de/sendungsverfolgung_dhlde.html',
21 | 'en' => 'http://www.dhl.com/en/hidden/component_library/express/local_express/dhl_de_tracking/en/tracking_dhlde.html'
22 | ];
23 |
24 | /**
25 | * @var string
26 | */
27 | protected $language = 'de';
28 |
29 |
30 | /**
31 | * Parse the response.
32 | *
33 | * @param string $contents
34 | *
35 | * @return Track
36 | * @throws \Exception
37 | */
38 | protected function buildResponse($contents)
39 | {
40 | $response = $this->json($contents);
41 |
42 | return $this->getTrack($response);
43 | }
44 |
45 |
46 | /**
47 | * Get the shipment status history.
48 | *
49 | * @param object $response
50 | *
51 | * @return Track
52 | */
53 | protected function getTrack($response)
54 | {
55 | $track = new Track;
56 |
57 | $shipment = $response->results[0];
58 |
59 | foreach ($shipment->checkpoints as $checkpoint) {
60 | $event = new Event;
61 |
62 | $event->setStatus($this->resolveStatus($checkpoint->description));
63 | $event->setLocation($this->getLocation($checkpoint));
64 | $event->setDescription($checkpoint->description);
65 | $event->setDate($this->getDate($checkpoint));
66 |
67 | if (isset($checkpoint->pIds)) {
68 | $event->addAdditionalDetails('pieces', $checkpoint->pIds);
69 | }
70 |
71 | $track->addEvent($event);
72 | }
73 |
74 | if ($this->isDelivered($shipment)) {
75 | $track->setRecipient($this->getRecipient($shipment));
76 | }
77 |
78 | if (isset($shipment->pieces) && isset($shipment->pieces->pIds)) {
79 | $track->addAdditionalDetails('pieces', $shipment->pieces->pIds);
80 | }
81 |
82 | return $track->sortEvents();
83 | }
84 |
85 |
86 | /**
87 | * Get the location.
88 | *
89 | * @param object $checkpoint
90 | *
91 | * @return string
92 | */
93 | protected function getLocation($checkpoint)
94 | {
95 | return $checkpoint->location;
96 | }
97 |
98 |
99 | /**
100 | * Get the formatted date.
101 | *
102 | * @param object $checkpoint
103 | *
104 | * @return string
105 | */
106 | protected function getDate($checkpoint)
107 | {
108 | $date = $this->translatedDate($checkpoint->date);
109 |
110 | return Carbon::createFromFormat('l, F j, Y G:i', $date . $checkpoint->time);
111 | }
112 |
113 |
114 | /**
115 | * Replace German month names and weekday names with the English names
116 | * so it can easily be parsed by Carbon.
117 | *
118 | * @param string $dateString
119 | *
120 | * @return string
121 | */
122 | protected function translatedDate($dateString)
123 | {
124 | return str_replace([
125 | 'Januar',
126 | 'Februar',
127 | 'März',
128 | 'April',
129 | 'Mai',
130 | 'Juni',
131 | 'Juli',
132 | 'August',
133 | 'Spetember',
134 | 'Oktober',
135 | 'November',
136 | 'Dezember',
137 | 'Montag',
138 | 'Dienstag',
139 | 'Mittwoch',
140 | 'Donnerstag',
141 | 'Freitag',
142 | 'Samstag',
143 | 'Sonntag',
144 | ], [
145 | 'January',
146 | 'February',
147 | 'March',
148 | 'April',
149 | 'May',
150 | 'June',
151 | 'July',
152 | 'August',
153 | 'September',
154 | 'October',
155 | 'November',
156 | 'December',
157 | 'Monday',
158 | 'Tuesday',
159 | 'Wednesday',
160 | 'Thursday',
161 | 'Friday',
162 | 'Saturday',
163 | 'Sunday',
164 | ], $dateString);
165 | }
166 |
167 |
168 | /**
169 | * Check if the shipment was delivered.
170 | *
171 | * @param $trackingInformation
172 | *
173 | * @return bool
174 | */
175 | protected function isDelivered($trackingInformation)
176 | {
177 | return isset($trackingInformation->delivery)
178 | && isset($trackingInformation->delivery->status)
179 | && $trackingInformation->delivery->status == 'delivered';
180 | }
181 |
182 |
183 | /**
184 | * Get the recipient / person who signed the delivery.
185 | *
186 | * @param object $shipment
187 | *
188 | * @return string
189 | */
190 | protected function getRecipient($shipment)
191 | {
192 | return $shipment->signature->signatory;
193 | }
194 |
195 |
196 | /**
197 | * Match a shipping status from the given description.
198 | *
199 | * @param string $description
200 | *
201 | * @return string
202 | *
203 | */
204 | protected function resolveStatus($description)
205 | {
206 | $statuses = [
207 | Track::STATUS_DELIVERED => [
208 | 'Delivered - Signed',
209 | 'Sendung zugestellt - übernommen',
210 | ],
211 | Track::STATUS_IN_TRANSIT => [
212 | 'With delivery courier',
213 | 'Sendung in Zustellung',
214 | 'Arrived at',
215 | 'Ankunft in der',
216 | 'Departed Facility',
217 | 'Verlässt DHL-Niederlassung',
218 | 'Transferred through',
219 | 'Sendung im Transit',
220 | 'Processed at',
221 | 'Sendung sortiert',
222 | 'Clearance processing',
223 | 'Verzollung abgeschlossen',
224 | 'Customs status updated',
225 | 'Verzollungsstatus aktualisiert',
226 | 'Shipment picked up',
227 | 'Sendung abgeholt',
228 | ],
229 | ];
230 |
231 | foreach ($statuses as $status => $needles) {
232 | foreach ($needles as $needle) {
233 | if (strpos($description, $needle) === 0) {
234 | return $status;
235 | }
236 | }
237 | }
238 |
239 | return Track::STATUS_UNKNOWN;
240 | }
241 |
242 |
243 | /**
244 | * Try to decode the JSON string into an object.
245 | *
246 | * @param $string
247 | *
248 | * @return object
249 | * @throws \Exception
250 | */
251 | protected function json($string)
252 | {
253 | $json = json_decode($string);
254 |
255 | if (!$json) {
256 | throw new \Exception("Unable to decode DHL Express Json string [$string] for [{$this->parcelNumber}].");
257 | }
258 |
259 | return $json;
260 | }
261 |
262 |
263 | /**
264 | * Build the user friendly url for the given tracking number.
265 | *
266 | * @param string $trackingNumber
267 | * @param null $language
268 | * @param array $params
269 | *
270 | * @return string
271 | */
272 | public function trackingUrl($trackingNumber, $language = null, $params = [])
273 | {
274 | $language = $language ?: $this->language;
275 |
276 | $url = array_key_exists(
277 | $language,
278 | $this->trackingUrls
279 | ) ? $this->trackingUrls[$language] : $this->trackingUrls['de'];
280 |
281 | $additionalParams = !empty($params) ? $params : $this->trackingUrlParams;
282 |
283 | $qry = http_build_query(array_merge([
284 | 'AWB' => $trackingNumber,
285 | 'brand' => 'DHL',
286 | ], $additionalParams));
287 |
288 | return $url . '?' . $qry;
289 | }
290 |
291 |
292 | /**
293 | * Get the endpoint url.
294 | *
295 | * @param string $trackingNumber
296 | * @param null $language
297 | * @param array $params
298 | *
299 | * @return string
300 | */
301 | protected function getEndpointUrl($trackingNumber, $language = null, $params = [])
302 | {
303 | $additionalParams = !empty($params) ? $params : $this->endpointUrlParams;
304 |
305 | $qry = http_build_query(array_merge([
306 | 'AWB' => $trackingNumber,
307 | 'languageCode' => $language ?: $this->language,
308 | ], $additionalParams));
309 |
310 | return $this->endpointUrl . '?' . $qry;
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/Trackers/PostCH.php:
--------------------------------------------------------------------------------
1 | language;
80 |
81 | $additionalParams = !empty($params) ? $params : $this->trackingUrlParams;
82 |
83 | $qry = http_build_query(array_merge([
84 | 'formattedParcelCodes' => $trackingNumber,
85 | 'lang' => $language,
86 | ], $additionalParams));
87 |
88 | return $this->trackingUrl . '?' . $qry;
89 | }
90 |
91 |
92 | /**
93 | * Build the endpoint url
94 | *
95 | * @param string $trackingNumber
96 | * @param string|null $language
97 | * @param array $params
98 | *
99 | * @return string
100 | */
101 | public function getEndpointUrl($trackingNumber, $language = null, $params = [])
102 | {
103 | $this->createApiUser();
104 |
105 | $results = json_decode(
106 | $this->fetch($this->searchEndpoint . '/' . $this->getHashForTrackingNumber() . '?' . http_build_query([
107 | 'userId' => static::$userId,
108 | ]))
109 | );
110 |
111 | return $this->serviceEndpoint . '/' . $results[0]->identity . '/events';
112 | }
113 |
114 |
115 | /**
116 | * Build the response array.
117 | *
118 | * @param string $response
119 | *
120 | * @return Track
121 | */
122 | protected function buildResponse($response)
123 | {
124 | $this->loadMessageCodeLookup();
125 |
126 | return array_reduce(json_decode($response), function ($track, $event) {
127 | return $track->addEvent(Event::fromArray([
128 | 'location' => empty($event->city)
129 | ? ''
130 | : sprintf("%s-%s %s", $event->country, $event->zip, $event->city),
131 | 'description' => $this->getDescriptionByCode($event->eventCode),
132 | 'date' => Carbon::parse($event->timestamp),
133 | 'status' => $this->resolveStatus($event->eventCode),
134 | ]));
135 | }, new Track);
136 | }
137 |
138 |
139 | /**
140 | * Load the message lookup array for the current language if
141 | * it doesn't exist yet.
142 | */
143 | protected function loadMessageCodeLookup()
144 | {
145 | if (array_key_exists($this->language, static::$messageCodeLookup)) {
146 | return;
147 | }
148 |
149 | static::$messageCodeLookup[$this->language] = json_decode($this->fetch(
150 | str_replace('{lang}', $this->language, $this->messagesEndpoint)
151 | ), true);
152 | }
153 |
154 |
155 | /**
156 | * Create an API and get the user ID, the CSRF token and the session cookie.
157 | * Those are required for subsequent requests against the API.
158 | */
159 | protected function createApiUser()
160 | {
161 | if (null !== static::$userId) {
162 | return;
163 | }
164 |
165 | $response = $this->getDataProvider()->client->request(
166 | 'GET', $this->apiUserEndpoint, [
167 | 'cookies' => $jar = new CookieJar,
168 | ]
169 | );
170 |
171 | foreach ($jar->toArray() as $cookie) {
172 | static::$cookies[$cookie['Name']] = $cookie['Value'];
173 | }
174 |
175 | $user = json_decode($response->getBody()->getContents());
176 |
177 | if (!$user) {
178 | return;
179 | }
180 |
181 | static::$userId = $user->userIdentifier;
182 | static::$CSRFToken = $response->getHeader('X-CSRF-TOKEN');
183 | }
184 |
185 |
186 | /**
187 | * Get the search hash value for the tracking number. The hash will be used later
188 | * to get the entity id for the tracking number.
189 | *
190 | * @return mixed
191 | */
192 | protected function getHashForTrackingNumber()
193 | {
194 | $url = $this->trackingNumberHashEndpoint . '?' . http_build_query([
195 | 'userId' => static::$userId,
196 | ]);
197 |
198 | $response = $this->getDataProvider()->client->post($url, [
199 | 'headers' => [
200 | 'Content-Type' => 'application/json',
201 | 'x-csrf-token' => static::$CSRFToken,
202 | 'Cookie' => array_reduce(array_keys(static::$cookies), function ($string, $cookieName) {
203 | return $string .= $cookieName . '=' . static::$cookies[$cookieName];
204 | }, ''),
205 | ],
206 | 'json' => [
207 | 'searchQuery' => $this->parcelNumber,
208 | ],
209 | ]);
210 |
211 | return json_decode($response->getBody()->getContents())->hash;
212 | }
213 |
214 |
215 | /**
216 | * Look up the description for the given event code.
217 | *
218 | * @param $code
219 | *
220 | * @return string
221 | */
222 | public function getDescriptionByCode($code)
223 | {
224 | $haystack = static::$messageCodeLookup[$this->language]['shipment-text--'];
225 |
226 | $pattern = $this->getRegexPattern($code);
227 |
228 | $matches = array_filter(array_keys($haystack), function ($key) use ($pattern) {
229 | return 1 === preg_match($pattern, $key);
230 | });
231 |
232 | return !empty($matches) ? $haystack[array_values($matches)[0]] : '';
233 | }
234 |
235 |
236 | /**
237 | * Build a regex pattern for the code so it will match the exact code or wildcards.
238 | * E. g. if the code is 'LETTER.*.88.912', it should also match with 'LETTER.*.*.912'
239 | * or 'LETTER.*.88.912.*'
240 | *
241 | * @param $code
242 | *
243 | * @return string
244 | */
245 | protected function getRegexPattern($code)
246 | {
247 | $pattern = array_reduce(explode('.', $code), function ($regex, $part) {
248 | if (1 === preg_match('/[a-z]+/i', $part)) {
249 | return $regex .= $part;
250 | }
251 |
252 | if ($part === '*') {
253 | return $regex .= "\.(\*|[a-z]+|-|_)";
254 | }
255 |
256 | return $regex .= "\.(\*|{$part})";
257 | }, '');
258 |
259 | return sprintf("/%s(\.\*)?$/i", $pattern);
260 | }
261 |
262 |
263 | /**
264 | * Match a shipping status from the given event code.
265 | *
266 | * @param $eventCode
267 | *
268 | * @return string
269 | */
270 | protected function resolveStatus($eventCode)
271 | {
272 | $statuses = [
273 | Track::STATUS_DELIVERED => [
274 | 'LETTER.*.88.40',
275 | 'LETTER.*.88.20',
276 | ],
277 | Track::STATUS_IN_TRANSIT => [
278 | 'LETTER.*.88.912',
279 | 'LETTER.*.88.915',
280 | 'LETTER.*.88.818',
281 | 'LETTER.*.88.819',
282 | 'LETTER.*.88.803',
283 | 'LETTER.*.88.10',
284 | 'LETTER.*.88.13',
285 | 'LETTER.*.88.18',
286 | 'LETTER.*.88.620',
287 | 'LETTER.*.88.820',
288 | 'LETTER.*.88.1300',
289 | 'LETTER.*.88.804',
290 | 'LETTER.*.103.620',
291 | ],
292 | Track::STATUS_PICKUP => [
293 | 'LETTER.*.88.21',
294 | ],
295 | Track::STATUS_WARNING => [],
296 | Track::STATUS_EXCEPTION => [],
297 | ];
298 |
299 | foreach ($statuses as $status => $needles) {
300 | if (in_array($eventCode, $needles)) {
301 | return $status;
302 | }
303 | }
304 |
305 | return Track::STATUS_UNKNOWN;
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/src/Trackers/UPS.php:
--------------------------------------------------------------------------------
1 | getCookies();
35 | }
36 |
37 | try {
38 | $response = $this->getDataProvider()->client->post($this->serviceUrl(), [
39 | 'headers' => [
40 | 'Content-Type' => 'application/json',
41 | 'X-XSRF-TOKEN' => static::$cookies['X-XSRF-TOKEN-ST'],
42 | 'Cookie' => implode(';', array_map(function ($name) {
43 | return $name . '=' . static::$cookies[$name];
44 | }, array_keys(static::$cookies))),
45 | ],
46 | 'json' => [
47 | 'Locale' => $this->getLanguageQueryParam($this->language),
48 | 'TrackingNumber' => [
49 | $this->parcelNumber,
50 | ],
51 | ],
52 | ])->getBody()->getContents();
53 |
54 | return json_decode($response, true);
55 | } catch (\Exception $e) {
56 | throw new \Exception("Could not fetch tracking data for [{$this->parcelNumber}].");
57 | }
58 | }
59 |
60 |
61 | protected function getCookies()
62 | {
63 | $this->getDataProvider()->client->request(
64 | 'GET', $this->trackingUrl($this->parcelNumber), [
65 | 'cookies' => $jar = new CookieJar,
66 | ]
67 | );
68 |
69 | foreach ($jar->toArray() as $cookie) {
70 | static::$cookies[$cookie['Name']] = $cookie['Value'];
71 | }
72 | }
73 |
74 |
75 | /**
76 | * @param $contents
77 | *
78 | * @return Track
79 | * @throws \Exception
80 | */
81 | protected function buildResponse($contents)
82 | {
83 | $track = new Track;
84 |
85 | foreach ($contents['trackDetails'][0]['shipmentProgressActivities'] as $progressActivity) {
86 | if (null === $progressActivity['activityScan']) {
87 | continue;
88 | }
89 |
90 | $track->addEvent(Event::fromArray([
91 | 'location' => $progressActivity['location'],
92 | 'description' => $progressActivity['activityScan'], //$this->getDescription($progressActivity),
93 | 'date' => $this->getDate($progressActivity),
94 | 'status' => $status = $this->resolveState($progressActivity['activityScan']),
95 | ]));
96 |
97 | if ($status == Track::STATUS_DELIVERED && isset($contents['trackDetails'][0]['receivedBy'])) {
98 | $track->setRecipient($contents['trackDetails'][0]['receivedBy']);
99 | }
100 |
101 | if ($status == Track::STATUS_PICKUP && isset($contents['trackDetails'][0]['upsAccessPoint'])) {
102 | $track->addAdditionalDetails('location', $contents['trackDetails'][0]['upsAccessPoint']);
103 | $track->addAdditionalDetails('pickupDueDate', $contents['trackDetails'][0]['upsAccessPoint']['pickupPackageByDate']);
104 | }
105 | }
106 |
107 | return $track->sortEvents();
108 | }
109 |
110 |
111 | protected function getDescription($activity)
112 | {
113 | if (!isset($activity['milestone'])) {
114 | return null;
115 | }
116 |
117 | if (!$this->descriptionLookup) {
118 | $this->loadDescriptionLookup();
119 | }
120 |
121 | return array_key_exists($activity['milestone']['name'], $this->descriptionLookup)
122 | ? $this->descriptionLookup[$activity['milestone']['name']]
123 | : null;
124 | }
125 |
126 |
127 | protected function loadDescriptionLookup()
128 | {
129 | try {
130 | $url = $this->descriptionLookupEndpoint
131 | . '?'
132 | . http_build_query(['loc' => $this->getLanguageQueryParam($this->language)]);
133 |
134 | $response = $this->getDataProvider()->get($url);
135 |
136 | $this->descriptionLookup = $this->extractKeysAndValues(json_decode($response, true));
137 | } catch (\Exception $e) {
138 | $this->descriptionLookup = [];
139 | }
140 | }
141 |
142 |
143 | protected function extractKeysAndValues($array)
144 | {
145 | return array_reduce((array)$array, function ($lookups, $value) {
146 | if (!is_array($value)) {
147 | return $lookups;
148 | }
149 |
150 | if (!array_key_exists('key', $value) && !array_key_exists('value', $value)) {
151 | return array_merge($lookups, $this->extractKeysAndValues($value));
152 | }
153 |
154 | $lookups[$value['key']] = $value['value'];
155 |
156 | return $lookups;
157 | }, []);
158 | }
159 |
160 |
161 | /**
162 | * Parse the date from the given strings.
163 | *
164 | * @param array $activity
165 | *
166 | * @return Carbon
167 | */
168 | protected function getDate($activity)
169 | {
170 | return Carbon::parse("{$activity['date']} {$activity['time']}");
171 | }
172 |
173 |
174 | protected function getStatuses()
175 | {
176 | return [
177 | Track::STATUS_PICKUP => [
178 | 'UPS Access Point™ possession',
179 | 'Beim UPS Access Point™',
180 | 'Delivered to UPS Access Point™',
181 | 'An UPS Access Point™ zugestellt',
182 | ],
183 | Track::STATUS_IN_TRANSIT => [
184 | 'Auftrag verarbeitet',
185 | 'Wird zugestellt',
186 | 'Ready for UPS',
187 | 'Scan',
188 | 'Out For Delivery',
189 | 'receiver requested a hold for a future delivery date',
190 | 'receiver was not available at the time of the first delivery attempt',
191 | 'war beim 1. Zustellversuch nicht anwesend',
192 | 'Adresse wurde korrigiert und die Zustellung neu terminiert',
193 | 'The address has been corrected',
194 | 'A final attempt will be made',
195 | 'ltiger Versuch erfolgt',
196 | 'Will deliver to a nearby UPS Access Point™ for customer pick up',
197 | 'Zustellung wird zur Abholung durch Kunden an nahem UPS Access Point™ abgegeben',
198 | 'Customer was not available when UPS attempted delivery',
199 | 'In Einrichtung eingetroffen',
200 | 'Arrived at Facility',
201 | 'In Einrichtung eingetroffen',
202 | 'Departed from Facility',
203 | 'Hat Einrichtung verlassen',
204 | 'Order Processed',
205 | 'Auftrag verarbeitet',
206 | 'Wird heute zugestellt',
207 | 'Processing at UPS Facility',
208 | 'Bearbeitung in UPS Einrichtung',
209 | ],
210 | Track::STATUS_WARNING => [
211 | 'attempting to obtain a new delivery address',
212 | 'eine neue Zustelladresse für den Empf',
213 | 'nderung für dieses Paket ist in Bearbeitung',
214 | 'A delivery change for this package is in progress',
215 | 'The receiver was not available at the time of the final delivery attempt',
216 | ],
217 | Track::STATUS_EXCEPTION => [
218 | 'Exception',
219 | 'Adressfehlers konnte die Sendung nicht zugestellt',
220 | 'nger ist unbekannt',
221 | 'The address is incomplete',
222 | 'ist falsch',
223 | 'is incorrect',
224 | 'ltigen Zustellversuch nicht anwesend',
225 | 'receiver was not available at the time of the final delivery attempt',
226 | 'verweigerte die Annahme',
227 | 'refused the delivery',
228 | ],
229 | Track::STATUS_DELIVERED => [
230 | 'Delivered',
231 | 'Zugestellt',
232 | ],
233 | ];
234 | }
235 |
236 |
237 | /**
238 | * Match a shipping status from the given description.
239 | *
240 | * @param $statusDescription
241 | *
242 | * @return string
243 | */
244 | protected function resolveState($statusDescription)
245 | {
246 | foreach ($this->getStatuses() as $status => $needles) {
247 | foreach ($needles as $needle) {
248 | if (stripos($statusDescription, $needle) !== false) {
249 | return $status;
250 | }
251 | }
252 | }
253 |
254 | return Track::STATUS_UNKNOWN;
255 | }
256 |
257 |
258 | /**
259 | * Build the url for the given tracking number.
260 | *
261 | * @param string $trackingNumber
262 | * @param null $language
263 | * @param array $params
264 | *
265 | * @return string
266 | */
267 | public function trackingUrl($trackingNumber, $language = null, $params = [])
268 | {
269 | $language = $language ?: $this->language;
270 |
271 | $additionalParams = !empty($params) ? $params : $this->trackingUrlParams;
272 |
273 | $qry = http_build_query(array_merge([
274 | 'loc' => $this->getLanguageQueryParam($language),
275 | 'tracknum' => $trackingNumber,
276 | ], $additionalParams));
277 |
278 | return $this->trackingUrl . '?' . $qry;
279 | }
280 |
281 |
282 | public function serviceUrl($language = null)
283 | {
284 | $language = $language ?: $this->language;
285 |
286 | return $this->serviceEndpoint
287 | . '?'
288 | . http_build_query([
289 | 'loc' => $this->getLanguageQueryParam($language),
290 | ]);
291 | }
292 |
293 |
294 | /**
295 | * Get the language value for the url query
296 | *
297 | * @param string $givenLanguage
298 | *
299 | * @return string
300 | */
301 | protected function getLanguageQueryParam($givenLanguage)
302 | {
303 | if ($givenLanguage == 'de') {
304 | return 'de_DE';
305 | }
306 |
307 | return 'en_US';
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/Trackers/DHL.php:
--------------------------------------------------------------------------------
1 | parsedJson = null;
49 |
50 | return parent::track($number, $language, $params);
51 | }
52 |
53 |
54 | /**
55 | * Get the contents of the given url.
56 | *
57 | * @param string $url
58 | *
59 | * @return string
60 | */
61 | protected function fetch($url)
62 | {
63 | if ($this->defaultDataProvider !== 'guzzle') {
64 | return $this->getDataProvider()->get($url);
65 | }
66 |
67 | return $this->getDataProvider()->get(
68 | $url,
69 | [
70 | 'timeout' => 5,
71 | 'headers' => [
72 | 'User-Agent' => 'tracking/1.0',
73 | 'Accept' => 'text/html',
74 | ]
75 | ]
76 | );
77 | }
78 |
79 |
80 | /**
81 | * @param string $contents
82 | *
83 | * @return Track
84 | * @throws \Exception
85 | */
86 | protected function buildResponse($contents)
87 | {
88 | $dom = new DOMDocument;
89 | @$dom->loadHTML($contents);
90 | $dom->preserveWhiteSpace = false;
91 |
92 | $domxpath = new DOMXPath($dom);
93 |
94 | return $this->getTrack($domxpath);
95 | }
96 |
97 |
98 | /**
99 | * Get the shipment status history.
100 | *
101 | * @param DOMXPath $xpath
102 | *
103 | * @return Track
104 | * @throws \Exception
105 | */
106 | protected function getTrack(DOMXPath $xpath)
107 | {
108 | $track = new Track;
109 |
110 | foreach ($this->getEvents($xpath) as $event) {
111 | $track->addEvent(Event::fromArray([
112 | 'description' => isset($event->status) ? strip_tags($event->status) : '',
113 | 'status' => $status = isset($event->status) ? $this->resolveStatus(strip_tags($event->status)) : '',
114 | 'date' => isset($event->datum) ? Carbon::parse($event->datum) : null,
115 | 'location' => isset($event->ort) ? $event->ort : '',
116 | ]));
117 |
118 | if ($status == Track::STATUS_DELIVERED && $recipient = $this->getRecipient($xpath)) {
119 | $track->setRecipient($recipient);
120 | }
121 | }
122 |
123 | return $track->sortEvents();
124 | }
125 |
126 |
127 | /**
128 | * Get the events.
129 | *
130 | * @param DOMXPath $xpath
131 | * @return array
132 | * @throws \Exception
133 | */
134 | protected function getEvents(DOMXPath $xpath)
135 | {
136 | $progress = $this->parseJson($xpath)->sendungen[0]->sendungsdetails->sendungsverlauf;
137 |
138 | return $progress->fortschritt > 0
139 | ? (array)$progress->events
140 | : [];
141 | }
142 |
143 |
144 | /**
145 | * Parse the recipient.
146 | *
147 | * @param DOMXPath $xpath
148 | *
149 | * @return null|string
150 | * @throws \Exception
151 | */
152 | protected function getRecipient(DOMXPath $xpath)
153 | {
154 | $deliveryDetails = $this->parseJson($xpath)->sendungen[0]->sendungsdetails->zustellung;
155 |
156 | return isset($deliveryDetails->empfaenger) && isset($deliveryDetails->empfaenger->name)
157 | ? $deliveryDetails->empfaenger->name
158 | : null;
159 | }
160 |
161 |
162 | /**
163 | * Parse the JSON from the script tag.
164 | *
165 | * @param DOMXPath $xpath
166 | * @return mixed|object
167 | * @throws \Exception
168 | */
169 | protected function parseJson(DOMXPath $xpath)
170 | {
171 | if ($this->parsedJson) {
172 | return $this->parsedJson;
173 | }
174 |
175 | $scriptTags = $xpath->query("//script");
176 |
177 | if ($scriptTags->length < 1) {
178 | throw new \Exception("Unable to parse DHL tracking data for [{$this->parcelNumber}].");
179 | }
180 |
181 | $matched = preg_match(
182 | "/initialState: JSON\.parse\((.*)\)\,/m",
183 | $scriptTags->item(2)->nodeValue,
184 | $matches
185 | );
186 |
187 | if ($matched !== 1) {
188 | throw new \Exception("Unable to parse DHL tracking data for [{$this->parcelNumber}].");
189 | }
190 |
191 | return $this->parsedJson = json_decode(json_decode($matches[1]));
192 | }
193 |
194 |
195 | /**
196 | * Match a shipping status from the given description.
197 | *
198 | * @param $statusDescription
199 | *
200 | * @return string
201 | */
202 | protected function resolveStatus($statusDescription)
203 | {
204 | $statuses = [
205 | Track::STATUS_DELIVERED => [
206 | 'aus der PACKSTATION abgeholt',
207 | 'erfolgreich zugestellt',
208 | 'hat die Sendung in der Filiale abgeholt',
209 | 'des Nachnahme-Betrags an den Zahlungsempf',
210 | 'Sendung wurde zugestellt',
211 | 'Die Sendung wurde ausgeliefert',
212 | 'shipment has been successfully delivered',
213 | 'recipient has picked up the shipment from the retail outlet',
214 | 'recipient has picked up the shipment from the PACKSTATION',
215 | 'item has been sent',
216 | 'delivered from the delivery depot to the recipient by simplified company delivery',
217 | 'per vereinfachter Firmenzustellung ab Eingangspaketzentrum zugestellt',
218 | 'im Rahmen der kontaktlosen Zustellung zugestellt',
219 | 'Pick-up was successful',
220 | 'Sendung wurde an den Empfänger zugestellt',
221 | 'Shipment has been delivered to the recipient',
222 | 'Shipment delivered',
223 | ],
224 | Track::STATUS_IN_TRANSIT => [
225 | 'in das Zustellfahrzeug geladen',
226 | 'im Start-Paketzentrum bearbeitet',
227 | 'im Ziel-Paketzentrum bearbeitet',
228 | 'im Paketzentrum bearbeitet',
229 | 'Auftragsdaten zu dieser Sendung wurden vom Absender elektronisch an DHL',
230 | 'auf dem Weg zur PACKSTATION',
231 | 'wird in eine PACKSTATION weitergeleitet',
232 | 'Die Sendung wurde abgeholt',
233 | 'im Export-Paketzentrum bearbeitet',
234 | 'Sendung wird ins Zielland',
235 | 'will be transported to the destination country',
236 | 'vom Absender in der Filiale eingeliefert',
237 | 'Sendung konnte nicht in die PACKSTATION eingestellt werden und wurde in eine Filiale',
238 | 'Sendung konnte nicht zugestellt werden und wird jetzt zur Abholung in die Filiale/Agentur gebracht',
239 | 'shipment has been picked up',
240 | 'instruction data for this shipment have been provided',
241 | 'shipment has been processed',
242 | 'shipment has been posted by the sender',
243 | 'hipment has been loaded onto the delivery vehicle',
244 | 'A 2nd attempt at delivery is being made',
245 | 'shipment is on its way to the PACKSTATION',
246 | 'forwarded to a PACKSTATION',
247 | 'shipment could not be delivered to the PACKSTATION and has been forwarded to a retail outlet',
248 | 'shipment could not be delivered, and the recipient has been notified',
249 | 'A 2nd attempt at delivery is being made',
250 | 'Es erfolgt ein 2. Zustellversuch',
251 | 'Sendung wurde elektronisch angekündigt',
252 | 'sendung wurde an DHL übergeben',
253 | 'Sendung ist in der Region des Empfängers angekommen',
254 | 'The shipment arrived in the region of recipient',
255 | 'Die Zustellung an einen gewünschten Ablageort/Nachbarn wurde gewählt',
256 | 'There is a preferred location/neighbour for this item',
257 | 'Die Sendung wurde für den Weitertransport vorbereitet',
258 | 'The shipment was prepared for onward transport',
259 | 'The shipment has been processed in the parcel center of origin',
260 | 'für den Weitertransport in die Region',
261 | 'Die Zustellung der Sendung verzögert sich',
262 | 'The delivery of the shipment is delayed',
263 | 'Für diese Sendung wurde eine Paketumleitung',
264 | 'Für diese Sendung wurde ein Ablageort',
265 | 'The recipient has selected a',
266 | 'Die Sendung wird für die Verladung ins Zustellfahrzeug vorbereitet',
267 | 'The shipment is being prepared for loading onto the delivery vehicle',
268 | 'Sendung hat die DHL-Station verlassen',
269 | 'Sendung wurde abgeholt',
270 | 'Shipment picked up in',
271 | 'In Auslieferung durch Kurier',
272 | 'With delivery courier',
273 | 'Sendung sortiert',
274 | 'Ankunft in der DHL Zustellstation',
275 | 'Arrival at DHL Delivery Station',
276 | 'Ankunft in DHL Station',
277 | 'Shipment has arrived at hub',
278 | 'Departed Facility',
279 | 'Arrived at sort facility in',
280 | 'Processed at',
281 | 'Shipment has arrived at delivery location',
282 | 'Sendung zur Aufbewahrung in der DHL Station',
283 | 'Die Sendung ist im Zielland/Zielgebiet eingetroffen',
284 | 'The shipment has arrived in the destination country/destination area',
285 | 'Die Sendung ist im Paketzentrum eingetroffen',
286 | 'The shipment has arrived at the parcel center',
287 | 'Die Sendung wird zur Verzollung im Zielland/Zielgebiet vorbereitet',
288 | 'Shipment is prepared for customs clearance in the destination country/destination area',
289 | 'Die Sendung wurde durch den Zoll im Zielland/Zielgebiet freigegeben',
290 | 'The shipment has cleared customs in the destination country/destination area',
291 | 'Die Sendung hat das Import-Paketzentrum im Zielland/Zielgebiet verlassen',
292 | 'The shipment has left the import parcel center in the destination country/destination area',
293 | 'Die Sendung wird im Zustell-Depot für die Zustellung vorbereitet',
294 | 'The shipment is being prepared for delivery in the delivery depot',
295 | 'The recipient was not present',
296 | 'wurde nicht angetroffen',
297 | ],
298 | Track::STATUS_PICKUP => [
299 | 'Die Sendung liegt in der PACKSTATION',
300 | 'Uhrzeit der Abholung kann der Benachrichtigungskarte entnommen werden',
301 | 'earliest time when it can be picked up can be found on the notification card',
302 | 'shipment is ready for pick-up at the PACKSTATION',
303 | 'Sendung wird zur Abholung in die',
304 | 'Sendung wurde zur Abholung in die',
305 | 'The shipment is being brought to',
306 | 'Die Sendung liegt ab sofort in der',
307 | 'The shipment is available for pick-up',
308 | 'Die Sendung liegt für den Empfänger zur Abholung bereit',
309 | 'The shipment is ready for pick-up by the recipient',
310 | 'Die Sendung liegt zur Abholung in',
311 | 'The shipment is ready for collection',
312 | ],
313 | Track::STATUS_WARNING => [
314 | 'Sendung konnte nicht zugestellt werden',
315 | 'shipment could not be delivered',
316 | 'attempting to obtain a new delivery address',
317 | 'eine neue Zustelladresse für den Empf',
318 | 'Sendung wurde fehlgeleitet und konnte nicht zugestellt werden. Die Sendung wird umadressiert und an den',
319 | 'shipment was misrouted and could not be delivered. The shipment will be readdressed and forwarded to the recipient',
320 | 'Leider war eine Zustellung der Sendung heute nicht möglich',
321 | 'Die Sendung wurde leider fehlgeleitet',
322 | 'Shipment on hold',
323 | ],
324 | Track::STATUS_EXCEPTION => [
325 | 'cksendung eingeleitet',
326 | 'Adressfehlers konnte die Sendung nicht zugestellt',
327 | 'nger ist unbekannt',
328 | 'The address is incomplete',
329 | 'ist falsch',
330 | 'is incorrect',
331 | 'recipient has not picked up the shipment',
332 | 'nicht in der Filiale abgeholt',
333 | 'The shipment is being returned',
334 | 'Es erfolgt eine Rücksendung',
335 | 'Zustellung der Sendung nicht möglich',
336 | 'recipient is unknown',
337 | 'Zustellung nicht möglich',
338 | 'Delivery not possible',
339 | ],
340 | ];
341 |
342 | foreach ($statuses as $status => $needles) {
343 | foreach ($needles as $needle) {
344 | if (stripos($statusDescription, $needle) !== false) {
345 | return $status;
346 | }
347 | }
348 | }
349 |
350 | return Track::STATUS_UNKNOWN;
351 | }
352 |
353 |
354 | /**
355 | * Build the url for the given tracking number.
356 | *
357 | * @param string $trackingNumber
358 | * @param string|null $language
359 | * @param array $params
360 | *
361 | * @return string
362 | */
363 | public function trackingUrl($trackingNumber, $language = null, $params = [])
364 | {
365 | $language = $language ?: $this->language;
366 |
367 | $additionalParams = !empty($params) ? $params : $this->trackingUrlParams;
368 |
369 | $urlParams = array_merge([
370 | 'lang' => $language,
371 | 'idc' => $trackingNumber,
372 | ], $additionalParams);
373 |
374 | $qry = http_build_query($urlParams);
375 |
376 | return $this->trackingUrl . '?' . $qry;
377 | }
378 |
379 |
380 | /**
381 | * Build the endpoint url
382 | *
383 | * @param string $trackingNumber
384 | * @param string|null $language
385 | * @param array $params
386 | *
387 | * @return string
388 | */
389 | protected function getEndpointUrl($trackingNumber, $language = null, $params = [])
390 | {
391 | $language = $language ?: $this->language;
392 |
393 | $additionalParams = !empty($params) ? $params : $this->endpointUrlParams;
394 |
395 | $urlParams = array_merge([
396 | 'lang' => $language,
397 | 'language' => $language,
398 | 'idc' => $trackingNumber,
399 | 'domain' => 'de',
400 | ], $additionalParams);
401 |
402 | return $this->serviceEndpoint . '%3F' . http_build_query($urlParams);
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/tests/mock/PostCH/unknown.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Track & Trace
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
58 |
65 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
222 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
Search results
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 | NB:
328 |
329 | As a private recipient, you have the option of being automatically notified of the status of the consignments that are on their way to you. More information under
“My consignments”
330 |
331 | If you are a business customer and the sender of this consignment, you can log in and view more details, such as the consignment image (if available).
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 | RB36689096DE
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
Unfortunately your search was unsuccessful. The causes of this could be: - Consignment has not yet been handed over to Swiss Post or scanned. - Shipping dates back more than 180 days (public search) or 360 days (search for logged-in business customers with a billing relationship) - Typing error in the number - Incorrect format for consignment number (see "Frequently Asked Questions" link)
385 |
Please check your details and/or try again in a few hours. Useful information can be found above under "Info".
386 |
We would be pleased to help if you have any questions.
387 |
388 | custcare@swisspost.ch
389 |
390 | Thank you.
391 |
392 |
International consignments can also be tracked via our partners: - TNT Swiss Post SA , leading provider of international courier and express services - Swiss Post GLS , transport specialist for business-to-business goods shipments
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
For import consignments, the enquiry must be initiated by the sender abroad via his or her postal company. Please contact the sender to ensure that he or she requests the enquiry.
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
433 |
434 |
435 |
447 |
448 |
449 |
--------------------------------------------------------------------------------