Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use JouwWeb\Sendcloud\Model\SenderAddress;
use JouwWeb\Sendcloud\Model\ShippingMethod;
use JouwWeb\Sendcloud\Model\ShippingProduct;
use JouwWeb\Sendcloud\Model\Tracking;
use JouwWeb\Sendcloud\Model\User;
use JouwWeb\Sendcloud\Model\WebhookEvent;
use Psr\Http\Message\RequestInterface;
Expand Down Expand Up @@ -824,4 +825,28 @@ protected function createParcelData(

return $parcelData;
}

/**
* Returns the tracking history of a Parcel.
*
* @param string $trackingNumber The tracking number of the Parcel.
* @return Tracking
* @throws SendcloudClientException
* @see https://api.sendcloud.dev/docs/sendcloud-public-api/branches/v2/tracking/operations/get-a-tracking
*/
public function getTracking(
string $trackingNumber,
): Tracking {
try {
$response = $this->guzzleClient->get('tracking/'.$trackingNumber);
$trackingData = json_decode((string)$response->getBody(), true);

return Tracking::fromData($trackingData);
} catch (TransferException $exception) {
throw Utility::parseGuzzleException(
$exception,
'An error occurred while getting tracking from the Sendcloud API.'
);
}
}
}
47 changes: 47 additions & 0 deletions src/Model/Tracking.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace JouwWeb\Sendcloud\Model;

class Tracking
{
/**
* @param int $parcelId ID for the `Parcel` related to the `Tracking`.
* @param string $carrierCode A carrier represented by a Sendcloud code.
* @param \DateTimeImmutable|null $createdAt Timestamp indicating when the `Parcel` was first tracked by Sendcloud’s systems.
* @param string $carrierTrackingUrl Carrier’s tracking page for this parcel.
* @param string|null $sendcloudTrackingUrl SendCloud’s Tracking page for this parcel. Null if the tracking page for the brand associated with the parcel has not been published.
* @param bool $isReturn True if the `Parcel` concerns a return to the merchant.
* @param bool $isToServicePoint True if the `Parcel` last mile is different than home address.
* @param bool $isMailBox Indicates whether this `Parcel` will be delivered to a mail box.
* @param \DateTimeImmutable|null $expectedDeliveryDate Estimated date of delivery.
* @param array $statuses List of all the package statuses, with timestamps, statuses and messages.
*/
public function __construct(
public readonly int $parcelId,
public readonly string $carrierCode,
public readonly \DateTimeImmutable $createdAt,
public readonly string $carrierTrackingUrl,
public readonly ?string $sendcloudTrackingUrl,
public readonly bool $isReturn,
public readonly bool $isToServicePoint,
public readonly bool $isMailBox,
public readonly ?\DateTimeImmutable $expectedDeliveryDate,
public readonly array $statuses,
) {
}
public static function fromData(array $data): self
{
return new self(
parcelId: (int)$data['parcel_id'],
carrierCode: (string)$data['carrier_code'],
createdAt: new \DateTimeImmutable((string)$data['created_at']),
carrierTrackingUrl: (string)$data['carrier_tracking_url'],
sendcloudTrackingUrl: (string)$data['sendcloud_tracking_url'],
isReturn: (bool)$data['is_return'],
isToServicePoint: (bool)$data['is_to_service_point'],
isMailBox: (bool)$data['is_mail_box'],
expectedDeliveryDate: new \DateTimeImmutable($data['expected_delivery_date']),
statuses: array_map(TrackingStatus::fromData(...), $data['statuses']),
);
}
}
33 changes: 33 additions & 0 deletions src/Model/TrackingStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace JouwWeb\Sendcloud\Model;

class TrackingStatus
{
/**
* @param \DateTimeImmutable $carrierUpdateTimestamp Timestamp related to the creation of this `TrackingStatus`.
* @param string $parcelStatusHistoryId ID of the `TrackingStatus`.
* @param string $parentStatus Current delivery status of the parcel. A same `parentStatus` can be related to various `carrierMessage`.
* @param string $carrierCode A carrier represented by a Sendcloud code. Can be an empty string, even if the `Tracking->carrierCode` is not empty.
* @param string $carrierMessage Status description specified by the carrier, more detailed and more "human-friendly" than `parentStatus`.
*/
public function __construct(
public readonly \DateTimeImmutable $carrierUpdateTimestamp,
public readonly string $parcelStatusHistoryId,
public readonly string $parentStatus,
public readonly string $carrierCode,
public readonly string $carrierMessage,
) {
}

public static function fromData(array $data): self
{
return new self(
carrierUpdateTimestamp: new \DateTimeImmutable($data['carrier_update_timestamp']),
parcelStatusHistoryId: (string)$data['parcel_status_history_id'],
parentStatus: (string)$data['parent_status'],
carrierCode: (string)$data['carrier_code'],
carrierMessage: (string)$data['carrier_message'],
);
}
}
63 changes: 63 additions & 0 deletions test/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -815,4 +815,67 @@ public function testGetParcelDocumentReturnsTheRequestedContent(): void

$this->assertEquals('The ZPL content', $this->client->getParcelDocument(1, Parcel::DOCUMENT_TYPE_LABEL, Parcel::DOCUMENT_CONTENT_TYPE_ZPL, Parcel::DOCUMENT_DPI_203));
}

public function testGetTracking(): void
{
$this->guzzleClientMock->expects($this->once())->method('request')->willReturn(new Response(
200,
[],
'{
"parcel_id": "123456789",
"carrier_code": "colissimo",
"created_at": "2026-01-14 09:58:57.726684+00:00",
"carrier_tracking_url": "https://tracking.eu-central-1-0.sendcloud.sc/forward?carrier=colissimo&code=fake_url",
"sendcloud_tracking_url": null,
"is_return": false,
"is_to_service_point": false,
"is_mail_box": false,
"expected_delivery_date": "2026-01-15",
"statuses": [{
"carrier_update_timestamp": "2026-01-14 09:58:00+00:00",
"parcel_status_history_id": "7654321000",
"parent_status": "announced-uncollected",
"carrier_code": "colissimo",
"carrier_message": "Your parcel will soon be handed over to us! It is being prepared by the sender."
},
{
"carrier_update_timestamp": "2026-01-14 09:58:57.726684+00:00",
"parcel_status_history_id": "7654321408",
"parent_status": "no-label",
"carrier_code": "",
"carrier_message": "No label"
},
{
"carrier_update_timestamp": "2026-01-17 10:30:00+00:00",
"parcel_status_history_id": "7654321253",
"parent_status": "delivered",
"carrier_code": "colissimo",
"carrier_message": "Your parcel has been delivered in your letter box."
}]}'
));

$tracking = $this->client->getTracking('ABCDEF');

// Assert all the data is get in Tracking
$this->assertEquals('123456789', $tracking->parcelId);
$this->assertEquals('colissimo', $tracking->carrierCode);
$this->assertEquals(new \DateTimeImmutable('2026-01-14 09:58:57.726684+00:00'), $tracking->createdAt);
$this->assertEquals('https://tracking.eu-central-1-0.sendcloud.sc/forward?carrier=colissimo&code=fake_url', $tracking->carrierTrackingUrl);
$this->assertEquals('', $tracking->sendcloudTrackingUrl);
$this->assertFalse($tracking->isReturn);
$this->assertFalse( $tracking->isToServicePoint);
$this->assertFalse($tracking->isMailBox);
$this->assertEquals(new \DateTimeImmutable('2026-01-15'), $tracking->expectedDeliveryDate);
$this->assertCount(3, $tracking->statuses);

// Assert all the data is get in TrackingStatus
$this->assertEquals(new \DateTimeImmutable('2026-01-17 10:30:00+00:00'), $tracking->statuses[2]->carrierUpdateTimestamp);
$this->assertEquals('7654321253', $tracking->statuses[2]->parcelStatusHistoryId);
$this->assertEquals('delivered', $tracking->statuses[2]->parentStatus);
$this->assertEquals('colissimo', $tracking->statuses[2]->carrierCode);
$this->assertEquals('Your parcel has been delivered in your letter box.', $tracking->statuses[2]->carrierMessage);

// Assertion for empty string
$this->assertEquals('', $tracking->statuses[1]->carrierCode);
}
}