Skip to content

Commit

Permalink
Merge pull request #3 from JouwWeb/international-parcel-support
Browse files Browse the repository at this point in the history
international-parcel-support
  • Loading branch information
villermen authored Feb 6, 2020
2 parents 9dccc49 + 588f3e5 commit ad462d3
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 83 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 JouwWeb
Copyright (c) 2020 JouwWeb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# SendCloud
# Sendcloud

[![CircleCI](https://circleci.com/gh/JouwWeb/sendcloud.svg?style=svg)](https://circleci.com/gh/JouwWeb/sendcloud)

This is a PHP library that provides a simple way to communicate with the SendCloud API. It was created because there
were no simple alternatives that follow good object-oriented code practices.
This is a PHP library that provides a simple way to communicate with the Sendcloud API. It was created because there
were no simple alternatives that follow good object-oriented code practices.

> NOTE: This library does not implement all SendCloud API functionality. If you require functionality that is missing
> NOTE: This library does not implement all Sendcloud API functionality. If you require functionality that is missing
please request it through a GitHub issue or pull request.

## Example
Expand All @@ -14,6 +14,7 @@ please request it through a GitHub issue or pull request.
use JouwWeb\SendCloud\Client;
use JouwWeb\SendCloud\Model\Address;
use JouwWeb\SendCloud\Model\Parcel;
use JouwWeb\SendCloud\Model\ParcelItem;
use JouwWeb\SendCloud\Model\WebhookEvent;
use JouwWeb\SendCloud\Exception\SendCloudRequestException;

Expand All @@ -29,20 +30,27 @@ foreach ($client->getShippingMethods() as $shippingMethod) {

// Create a parcel and label
try {
// Most of these arguments are optional and will fall back to defaults configured in SendCloud
// Most of these arguments are optional and will fall back to defaults configured in Sendcloud
$parcel = $client->createParcel(
new Address('Customer name', 'Customer company name', 'Customer street', '4A', 'City', '9999ZZ', 'NL', '[email protected]', '+31612345678'),
null, // Service point ID
'20190001', // Order number
2500 // Weight (2.5kg)
2500, // Weight (2.5kg)
// Below options are only required when shipping outside the EU
'customsInvoiceNumber',
Parcel::CUSTOMS_SHIPMENT_TYPE_COMMERCIAL_GOODS,
[
new ParcelItem('green tea', 1, 123, 15.20, '090210', 'EC'),
new ParcelItem('cardboard', 3, 50, 0.20, '090210', 'NL'),
]
);

$parcel = $client->createLabel(
$parcel,
8, // Shipping method ID
null // Default sender address
);

$pdf = $client->getLabelPdf($parcel, Parcel::LABEL_FORMAT_A4_BOTTOM_RIGHT);

var_dump($parcel, $pdf);
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jouwweb/sendcloud",
"description": "Provides a client to interact with the SendCloud API in an object-oriented way.",
"description": "Provides a client to interact with the Sendcloud API in an object-oriented way.",
"keywords": [
"sendcloud",
"client",
Expand Down
165 changes: 113 additions & 52 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace JouwWeb\SendCloud;

use JouwWeb\SendCloud\Model\ParcelItem;
use function GuzzleHttp\default_user_agent;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
Expand All @@ -18,7 +19,7 @@
use Psr\Http\Message\RequestInterface;

/**
* Client to perform calls on the SendCloud API.
* Client to perform calls on the Sendcloud API.
*/
class Client
{
Expand Down Expand Up @@ -66,7 +67,7 @@ public function __construct(
}

/**
* Fetches basic details about the SendCloud account.
* Fetches basic details about the Sendcloud account.
*
* @return User
* @throws SendCloudRequestException
Expand All @@ -76,12 +77,12 @@ public function getUser(): User
try {
return new User(json_decode((string)$this->guzzleClient->get('user')->getBody(), true)['user']);
} catch (RequestException $exception) {
throw $this->parseRequestException($exception, 'An error occurred while fetching the SendCloud user.');
throw $this->parseRequestException($exception, 'An error occurred while fetching the Sendcloud user.');
}
}

/**
* Fetches available SendCloud shipping methods.
* Fetches available Sendcloud shipping methods.
*
* @param int|null $servicePointId If passed, only shipping methods to the service point will be returned.
* @return ShippingMethod[]
Expand Down Expand Up @@ -118,27 +119,33 @@ public function getShippingMethods(?int $servicePointId = null): array
} catch (RequestException $exception) {
throw $this->parseRequestException(
$exception,
'An error occurred while fetching shipping methods from the SendCloud API.'
'An error occurred while fetching shipping methods from the Sendcloud API.'
);
}
}

/**
* Creates a parcel in SendCloud.
* Creates a parcel in Sendcloud.
*
* @param Address $shippingAddress Address to be shipped to.
* @param int|null $servicePointId The order will be shipped to the service point if supplied. $shippingAddress is
* still required as it will be printed on the label.
* @param string|null $orderNumber
* @param int|null $weight Weight of the parcel in grams. The default set in SendCloud will be used if null or zero.
* @param int|null $weight Weight of the parcel in grams. The default set in Sendcloud will be used if null or zero.
* @param string|null $customsInvoiceNumber
* @param int|null One of {@see Parcel::CUSTOMS_SHIPMENT_TYPES}.
* @param ParcelItem[]|null $items Items contained in the parcel.
* @return Parcel
* @throws SendCloudRequestException
*/
public function createParcel(
Address $shippingAddress,
?int $servicePointId,
?string $orderNumber = null,
?int $weight = null
?int $weight = null,
?string $customsInvoiceNumber = null,
?int $customsShipmentType = null,
?array $items = null
): Parcel {
$parcelData = $this->getParcelData(
null,
Expand All @@ -148,7 +155,10 @@ public function createParcel(
$weight,
false,
null,
null
null,
$customsInvoiceNumber,
$customsShipmentType,
$items
);

try {
Expand All @@ -160,7 +170,7 @@ public function createParcel(

return new Parcel(json_decode((string)$response->getBody(), true)['parcel']);
} catch (RequestException $exception) {
throw $this->parseRequestException($exception, 'Could not create parcel in SendCloud.');
throw $this->parseRequestException($exception, 'Could not create parcel in Sendcloud.');
}
}

Expand All @@ -182,6 +192,9 @@ public function updateParcel($parcel, Address $shippingAddress): Parcel
null,
false,
null,
null,
null,
null,
null
);

Expand All @@ -203,7 +216,7 @@ public function updateParcel($parcel, Address $shippingAddress): Parcel
*
* @param Parcel|int $parcel
* @param int $shippingMethodId
* @param SenderAddress|int|Address|null $senderAddress Passing null will pick SendCloud's default. An Address will
* @param SenderAddress|int|Address|null $senderAddress Passing null will pick Sendcloud's default. An Address will
* use undocumented behavior that will disable branding personalizations.
* @return Parcel
* @throws SendCloudRequestException
Expand All @@ -218,7 +231,10 @@ public function createLabel($parcel, int $shippingMethodId, $senderAddress): Par
null,
true,
$shippingMethodId,
$senderAddress
$senderAddress,
null,
null,
null
);

try {
Expand All @@ -230,7 +246,7 @@ public function createLabel($parcel, int $shippingMethodId, $senderAddress): Par

return new Parcel(json_decode((string)$response->getBody(), true)['parcel']);
} catch (RequestException $exception) {
throw $this->parseRequestException($exception, 'Could not create parcel with SendCloud.');
throw $this->parseRequestException($exception, 'Could not create parcel with Sendcloud.');
}
}

Expand Down Expand Up @@ -424,6 +440,9 @@ public function getReturnPortalUrl($parcel): ?string
* @param int|null $shippingMethodId Required if requesting a label.
* @param SenderAddress|int|Address|null $senderAddress Passing null will pick SendCloud's default. An Address will
* use undocumented behavior that will disable branding personalizations.
* @param string|null $customsInvoiceNumber
* @param int|null One of {@see Parcel::CUSTOMS_SHIPMENT_TYPES}.
* @param ParcelItem[]|null $items
* @return mixed[]
*/
protected function getParcelData(
Expand All @@ -434,7 +453,10 @@ protected function getParcelData(
?int $weight,
bool $requestLabel,
?int $shippingMethodId,
$senderAddress
$senderAddress,
?string $customsInvoiceNumber,
?int $customsShipmentType,
?array $items
): array {
$parcelData = [];

Expand Down Expand Up @@ -465,53 +487,92 @@ protected function getParcelData(
}

if ($weight) {
$parcelData['weight'] = ceil($weight / 1000);
$parcelData['weight'] = (string)($weight / 1000);
}

if (!$requestLabel) {
return $parcelData;
if ($customsInvoiceNumber) {
$parcelData['customs_invoice_nr'] = $customsInvoiceNumber;
}

// Additional fields are added when requesting a label
$parcelData['request_label'] = true;
if ($customsShipmentType !== null) {
if (!in_array($customsShipmentType, Parcel::CUSTOMS_SHIPMENT_TYPES)) {
throw new \InvalidArgumentException(sprintf('Invalid customs shipment type %s.', $customsShipmentType));
}

// Sender address
if ($senderAddress instanceof SenderAddress) {
$senderAddress = $senderAddress->getId();
}
if (is_int($senderAddress)) {
/** @var int $senderAddress */
$parcelData['sender_address'] = $senderAddress;
} elseif ($senderAddress instanceof Address) {
/** @var Address $senderAddress */
$parcelData = array_merge($parcelData, [
'from_name' => $senderAddress->getName(),
'from_company_name' => $senderAddress->getCompanyName() ?? '',
'from_address_1' => $senderAddress->getStreet(),
'from_address_2' => '',
'from_house_number' => $senderAddress->getHouseNumber(),
'from_city' => $senderAddress->getCity(),
'from_postal_code' => $senderAddress->getPostalCode(),
'from_country' => $senderAddress->getCountryCode(),
'from_telephone' => $senderAddress->getPhoneNumber() ?? '',
'from_email' => $senderAddress->getEmailAddress(),
]);
} elseif ($senderAddress !== null) {
throw new \InvalidArgumentException(
'$senderAddressIdOrAddress must be an integer, an Address or null when requesting a label.'
);
$parcelData['customs_shipment_type'] = $customsShipmentType;
}

// Shipping method
if (!$shippingMethodId) {
throw new \InvalidArgumentException(
'$shippingMethodId must be passed when requesting a label.'
);
if ($items) {
$itemsData = [];

foreach (array_values($items) as $index => $item) {
if (!($item instanceof ParcelItem)) {
throw new \InvalidArgumentException(sprintf(
'Parcel item at index %s is not an instance of ParcelItem.',
$index
));
}

$itemData = [
'description' => $item->getDescription(),
'quantity' => $item->getQuantity(),
'weight' => (string)($item->getWeight() / 1000),
'value' => $item->getValue(),
];
if ($item->getHarmonizedSystemCode()) {
$itemData['hs_code'] = $item->getHarmonizedSystemCode();
}
if ($item->getOriginCountryCode()) {
$itemData['origin_country'] = $item->getOriginCountryCode();
}
$itemsData[] = $itemData;
}

$parcelData['parcel_items'] = $itemsData;
}

$parcelData['shipment'] = [
'id' => $shippingMethodId,
];
// Additional fields are only added when requesting a label
if ($requestLabel) {
$parcelData['request_label'] = true;

// Sender address
if ($senderAddress instanceof SenderAddress) {
$senderAddress = $senderAddress->getId();
}
if (is_int($senderAddress)) {
/** @var int $senderAddress */
$parcelData['sender_address'] = $senderAddress;
} elseif ($senderAddress instanceof Address) {
/** @var Address $senderAddress */
$parcelData = array_merge($parcelData, [
'from_name' => $senderAddress->getName(),
'from_company_name' => $senderAddress->getCompanyName() ?? '',
'from_address_1' => $senderAddress->getStreet(),
'from_address_2' => '',
'from_house_number' => $senderAddress->getHouseNumber(),
'from_city' => $senderAddress->getCity(),
'from_postal_code' => $senderAddress->getPostalCode(),
'from_country' => $senderAddress->getCountryCode(),
'from_telephone' => $senderAddress->getPhoneNumber() ?? '',
'from_email' => $senderAddress->getEmailAddress(),
]);
} elseif ($senderAddress !== null) {
throw new \InvalidArgumentException(
'$senderAddressIdOrAddress must be an integer, an Address or null when requesting a label.'
);
}

// Shipping method
if (!$shippingMethodId) {
throw new \InvalidArgumentException(
'$shippingMethodId must be passed when requesting a label.'
);
}

$parcelData['shipment'] = [
'id' => $shippingMethodId,
];
}

return $parcelData;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Exception/SendCloudRequestException.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function __construct(
}

/**
* Returns the code reported by SendCloud when available. This usually equals the HTTP status code.
* Returns the code reported by Sendcloud when available. This usually equals the HTTP status code.
*
* @return int|null
*/
Expand All @@ -48,7 +48,7 @@ public function getSendCloudCode(): ?int
}

/**
* Returns the error message reported by SendCloud when available.
* Returns the error message reported by Sendcloud when available.
*
* @return string|null
*/
Expand Down
Loading

0 comments on commit ad462d3

Please sign in to comment.