From cdcd62676c7f915b7909c7c3c2e9e7390f94d23c Mon Sep 17 00:00:00 2001 From: Ramon Kleiss Date: Fri, 2 Aug 2019 14:09:55 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 15 ++ .gitattributes | 16 ++ .gitignore | 5 + .scrutinizer.yml | 31 +++ .travis.yml | 33 +++ README.md | 3 + composer.json | 52 +++++ examples/01-get-orders.php | 43 ++++ examples/02-create-shipments.php | 41 ++++ .../03-creating-updating-deleting-offers.php | 61 +++++ phpcs.xml.dist | 14 ++ phpunit.xml.dist | 28 +++ src/Client.php | 135 +++++++++++ src/Exception/HttpException.php | 44 ++++ src/Exception/OfferNotFoundException.php | 6 + src/Exception/OrderNotFoundException.php | 6 + .../ProcessStatusNotFoundException.php | 6 + .../ProcessStillPendingException.php | 25 ++ src/Exception/ShipmentNotFoundException.php | 6 + src/Model/AbstractModel.php | 42 ++++ src/Model/AddressDetails.php | 25 ++ src/Model/Offer.php | 30 +++ src/Model/Order.php | 37 +++ src/Model/OrderCustomerDetails.php | 21 ++ src/Model/OrderItem.php | 54 +++++ src/Model/Pricing.php | 10 + src/Model/ProcessStatus.php | 39 ++++ src/Model/ReducedOrder.php | 29 +++ src/Model/ReducedOrderItem.php | 39 ++++ src/Model/Shipment.php | 32 +++ src/Model/ShipmentItem.php | 62 +++++ src/Model/Stock.php | 10 + src/Offer.php | 145 ++++++++++++ src/Order.php | 73 ++++++ src/ProcessStatus.php | 89 +++++++ src/Shipment.php | 119 ++++++++++ tests/ClientTest.php | 63 +++++ tests/Exception/HttpExceptionTest.php | 48 ++++ tests/Fixtures/http/200-offer | 45 ++++ tests/Fixtures/http/200-order | 47 ++++ tests/Fixtures/http/200-orders | 32 +++ .../Fixtures/http/200-process-status-pending | 19 ++ .../Fixtures/http/200-process-status-success | 19 ++ tests/Fixtures/http/200-shipment | 40 ++++ tests/Fixtures/http/200-shipments | 53 +++++ tests/Fixtures/http/200-token | 9 + tests/Fixtures/http/202-process-status | 19 ++ tests/Fixtures/http/404-not-found | 20 ++ tests/Fixtures/json/error.json | 14 ++ tests/Fixtures/json/offer.json | 39 ++++ tests/Fixtures/json/order-item.json | 13 ++ tests/Fixtures/json/order.json | 43 ++++ tests/Fixtures/json/process-status.json | 13 ++ tests/Fixtures/json/reduced-order-item.json | 6 + tests/Fixtures/json/reduced-order.json | 12 + tests/Fixtures/json/shipment-item.json | 13 ++ tests/Fixtures/json/shipment.json | 34 +++ tests/Model/OfferTest.php | 30 +++ tests/Model/OrderItemTest.php | 83 +++++++ tests/Model/OrderTest.php | 41 ++++ tests/Model/ProcessStatusTest.php | 69 ++++++ tests/Model/ReducedOrderItemTest.php | 45 ++++ tests/Model/ReducedOrderTest.php | 35 +++ tests/Model/ShipmentItemTest.php | 39 ++++ tests/Model/ShipmentTest.php | 50 ++++ tests/OfferTest.php | 73 ++++++ tests/OrderTest.php | 103 ++++++++ tests/ProcessStatusTest.php | 105 +++++++++ tests/ShipmentTest.php | 219 ++++++++++++++++++ 69 files changed, 2919 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 composer.json create mode 100644 examples/01-get-orders.php create mode 100644 examples/02-create-shipments.php create mode 100644 examples/03-creating-updating-deleting-offers.php create mode 100644 phpcs.xml.dist create mode 100644 phpunit.xml.dist create mode 100644 src/Client.php create mode 100644 src/Exception/HttpException.php create mode 100644 src/Exception/OfferNotFoundException.php create mode 100644 src/Exception/OrderNotFoundException.php create mode 100644 src/Exception/ProcessStatusNotFoundException.php create mode 100644 src/Exception/ProcessStillPendingException.php create mode 100644 src/Exception/ShipmentNotFoundException.php create mode 100644 src/Model/AbstractModel.php create mode 100644 src/Model/AddressDetails.php create mode 100644 src/Model/Offer.php create mode 100644 src/Model/Order.php create mode 100644 src/Model/OrderCustomerDetails.php create mode 100644 src/Model/OrderItem.php create mode 100644 src/Model/Pricing.php create mode 100644 src/Model/ProcessStatus.php create mode 100644 src/Model/ReducedOrder.php create mode 100644 src/Model/ReducedOrderItem.php create mode 100644 src/Model/Shipment.php create mode 100644 src/Model/ShipmentItem.php create mode 100644 src/Model/Stock.php create mode 100644 src/Offer.php create mode 100644 src/Order.php create mode 100644 src/ProcessStatus.php create mode 100644 src/Shipment.php create mode 100644 tests/ClientTest.php create mode 100644 tests/Exception/HttpExceptionTest.php create mode 100644 tests/Fixtures/http/200-offer create mode 100644 tests/Fixtures/http/200-order create mode 100644 tests/Fixtures/http/200-orders create mode 100644 tests/Fixtures/http/200-process-status-pending create mode 100644 tests/Fixtures/http/200-process-status-success create mode 100644 tests/Fixtures/http/200-shipment create mode 100644 tests/Fixtures/http/200-shipments create mode 100644 tests/Fixtures/http/200-token create mode 100644 tests/Fixtures/http/202-process-status create mode 100644 tests/Fixtures/http/404-not-found create mode 100644 tests/Fixtures/json/error.json create mode 100644 tests/Fixtures/json/offer.json create mode 100644 tests/Fixtures/json/order-item.json create mode 100644 tests/Fixtures/json/order.json create mode 100644 tests/Fixtures/json/process-status.json create mode 100644 tests/Fixtures/json/reduced-order-item.json create mode 100644 tests/Fixtures/json/reduced-order.json create mode 100644 tests/Fixtures/json/shipment-item.json create mode 100644 tests/Fixtures/json/shipment.json create mode 100644 tests/Model/OfferTest.php create mode 100644 tests/Model/OrderItemTest.php create mode 100644 tests/Model/OrderTest.php create mode 100644 tests/Model/ProcessStatusTest.php create mode 100644 tests/Model/ReducedOrderItemTest.php create mode 100644 tests/Model/ReducedOrderTest.php create mode 100644 tests/Model/ShipmentItemTest.php create mode 100644 tests/Model/ShipmentTest.php create mode 100644 tests/OfferTest.php create mode 100644 tests/OrderTest.php create mode 100644 tests/ProcessStatusTest.php create mode 100644 tests/ShipmentTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..48a95b6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/.styleci.yml export-ignore +/.travis.yml export-ignore +/PULL_REQUEST_TEMPLATE.md export-ignore +/ISSUE_TEMPLATE.md export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a064c15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +composer.lock +vendor +phpcs.xml +phpunit.xml \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..27497c2 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,31 @@ +build: + nodes: + analysis: + project_setup: + override: true + tests: + override: [php-scrutinizer-run] + +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + +tools: + external_code_coverage: + timeout: 600 + runs: 3 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ff6cafb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,33 @@ +dist: trusty +language: php + +php: + - 7.3 + +# This triggers builds to run on the new TravisCI infrastructure. +# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ +sudo: false + +## Cache composer +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: 7.1 + env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' + +before_script: + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist + +script: + - vendor/bin/phpcs --standard=psr2 src/ + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - | + if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' ]]; then + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.clover + fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..492109b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Bol.com Retailer API client for PHP + +This is an open source PHP client for the [Bol.com Retailer API](https://developers.bol.com/newretailerapiv3/). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3eaf083 --- /dev/null +++ b/composer.json @@ -0,0 +1,52 @@ +{ + "name": "picqer/bol-retailer-php-client", + "type": "library", + "description": "Bol.com Retailer API client", + "keywords": [ + "picqer", + "bol-retailer-php-client" + ], + "homepage": "https://github.com/picqer/bol-retailer-php-client", + "license": "MIT", + "authors": [ + { + "name": "Ramon Kleiss", + "email": "ramon@picqer.com", + "homepage": "https://picqer.com", + "role": "Developer" + } + ], + "require": { + "php": "~7.1", + "ext-json": "^1.5", + "guzzlehttp/guzzle": "^6.3" + }, + "require-dev": { + "phpstan/phpstan": "^0.11.12", + "phpunit/phpunit": ">=7.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "autoload": { + "psr-4": { + "Picqer\\BolRetailer\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Picqer\\BolRetailer\\Tests\\": "tests" + } + }, + "scripts": { + "test": "phpunit", + "check-style": "phpcs src tests", + "fix-style": "phpcbf src tests" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/examples/01-get-orders.php b/examples/01-get-orders.php new file mode 100644 index 0000000..369ec4c --- /dev/null +++ b/examples/01-get-orders.php @@ -0,0 +1,43 @@ +orderId); + + printf( + "Ordered by \"%s %s\":\n", + $order->customerDetails->billingDetails->firstName, + $order->customerDetails->billingDetails->surName + ); + + foreach ($order->orderItems as $orderItem) { + printf( + "\t%s:\t%s (%dx) à € %.2f\n", + $orderItem->orderItemId, + $orderItem->title, + $orderItem->quantity, + $orderItem->offerPrice + ); + } +} diff --git a/examples/02-create-shipments.php b/examples/02-create-shipments.php new file mode 100644 index 0000000..6f37143 --- /dev/null +++ b/examples/02-create-shipments.php @@ -0,0 +1,41 @@ +orderItems[0], [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] +]); + +// You can now choose to wait until the process completes: +// +// ```php +// $processStatus->waitUntilComplete(20, 3); +// ``` +// +// Since the demo API of Bol.com does not support dynamic process statuses, we will not wait. + +printf("Waiting for process with ID \"%s\"\n", $processStatus->id); + +// You can also opt to create a shipment for an entire order. This will return an array of `ProcessStatus` objects. + +$processStatuses = Picqer\BolRetailer\Shipment::createForOrder($order, [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] +]); diff --git a/examples/03-creating-updating-deleting-offers.php b/examples/03-creating-updating-deleting-offers.php new file mode 100644 index 0000000..231bef9 --- /dev/null +++ b/examples/03-creating-updating-deleting-offers.php @@ -0,0 +1,61 @@ + "0000007740404", + "condition" => [ + "name" => "AS_NEW", + "category" => "SECONDHAND", + "comment" => "Heeft een koffie vlek op de kaft." + ], + "referenceCode" => "REF12345", + "onHoldByRetailer" => false, + "unknownProductTitle" => "Unknown Product Title", + "pricing" => [ + "bundlePrices" => [ + [ + "quantity" => 1, + "price" => 9.99 + ] + ] + ], + "stock" => [ + "amount" => 6, + "managedByRetailer" => false + ], + "fulfilment" => [ + "type" => "FBR", + "deliveryCode" => "24uurs-23" + ] +]); + +// You can use the update method to update offers. This will return a process status because it runs asynchronously. +// To update the stock level of an offer, you can use the `updateStock` method. + +$offer = Picqer\BolRetailer\Offer::get('13722de8-8182-d161-5422-4a0a1caab5c8'); +$processStatus = $offer->updateStock(5, true); + +// Wait until the process is complete. +// $processStatus->waitUntilComplete(); + +// And refresh the offer model +$offer->refresh(); +printf("Current stock level: %d\n", $offer->stock->amount); + +// And you can also delete an offer. +$offer->delete(); + +// And finally, to get an offer by its ID you can do the same as with orders +$offer = Picqer\BolRetailer\Offer::get('13722de8-8182-d161-5422-4a0a1caab5c8'); diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..88c64fd --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,14 @@ + + + The coding standard of bol-retailer-php-client package + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..6056433 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + tests + + + + + src/ + + + + + + + + + diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..3fc3ad9 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,135 @@ + $clientId, 'client_secret' => $clientSecret, 'grant_type' => 'client_credentials' ]; + $headers = [ 'Accept' => 'application/json' ]; + + $response = static::getHttp()->request('POST', 'https://login.bol.com/token', [ + 'headers' => $headers, + 'form_params' => $params + ]); + + $token = json_decode((string) $response->getBody(), true); + $token['expires_at'] = time() + $token['expires_in'] ?? 0; + + static::$token = $token; + } + + /** + * Clear the credentials of the client. This will effectively sign out. + */ + public static function clearCredentials(): void + { + static::$token = null; + } + + /** + * Check if the client is authenticated. + * + * @return bool + */ + public static function isAuthenticated(): bool + { + if (!is_array(static::$token)) { + return false; + } + + if (!isset(static::$token['expires_at']) || !isset(static::$token['access_token'])) { + return false; + } + + return static::$token['expires_at'] > time(); + } + + /** + * Configure whether the demo endpoints or real endpoints should be used. + * + * @param bool $enabled Set to `true` to enable demo mode, `false` otherwise. + */ + public static function setDemoMode(bool $enabled): void + { + static::$http = null; + static::$isDemoMode = $enabled; + } + + /** + * Perform an API call. + * + * @param string $method The HTTP method used for the API call. + * @param string $uri The URI to call. + * @param array $options The request options. + * + * @return ResponseInterface + */ + public static function request(string $method, string $uri, array $options = []): ResponseInterface + { + $options = static::addAuthenticationOptions($options); + + return static::getHttp()->request($method, $uri, $options); + } + + /** + * Set the HTTP client used for API calls. + * + * @param HttpInterface $http + */ + public static function setHttp(HttpInterface $http): void + { + static::$http = $http; + } + + private static function addAuthenticationOptions(array $options): array + { + if (!static::isAuthenticated()) { + return $options; + } + + $authorization = [ + 'Authorization' => sprintf('Bearer %s', static::$token['access_token']), + ]; + + $options['headers'] = array_merge($options['headers'] ?? [], $authorization); + + return $options; + } + + private static function getHttp(): HttpInterface + { + if (!static::$http instanceof HttpInterface) { + $baseUri = static::$isDemoMode ? 'https://api.bol.com/retailer-demo/' : 'https://api.bol.com/retailer/'; + + static::$http = new Http([ + 'base_uri' => $baseUri, + 'headers' => [ + 'Accept' => 'application/vnd.retailer.v3+json', + 'Content-Type' => 'application/vnd.retailer.v3+json', + ] + ]); + } + + return static::$http; + } +} diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php new file mode 100644 index 0000000..67e72a7 --- /dev/null +++ b/src/Exception/HttpException.php @@ -0,0 +1,44 @@ +error = $error; + } + + public function getType() + { + return $this->error['type']; + } + + public function getStatus() + { + return $this->error['status']; + } + + public function getDetail() + { + return $this->error['detail']; + } + + public function getError() + { + return $this->error; + } +} diff --git a/src/Exception/OfferNotFoundException.php b/src/Exception/OfferNotFoundException.php new file mode 100644 index 0000000..c1cdaed --- /dev/null +++ b/src/Exception/OfferNotFoundException.php @@ -0,0 +1,6 @@ +id + )); + + $this->processStatus = $processStatus; + } +} diff --git a/src/Exception/ShipmentNotFoundException.php b/src/Exception/ShipmentNotFoundException.php new file mode 100644 index 0000000..314991e --- /dev/null +++ b/src/Exception/ShipmentNotFoundException.php @@ -0,0 +1,6 @@ +data = $data; + } + + /** + * {@inheritdoc} + */ + public function __get($property) + { + $getter = sprintf('get%s', ucfirst($property)); + + if (method_exists($this, $getter)) { + return $this->{$getter}(); + } + + return $this->data[$property] ?? null; + } + + /** + * Merge the given data into the model. + * + * @param array $data The data to merge into the model. + */ + public function merge(array $data): void + { + $this->data = array_merge($this->data, $data); + } +} diff --git a/src/Model/AddressDetails.php b/src/Model/AddressDetails.php new file mode 100644 index 0000000..a3214e8 --- /dev/null +++ b/src/Model/AddressDetails.php @@ -0,0 +1,25 @@ +data['stock']); + } + + protected function getPricing(): array + { + return array_map(function (array $data) { + return new Pricing($data); + }, $this->data['pricing']['bundlePrices'] ?? []); + } +} diff --git a/src/Model/Order.php b/src/Model/Order.php new file mode 100644 index 0000000..a60f6ea --- /dev/null +++ b/src/Model/Order.php @@ -0,0 +1,37 @@ +data['orderItems'] ?? []); + } + + protected function getOrderPlacedAt(): ?DateTime + { + $parsedTimestamp = DateTime::createFromFormat( + DateTime::ATOM, + $this->data['dateTimeOrderPlaced'] ?? null + ); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } + + protected function getCustomerDetails(): OrderCustomerDetails + { + return new OrderCustomerDetails($this->data['customerDetails']); + } +} diff --git a/src/Model/OrderCustomerDetails.php b/src/Model/OrderCustomerDetails.php new file mode 100644 index 0000000..37af02e --- /dev/null +++ b/src/Model/OrderCustomerDetails.php @@ -0,0 +1,21 @@ +data['billingDetails']); + } + + protected function getShipmentDetails(): AddressDetails + { + return new AddressDetails($this->data['shipmentDetails']); + } +} diff --git a/src/Model/OrderItem.php b/src/Model/OrderItem.php new file mode 100644 index 0000000..d43ac00 --- /dev/null +++ b/src/Model/OrderItem.php @@ -0,0 +1,54 @@ +order = $order; + } + + protected function getOrder(): Order + { + return $this->order; + } + + protected function getLatestDeliveryDate(): ?DateTime + { + $parsedTimestamp = DateTime::createFromFormat('Y-m-d', $this->data['latestDeliveryDate'] ?? null); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } +} diff --git a/src/Model/Pricing.php b/src/Model/Pricing.php new file mode 100644 index 0000000..99a8963 --- /dev/null +++ b/src/Model/Pricing.php @@ -0,0 +1,10 @@ +status === 'PENDING'; + } + + protected function getIsSuccess(): bool + { + return $this->status === 'SUCCESS'; + } + + protected function getIsFailure(): bool + { + return $this->status === 'FAILURE'; + } + + protected function getIsTimeout(): bool + { + return $this->status === 'TIMEOUT'; + } +} diff --git a/src/Model/ReducedOrder.php b/src/Model/ReducedOrder.php new file mode 100644 index 0000000..242a26f --- /dev/null +++ b/src/Model/ReducedOrder.php @@ -0,0 +1,29 @@ +data['orderItems']); + } + + protected function getOrderPlacedAt(): ?DateTime + { + $parsedTimestamp = DateTime::createFromFormat( + DateTime::ATOM, + $this->data['dateTimeOrderPlaced'] + ); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } +} diff --git a/src/Model/ReducedOrderItem.php b/src/Model/ReducedOrderItem.php new file mode 100644 index 0000000..8c1980c --- /dev/null +++ b/src/Model/ReducedOrderItem.php @@ -0,0 +1,39 @@ +order = $order; + } + + protected function getOrder(): ReducedOrder + { + return $this->order; + } + + protected function getCancelRequest(): bool + { + return (bool) $this->data['cancelRequest']; + } +} diff --git a/src/Model/Shipment.php b/src/Model/Shipment.php new file mode 100644 index 0000000..4549c95 --- /dev/null +++ b/src/Model/Shipment.php @@ -0,0 +1,32 @@ +data['shipmentDate'] ?? null + ); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } + + protected function getShipmentItems(): array + { + return array_map(function (array $data) { + return new ShipmentItem($this, $data); + }, $this->data['shipmentItems'] ?? []); + } +} diff --git a/src/Model/ShipmentItem.php b/src/Model/ShipmentItem.php new file mode 100644 index 0000000..6b35937 --- /dev/null +++ b/src/Model/ShipmentItem.php @@ -0,0 +1,62 @@ +shipment = $shipment; + } + + protected function getShipment(): Shipment + { + return $this->shipment; + } + + protected function getOrderDate(): ?DateTime + { + $parsedTimestamp = DateTime::createFromFormat( + DateTime::ATOM, + $this->data['orderDate'] ?? null + ); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } + + protected function getLatestDeliveryDate(): ?DateTime + { + $parsedTimestamp = DateTime::createFromFormat( + DateTime::ATOM, + $this->data['latestDeliveryDate'] ?? null + ); + + return $parsedTimestamp instanceof DateTime ? $parsedTimestamp : null; + } +} diff --git a/src/Model/Stock.php b/src/Model/Stock.php new file mode 100644 index 0000000..c177fac --- /dev/null +++ b/src/Model/Stock.php @@ -0,0 +1,10 @@ +getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Create a new offer. + * + * @param array $data The data of the offer to create. + * + * @return ProcessStatus + */ + public static function create(array $data): ProcessStatus + { + try { + $response = Client::request('POST', "offers", ['body' => json_encode($data)]); + + return new ProcessStatus(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Refresh the details of the offer. + */ + public function refresh(): void + { + $id = $this->offerId; + + try { + $response = Client::request('GET', "offers/${id}"); + + $this->merge(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Update the details of an offer. + * + * @param array $data The new details of the offer. + * + * @return ProcessStatus + */ + public function update(array $data): ProcessStatus + { + $id = $this->offerId; + + try { + $response = Client::request('PUT', "offers/${id}", ['body' => json_encode($data)]); + + return new ProcessStatus(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Update the stock level of an offer. + * + * @param integer $amount The stock level of the offer. + * @param bool $managedByRetailer Configures whether the retailer manages the stock levels or that bol.com + * should calculate the corrected stock based on actual open orders. In case the + * configuration is set to `false`, all open orders are used to calculate the + * corrected stock. In case the configuration is set to `true`, only orders that + * are placed after the last offer update are taken into account. Default is set + * to `false`. + */ + public function updateStock(int $amount, bool $managedByRetailer = true): ProcessStatus + { + $id = $this->offerId; + $content = json_encode([ 'amount' => $amount, 'managedByRetailer' => $managedByRetailer ]); + + try { + $response = Client::request('PUT', "offers/${id}/stock", ['body' => $content]); + + return new ProcessStatus(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Delete an existing offer. + * + * @return ProcessStatus + */ + public function delete(): ProcessStatus + { + $id = $this->offerId; + + try { + $response = Client::request('DELETE', "offers/${id}"); + + return new ProcessStatus(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + private static function handleException(ClientException $e) + { + $response = $e->getResponse(); + + if ($response && $response->getStatusCode() === 404) { + throw new OfferNotFoundException( + json_decode((string) $response->getBody(), true), + 404, + $e + ); + } elseif ($response) { + throw new HttpException( + json_decode((string) $response->getBody(), true), + $response->getStatusCode(), + $e + ); + } + + throw $e; + } +} diff --git a/src/Order.php b/src/Order.php new file mode 100644 index 0000000..e2013c6 --- /dev/null +++ b/src/Order.php @@ -0,0 +1,73 @@ +getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Get all open orders. + * + * @param int $page The page to get the orders from. + * @param string $method The fulfilment method of the orders to list. + * + * @return Model\ReducedOrder[] + */ + public static function all(int $page = 1, string $method = 'FBR'): array + { + $query = [ 'page' => $page, 'fulfilment-method' => $method ]; + + try { + $response = Client::request('GET', 'orders', ['query' => $query]); + $response = json_decode((string) $response->getBody(), true); + } catch (ClientException $e) { + static::handleException($e); + } + + return array_map(function (array $data) { + return new Model\ReducedOrder($data); + }, $response['orders'] ?? []); + } + + private static function handleException(ClientException $e) + { + $response = $e->getResponse(); + + if ($response && $response->getStatusCode() === 404) { + throw new OrderNotFoundException( + json_decode((string) $response->getBody(), true), + 404, + $e + ); + } elseif ($response) { + throw new HttpException( + json_decode((string) $response->getBody(), true), + $response->getStatusCode(), + $e + ); + } + + throw $e; + } +} diff --git a/src/ProcessStatus.php b/src/ProcessStatus.php new file mode 100644 index 0000000..57446fc --- /dev/null +++ b/src/ProcessStatus.php @@ -0,0 +1,89 @@ +getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Refresh the details of the current process status. + */ + public function refresh(): void + { + $id = $this->id; + + try { + $response = Client::request('GET', "process-status/${id}"); + + $this->merge(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Wait until the process is marked as something other than `PENDING`. + * + * An exception will be thrown if the process status is not something other than `PENDING` after the given number + * of `maxRetries`. + * + * @param int $maxRetries The maximum number of times the process status should be refreshed. + * @param int $timeout The number of seconds to wait between fetching updates from the server. + * + * @throws ProcessStillPendingException when the maximum number of retries is reached and the process is still + * in the `PENDING` status. + */ + public function waitUntilComplete(int $maxRetries = 20, int $timeout = 3): void + { + for ($i = 0; $i < $maxRetries && $this->isPending; $i++) { + $this->refresh(); + sleep($timeout); + } + + if ($this->isPending) { + throw new ProcessStillPendingException($this); + } + } + + private static function handleException(ClientException $e) + { + $response = $e->getResponse(); + + if ($response && $response->getStatusCode() === 404) { + throw new ProcessStatusNotFoundException( + json_decode((string) $response->getBody(), true), + 404, + $e + ); + } elseif ($response) { + throw new HttpException( + json_decode((string) $response->getBody(), true), + $response->getStatusCode(), + $e + ); + } + + throw $e; + } +} diff --git a/src/Shipment.php b/src/Shipment.php new file mode 100644 index 0000000..caca40a --- /dev/null +++ b/src/Shipment.php @@ -0,0 +1,119 @@ +getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Get all existing shipments. + * + * @param integer $page The page to get the shipments from. + * @param string $order The order to get the shipments for. + * @param string $method The fulfilment method used for the orders to list the shipments for. + * + * @return self[] + */ + public static function all(int $page = 1, ?string $order = null, string $method = 'FBR'): array + { + $query = array_filter([ 'page' => $page, 'order-id' => $order, 'fulfilment-method' => $method ]); + + try { + $response = Client::request('GET', 'shipments', ['query' => $query]); + $response = json_decode((string) $response->getBody(), true); + } catch (ClientException $e) { + static::handleException($e); + } + + return array_map(function (array $data) { + return new self($data); + }, $response['shipments'] ?? []); + } + + /** + * Create a new shipment for the given order item. + * + * @param string|OrderItem|ReducedOrderItem $orderItem The order item to create the shipment for. + * @param array $data The data of the shipment to create. + * + * @return ProcessStatus + */ + public static function create($orderItem, array $data): ProcessStatus + { + $orderItemId = $orderItem instanceof OrderItem || $orderItem instanceof ReducedOrderItem + ? $orderItem->orderItemId + : $orderItem; + + $uri = "orders/${orderItemId}/shipment"; + + try { + $response = Client::request('PUT', $uri, ['body' => json_encode($data)]); + + return new ProcessStatus(json_decode((string) $response->getBody(), true)); + } catch (ClientException $e) { + static::handleException($e); + } + } + + /** + * Create a shipment for each ordered item. + * + * @param string|Order|ReducedOrder $order The order to create shipments for. + * @param array $data The data of the shipment created. + * + * @return ProcessStatus[] + */ + public static function createForOrder($order, array $data): array + { + $order = is_string($order) ? \Picqer\BolRetailer\Order::get($order) : $order; + + return array_map(function ($orderItem) use ($data) { + return static::create($orderItem, $data); + }, is_null($order) ? [] : $order->orderItems); + } + + private static function handleException(ClientException $e) + { + $response = $e->getResponse(); + + if ($response && $response->getStatusCode() === 404) { + throw new ShipmentNotFoundException( + json_decode((string) $response->getBody(), true), + 404, + $e + ); + } elseif ($response) { + throw new HttpException( + json_decode((string) $response->getBody(), true), + $response->getStatusCode(), + $e + ); + } + + throw $e; + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..85a2081 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,63 @@ +http = $this->prophesize(ClientInterface::class); + + Client::setHttp($this->http->reveal()); + } + + public function testAuthenticateWithCredentials() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-token')); + + $this->http + ->request('POST', 'https://login.bol.com/token', [ + 'headers' => [ 'Accept' => 'application/json' ], + 'form_params' => [ + 'client_id' => 'secret_id', + 'client_secret' => 'somesupersecretvaluethatshouldnotbeshared', + 'grant_type' => 'client_credentials' + ] + ])->willReturn($response); + + Client::setCredentials('secret_id', 'somesupersecretvaluethatshouldnotbeshared'); + $this->assertTrue(Client::isAuthenticated()); + + Client::clearCredentials(); + $this->assertFalse(Client::isAuthenticated()); + } + + public function testPerformHttpRequest() + { + $response = $this->prophesize(ResponseInterface::class)->reveal(); + + $this->http + ->request('GET', 'status', []) + ->willReturn($response); + + $this->assertEquals($response, Client::request('GET', 'status')); + } + + public function testPerformHttpRequestWithOptions() + { + $response = $this->prophesize(ResponseInterface::class)->reveal(); + + $this->http + ->request('GET', 'status', [ 'query' => [ 'foo' => 'bar' ]]) + ->willReturn($response); + + $this->assertEquals($response, Client::request('GET', 'status', [ 'query' => [ 'foo' => 'bar' ]])); + } +} diff --git a/tests/Exception/HttpExceptionTest.php b/tests/Exception/HttpExceptionTest.php new file mode 100644 index 0000000..6efb62c --- /dev/null +++ b/tests/Exception/HttpExceptionTest.php @@ -0,0 +1,48 @@ +previous = new \Exception(); + $this->exception = new HttpException( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/error.json'), true), + 404, + $this->previous + ); + } + + public function testContainsType() + { + $this->assertEquals('http://api.bol.com/problems', $this->exception->getType()); + } + + public function testContainsMessage() + { + $this->assertEquals('Error validating request body. Consult the bol.com API documentation for more information.', $this->exception->getMessage()); + } + + public function testContainsStatus() + { + $this->assertEquals('40X', $this->exception->getStatus()); + } + + public function testContainsDetail() + { + $this->assertEquals('Bad request', $this->exception->getDetail()); + } + + public function testContainsError() + { + $this->assertEquals( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/error.json'), true), + $this->exception->getError() + ); + } +} diff --git a/tests/Fixtures/http/200-offer b/tests/Fixtures/http/200-offer new file mode 100644 index 0000000..c3cdf84 --- /dev/null +++ b/tests/Fixtures/http/200-offer @@ -0,0 +1,45 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "offerId": "6ff736b5-cdd0-4150-8c67-78269ee986f5", + "ean": "0000007740404", + "referenceCode": "REF12345", + "onHoldByRetailer": false, + "unknownProductTitle": "Unknown Product Title", + "pricing": { + "bundlePrices": [ + { + "quantity": 1, + "price": 9.99 + } + ] + }, + "stock": { + "amount": 6, + "correctedStock": 5, + "managedByRetailer": false + }, + "fulfilment": { + "type": "FBR", + "deliveryCode": "24uurs-23" + }, + "store": { + "productTitle": "Product Title", + "visible": "NL, BE" + }, + "condition": { + "name": "AS_NEW", + "category": "SECONDHAND", + "comment": "Heeft een koffie vlek op de kaft." + }, + "notPublishableReasons": [ + { + "code": "4003", + "description": "The seller is on holiday." + } + ] +} diff --git a/tests/Fixtures/http/200-order b/tests/Fixtures/http/200-order new file mode 100644 index 0000000..aea816c --- /dev/null +++ b/tests/Fixtures/http/200-order @@ -0,0 +1,47 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "orderId" : "1043946570", + "dateTimeOrderPlaced" : "2019-04-29T16:18:21+02:00", + "customerDetails" : { + "shipmentDetails" : { + "salutationCode" : "01", + "firstName" : "Pieter", + "surName" : "Post", + "streetName" : "Skywalkerstraat", + "houseNumber" : "21", + "zipCode" : "1234 AB", + "city" : "PLATOONDORP", + "countryCode" : "NL", + "email" : "2puwjjic7wxsbmkj3bc2v6jsj2wimk@verkopen.test2.bol.com" + }, + "billingDetails" : { + "salutationCode" : "01", + "firstName" : "Pieter", + "surName" : "Post", + "streetName" : "Skywalkerstraat", + "houseNumber" : "21", + "zipCode" : "1234 AB", + "city" : "PLATOONDORP", + "countryCode" : "NL", + "email" : "2puwjjic7wxsbmkj3bc2v6jsj2wimk@verkopen.test2.bol.com" + } + }, + "orderItems" : [ { + "orderItemId" : "6107989317", + "offerReference" : "MijnOffer6627", + "ean" : "8785075035214", + "title" : "Star Wars - The happy family 2", + "quantity" : 3, + "offerPrice" : 22.98, + "transactionFee" : 2.22, + "latestDeliveryDate" : "2019-05-01", + "offerCondition" : "NEW", + "cancelRequest" : false, + "fulfilmentMethod" : "FBR" + } ] +} diff --git a/tests/Fixtures/http/200-orders b/tests/Fixtures/http/200-orders new file mode 100644 index 0000000..f6dec00 --- /dev/null +++ b/tests/Fixtures/http/200-orders @@ -0,0 +1,32 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "orders" : [ { + "orderId" : "1043946570", + "dateTimeOrderPlaced" : "2019-04-29T16:18:21+02:00", + "orderItems" : [ { + "orderItemId" : "6042823871", + "ean" : "8785075035214", + "cancelRequest" : false, + "quantity" : 3 + } ] + }, { + "orderId" : "1042831430", + "dateTimeOrderPlaced" : "2019-04-20T10:58:39+02:00", + "orderItems" : [ { + "orderItemId" : "6107331382", + "ean" : "8712626055143", + "cancelRequest" : false, + "quantity" : 1 + }, { + "orderItemId" : "6107331383", + "ean" : "8804269223123", + "cancelRequest" : false, + "quantity" : 1 + } ] + } ] +} diff --git a/tests/Fixtures/http/200-process-status-pending b/tests/Fixtures/http/200-process-status-pending new file mode 100644 index 0000000..294b014 --- /dev/null +++ b/tests/Fixtures/http/200-process-status-pending @@ -0,0 +1,19 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "id" : 1, + "entityId" : "555551", + "eventType" : "CONFIRM_SHIPMENT", + "description" : "This is a mocked process status.", + "status" : "PENDING", + "createTimestamp" : "2019-07-23T16:59:39.018+02:00", + "links" : [ { + "rel" : "self", + "href" : "https://api.bol.com/retailer-demo/process-status/1", + "method" : "GET" + } ] +} diff --git a/tests/Fixtures/http/200-process-status-success b/tests/Fixtures/http/200-process-status-success new file mode 100644 index 0000000..3427835 --- /dev/null +++ b/tests/Fixtures/http/200-process-status-success @@ -0,0 +1,19 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "id" : 1, + "entityId" : "555551", + "eventType" : "CONFIRM_SHIPMENT", + "description" : "This is a mocked process status.", + "status" : "SUCCESS", + "createTimestamp" : "2019-07-23T16:59:39.018+02:00", + "links" : [ { + "rel" : "self", + "href" : "https://api.bol.com/retailer-demo/process-status/1", + "method" : "GET" + } ] +} diff --git a/tests/Fixtures/http/200-shipment b/tests/Fixtures/http/200-shipment new file mode 100644 index 0000000..3c75d3e --- /dev/null +++ b/tests/Fixtures/http/200-shipment @@ -0,0 +1,40 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "shipmentId" : 953992381, + "shipmentDate" : "2018-04-20T15:10:05+02:00", + "shipmentReference" : "Shipment5", + "shipmentItems" : [ { + "orderItemId" : "6107989317", + "orderId" : "7616247328", + "orderDate" : "2018-04-20T10:58:39+02:00", + "latestDeliveryDate" : "2018-04-21T00:00:00+02:00", + "ean" : "8718526069334", + "title" : "Star Wars - Nappy Star wars T-shirt - XL", + "quantity" : 1, + "offerPrice" : 25.0, + "offerCondition" : "NEW", + "offerReference" : "Test4", + "fulfilmentMethod" : "FBB" + } ], + "transport" : { + "transportId" : 356972369, + "transporterCode" : "TNT", + "trackAndTrace" : "3SCOLD1234567" + }, + "customerDetails" : { + "salutationCode" : "01", + "firstName" : "Chewbakka", + "surname" : "Wookiee", + "streetName" : "Kashyyykstraat", + "houseNumber" : "100", + "zipCode" : "3528BJ", + "city" : "Utrecht", + "countryCode" : "NL", + "email" : "2awq74thj24b7rsxqubtvl3v3efq66@verkopen.test2.bol.com" + } +} diff --git a/tests/Fixtures/http/200-shipments b/tests/Fixtures/http/200-shipments new file mode 100644 index 0000000..352470e --- /dev/null +++ b/tests/Fixtures/http/200-shipments @@ -0,0 +1,53 @@ +HTTP/1.1 200 OK +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "shipments" : [ { + "shipmentId" : 914587795, + "shipmentDate" : "2018-04-20T17:20:11+02:00", + "shipmentReference" : "Shipment1", + "shipmentItems" : [ { + "orderItemId" : "6107434013", + "orderId" : "7616222250" + } ], + "transport" : { + "transportId" : 358612589 + } + }, { + "shipmentId" : 953266576, + "shipmentDate" : "2018-04-20T17:10:19+02:00", + "shipmentReference" : "Shipment2", + "shipmentItems" : [ { + "orderItemId" : "6107331383", + "orderId" : "7616222700" + } ], + "transport" : { + "transportId" : 356988715 + } + }, { + "shipmentId" : 953267579, + "shipmentDate" : "2018-04-20T16:46:01+02:00", + "shipmentReference" : "Shipment3", + "shipmentItems" : [ { + "orderItemId" : "6107432387", + "orderId" : "7616222700" + } ], + "transport" : { + "transportId" : 356988715 + } + }, { + "shipmentId" : 953316694, + "shipmentDate" : "2018-04-20T07:33:11+02:00", + "shipmentReference" : "Shipment4", + "shipmentItems" : [ { + "orderItemId" : "6702312887", + "orderId" : "4616526971" + } ], + "transport" : { + "transportId" : 356567193 + } + } ] +} diff --git a/tests/Fixtures/http/200-token b/tests/Fixtures/http/200-token new file mode 100644 index 0000000..31f7c49 --- /dev/null +++ b/tests/Fixtures/http/200-token @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-Type: application/json;charset=UTF-8 + +{ + "access_token": "sometoken", + "token_type": "Bearer", + "expires_in": 299, + "scope": "RETAILER" +} diff --git a/tests/Fixtures/http/202-process-status b/tests/Fixtures/http/202-process-status new file mode 100644 index 0000000..dc35a19 --- /dev/null +++ b/tests/Fixtures/http/202-process-status @@ -0,0 +1,19 @@ +HTTP/1.1 202 Accepted +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "id" : 1, + "entityId" : "555551", + "eventType" : "CONFIRM_SHIPMENT", + "description" : "This is a mocked process status.", + "status" : "PENDING", + "createTimestamp" : "2019-07-23T16:59:39.018+02:00", + "links" : [ { + "rel" : "self", + "href" : "https://api.bol.com/retailer-demo/process-status/1", + "method" : "GET" + } ] +} diff --git a/tests/Fixtures/http/404-not-found b/tests/Fixtures/http/404-not-found new file mode 100644 index 0000000..1b185aa --- /dev/null +++ b/tests/Fixtures/http/404-not-found @@ -0,0 +1,20 @@ +HTTP/1.1 404 Not Found +Content-Type: application/vnd.retailer.v3+json +X-RateLimit-Limit: 10 +X-RateLimit-Reset: 1539261914 +X-RateLimit-Remaining: 2 + +{ + "type": "http://api.bol.com/problems", + "title": "Error validating request body. Consult the bol.com API documentation for more information.", + "status": "40X", + "detail": "Bad request", + "host": "Instance-001", + "instance": "https://api.bol.com//retailer/resource", + "violations": [ + { + "name": "exampleValue", + "reason": "Request contains invalid value(s): 'INVALID', allowed values: ALLOWED_VALUE1, ALLOWED_VALUE2." + } + ] +} diff --git a/tests/Fixtures/json/error.json b/tests/Fixtures/json/error.json new file mode 100644 index 0000000..9d7d416 --- /dev/null +++ b/tests/Fixtures/json/error.json @@ -0,0 +1,14 @@ +{ + "type": "http://api.bol.com/problems", + "title": "Error validating request body. Consult the bol.com API documentation for more information.", + "status": "40X", + "detail": "Bad request", + "host": "Instance-001", + "instance": "https://api.bol.com//retailer/resource", + "violations": [ + { + "name": "exampleValue", + "reason": "Request contains invalid value(s): 'INVALID', allowed values: ALLOWED_VALUE1, ALLOWED_VALUE2." + } + ] +} diff --git a/tests/Fixtures/json/offer.json b/tests/Fixtures/json/offer.json new file mode 100644 index 0000000..a1481fc --- /dev/null +++ b/tests/Fixtures/json/offer.json @@ -0,0 +1,39 @@ +{ + "offerId": "6ff736b5-cdd0-4150-8c67-78269ee986f5", + "ean": "0000007740404", + "referenceCode": "REF12345", + "onHoldByRetailer": false, + "unknownProductTitle": "Unknown Product Title", + "pricing": { + "bundlePrices": [ + { + "quantity": 1, + "price": 9.99 + } + ] + }, + "stock": { + "amount": 6, + "correctedStock": 5, + "managedByRetailer": false + }, + "fulfilment": { + "type": "FBR", + "deliveryCode": "24uurs-23" + }, + "store": { + "productTitle": "Product Title", + "visible": "NL, BE" + }, + "condition": { + "name": "AS_NEW", + "category": "SECONDHAND", + "comment": "Heeft een koffie vlek op de kaft." + }, + "notPublishableReasons": [ + { + "code": "4003", + "description": "The seller is on holiday." + } + ] +} diff --git a/tests/Fixtures/json/order-item.json b/tests/Fixtures/json/order-item.json new file mode 100644 index 0000000..594b842 --- /dev/null +++ b/tests/Fixtures/json/order-item.json @@ -0,0 +1,13 @@ +{ + "orderItemId" : "6042823871", + "offerReference" : "MijnOffer6627", + "ean" : "8785075035214", + "title" : "Star Wars - The happy family 2", + "quantity" : 3, + "offerPrice" : 22.98, + "transactionFee" : 2.22, + "latestDeliveryDate" : "2019-05-01", + "offerCondition" : "NEW", + "cancelRequest" : false, + "fulfilmentMethod" : "FBR" +} diff --git a/tests/Fixtures/json/order.json b/tests/Fixtures/json/order.json new file mode 100644 index 0000000..7b5eace --- /dev/null +++ b/tests/Fixtures/json/order.json @@ -0,0 +1,43 @@ +{ + "orderId": "1043965710", + "dateTimeOrderPlaced": "2019-04-30T19:56:39+02:00", + "customerDetails": { + "shipmentDetails": { + "salutationCode": "01", + "firstName": "Pieter", + "surName": "Post", + "streetName": "Skywalkerstraat", + "houseNumber": "21", + "zipCode": "1234 AB", + "city": "PLATOONDORP", + "countryCode": "NL", + "email": "2yldzdi2wjcf5ir4sycq7lufqpytxy@verkopen.test2.bol.com" + }, + "billingDetails": { + "salutationCode": "01", + "firstName": "Pieter", + "surName": "Post", + "streetName": "Skywalkerstraat", + "houseNumber": "21", + "zipCode": "1234 AB", + "city": "PLATOONDORP", + "countryCode": "NL", + "email": "2yldzdi2wjcf5ir4sycq7lufqpytxy@verkopen.test2.bol.com" + } + }, + "orderItems": [ + { + "orderItemId": "6107989317", + "offerReference": "MijnOffer6627", + "ean": "8785075035214", + "title": "Star Wars - The happy family", + "quantity": 2, + "offerPrice": 23.99, + "transactionFee": 2.22, + "latestDeliveryDate": "2019-05-01", + "offerCondition": "NEW", + "cancelRequest": false, + "fulfilmentMethod": "FBB" + } + ] +} diff --git a/tests/Fixtures/json/process-status.json b/tests/Fixtures/json/process-status.json new file mode 100644 index 0000000..26f09f3 --- /dev/null +++ b/tests/Fixtures/json/process-status.json @@ -0,0 +1,13 @@ +{ + "id" : 1, + "entityId" : "555551", + "eventType" : "CONFIRM_SHIPMENT", + "description" : "Lorem ipsum dolor sit amet.", + "status" : "PENDING", + "createTimestamp" : "2019-07-23T16:59:39.018+02:00", + "links" : [ { + "rel" : "self", + "href" : "https://api.bol.com/retailer-demo/process-status/1", + "method" : "GET" + } ] +} diff --git a/tests/Fixtures/json/reduced-order-item.json b/tests/Fixtures/json/reduced-order-item.json new file mode 100644 index 0000000..d815311 --- /dev/null +++ b/tests/Fixtures/json/reduced-order-item.json @@ -0,0 +1,6 @@ +{ + "orderItemId": "6042823871", + "ean": "8785075035214", + "cancelRequest": false, + "quantity": 3 +} diff --git a/tests/Fixtures/json/reduced-order.json b/tests/Fixtures/json/reduced-order.json new file mode 100644 index 0000000..eb4695d --- /dev/null +++ b/tests/Fixtures/json/reduced-order.json @@ -0,0 +1,12 @@ +{ + "orderId": "1043946570", + "dateTimeOrderPlaced": "2019-04-29T16:18:21+02:00", + "orderItems": [ + { + "orderItemId": "6107989317", + "ean": "8785075035214", + "cancelRequest": false, + "quantity": 3 + } + ] +} diff --git a/tests/Fixtures/json/shipment-item.json b/tests/Fixtures/json/shipment-item.json new file mode 100644 index 0000000..61549fb --- /dev/null +++ b/tests/Fixtures/json/shipment-item.json @@ -0,0 +1,13 @@ +{ + "orderItemId" : "6107989317", + "orderId" : "7616247328", + "orderDate" : "2018-04-20T10:58:39+02:00", + "latestDeliveryDate" : "2018-04-21T00:00:00+02:00", + "ean" : "8718526069334", + "title" : "Star Wars - Nappy Star wars T-shirt - XL", + "quantity" : 1, + "offerPrice" : 25.0, + "offerCondition" : "NEW", + "offerReference" : "Test4", + "fulfilmentMethod" : "FBB" +} diff --git a/tests/Fixtures/json/shipment.json b/tests/Fixtures/json/shipment.json new file mode 100644 index 0000000..d0e92e5 --- /dev/null +++ b/tests/Fixtures/json/shipment.json @@ -0,0 +1,34 @@ +{ + "shipmentId" : 953992381, + "shipmentDate" : "2018-04-20T15:10:05+02:00", + "shipmentReference" : "Shipment5", + "shipmentItems" : [ { + "orderItemId" : "6107989317", + "orderId" : "7616247328", + "orderDate" : "2018-04-20T10:58:39+02:00", + "latestDeliveryDate" : "2018-04-21T00:00:00+02:00", + "ean" : "8718526069334", + "title" : "Star Wars - Nappy Star wars T-shirt - XL", + "quantity" : 1, + "offerPrice" : 25.0, + "offerCondition" : "NEW", + "offerReference" : "Test4", + "fulfilmentMethod" : "FBB" + } ], + "transport" : { + "transportId" : 356972369, + "transporterCode" : "TNT", + "trackAndTrace" : "3SCOLD1234567" + }, + "customerDetails" : { + "salutationCode" : "01", + "firstName" : "Chewbakka", + "surname" : "Wookiee", + "streetName" : "Kashyyykstraat", + "houseNumber" : "100", + "zipCode" : "3528BJ", + "city" : "Utrecht", + "countryCode" : "NL", + "email" : "2awq74thj24b7rsxqubtvl3v3efq66@verkopen.test2.bol.com" + } +} diff --git a/tests/Model/OfferTest.php b/tests/Model/OfferTest.php new file mode 100644 index 0000000..9a22bcc --- /dev/null +++ b/tests/Model/OfferTest.php @@ -0,0 +1,30 @@ +offer = new Offer( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/offer.json'), true) + ); + } + + public function testContainsStock() + { + $this->assertInstanceOf(Stock::class, $this->offer->stock); + } + + public function testContainsPricing() + { + $this->assertIsArray($this->offer->pricing); + $this->assertCount(1, $this->offer->pricing); + $this->assertInstanceOf(Pricing::class, $this->offer->pricing[0]); + } +} diff --git a/tests/Model/OrderItemTest.php b/tests/Model/OrderItemTest.php new file mode 100644 index 0000000..fa2b666 --- /dev/null +++ b/tests/Model/OrderItemTest.php @@ -0,0 +1,83 @@ +order = $this->prophesize(Order::class)->reveal(); + + $this->item = new OrderItem( + $this->order, + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/order-item.json'), true) + ); + } + + public function testContainsOrderItemId() + { + $this->assertEquals('6042823871', $this->item->orderItemId); + } + + public function testContainsOfferReference() + { + $this->assertEquals('MijnOffer6627', $this->item->offerReference); + } + + public function testContainsEan() + { + $this->assertEquals('8785075035214', $this->item->ean); + } + + public function testContainsTitle() + { + $this->assertEquals('Star Wars - The happy family 2', $this->item->title); + } + + public function testContainsQuantity() + { + $this->assertEquals(3, $this->item->quantity); + } + + public function testContainsOfferPrice() + { + $this->assertEquals(22.98, $this->item->offerPrice); + } + + public function testContainsTransactionFee() + { + $this->assertEquals(2.22, $this->item->transactionFee); + } + + public function testContainsLatestDeliveryDate() + { + $expected = \DateTime::createFromFormat('Y-m-d', '2019-05-01'); + + $this->assertEquals($expected, $this->item->latestDeliveryDate); + } + + public function testContainsOfferCondition() + { + $this->assertEquals('NEW', $this->item->offerCondition); + } + + public function testContainsFulfilmentMethod() + { + $this->assertEquals('FBR', $this->item->fulfilmentMethod); + } + + public function testContainsOrder() + { + $this->assertEquals($this->order, $this->item->order); + } + + public function testIsCancelled() + { + $this->assertFalse($this->item->cancelRequest); + } +} diff --git a/tests/Model/OrderTest.php b/tests/Model/OrderTest.php new file mode 100644 index 0000000..14069a1 --- /dev/null +++ b/tests/Model/OrderTest.php @@ -0,0 +1,41 @@ +order = new Order( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/order.json'), true) + ); + } + + public function testContainsOrderId() + { + $this->assertEquals('1043965710', $this->order->orderId); + } + + public function testContainsOrderPlacedAt() + { + $expected = \DateTime::createFromFormat(\DateTime::ATOM, '2019-04-30T19:56:39+02:00'); + + $this->assertEquals($expected, $this->order->orderPlacedAt); + } + + public function testContainsCustomerDetails() + { + $this->assertInstanceOf(OrderCustomerDetails::class, $this->order->customerDetails); + } + + public function testContainsOrderItems() + { + $this->assertCount(1, $this->order->orderItems); + $this->assertInstanceOf(OrderItem::class, $this->order->orderItems[0]); + } +} diff --git a/tests/Model/ProcessStatusTest.php b/tests/Model/ProcessStatusTest.php new file mode 100644 index 0000000..bdac31f --- /dev/null +++ b/tests/Model/ProcessStatusTest.php @@ -0,0 +1,69 @@ +processStatus = new ProcessStatus( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/process-status.json'), true) + ); + } + + public function testContainsId() + { + $this->assertEquals('1', $this->processStatus->id); + } + + public function testContainsEntityId() + { + $this->assertEquals('555551', $this->processStatus->entityId); + } + + public function testContainsEventType() + { + $this->assertEquals('CONFIRM_SHIPMENT', $this->processStatus->eventType); + } + + public function testContainsDescription() + { + $this->assertEquals('Lorem ipsum dolor sit amet.', $this->processStatus->description); + } + + public function testContainsStatus() + { + $this->assertEquals('PENDING', $this->processStatus->status); + } + + /** + * @dataProvider statusProvider + */ + public function testContainsStatusProperties( + string $status, + bool $isPending, + bool $isSuccess, + bool $isFailure, + bool $isTimeout + ) { + $this->processStatus->merge([ 'status' => $status ]); + + $this->assertEquals($isPending, $this->processStatus->isPending); + $this->assertEquals($isSuccess, $this->processStatus->isSuccess); + $this->assertEquals($isFailure, $this->processStatus->isFailure); + $this->assertEquals($isTimeout, $this->processStatus->isTimeout); + } + + public function statusProvider() + { + return [ + [ 'PENDING', true, false, false, false ], + [ 'SUCCESS', false, true, false, false ], + [ 'FAILURE', false, false, true, false ], + [ 'TIMEOUT', false, false, false, true ], + ]; + } +} diff --git a/tests/Model/ReducedOrderItemTest.php b/tests/Model/ReducedOrderItemTest.php new file mode 100644 index 0000000..dd1d51d --- /dev/null +++ b/tests/Model/ReducedOrderItemTest.php @@ -0,0 +1,45 @@ +order = $this->prophesize(ReducedOrder::class)->reveal(); + + $this->item = new ReducedOrderItem( + $this->order, + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/reduced-order-item.json'), true) + ); + } + + public function testContainsOrderItemId() + { + $this->assertEquals('6042823871', $this->item->orderItemId); + } + + public function testContainsEan() + { + $this->assertEquals('8785075035214', $this->item->ean); + } + + public function testContainsQuantity() + { + $this->assertEquals(3, $this->item->quantity); + } + + public function testContainsOrder() + { + $this->assertEquals($this->order, $this->item->order); + } + + public function testIsCancelled() + { + $this->assertFalse($this->item->cancelRequest); + } +} diff --git a/tests/Model/ReducedOrderTest.php b/tests/Model/ReducedOrderTest.php new file mode 100644 index 0000000..2075255 --- /dev/null +++ b/tests/Model/ReducedOrderTest.php @@ -0,0 +1,35 @@ +order = new ReducedOrder( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/reduced-order.json'), true) + ); + } + + public function testContainsOrderId() + { + $this->assertEquals('1043946570', $this->order->orderId); + } + + public function testContainsOrderPlacedAt() + { + $expected = \DateTime::createFromFormat(\DateTime::ATOM, '2019-04-29T16:18:21+02:00'); + + $this->assertEquals($expected, $this->order->orderPlacedAt); + } + + public function testContainsOrderItems() + { + $this->assertCount(1, $this->order->orderItems); + $this->assertInstanceOf(ReducedOrderItem::class, $this->order->orderItems[0]); + } +} diff --git a/tests/Model/ShipmentItemTest.php b/tests/Model/ShipmentItemTest.php new file mode 100644 index 0000000..d7f0d4b --- /dev/null +++ b/tests/Model/ShipmentItemTest.php @@ -0,0 +1,39 @@ +shipment = $this->prophesize(Shipment::class)->reveal(); + $this->item = new ShipmentItem( + $this->shipment, + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/shipment-item.json'), true) + ); + } + + public function testContainsOrderDate() + { + $expected = \DateTime::createFromFormat(\DateTime::ATOM, '2018-04-20T10:58:39+02:00'); + + $this->assertEquals($expected, $this->item->orderDate); + } + + public function testContainsLatestDeliveryDate() + { + $expected = \DateTime::createFromFormat(\DateTime::ATOM, '2018-04-21T00:00:00+02:00'); + + $this->assertEquals($expected, $this->item->latestDeliveryDate); + } + + public function testContainsShipment() + { + $this->assertEquals($this->shipment, $this->item->shipment); + } +} diff --git a/tests/Model/ShipmentTest.php b/tests/Model/ShipmentTest.php new file mode 100644 index 0000000..468cff9 --- /dev/null +++ b/tests/Model/ShipmentTest.php @@ -0,0 +1,50 @@ +shipment = new Shipment( + json_decode(file_get_contents(__DIR__ . '/../Fixtures/json/shipment.json'), true) + ); + } + + public function testContainsShipmentId() + { + $this->assertEquals('953992381', $this->shipment->shipmentId); + } + + public function testContainsShipmentReference() + { + $this->assertEquals('Shipment5', $this->shipment->shipmentReference); + } + + public function testContainsShipmentDate() + { + $expected = \DateTime::createFromFormat(\DateTime::ATOM, '2018-04-20T15:10:05+02:00'); + + $this->assertEquals($expected, $this->shipment->shipmentDate); + } + + public function testContainsShipmentItems() + { + $this->assertCount(1, $this->shipment->shipmentItems); + $this->assertInstanceOf(ShipmentItem::class, $this->shipment->shipmentItems[0]); + } + + public function testContainsTransport() + { + $this->assertIsArray($this->shipment->transport); + } + + public function testContainsCustomerDetails() + { + $this->assertIsArray($this->shipment->customerDetails); + } +} diff --git a/tests/OfferTest.php b/tests/OfferTest.php new file mode 100644 index 0000000..36bbdca --- /dev/null +++ b/tests/OfferTest.php @@ -0,0 +1,73 @@ +http = $this->prophesize(ClientInterface::class); + + Client::setHttp($this->http->reveal()); + } + + public function testGetOffer() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-offer')); + + $this->http + ->request('GET', 'offers/6ff736b5-cdd0-4150-8c67-78269ee986f5', []) + ->willReturn($response); + + $offer = Offer::get('6ff736b5-cdd0-4150-8c67-78269ee986f5'); + + $this->assertInstanceOf(Model\Offer::class, $offer); + $this->assertEquals('6ff736b5-cdd0-4150-8c67-78269ee986f5', $offer->offerId); + } + + /** + * @expectedException Picqer\BolRetailer\Exception\OfferNotFoundException + */ + public function testThrowExceptionWhenProcessStatusNotFound() + { + $request = $this->prophesize(RequestInterface::class); + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/404-not-found')); + $exception = new ClientException('', $request->reveal(), $response); + + $this->http + ->request('GET', 'offers/1234', []) + ->willThrow($exception); + + Offer::get('1234'); + } + + public function testUpdateStockLevel() + { + $responses = []; + $responses[] = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-offer')); + $responses[] = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/202-process-status')); + + $this->http + ->request('GET', 'offers/6ff736b5-cdd0-4150-8c67-78269ee986f5', []) + ->willReturn($responses[0]); + + $this->http + ->request('PUT', 'offers/6ff736b5-cdd0-4150-8c67-78269ee986f5/stock', [ 'body' => json_encode([ 'amount' => 25, 'managedByRetailer' => true ]) ]) + ->willReturn($responses[1]); + + $offer = Offer::get('6ff736b5-cdd0-4150-8c67-78269ee986f5'); + $processStatus = $offer->updateStock(25, true); + + $this->assertInstanceOf(ProcessStatus::class, $processStatus); + } +} diff --git a/tests/OrderTest.php b/tests/OrderTest.php new file mode 100644 index 0000000..b2186fa --- /dev/null +++ b/tests/OrderTest.php @@ -0,0 +1,103 @@ +http = $this->prophesize(ClientInterface::class); + + Client::setHttp($this->http->reveal()); + } + + public function testGetAllOrders() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-orders')); + + $this->http + ->request('GET', 'orders', [ 'query' => [ 'page' => 1, 'fulfilment-method' => 'FBR' ]]) + ->willReturn($response); + + $orders = Order::all(); + + $this->assertCount(2, $orders); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[0]); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[1]); + $this->assertEquals('1043946570', $orders[0]->orderId); + $this->assertEquals('1042831430', $orders[1]->orderId); + } + + public function testGetAllOrdersWithPage() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-orders')); + + $this->http + ->request('GET', 'orders', [ 'query' => [ 'page' => 2, 'fulfilment-method' => 'FBR' ]]) + ->willReturn($response); + + $orders = Order::all(2); + + $this->assertCount(2, $orders); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[0]); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[1]); + $this->assertEquals('1043946570', $orders[0]->orderId); + $this->assertEquals('1042831430', $orders[1]->orderId); + } + + public function testGetAllOrdersWithPageAndFulfilmentMethod() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-orders')); + + $this->http + ->request('GET', 'orders', [ 'query' => [ 'page' => 2, 'fulfilment-method' => 'FBB' ]]) + ->willReturn($response); + + $orders = Order::all(2, 'FBB'); + + $this->assertCount(2, $orders); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[0]); + $this->assertInstanceOf(Model\ReducedOrder::class, $orders[1]); + $this->assertEquals('1043946570', $orders[0]->orderId); + $this->assertEquals('1042831430', $orders[1]->orderId); + } + + public function testGetSingleOrderById() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-order')); + + $this->http + ->request('GET', 'orders/1043946570', []) + ->willReturn($response); + + $order = Order::get('1043946570'); + + $this->assertInstanceOf(Model\Order::class, $order); + $this->assertEquals('1043946570', $order->orderId); + } + + /** + * @expectedException Picqer\BolRetailer\Exception\OrderNotFoundException + */ + public function testThrowExceptionWhenOrderNotFound() + { + $request = $this->prophesize(RequestInterface::class); + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/404-not-found')); + $exception = new ClientException('', $request->reveal(), $response); + + $this->http + ->request('GET', 'orders/1234', []) + ->willThrow($exception); + + Order::get('1234'); + } +} diff --git a/tests/ProcessStatusTest.php b/tests/ProcessStatusTest.php new file mode 100644 index 0000000..11a1d9d --- /dev/null +++ b/tests/ProcessStatusTest.php @@ -0,0 +1,105 @@ +http = $this->prophesize(ClientInterface::class); + + Client::setHttp($this->http->reveal()); + } + + public function testGetSingleProcessStatusById() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-success')); + + $this->http + ->request('GET', 'process-status/1', []) + ->willReturn($response); + + $processStatus = ProcessStatus::get('1'); + + $this->assertInstanceOf(Model\ProcessStatus::class, $processStatus); + $this->assertEquals('1', $processStatus->id); + } + + public function testRefreshFromServer() + { + $responses = [ + Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-pending')), + Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-success')), + ]; + + $this->http + ->request('GET', 'process-status/1', []) + ->willReturn($responses[0], $responses[1]); + + $processStatus = ProcessStatus::get('1'); + $this->assertTrue($processStatus->isPending); + + $processStatus->refresh(); + $this->assertTrue($processStatus->isSuccess); + } + + public function testWaitUntilComplete() + { + $responses = [ + Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-pending')), + Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-success')), + ]; + + $this->http + ->request('GET', 'process-status/1', []) + ->willReturn($responses[0], $responses[1]); + + $processStatus = ProcessStatus::get('1'); + $this->assertTrue($processStatus->isPending); + + $processStatus->waitUntilComplete(5, 0); + $this->assertTrue($processStatus->isSuccess); + } + + /** + * @expectedException Picqer\BolRetailer\Exception\ProcessStillPendingException + * @expectedExceptionMessage The process "1" is still in status "PENDING" after the maximum number of retries is reached. + */ + public function testThrowExceptionIfRetryLimitIsReached() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-process-status-pending')); + + $this->http + ->request('GET', 'process-status/1', []) + ->shouldBeCalledTimes(21) + ->willReturn($response); + + $processStatus = ProcessStatus::get('1'); + $processStatus->waitUntilComplete(20, 0); + } + + /** + * @expectedException Picqer\BolRetailer\Exception\ProcessStatusNotFoundException + */ + public function testThrowExceptionWhenProcessStatusNotFound() + { + $request = $this->prophesize(RequestInterface::class); + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/404-not-found')); + $exception = new ClientException('', $request->reveal(), $response); + + $this->http + ->request('GET', 'process-status/1234', []) + ->willThrow($exception); + + ProcessStatus::get('1234'); + } +} diff --git a/tests/ShipmentTest.php b/tests/ShipmentTest.php new file mode 100644 index 0000000..cd3abb5 --- /dev/null +++ b/tests/ShipmentTest.php @@ -0,0 +1,219 @@ +http = $this->prophesize(ClientInterface::class); + + Client::setHttp($this->http->reveal()); + } + + public function testGetAllShipments() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-shipments')); + + $this->http + ->request('GET', 'shipments', [ 'query' => [ 'page' => 1, 'fulfilment-method' => 'FBR' ]]) + ->willReturn($response); + + $shipments = Shipment::all(); + + $this->assertCount(4, $shipments); + $this->assertInstanceOf(Shipment::class, $shipments[0]); + $this->assertInstanceOf(Shipment::class, $shipments[1]); + $this->assertInstanceOf(Shipment::class, $shipments[2]); + $this->assertInstanceOf(Shipment::class, $shipments[3]); + $this->assertEquals('914587795', $shipments[0]->shipmentId); + $this->assertEquals('953266576', $shipments[1]->shipmentId); + $this->assertEquals('953267579', $shipments[2]->shipmentId); + $this->assertEquals('953316694', $shipments[3]->shipmentId); + } + + public function testGetAllShipmentsWithPage() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-shipments')); + + $this->http + ->request('GET', 'shipments', [ 'query' => [ 'page' => 2, 'fulfilment-method' => 'FBR' ]]) + ->willReturn($response); + + $shipments = Shipment::all(2); + + $this->assertCount(4, $shipments); + $this->assertInstanceOf(Shipment::class, $shipments[0]); + $this->assertInstanceOf(Shipment::class, $shipments[1]); + $this->assertInstanceOf(Shipment::class, $shipments[2]); + $this->assertInstanceOf(Shipment::class, $shipments[3]); + $this->assertEquals('914587795', $shipments[0]->shipmentId); + $this->assertEquals('953266576', $shipments[1]->shipmentId); + $this->assertEquals('953267579', $shipments[2]->shipmentId); + $this->assertEquals('953316694', $shipments[3]->shipmentId); + } + + public function testGetAllShipmentsWithPageAndOrder() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-shipments')); + + $this->http + ->request('GET', 'shipments', [ 'query' => [ 'page' => 2, 'order-id' => "1234", 'fulfilment-method' => 'FBR' ]]) + ->willReturn($response); + + $shipments = Shipment::all(2, '1234'); + + $this->assertCount(4, $shipments); + $this->assertInstanceOf(Shipment::class, $shipments[0]); + $this->assertInstanceOf(Shipment::class, $shipments[1]); + $this->assertInstanceOf(Shipment::class, $shipments[2]); + $this->assertInstanceOf(Shipment::class, $shipments[3]); + $this->assertEquals('914587795', $shipments[0]->shipmentId); + $this->assertEquals('953266576', $shipments[1]->shipmentId); + $this->assertEquals('953267579', $shipments[2]->shipmentId); + $this->assertEquals('953316694', $shipments[3]->shipmentId); + } + + public function testGetAllShipmentsWithPageAndFulfilmentMethod() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-shipments')); + + $this->http + ->request('GET', 'shipments', [ 'query' => [ 'page' => 2, 'fulfilment-method' => 'FBB' ]]) + ->willReturn($response); + + $shipments = Shipment::all(2, null, 'FBB'); + + $this->assertCount(4, $shipments); + $this->assertInstanceOf(Shipment::class, $shipments[0]); + $this->assertInstanceOf(Shipment::class, $shipments[1]); + $this->assertInstanceOf(Shipment::class, $shipments[2]); + $this->assertInstanceOf(Shipment::class, $shipments[3]); + $this->assertEquals('914587795', $shipments[0]->shipmentId); + $this->assertEquals('953266576', $shipments[1]->shipmentId); + $this->assertEquals('953267579', $shipments[2]->shipmentId); + $this->assertEquals('953316694', $shipments[3]->shipmentId); + } + + public function testGetSingleOrderById() + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-shipment')); + + $this->http + ->request('GET', 'shipments/953992381', []) + ->willReturn($response); + + $shipment = Shipment::get('953992381'); + + $this->assertInstanceOf(Shipment::class, $shipment); + $this->assertEquals('953992381', $shipment->shipmentId); + } + + /** + * @dataProvider orderItemProvider + */ + public function testCreateShipment($orderItem, string $uri) + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/202-process-status')); + $expected = [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] + ]; + + $this->http + ->request('PUT', $uri, [ 'body' => json_encode($expected) ]) + ->willReturn($response); + + $processStatus = Shipment::create($orderItem, [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] + ]); + + $this->assertInstanceOf(ProcessStatus::class, $processStatus); + $this->assertTrue($processStatus->isPending); + } + + public function orderItemProvider() + { + $orderItem = new OrderItem( + $this->prophesize(Order::class)->reveal(), + [ 'orderItemId' => '1234' ] + ); + + $reducedOrderItem = new ReducedOrderItem( + $this->prophesize(ReducedOrder::class)->reveal(), + [ 'orderItemId' => '1234' ] + ); + + return [ + [ '1234', "orders/1234/shipment" ], + [ $orderItem, "orders/1234/shipment" ], + [ $reducedOrderItem, "orders/1234/shipment" ], + ]; + } + + /** + * @dataProvider orderProvider + */ + public function testCreateForOrder($order, $uris) + { + $response = Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/202-process-status')); + $expected = [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] + ]; + + $this->http + ->request('GET', "orders/1234", []) + ->willReturn(Psr7\parse_response(file_get_contents(__DIR__ . '/Fixtures/http/200-order'))); + + foreach ($uris as $uri) { + $this->http + ->request('PUT', $uri, ['body' => json_encode($expected)]) + ->willReturn($response); + } + + $processStatuses = Shipment::createForOrder($order, [ + 'transport' => [ + 'transporterCode' => 'TNT', + 'trackAndTrace' => '3SBOL0987654321' + ] + ]); + + $this->assertCount(1, $processStatuses); + } + + public function orderProvider() + { + $order = new Order( + json_decode(file_get_contents(__DIR__ . '/Fixtures/json/order.json'), true) + ); + + $reducedOrder = new ReducedOrder( + json_decode(file_get_contents(__DIR__ . '/Fixtures/json/reduced-order.json'), true) + ); + + return [ + [ "1234", [ "orders/6107989317/shipment" ] ], + [ $order, [ "orders/6107989317/shipment" ] ], + [ $reducedOrder, [ "orders/6107989317/shipment" ] ], + ]; + } +}