diff --git a/src/MyParcelComApi.php b/src/MyParcelComApi.php index 5977c8e7..986a244e 100644 --- a/src/MyParcelComApi.php +++ b/src/MyParcelComApi.php @@ -9,9 +9,7 @@ use GuzzleHttp\RequestOptions; use MyParcelCom\Sdk\Authentication\AuthenticatorInterface; use MyParcelCom\Sdk\Exceptions\InvalidResourceException; -use MyParcelCom\Sdk\Exceptions\MyParcelComException; use MyParcelCom\Sdk\Resources\Interfaces\CarrierInterface; -use MyParcelCom\Sdk\Resources\Interfaces\FileInterface; use MyParcelCom\Sdk\Resources\Interfaces\RegionInterface; use MyParcelCom\Sdk\Resources\Interfaces\ResourceFactoryInterface; use MyParcelCom\Sdk\Resources\Interfaces\ResourceInterface; @@ -20,6 +18,9 @@ use MyParcelCom\Sdk\Resources\Interfaces\ShipmentInterface; use MyParcelCom\Sdk\Resources\Interfaces\ShopInterface; use MyParcelCom\Sdk\Resources\ResourceFactory; +use MyParcelCom\Sdk\Shipments\ContractSelector; +use MyParcelCom\Sdk\Shipments\PriceCalculator; +use MyParcelCom\Sdk\Shipments\ServiceMatcher; use MyParcelCom\Sdk\Validators\ShipmentValidator; use Psr\Http\Message\ResponseInterface; use Psr\SimpleCache\CacheInterface; @@ -29,103 +30,9 @@ class MyParcelComApi implements MyParcelComApiInterface { - private $shipments = [ - 'shipment-id-1' => [ - 'id' => 'shipment-id-1', - 'recipient_address' => [ - 'street_1' => 'Some road', - 'street_2' => 'Room 3', - 'street_number' => 17, - 'street_number_suffix' => 'A', - 'postal_code' => '1GL HF1', - 'city' => 'Cardiff', - 'region_code' => 'CRF', - 'country_code' => 'GB', - 'first_name' => 'John', - 'last_name' => 'Doe', - 'company' => 'Acme Jewelry Co.', - 'email' => 'john@doe.com', - 'phone_number' => '+31 234 567 890', - ], - 'sender_address' => [ - 'street_1' => 'Some road', - 'street_2' => 'Room 3', - 'street_number' => 17, - 'street_number_suffix' => 'A', - 'postal_code' => '1GL HF1', - 'city' => 'Cardiff', - 'region_code' => 'CRF', - 'country_code' => 'GB', - 'first_name' => 'John', - 'last_name' => 'Doe', - 'company' => 'Acme Jewelry Co.', - 'email' => 'john@doe.com', - 'phone_number' => '+31 234 567 890', - ], - 'pickup_location' => [ - 'code' => '123456', - 'address' => [ - 'street_1' => 'Some road', - 'street_2' => 'Room 3', - 'street_number' => 17, - 'street_number_suffix' => 'A', - 'postal_code' => '1GL HF1', - 'city' => 'Cardiff', - 'region_code' => 'CRF', - 'country_code' => 'GB', - 'first_name' => 'John', - 'last_name' => 'Doe', - 'company' => 'Acme Jewelry Co.', - 'email' => 'john@doe.com', - 'phone_number' => '+31 234 567 890', - ], - ], - 'description' => 'order #8008135', - 'price' => 100, - 'currency' => 'EUR', - 'insuranceAmount' => 100, - 'barcode' => '3SABCD0123456789', - 'weight' => 24, - 'physical_properties' => [ - 'height' => 24, - 'width' => 50, - 'length' => 50, - 'volume' => 50, - 'weight' => 24, - ], - 'service_options' => [ - [ - 'id' => 'service-id-1', - ], - ], - 'shop' => [ - 'id' => 'shop-id-1', - ], - 'status' => [ - 'id' => 'status-id-1', - ], - 'service' => [ - 'id' => 'service-id-1', - ], - 'contract' => [ - 'id' => 'contract-id-1', - ], - 'files' => [ - [ - 'id' => 'file-id-1', - 'resource_type' => FileInterface::RESOURCE_TYPE_LABEL, - 'formats' => [['mime_type' => 'image/png', 'extension' => 'png']], - 'base64_data' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', - ], - [ - 'id' => 'file-id-2', - 'resource_type' => FileInterface::RESOURCE_TYPE_PRINTCODE, - 'formats' => [['mime_type' => 'image/png', 'extension' => 'png']], - 'base64_data' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkkPxfDwAC0gGZ9+czaQAAAABJRU5ErkJggg==', - ], - ], - ], - ]; + const API_VERSION = 'v1'; + const TTL_WEEK = 604800; + const TTL_10MIN = 600; /** @var string */ protected $apiUri; @@ -145,7 +52,8 @@ class MyParcelComApi implements MyParcelComApiInterface private static $singleton; /** - * + * Create an singleton instance of this class, which will be available in + * subsequent calls to `getSingleton()`. * * @param AuthenticatorInterface $authenticator * @param string $apiUri @@ -164,6 +72,8 @@ public static function createSingleton( } /** + * Get the singleton instance created. + * * @return MyParcelComApi */ public static function getSingleton() @@ -172,6 +82,10 @@ public static function getSingleton() } /** + * Create an instance for the api with given uri. If no cache is given, the + * filesystem is used for caching. If no resource factory is given, the + * default factory is used. + * * @param string $apiUri * @param CacheInterface|null $cache * @param ResourceFactoryInterface|null $resourceFactory @@ -208,7 +122,7 @@ public function authenticate(AuthenticatorInterface $authenticator) public function getRegions($countryCode = null, $regionCode = null) { // These resources can be stored for a week. - $regions = $this->getResourcesPromise($this->apiUri . self::PATH_REGIONS, 604800) + $regions = $this->getResourcesPromise($this->apiUri . self::PATH_REGIONS, self::TTL_WEEK) ->wait(); // For now, we need to manually filter the regions. @@ -224,7 +138,7 @@ public function getRegions($countryCode = null, $regionCode = null) public function getCarriers() { // These resources can be stored for a week. - return $this->getResourcesPromise($this->apiUri . self::PATH_CARRIERS, 604800) + return $this->getResourcesPromise($this->apiUri . self::PATH_CARRIERS, self::TTL_WEEK) ->wait(); } @@ -267,7 +181,7 @@ public function getPickUpDropOffLocations( $carrierUri = str_replace('{carrier_id}', $carrier->getId(), $uri); // These resources can be stored for a week. - return $this->getResourcesPromise($carrierUri, 604800) + return $this->getResourcesPromise($carrierUri, self::TTL_WEEK) ->otherwise(function (RequestException $reason) { return $this->handleRequestException($reason); }); @@ -285,7 +199,7 @@ public function getShops() { // These resources can be stored for a week. Or should be removed from // cache when updated - return $this->getResourcesPromise($this->apiUri . self::PATH_SHOPS, 604800) + return $this->getResourcesPromise($this->apiUri . self::PATH_SHOPS, self::TTL_WEEK) ->wait(); } @@ -310,14 +224,17 @@ public function getDefaultShop() public function getServices(ShipmentInterface $shipment = null) { // Services can be cached for a week. - $services = $this->getResourcesPromise($this->apiUri . self::PATH_SERVICES, 604800) + $services = $this->getResourcesPromise($this->apiUri . self::PATH_SERVICES, self::TTL_WEEK) ->wait(); if ($shipment !== null) { - $services = array_filter($services, function (ServiceInterface $service) { - // TODO + if ($shipment->getSenderAddress() === null) { + $shipment->setSenderAddress($this->getDefaultShop()->getReturnAddress()); + } - return true; + $matcher = new ServiceMatcher(); + $services = array_filter($services, function (ServiceInterface $service) use ($shipment, $matcher) { + return $matcher->matches($shipment, $service); }); } @@ -336,31 +253,31 @@ public function getServicesForCarrier(CarrierInterface $carrier) } /** - * @todo * {@inheritdoc} */ public function getShipments(ShopInterface $shop = null) { - return array_map(function ($shipment) use ($shop) { - return $shop - ? $this->resourceFactory->create(ResourceInterface::TYPE_SHIPMENT, $shipment)->setShop($shop) - : $this->resourceFactory->create(ResourceInterface::TYPE_SHIPMENT, $shipment); - }, $this->shipments); + $shipments = $this->getResourcesPromise($this->apiUri . self::PATH_SHIPMENTS)->wait(); + + if ($shop === null) { + return $shipments; + } + + // For now filter manually. + return array_filter($shipments, function (ShipmentInterface $shipment) use ($shop) { + return $shipment->getShop()->getId() === $shop->getId(); + }); } /** - * @todo * {@inheritdoc} */ public function getShipment($id) { - return isset($this->shipments[$id]) - ? $this->resourceFactory->create(ResourceInterface::TYPE_SHIPMENT, $this->shipments[$id]) - : null; + return $this->getResourceById(ResourceInterface::TYPE_SHIPMENT, $id); } /** - * @todo * {@inheritdoc} */ public function createShipment(ShipmentInterface $shipment) @@ -370,14 +287,6 @@ public function createShipment(ShipmentInterface $shipment) $shipment->setShop($this->getDefaultShop()); } - // If no service is set, default to the first shipment that is available - // for this shipment's configuration. - if ($shipment->getService() === null) { - $shipment->setService( - reset($this->getServices($shipment)) - ); - } - // If no sender address has been set, default to the shop's return // address. if ($shipment->getSenderAddress() === null) { @@ -386,7 +295,13 @@ public function createShipment(ShipmentInterface $shipment) ); } + // If no contract is set, select the cheapest one. + if ($shipment->getContract() === null) { + $this->determineContract($shipment); + } + $validator = new ShipmentValidator($shipment); + if (!$validator->isValid()) { $exception = new InvalidResourceException( 'Could not create shipment, shipment was invalid or incomplete' @@ -396,11 +311,51 @@ public function createShipment(ShipmentInterface $shipment) throw $exception; } - $shipment->setPrice(650); - $shipment->setCurrency('GBP'); - $shipment->setId('shipment-id-99'); + return $this->postResource($shipment); + } + + /** + * Determine what contract (and service) to use for given shipment and + * update the shipment. + * + * @param ShipmentInterface $shipment + * @return $this + */ + protected function determineContract(ShipmentInterface $shipment) + { + + if ($shipment->getService() !== null) { + $shipment->setContract((new ContractSelector())->selectCheapest( + $shipment, + $shipment->getService()->getContracts() + )); + + return $this; + } + $selector = new ContractSelector(); + $calculator = new PriceCalculator(); + $contracts = array_map( + function (ServiceInterface $service) use ($selector, $calculator, $shipment) { + $contract = $selector->selectCheapest($shipment, $service->getContracts()); + + return [ + 'price' => $calculator->calculate($shipment, $contract), + 'contract' => $contract, + 'service' => $service, + ]; + }, + $this->getServices($shipment) + ); + + usort($contracts, function ($a, $b) { + return $a['price'] - $b['price']; + }); + + $cheapest = reset($contracts); + $shipment->setContract($cheapest['contract']) + ->setService($cheapest['service']); - return $shipment; + return $this; } /** @@ -490,7 +445,7 @@ protected function getHttpClient() * @return PromiseInterface * @internal param string $path */ - protected function getResourcesPromise($url, $ttl = 600) + protected function getResourcesPromise($url, $ttl = self::TTL_10MIN) { $cacheKey = 'get.' . str_replace([':', '{', '}', '(', ')', '/', '\\', '@'], '-', $url); if (($resources = $this->cache->get($cacheKey))) { @@ -559,7 +514,10 @@ protected function jsonToResources(array $json) */ private function flattenResourceData(array $resourceData) { - $data = ['id' => $resourceData['id']]; + $data = [ + 'id' => $resourceData['id'], + 'type' => $resourceData['type'], + ]; if (isset($resourceData['attributes'])) { $data += $resourceData['attributes']; @@ -571,6 +529,10 @@ private function flattenResourceData(array $resourceData) }, $resourceData['relationships']); } + if (isset($resourceData['links'])) { + $data['links'] = $resourceData['links']; + } + return $data; } @@ -584,8 +546,8 @@ protected function handleRequestException(RequestException $exception) if ($response->getStatusCode() !== 401 || $this->authRetry) { // TODO actually do something - echo (string)$exception->getRequest()->getUri(); - echo (string)$exception->getResponse()->getBody(); + // echo (string)$exception->getRequest()->getUri(); + // echo (string)$exception->getResponse()->getBody(); throw $exception; } @@ -603,16 +565,60 @@ protected function handleRequestException(RequestException $exception) */ public function getResourceById($resourceType, $id) { - $url = implode( - '/', + return reset($this->getResourcesPromise( + $this->getResourceUri($resourceType, $id) + )->wait()); + } + + /** + * {@inheritdoc} + */ + public function getResourcesFromUri($uri) + { + return $this->getResourcesPromise($uri)->wait(); + } + + /** + * Post given resource and return the resource that was returned. + * + * @param ResourceInterface $resource + * @return ResourceInterface|null + */ + protected function postResource(ResourceInterface $resource) + { + $promise = $this->getHttpClient()->requestAsync( + 'post', + $this->getResourceUri($resource->getType()), [ + RequestOptions::JSON => ['data' => $resource], + ] + )->then(function (ResponseInterface $response) { + $json = \GuzzleHttp\json_decode($response->getBody(), true); + + return $this->jsonToResources($json['data']); + }, function (RequestException $reason) { + return $this->handleRequestException($reason); + }); + + + return reset($promise->wait()); + } + + /** + * @param string $resourceType + * @param string|null $id + * @return string + */ + protected function getResourceUri($resourceType, $id = null) + { + return implode( + '/', + array_filter([ $this->apiUri, - 'v1', + self::API_VERSION, $resourceType, $id, - ] + ]) ); - - return reset($this->getResourcesPromise($url)->wait()); } } diff --git a/src/MyParcelComApiInterface.php b/src/MyParcelComApiInterface.php index 624f5986..4d5dca27 100644 --- a/src/MyParcelComApiInterface.php +++ b/src/MyParcelComApiInterface.php @@ -18,6 +18,7 @@ interface MyParcelComApiInterface const PATH_PUDO_LOCATIONS = '/v1/carriers/{carrier_id}/pickup-dropoff-locations/{country_code}/{postal_code}'; const PATH_REGIONS = '/v1/regions'; const PATH_SERVICES = '/v1/services'; + const PATH_SHIPMENTS = '/v1/shipments'; const PATH_SHOPS = '/v1/shops'; /** @@ -141,4 +142,12 @@ public function createShipment(ShipmentInterface $shipment); * @return ResourceInterface */ public function getResourceById($resourceType, $id); + + /** + * Get an array of all the resources from given uri. + * + * @param string $uri + * @return ResourceInterface[] + */ + public function getResourcesFromUri($uri); } diff --git a/src/Resources/Proxy/RegionProxy.php b/src/Resources/Proxy/RegionProxy.php new file mode 100644 index 00000000..4ca9eee9 --- /dev/null +++ b/src/Resources/Proxy/RegionProxy.php @@ -0,0 +1,140 @@ +id = $id; + + return $this; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $countryCode + * @return $this + */ + public function setCountryCode($countryCode) + { + $this->getResource()->setCountryCode($countryCode); + + return $this; + } + + /** + * @return string + */ + public function getCountryCode() + { + return $this->getResource()->getCountryCode(); + } + + /** + * @param string $regionCode + * @return $this + */ + public function setRegionCode($regionCode) + { + $this->getResource()->setRegionCode($regionCode); + + return $this; + } + + /** + * @return string + */ + public function getRegionCode() + { + return $this->getResource()->getRegionCode(); + } + + /** + * @param string $currency + * @return $this + */ + public function setCurrency($currency) + { + $this->getResource()->setCurrency($currency); + + return $this; + } + + /** + * @return string + */ + public function getCurrency() + { + return $this->getResource()->getCurrency(); + } + + /** + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->getResource()->setName($name); + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->getResource()->getName(); + } + + /** + * This function puts all object properties in an array and returns it. + * + * @return array + */ + public function jsonSerialize() + { + $values = get_object_vars($this); + unset($values['resource']); + unset($values['api']); + + return $this->arrayValuesToArray($values); + } +} diff --git a/src/Resources/Proxy/ShopProxy.php b/src/Resources/Proxy/ShopProxy.php new file mode 100644 index 00000000..319a172d --- /dev/null +++ b/src/Resources/Proxy/ShopProxy.php @@ -0,0 +1,161 @@ +id = $id; + + return $this; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param $name + * @return $this + */ + public function setName($name) + { + $this->getResource()->setName($name); + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->getResource()->getName(); + } + + /** + * @param AddressInterface $billingAddress + * @return $this + */ + public function setBillingAddress(AddressInterface $billingAddress) + { + $this->getResource()->setBillingAddress($billingAddress); + + return $this; + } + + /** + * @return AddressInterface + */ + public function getBillingAddress() + { + return $this->getResource()->getBillingAddress(); + } + + /** + * @param AddressInterface $returnAddress + * @return $this + */ + public function setReturnAddress(AddressInterface $returnAddress) + { + $this->getResource()->setReturnAddress($returnAddress); + + return $this; + } + + /** + * @return AddressInterface + */ + public function getReturnAddress() + { + return $this->getResource()->getReturnAddress(); + } + + /** + * @param RegionInterface $region + * @return $this + */ + public function setRegion(RegionInterface $region) + { + $this->getResource()->setRegion($region); + + return $this; + } + + /** + * @return RegionInterface + */ + public function getRegion() + { + return $this->getResource()->getRegion(); + } + + /** + * @param int|DateTime $time + * @return $this + */ + public function setCreatedAt($time) + { + $this->getResource()->setCreatedAt($time); + + return $this; + } + + /** + * @return DateTime + */ + public function getCreatedAt() + { + return $this->getResource()->getCreatedAt(); + } + + /** + * This function puts all object properties in an array and returns it. + * + * @return array + */ + public function jsonSerialize() + { + $values = get_object_vars($this); + unset($values['resource']); + unset($values['api']); + + return $this->arrayValuesToArray($values); + } +} diff --git a/src/Resources/Proxy/StatusProxy.php b/src/Resources/Proxy/StatusProxy.php new file mode 100644 index 00000000..56102fb5 --- /dev/null +++ b/src/Resources/Proxy/StatusProxy.php @@ -0,0 +1,196 @@ +id = $id; + + return $this; + } + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $code + * @return $this + */ + public function setCode($code) + { + $this->getResource()->setCode($code); + + return $this; + } + + /** + * @return string + */ + public function getCode() + { + return $this->getResource()->getCode(); + } + + /** + * @param string $level + * @return $this + */ + public function setLevel($level) + { + $this->getResource()->setLevel($level); + + return $this; + } + + /** + * @return string + */ + public function getLevel() + { + return $this->getResource()->getLevel(); + } + + /** + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->getResource()->setName($name); + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->getResource()->getName(); + } + + /** + * @param string $description + * @return $this + */ + public function setDescription($description) + { + $this->getResource()->setDescription($description); + + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->getResource()->getDescription(); + } + + /** + * @param string $carrierCode + * @return $this + */ + public function setCarrierStatusCode($carrierCode) + { + $this->getResource()->setCarrierStatusCode($carrierCode); + + return $this; + } + + /** + * @return string + */ + public function getCarrierStatusCode() + { + return $this->getResource()->getCarrierStatusCode(); + } + + /** + * @param string $carrierDescription + * @return $this + */ + public function setCarrierStatusDescription($carrierDescription) + { + $this->getResource()->setCarrierStatusDescription($carrierDescription); + + return $this; + } + + /** + * @return string + */ + public function getCarrierStatusDescription() + { + return $this->getResource()->getCode(); + } + + /** + * @param int|\DateTime $timestamp + * @return $this + */ + public function setCarrierTimestamp($timestamp) + { + $this->getResource()->setCarrierTimestamp($timestamp); + + return $this; + } + + /** + * @return \DateTime + */ + public function getCarrierTimestamp() + { + return $this->getResource()->getCarrierTimestamp(); + } + + /** + * This function puts all object properties in an array and returns it. + * + * @return array + */ + public function jsonSerialize() + { + $values = get_object_vars($this); + unset($values['resource']); + unset($values['api']); + + return $this->arrayValuesToArray($values); + } +} diff --git a/src/Resources/ResourceFactory.php b/src/Resources/ResourceFactory.php index 400b3f47..51386f08 100644 --- a/src/Resources/ResourceFactory.php +++ b/src/Resources/ResourceFactory.php @@ -25,6 +25,9 @@ use MyParcelCom\Sdk\Resources\Interfaces\ShopInterface; use MyParcelCom\Sdk\Resources\Interfaces\StatusInterface; use MyParcelCom\Sdk\Resources\Proxy\FileProxy; +use MyParcelCom\Sdk\Resources\Proxy\RegionProxy; +use MyParcelCom\Sdk\Resources\Proxy\ShopProxy; +use MyParcelCom\Sdk\Resources\Proxy\StatusProxy; use MyParcelCom\Sdk\Utils\StringUtils; use ReflectionParameter; @@ -37,8 +40,6 @@ class ResourceFactory implements ResourceFactoryInterface, ResourceProxyInterfac ResourceInterface::TYPE_PUDO_LOCATION => PickUpDropOffLocation::class, ResourceInterface::TYPE_REGION => Region::class, ResourceInterface::TYPE_SHOP => Shop::class, - ResourceInterface::TYPE_SERVICE => Service::class, - ResourceInterface::TYPE_SERVICE_GROUP => ServiceGroup::class, ResourceInterface::TYPE_SERVICE_OPTION => ServiceOption::class, ResourceInterface::TYPE_SERVICE_INSURANCE => ServiceInsurance::class, ResourceInterface::TYPE_STATUS => Status::class, @@ -54,8 +55,6 @@ class ResourceFactory implements ResourceFactoryInterface, ResourceProxyInterfac RegionInterface::class => Region::class, ShipmentInterface::class => Shipment::class, ShopInterface::class => Shop::class, - ServiceInterface::class => Service::class, - ServiceGroupInterface::class => ServiceGroup::class, ServiceOptionInterface::class => ServiceOption::class, ServiceInsuranceInterface::class => ServiceInsurance::class, StatusInterface::class => Status::class, @@ -67,35 +66,136 @@ class ResourceFactory implements ResourceFactoryInterface, ResourceProxyInterfac public function __construct() { $shipmentFactory = [$this, 'shipmentFactory']; + $serviceFactory = [$this, 'serviceFactory']; + $serviceGroupFactory = [$this, 'serviceGroupFactory']; $this->setFactoryForType(ResourceInterface::TYPE_SHIPMENT, $shipmentFactory); $this->setFactoryForType(ShipmentInterface::class, $shipmentFactory); + $this->setFactoryForType(ResourceInterface::TYPE_SERVICE, $serviceFactory); + $this->setFactoryForType(ServiceInterface::class, $serviceFactory); + $this->setFactoryForType(ResourceInterface::TYPE_SERVICE_GROUP, $serviceGroupFactory); + $this->setFactoryForType(ServiceGroupInterface::class, $serviceGroupFactory); } /** * Shipment factory method that creates proxies for all relationships. * - * @param $type - * @param $attributes + * @param string $type + * @param array $attributes * @return Shipment */ - protected function shipmentFactory($type, &$attributes){ + protected function shipmentFactory($type, array &$attributes) + { $shipment = new Shipment(); + if (isset($attributes['files'])) { array_walk($attributes['files'], function ($file) use ($shipment) { $shipment->addFile( - (new FileProxy()) - ->setMyParcelComApi($this->api) - ->setId($file['id']) + (new FileProxy())->setMyParcelComApi($this->api)->setId($file['id']) ); }); unset($attributes['files']); } + if (isset($attributes['shop']['id'])) { + $shipment->setShop( + (new ShopProxy())->setMyParcelComApi($this->api)->setId($attributes['shop']['id']) + ); + + unset($attributes['shop']); + } + + if (isset($attributes['status']['id'])) { + $shipment->setStatus( + (new StatusProxy())->setMyParcelComApi($this->api)->setId($attributes['status']['id']) + ); + + unset($attributes['status']); + } + return $shipment; } + /** + * Service factory method that creates proxies for all relationships. + * + * @param string $type + * @param array $attributes + * @return Service + */ + protected function serviceFactory($type, array &$attributes) + { + $service = new Service(); + + if (isset($attributes['region_from']['id'])) { + $service->setRegionFrom( + (new RegionProxy())->setMyParcelComApi($this->api)->setId($attributes['region_from']['id']) + ); + + unset($attributes['region_from']); + } + if (isset($attributes['region_to']['id'])) { + $service->setRegionTo( + (new RegionProxy())->setMyParcelComApi($this->api)->setId($attributes['region_to']['id']) + ); + + unset($attributes['region_to']); + } + + if (isset($attributes['links']['contracts'])) { + $service->setContracts($this->api->getResourcesFromUri($attributes['links']['contracts'])); + + unset($attributes['links']['contracts']); + } + + return $service; + } + + /** + * ServiceGroup factory method. + * + * @param string $type + * @param array $attributes + * @return ServiceGroup + */ + protected function serviceGroupFactory($type, &$attributes) + { + $serviceGroup = new ServiceGroup(); + + if (!isset($attributes['attributes'])) { + return $serviceGroup; + } + + if (isset($attributes['attributes']['price']['amount'])) { + $attributes += [ + 'price' => $attributes['attributes']['price']['amount'], + 'currency' => $attributes['attributes']['price']['currency'], + ]; + } + if (isset($attributes['attributes']['weight']['min'])) { + $attributes += [ + 'weight_min' => $attributes['attributes']['weight']['min'], + 'weight_max' => $attributes['attributes']['weight']['max'], + ]; + } + if (isset($attributes['attributes']['step_price']['amount'])) { + $attributes += [ + 'step_price' => $attributes['attributes']['step_price']['amount'], + 'currency' => $attributes['attributes']['step_price']['currency'], + ]; + } + if (isset($attributes['attributes']['step_size'])) { + $attributes += [ + 'step_size' => $attributes['attributes']['step_size'], + ]; + } + + unset($attributes['attributes']); + + return $serviceGroup; + } + /** * {@inheritdoc} */ @@ -108,6 +208,8 @@ public function create($type, array $attributes = []) } /** + * Set a factory method or class string for given resource type. + * * @param string $type * @param callable|string $factory */ @@ -215,6 +317,7 @@ protected function hydrate($resource, array $attributes) } if ($entry instanceof $className) { $resource->$adder($entry); + continue; } } diff --git a/src/Resources/Traits/JsonSerializable.php b/src/Resources/Traits/JsonSerializable.php index e462c402..ceb27e1e 100644 --- a/src/Resources/Traits/JsonSerializable.php +++ b/src/Resources/Traits/JsonSerializable.php @@ -18,8 +18,12 @@ public function jsonSerialize() if (isset($json['attributes']) && $this->isEmpty($json['attributes'])) { unset($json['attributes']); } - if (isset($json['relationships']) && $this->isEmpty($json['relationships'])) { - unset($json['relationships']); + if (isset($json['relationships'])) { + if ($this->isEmpty($json['relationships'])) { + unset($json['relationships']); + } else { + $json['relationships'] = $this->removeRelationshipAttributes($json['relationships']); + } } if (isset($json['meta']) && $this->isEmpty($json['meta'])) { unset($json['meta']); @@ -74,4 +78,31 @@ private function arrayValuesToArray(array $arrayValues) return $array; } + + /** + * Remove all the attributes from the relationships, so it only has `id` and + * `type` values. + * + * @param array $relationships + * @return array + */ + private function removeRelationshipAttributes(array $relationships) + { + foreach ($relationships as $name => &$relationship) { + if (empty($relationship['data'])) { + unset($relationships[$name]); + continue; + } + if (isset($relationship['data']['id'])) { + unset($relationship['data']['attributes'], $relationship['data']['relationships']); + continue; + } + foreach ($relationship['data'] as &$relationResource) { + unset($relationResource['attributes'], $relationResource['relationships']); + } + } + unset($relationship); + + return $relationships; + } } diff --git a/src/Resources/Traits/ProxiesResource.php b/src/Resources/Traits/ProxiesResource.php index 24988c9d..8ec7d73a 100644 --- a/src/Resources/Traits/ProxiesResource.php +++ b/src/Resources/Traits/ProxiesResource.php @@ -13,6 +13,8 @@ trait ProxiesResource private $api; /** + * Set the api to use when retrieving the resource. + * * @param MyParcelComApiInterface $api * @return $this */ @@ -23,6 +25,11 @@ public function setMyParcelComApi(MyParcelComApiInterface $api = null) return $this; } + /** + * Get the resource that this instance is a proxy for. + * + * @return ResourceInterface + */ protected function getResource() { if (!isset($this->resource) && isset($this->api)) { diff --git a/src/Shipments/ContractSelector.php b/src/Shipments/ContractSelector.php new file mode 100644 index 00000000..39ab8d41 --- /dev/null +++ b/src/Shipments/ContractSelector.php @@ -0,0 +1,36 @@ +getMatchedOptions( + $shipment, + $matcher->getMatchedWeightGroups($shipment, $contracts) + ); + + $calculator = new PriceCalculator(); + foreach ($matchingContracts as $contract) { + $prices[$calculator->calculate($shipment, $contract)] = $contract; + } + + ksort($prices); + + return reset($prices); + } +} diff --git a/src/Shipments/PriceCalculator.php b/src/Shipments/PriceCalculator.php new file mode 100644 index 00000000..a6307020 --- /dev/null +++ b/src/Shipments/PriceCalculator.php @@ -0,0 +1,178 @@ +getContract(); + } + + if ($contract === null) { + throw new MyParcelComException( + 'Cannot calculate a price for given shipment without a contract' + ); + } + + return $this->calculateGroupPrice($shipment, $contract) + + $this->calculateOptionsPrice($shipment, $contract) + + $this->calculateInsurancePrice($shipment, $contract); + } + + /** + * Calculate the price based on the weight group for given shipment. + * Optionally a contract can be supplied for the price calculations. If no + * contract is given, the contract on the shipment is used. + * + * @param ShipmentInterface $shipment + * @param ContractInterface|null $contract + * @return int + */ + public function calculateGroupPrice(ShipmentInterface $shipment, ContractInterface $contract = null) + { + if ($contract === null) { + $contract = $shipment->getContract(); + } + + if ($contract === null) { + throw new MyParcelComException( + 'Cannot calculate a price for given shipment without a contract' + ); + } + + $price = 0; + + // Order all the groups to have the highest weight group last. + $groups = $contract->getGroups(); + usort($groups, function (ServiceGroupInterface $a, ServiceGroupInterface $b) { + // Sort based on the min weight; + $minDiff = $a->getWeightMin() - $b->getWeightMin(); + if ($minDiff !== 0) { + return $minDiff; + } + + // If the min weight of both groups are equal, sort by max weight. + $maxDiff = $a->getWeightMax() - $b->getWeightMax(); + if ($maxDiff !== 0) { + return $maxDiff; + } + + // If the max weights are equal, sort by step size. + $stepDiff = $a->getStepSize() - $b->getStepSize(); + if ($stepDiff !== 0) { + return $stepDiff; + } + + // If the step sizes are equal, sort by price. + return $a->getPrice() - $b->getPrice(); + }); + + // Find the weight group this shipment is in. + foreach ($groups as $group) { + if ($shipment->getWeight() >= $group->getWeightMin() + || $shipment->getWeight() <= $group->getWeightMax()) { + break; + } + } + + // Add the group price to the price + $price += $group->getPrice(); + + // Add any weight over the max to the price. + if ($shipment->getWeight() > $group->getWeightMax() + && $group->getStepSize() + && $group->getStepPrice()) { + $price += + ceil(($shipment->getWeight() - $group->getWeightMax()) / $group->getStepSize()) + * $group->getStepPrice(); + } + + return (int)$price; + } + + /** + * Calculate the price based on the selected options for given shipment. + * Optionally a contract can be supplied for the price calculations. If no + * contract is given, the contract on the shipment is used. + * + * @param ShipmentInterface $shipment + * @param ContractInterface|null $contract + * @return int + */ + public function calculateOptionsPrice(ShipmentInterface $shipment, ContractInterface $contract = null) + { + if ($contract === null) { + $contract = $shipment->getContract(); + } + + if ($contract === null) { + throw new MyParcelComException('Cannot calculate a price for given shipment without a contract'); + } + + $price = 0; + + $optionPrices = []; + foreach ($contract->getOptions() as $option) { + $optionPrices[$option->getId()] = $option->getPrice(); + } + + foreach ($shipment->getOptions() as $option) { + $price += $optionPrices[$option->getId()]; + } + + return (int)$price; + } + + /** + * Calculate the price based on the desired insurance for given shipment. + * Optionally a contract can be supplied for the price calculations. If no + * contract is given, the contract on the shipment is used. + * + * @param ShipmentInterface $shipment + * @param ContractInterface|null $contract + * @return int + */ + public function calculateInsurancePrice(ShipmentInterface $shipment, ContractInterface $contract = null) + { + if ($contract === null) { + $contract = $shipment->getContract(); + } + + if ($contract === null) { + throw new MyParcelComException('Cannot calculate a price for given shipment without a contract'); + } + + if (!$shipment->getInsuranceAmount() || !($insurances = $contract->getInsurances())) { + return 0; + } + + usort($insurances, function (ServiceInsuranceInterface $a, ServiceInsuranceInterface $b) { + return $a->getCovered() - $b->getCovered(); + }); + + foreach ($insurances as $insurance) { + if ($shipment->getInsuranceAmount() <= $insurance->getCovered()) { + break; + } + } + + return $insurance->getPrice(); + } +} diff --git a/src/Shipments/ServiceMatcher.php b/src/Shipments/ServiceMatcher.php new file mode 100644 index 00000000..5704ab5b --- /dev/null +++ b/src/Shipments/ServiceMatcher.php @@ -0,0 +1,145 @@ +matchesRegion($shipment, $service) + && ($weightContracts = $this->getMatchedWeightGroups($shipment, $service->getContracts())) + && ($optionContracts = $this->getMatchedOptions($shipment, $weightContracts)) + && $this->getMatchedInsurances($shipment, $optionContracts); + } + + /** + * Returns true if the sender and and recipient address on the shipment + * match the regions from and to on the service. + * + * @param ShipmentInterface $shipment + * @param ServiceInterface $service + * @return bool + */ + public function matchesRegion(ShipmentInterface $shipment, ServiceInterface $service) + { + if ($shipment->getRecipientAddress() === null) { + throw new InvalidResourceException( + 'Missing `recipient_address` on `shipments` resource' + ); + } + if ($shipment->getSenderAddress() === null) { + throw new InvalidResourceException( + 'Missing `sender_address` on `shipments` resource' + ); + } + + return $this->addressMatchesRegion($shipment->getRecipientAddress(), $service->getRegionTo()) + && $this->addressMatchesRegion($shipment->getSenderAddress(), $service->getRegionFrom()); + } + + /** + * Returns true if the address matches given region. + * + * @param AddressInterface $address + * @param RegionInterface $region + * @return bool + */ + private function addressMatchesRegion(AddressInterface $address, RegionInterface $region) + { + // TODO use child regions from given region to match on + return $address->getCountryCode() === $region->getCountryCode() + && $address->getRegionCode() === $region->getRegionCode(); + } + + /** + * Returns a subset of the given contracts that have weight groups that + * match the weight of the shipment. + * + * @param ShipmentInterface $shipment + * @param ContractInterface[] $contracts + * @return ContractInterface[] + */ + public function getMatchedWeightGroups(ShipmentInterface $shipment, array $contracts) + { + $matches = []; + foreach ($contracts as $contract) { + foreach ($contract->getGroups() as $group) { + if ($group->getWeightMin() <= $shipment->getWeight() + && $group->getWeightMax() >= $shipment->getWeight()) { + $matches[] = $contract; + continue 2; + } + } + } + + return $matches; + } + + /** + * Returns a subset of the given contracts that have all the options that + * the shipment requires. + * + * @param ShipmentInterface $shipment + * @param ContractInterface[] $contracts + * @return ContractInterface[] + */ + public function getMatchedOptions(ShipmentInterface $shipment, array $contracts) + { + $optionIds = array_map(function (ServiceOptionInterface $option) { + return $option->getId(); + }, $shipment->getOptions()); + + $matches = []; + foreach ($contracts as $contract) { + $contractOptionIds = array_map(function (ServiceOptionInterface $option) use ($optionIds) { + return $option->getId(); + }, $contract->getOptions()); + + if (!array_diff($optionIds, $contractOptionIds)) { + $matches[] = $contract; + } + } + + return $matches; + } + + /** + * Returns a subset of the given contracts that can cover the desired + * insurance of the shipment. + * + * @param ShipmentInterface $shipment + * @param ContractInterface[] $contracts + * @return ContractInterface[] + */ + public function getMatchedInsurances(ShipmentInterface $shipment, array $contracts) + { + if (!$shipment->getInsuranceAmount()) { + return $contracts; + } + + return array_filter($contracts, function (ContractInterface $contract) use ($shipment) { + foreach ($contract->getInsurances() as $insurance) { + if ($shipment->getInsuranceAmount() <= $insurance->getCovered()) { + return true; + } + } + + return false; + }); + } +} diff --git a/src/Validators/ShipmentValidator.php b/src/Validators/ShipmentValidator.php index f262aa87..6b65e49a 100644 --- a/src/Validators/ShipmentValidator.php +++ b/src/Validators/ShipmentValidator.php @@ -37,11 +37,12 @@ public function isValid() */ protected function checkRequired() { - $required = ['weight', 'service', 'recipient_address', 'sender_address', 'shop']; + $required = ['weight', 'service', 'recipient_address', 'sender_address', 'shop', 'contract']; array_walk($required, function ($required) { $getter = 'get' . StringUtils::snakeToPascalCase($required); - if (empty($this->shipment->$getter())) { + $value = $this->shipment->$getter(); + if (empty($value)) { $this->addError(sprintf('Required property `%s` is empty', $required)); } }); diff --git a/tests/Feature/MyParcelComApiTest.php b/tests/Feature/MyParcelComApiTest.php index 305f3dd4..b5670e4d 100644 --- a/tests/Feature/MyParcelComApiTest.php +++ b/tests/Feature/MyParcelComApiTest.php @@ -64,6 +64,23 @@ public function setUp() )); } + $returnJson = file_get_contents($filePath); + if ($method === 'post') { + // Any post will have the data from the stub added to the + // original request. This simulates the api creating the + // resource and returning it with added attributes. + $returnJson = \GuzzleHttp\json_encode( + array_merge_recursive( + \GuzzleHttp\json_decode($returnJson, true), + // You may wonder why we would encode and then + // decode this, but it is possible that the json in + // the options array is not an associative array, + // which we need to be able to merge. + \GuzzleHttp\json_decode(\GuzzleHttp\json_encode($options['json']), true) + ) + ); + } + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->disableOriginalClone() @@ -71,7 +88,7 @@ public function setUp() ->disallowMockingUnknownTypes() ->getMock(); $response->method('getBody') - ->willReturn(file_get_contents($filePath)); + ->willReturn($returnJson); return promise_for($response); }); @@ -121,7 +138,7 @@ public function testCreateMinimumViableShipment() ->setWeight(500) ->setRecipientAddress($recipient); - $this->api->createShipment($shipment); + $shipment = $this->api->createShipment($shipment); $this->assertNotNull( $shipment->getService(), @@ -198,7 +215,7 @@ public function testGetGbRegions() $regions = $this->api->getRegions('GB'); $this->assertInternalType('array', $regions); - $this->assertCount(4, $regions); + $this->assertCount(5, $regions); array_walk($regions, function ($region) { $this->assertInstanceOf(RegionInterface::class, $region); $this->assertEquals('GB', $region->getCountryCode()); @@ -286,7 +303,13 @@ public function testGetShipmentsForShop() $this->assertInternalType('array', $shipments); array_walk($shipments, function ($shipment) use ($shop) { $this->assertInstanceOf(ShipmentInterface::class, $shipment); - $this->assertEquals($shop, $shipment->getShop()); + $this->assertEquals($shop->getId(), $shipment->getShop()->getId()); + $this->assertEquals($shop->getType(), $shipment->getShop()->getType()); + $this->assertEquals($shop->getCreatedAt(), $shipment->getShop()->getCreatedAt()); + $this->assertEquals($shop->getBillingAddress(), $shipment->getShop()->getBillingAddress()); + $this->assertEquals($shop->getReturnAddress(), $shipment->getShop()->getReturnAddress()); + $this->assertEquals($shop->getName(), $shipment->getShop()->getName()); + $this->assertEquals($shop->getRegion(), $shipment->getShop()->getRegion()); }); }); } @@ -341,9 +364,9 @@ public function testGetResourceById() (new Address()) ->setStreet1('Hoofdweg') ->setStreetNumber(679) - ->setPostalCode('2131 BC') - ->setCity('Hoofddorp') - ->setCountryCode('NL') + ->setPostalCode('1AA BB2') + ->setCity('London') + ->setCountryCode('GB') ->setFirstName('Mister') ->setLastName('Billing') ->setCompany('MyParcel.com') @@ -355,9 +378,9 @@ public function testGetResourceById() (new Address()) ->setStreet1('Hoofdweg') ->setStreetNumber(679) - ->setPostalCode('2131 BC') - ->setCity('Hoofddorp') - ->setCountryCode('NL') + ->setPostalCode('1AA BB2') + ->setCity('London') + ->setCountryCode('GB') ->setFirstName('Mister') ->setLastName('Return') ->setCompany('MyParcel.com') diff --git a/tests/Stubs/get/https---api-v1-regions-c1048135-db45-404e-adac-fdecd0c7134a.json b/tests/Stubs/get/https---api-v1-regions-c1048135-db45-404e-adac-fdecd0c7134a.json new file mode 100644 index 00000000..47597f5b --- /dev/null +++ b/tests/Stubs/get/https---api-v1-regions-c1048135-db45-404e-adac-fdecd0c7134a.json @@ -0,0 +1,14 @@ +{ + "data": { + "id": "c1048135-db45-404e-adac-fdecd0c7134a", + "type": "regions", + "attributes": { + "country_code": "GB", + "name": "United Kingdom", + "region_code": null + }, + "links": { + "self": "https://api/v1/regions/c1048135-db45-404e-adac-fdecd0c7134a" + } + } +} diff --git a/tests/Stubs/get/https---api-v1-regions-page-size--30-page-number--3.json b/tests/Stubs/get/https---api-v1-regions-page-size--30-page-number--3.json index 023742b8..4d2babc4 100644 --- a/tests/Stubs/get/https---api-v1-regions-page-size--30-page-number--3.json +++ b/tests/Stubs/get/https---api-v1-regions-page-size--30-page-number--3.json @@ -161,15 +161,15 @@ } }, { - "id": "2f458d66-8514-479d-9a2e-92dad285a7e9", + "id": "c1048135-db45-404e-adac-fdecd0c7134a", "type": "regions", "attributes": { - "country_code": "ES", - "name": "Canary Islands", - "region_code": "CN" + "country_code": "GB", + "name": "United Kingdom", + "region_code": null }, "links": { - "self": "https://api/v1/regions/2f458d66-8514-479d-9a2e-92dad285a7e9" + "self": "https://api/v1/regions/c1048135-db45-404e-adac-fdecd0c7134a" } }, { diff --git a/tests/Stubs/get/https---api-v1-services-433285bb-2e34-435c-9109-1120e7c4bce4-contracts.json b/tests/Stubs/get/https---api-v1-services-433285bb-2e34-435c-9109-1120e7c4bce4-contracts.json new file mode 100644 index 00000000..37a06d59 --- /dev/null +++ b/tests/Stubs/get/https---api-v1-services-433285bb-2e34-435c-9109-1120e7c4bce4-contracts.json @@ -0,0 +1,163 @@ +{ + "data": [ + { + "id": "e7108aba-bd2a-4f30-b1f7-50f359c955f2", + "type": "contracts", + "attributes": { + "groups": [ + { + "id": "f80ffe51-f9d9-4f33-ab67-aeef6f581873", + "type": "service-groups", + "attributes": { + "weight": { + "min": 0, + "max": 20000 + }, + "step_size": 1, + "price": { + "amount": 800, + "currency": "EUR" + }, + "step_price": { + "amount": 2, + "currency": "EUR" + } + } + }, + { + "id": "2f093655-eb24-4ffd-8dd3-305cbaf8ba7e", + "type": "service-groups", + "attributes": { + "weight": { + "min": 20001, + "max": 50000 + }, + "step_size": 1, + "price": { + "amount": 2500, + "currency": "EUR" + }, + "step_price": { + "amount": 1, + "currency": "EUR" + } + } + } + ], + "options": [ + { + "type": "service-options", + "id": "00ea9fb8-e5a9-4818-bf22-c3574f5fe12b", + "attributes": { + "name": "signature", + "price": { + "amount": 100, + "currency": "EUR" + } + } + } + ], + "insurances": [ + { + "type": "service-insurances", + "id": "a1b42172-74e7-4254-83a7-0deb4d5216bf", + "attributes": { + "covered": { + "amount": 25000, + "currency": "EUR" + }, + "price": { + "amount": 1000, + "currency": "EUR" + } + } + } + ] + } + }, + { + "id": "f94dda81-c418-4077-ba7c-87ddf9076c28", + "type": "contracts", + "attributes": { + "groups": [ + { + "id": "3b339147-9919-4f74-9107-dbc89d6a1de9", + "type": "service-groups", + "attributes": { + "weight": { + "min": 0, + "max": 20000 + }, + "step_size": 1, + "price": { + "amount": 900, + "currency": "EUR" + }, + "step_price": { + "amount": 2, + "currency": "EUR" + } + } + }, + { + "id": "46db61dc-e15d-4645-b3d8-5e899c6e1fc0", + "type": "service-groups", + "attributes": { + "weight": { + "min": 20001, + "max": 50000 + }, + "step_size": 1, + "price": { + "amount": 2400, + "currency": "EUR" + }, + "step_price": { + "amount": 1, + "currency": "EUR" + } + } + } + ], + "options": [ + { + "type": "service-options", + "id": "ad3b8030-0da1-40bb-ac07-d1bdbaeaa3e7", + "attributes": { + "name": "signature", + "price": { + "amount": 90, + "currency": "EUR" + } + } + } + ], + "insurances": [ + { + "type": "service-insurances", + "id": "9d5d631d-af3b-4b0a-99e5-5995688a8e88", + "attributes": { + "covered": { + "amount": 25000, + "currency": "EUR" + }, + "price": { + "amount": 1200, + "currency": "EUR" + } + } + } + ] + } + } + ], + "meta": { + "total_pages": 1, + "total_records": 1 + }, + "links": { + "self": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4/contracts?page[size]=30&page[number]=1", + "first": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4/contracts?page[size]=30&page[number]=1", + "last": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4/contracts?page[size]=30&page[number]=1" + } +} diff --git a/tests/Stubs/get/https---api-v1-services-605c33ba-5504-4bb0-9467-d8901195923c-contracts.json b/tests/Stubs/get/https---api-v1-services-605c33ba-5504-4bb0-9467-d8901195923c-contracts.json new file mode 100644 index 00000000..97f8e059 --- /dev/null +++ b/tests/Stubs/get/https---api-v1-services-605c33ba-5504-4bb0-9467-d8901195923c-contracts.json @@ -0,0 +1,163 @@ +{ + "data": [ + { + "id": "e7108aba-bd2a-4f30-b1f7-50f359c955f2", + "type": "contracts", + "attributes": { + "groups": [ + { + "id": "f80ffe51-f9d9-4f33-ab67-aeef6f581873", + "type": "service-groups", + "attributes": { + "weight": { + "min": 0, + "max": 20000 + }, + "step_size": 1, + "price": { + "amount": 800, + "currency": "EUR" + }, + "step_price": { + "amount": 2, + "currency": "EUR" + } + } + }, + { + "id": "2f093655-eb24-4ffd-8dd3-305cbaf8ba7e", + "type": "service-groups", + "attributes": { + "weight": { + "min": 20001, + "max": 50000 + }, + "step_size": 1, + "price": { + "amount": 2500, + "currency": "EUR" + }, + "step_price": { + "amount": 1, + "currency": "EUR" + } + } + } + ], + "options": [ + { + "type": "service-options", + "id": "00ea9fb8-e5a9-4818-bf22-c3574f5fe12b", + "attributes": { + "name": "signature", + "price": { + "amount": 100, + "currency": "EUR" + } + } + } + ], + "insurances": [ + { + "type": "service-insurances", + "id": "a1b42172-74e7-4254-83a7-0deb4d5216bf", + "attributes": { + "covered": { + "amount": 25000, + "currency": "EUR" + }, + "price": { + "amount": 1000, + "currency": "EUR" + } + } + } + ] + } + }, + { + "id": "f94dda81-c418-4077-ba7c-87ddf9076c28", + "type": "contracts", + "attributes": { + "groups": [ + { + "id": "3b339147-9919-4f74-9107-dbc89d6a1de9", + "type": "service-groups", + "attributes": { + "weight": { + "min": 0, + "max": 20000 + }, + "step_size": 1, + "price": { + "amount": 900, + "currency": "EUR" + }, + "step_price": { + "amount": 2, + "currency": "EUR" + } + } + }, + { + "id": "46db61dc-e15d-4645-b3d8-5e899c6e1fc0", + "type": "service-groups", + "attributes": { + "weight": { + "min": 20001, + "max": 50000 + }, + "step_size": 1, + "price": { + "amount": 2400, + "currency": "EUR" + }, + "step_price": { + "amount": 1, + "currency": "EUR" + } + } + } + ], + "options": [ + { + "type": "service-options", + "id": "ad3b8030-0da1-40bb-ac07-d1bdbaeaa3e7", + "attributes": { + "name": "signature", + "price": { + "amount": 90, + "currency": "EUR" + } + } + } + ], + "insurances": [ + { + "type": "service-insurances", + "id": "9d5d631d-af3b-4b0a-99e5-5995688a8e88", + "attributes": { + "covered": { + "amount": 25000, + "currency": "EUR" + }, + "price": { + "amount": 1200, + "currency": "EUR" + } + } + } + ] + } + } + ], + "meta": { + "total_pages": 1, + "total_records": 1 + }, + "links": { + "self": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c/contracts?page[size]=30&page[number]=1", + "first": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c/contracts?page[size]=30&page[number]=1", + "last": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c/contracts?page[size]=30&page[number]=1" + } +} diff --git a/tests/Stubs/get/https---api-v1-services-a3057e77-005b-4945-a41c-20ddbe4dab08-contracts.json b/tests/Stubs/get/https---api-v1-services-a3057e77-005b-4945-a41c-20ddbe4dab08-contracts.json new file mode 100644 index 00000000..e3e21175 --- /dev/null +++ b/tests/Stubs/get/https---api-v1-services-a3057e77-005b-4945-a41c-20ddbe4dab08-contracts.json @@ -0,0 +1,113 @@ +{ + "data": [ + { + "id": "48533f26-8502-43f4-a83a-ebbadc238024", + "type": "contracts", + "attributes": { + "groups": [ + { + "id": "01a4a016-ff29-45ca-832d-92382f9ae243", + "type": "service-groups", + "attributes": { + "weight": { + "min": 0, + "max": 10000 + }, + "step_size": 1, + "price": { + "amount": 800, + "currency": "EUR" + }, + "step_price": { + "amount": 0, + "currency": "EUR" + } + } + }, + { + "id": "51e5530d-2fed-4357-8d85-7c399ed44ffb", + "type": "service-groups", + "attributes": { + "weight": { + "min": 10001, + "max": 30000 + }, + "step_size": 1, + "price": { + "amount": 2500, + "currency": "EUR" + }, + "step_price": { + "amount": 0, + "currency": "EUR" + } + } + } + ], + "options": [ + { + "type": "service-options", + "id": "00ea9fb8-e5a9-4818-bf22-c3574f5fe12b", + "attributes": { + "name": "signature", + "price": { + "amount": 100, + "currency": "EUR" + } + } + }, + { + "type": "service-options", + "id": "a6e63d46-5395-49a2-9111-df80f15b35de", + "attributes": { + "name": "cash on delivery", + "price": { + "amount": 250, + "currency": "EUR" + } + } + } + ], + "insurances": [ + { + "type": "service-insurances", + "id": "5e82f025-5512-46af-98f1-646e589d6190", + "attributes": { + "covered": { + "amount": 10000, + "currency": "EUR" + }, + "price": { + "amount": 500, + "currency": "EUR" + } + } + }, + { + "type": "service-insurances", + "id": "64f08f09-d840-4f09-a787-87d9b0854088", + "attributes": { + "covered": { + "amount": 50000, + "currency": "EUR" + }, + "price": { + "amount": 1000, + "currency": "EUR" + } + } + } + ] + } + } + ], + "meta": { + "total_pages": 1, + "total_records": 1 + }, + "links": { + "self": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08/contracts?page[size]=30&page[number]=1", + "first": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08/contracts?page[size]=30&page[number]=1", + "last": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08/contracts?page[size]=30&page[number]=1" + } +} diff --git a/tests/Stubs/get/https---api-v1-services.json b/tests/Stubs/get/https---api-v1-services.json index 9e58670e..b3e3b292 100644 --- a/tests/Stubs/get/https---api-v1-services.json +++ b/tests/Stubs/get/https---api-v1-services.json @@ -8,7 +8,8 @@ "package_type": "letter" }, "links": { - "self": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08" + "self": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08", + "contracts": "https://api/v1/services/a3057e77-005b-4945-a41c-20ddbe4dab08/contracts" }, "relationships": { "carrier": { @@ -48,7 +49,8 @@ "package_type": "letterbox" }, "links": { - "self": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4" + "self": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4", + "contracts": "https://api/v1/services/433285bb-2e34-435c-9109-1120e7c4bce4/contracts" }, "relationships": { "carrier": { @@ -88,7 +90,8 @@ "package_type": "parcel" }, "links": { - "self": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c" + "self": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c", + "contracts": "https://api/v1/services/605c33ba-5504-4bb0-9467-d8901195923c/contracts" }, "relationships": { "carrier": { diff --git a/tests/Stubs/get/https---api-v1-shipments-shipment-id-1.json b/tests/Stubs/get/https---api-v1-shipments-shipment-id-1.json new file mode 100644 index 00000000..001c1f02 --- /dev/null +++ b/tests/Stubs/get/https---api-v1-shipments-shipment-id-1.json @@ -0,0 +1,143 @@ +{ + "data": { + "type": "shipments", + "id": "shipment-id-1", + "attributes": { + "recipient_address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + }, + "sender_address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + }, + "pickup_location": { + "code": "123456", + "address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + } + }, + "description": "order #8008135", + "price": { + "amount": 100, + "currency": "EUR" + }, + "insurance": { + "amount": 100, + "currency": "EUR" + }, + "barcode": "3SABCD0123456789", + "weight": 24, + "physical_properties": { + "height": 24, + "width": 50, + "length": 50, + "volume": 50, + "weight": 24 + }, + "created_at": 1504801719, + "updated_at": 1504801719, + "synced_at": 1504801719 + }, + "relationships": { + "service_options": { + "data": [ + { + "type": "service-options", + "id": "service-option-id-1" + } + ] + }, + "parent": { + "data": { + "type": "shipments", + "id": "shipment-id-0" + }, + "links": { + "related": "https://api/v1/shipments/shipment-id-0" + } + }, + "shop": { + "data": { + "type": "shops", + "id": "shop-id-1" + }, + "links": { + "related": "https://api/v1/shops/shop-id-1" + } + }, + "status": { + "data": { + "type": "statuses", + "id": "status-id-1" + }, + "links": { + "related": "https://api/v1/statuses/status-id-1" + } + }, + "service": { + "data": { + "type": "services", + "id": "service-id-1" + }, + "links": { + "related": "https://api/v1/services/service-id-1" + } + }, + "contract": { + "data": { + "type": "contracts", + "id": "contract-id-1" + } + }, + "files": { + "data": [ + { + "type": "files", + "id": "file-id-1" + } + ], + "links": { + "related": "https://api/v1/shipments/shipment-id-1/files" + } + } + }, + "links": { + "self": "https://api/v1/shipments/shipment-id-1" + } + } +} diff --git a/tests/Stubs/get/https---api-v1-shipments.json b/tests/Stubs/get/https---api-v1-shipments.json new file mode 100644 index 00000000..998e212c --- /dev/null +++ b/tests/Stubs/get/https---api-v1-shipments.json @@ -0,0 +1,154 @@ +{ + "data": [ + { + "type": "shipments", + "id": "shipment-id-1", + "attributes": { + "recipient_address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + }, + "sender_address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + }, + "pickup_location": { + "code": "123456", + "address": { + "street_1": "Some road", + "street_2": "Room 3", + "street_number": 17, + "street_number_suffix": "A", + "postal_code": "1GL HF1", + "city": "Cardiff", + "region_code": "CRF", + "country_code": "GB", + "first_name": "John", + "last_name": "Doe", + "company": "Acme Jewelry Co.", + "email": "john@doe.com", + "phone_number": "+31 234 567 890" + } + }, + "description": "order #8008135", + "price": { + "amount": 100, + "currency": "EUR" + }, + "insurance": { + "amount": 100, + "currency": "EUR" + }, + "barcode": "3SABCD0123456789", + "weight": 24, + "physical_properties": { + "height": 24, + "width": 50, + "length": 50, + "volume": 50, + "weight": 24 + }, + "created_at": 1504801719, + "updated_at": 1504801719, + "synced_at": 1504801719 + }, + "relationships": { + "service_options": { + "data": [ + { + "type": "service-options", + "id": "service-option-id-1" + } + ] + }, + "parent": { + "data": { + "type": "shipments", + "id": "shipment-id-0" + }, + "links": { + "related": "https://api/v1/shipments/shipment-id-0" + } + }, + "shop": { + "data": { + "type": "shops", + "id": "shop-id-1" + }, + "links": { + "related": "https://api/v1/shops/shop-id-1" + } + }, + "status": { + "data": { + "type": "statuses", + "id": "status-id-1" + }, + "links": { + "related": "https://api/v1/statuses/status-id-1" + } + }, + "service": { + "data": { + "type": "services", + "id": "service-id-1" + }, + "links": { + "related": "https://api/v1/services/service-id-1" + } + }, + "contract": { + "data": { + "type": "contracts", + "id": "contract-id-1" + } + }, + "files": { + "data": [ + { + "type": "files", + "id": "file-id-1" + } + ], + "links": { + "related": "https://api/v1/shipments/shipment-id-1/files" + } + } + }, + "links": { + "self": "https://api/v1/shipments/shipment-id-1" + } + } + ], + "meta": { + "total_pages": 1, + "total_records": 1 + }, + "links": { + "self": "https://api/v1/shipments?page[number]=1&page[size]=30", + "first": "https://api/v1/shipments?page[number]=1&page[size]=30", + "last": "https://api/v1/shipments?page[number]=1&page[size]=30" + } +} diff --git a/tests/Stubs/get/https---api-v1-shops-shop-id-1.json b/tests/Stubs/get/https---api-v1-shops-shop-id-1.json index e9847354..a931e5e9 100644 --- a/tests/Stubs/get/https---api-v1-shops-shop-id-1.json +++ b/tests/Stubs/get/https---api-v1-shops-shop-id-1.json @@ -7,9 +7,9 @@ "billing_address": { "street_1": "Hoofdweg", "street_number": 679, - "postal_code": "2131 BC", - "city": "Hoofddorp", - "country_code": "NL", + "postal_code": "1AA BB2", + "city": "London", + "country_code": "GB", "first_name": "Mister", "last_name": "Billing", "company": "MyParcel.com", @@ -19,9 +19,9 @@ "return_address": { "street_1": "Hoofdweg", "street_number": 679, - "postal_code": "2131 BC", - "city": "Hoofddorp", - "country_code": "NL", + "postal_code": "1AA BB2", + "city": "London", + "country_code": "GB", "first_name": "Mister", "last_name": "Return", "company": "MyParcel.com", @@ -40,7 +40,7 @@ "type": "regions" }, "links": { - "related": "https://api/v1/regions/c1048135-db45-404e-adac-fdecd0c7134a" + "related": "https://api/v1/regions/shop-id-1" } } } diff --git a/tests/Stubs/get/https---api-v1-shops.json b/tests/Stubs/get/https---api-v1-shops.json index 875cbce4..2fa848ac 100644 --- a/tests/Stubs/get/https---api-v1-shops.json +++ b/tests/Stubs/get/https---api-v1-shops.json @@ -1,16 +1,16 @@ { "data": [ { - "id": "d76d4b2f-e834-474d-ab20-cf1379385bf6", + "id": "shop-id-1", "type": "shops", "attributes": { "name": "Testshop", "billing_address": { "street_1": "Hoofdweg", "street_number": 679, - "postal_code": "2131 BC", - "city": "Hoofddorp", - "country_code": "NL", + "postal_code": "1AA BB2", + "city": "London", + "country_code": "GB", "first_name": "Mister", "last_name": "Billing", "company": "MyParcel.com", @@ -20,11 +20,11 @@ "return_address": { "street_1": "Hoofdweg", "street_number": 679, - "postal_code": "2131 BC", - "city": "Hoofddorp", - "country_code": "NL", + "postal_code": "1AA BB2", + "city": "London", + "country_code": "GB", "first_name": "Mister", - "last_name": "Billing", + "last_name": "Return", "company": "MyParcel.com", "email": "info@myparcel.com", "phone_number": "+31 85 208 5997" @@ -32,7 +32,7 @@ "created_at": 1509378904 }, "links": { - "self": "https://api/v1/shops/d76d4b2f-e834-474d-ab20-cf1379385bf6" + "self": "https://api/v1/shops/shop-id-1" }, "relationships": { "region": { @@ -41,7 +41,7 @@ "type": "regions" }, "links": { - "related": "https://api/v1/regions/c1048135-db45-404e-adac-fdecd0c7134a" + "related": "https://api/v1/regions/shop-id-1" } } } diff --git a/tests/Stubs/post/https---api-v1-shipments.json b/tests/Stubs/post/https---api-v1-shipments.json new file mode 100644 index 00000000..dfce4ab2 --- /dev/null +++ b/tests/Stubs/post/https---api-v1-shipments.json @@ -0,0 +1,8 @@ +{ + "data": { + "id": "new-shipment", + "attributes": { + "price": 6000 + } + } +} diff --git a/tests/Unit/ShipmentTest.php b/tests/Unit/ShipmentTest.php index 06096072..3dd9a65c 100644 --- a/tests/Unit/ShipmentTest.php +++ b/tests/Unit/ShipmentTest.php @@ -8,7 +8,6 @@ use MyParcelCom\Sdk\Resources\Interfaces\PhysicalPropertiesInterface; use MyParcelCom\Sdk\Resources\Interfaces\ServiceInterface; use MyParcelCom\Sdk\Resources\Interfaces\ServiceOptionInterface; -use MyParcelCom\Sdk\Resources\Interfaces\ShipmentInterface; use MyParcelCom\Sdk\Resources\Interfaces\ShopInterface; use MyParcelCom\Sdk\Resources\Interfaces\StatusInterface; use MyParcelCom\Sdk\Resources\Shipment;