diff --git a/README.md b/README.md index c12e9d4..03cd70d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ foreach ($stores as $store) { ### SDK resources documentation - [Inventory management](docs/catalog.md) +- [Order management](docs/order.md) ### Generates XML compliant feed for import diff --git a/composer.json b/composer.json index 1ea9e59..83ce8bf 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,8 @@ }, "scripts": { "test": [ - "@php vendor/bin/phpcs --colors --report=full --standard=resources/phpcs/ruleset.xml --report-checkstyle=build/phpcs/phpcs.xml --report-emacs=build/phpcs/phpcs.log --extensions=php -p src", - "@php vendor/bin/phpunit --configuration=phpunit.xml --coverage-html=build/phpunit/coverage/" + "@php vendor/bin/phpunit --configuration=phpunit.xml --coverage-html=build/phpunit/coverage/", + "@php vendor/bin/phpcs --colors --report=full --standard=resources/phpcs/ruleset.xml --report-checkstyle=build/phpcs/phpcs.xml --report-emacs=build/phpcs/phpcs.log --extensions=php -p src" ] }, "scripts-descriptions": { diff --git a/docs/order.md b/docs/order.md new file mode 100644 index 0000000..d9a35f3 --- /dev/null +++ b/docs/order.md @@ -0,0 +1,164 @@ +# Order + +## Access + +Accessing order operation can be done from the store. + +```php +getMainStore()->getOrderApi(); +``` + +## Operations + +From order API you can then access all available operation : +```php +accept('ref3', 'amazon') + ->refuse('ref4', 'amazon') + ->ship('ref5', 'amazon') + ->cancel('ref1', 'amazon'); +$orderApi->execute($updateOperation); +``` + +Operations allowed on existing order will always be accepted as they are treated asynchronously. +When sending operation on order you will receive a collection of tickets corresponding to tasks in our system +that will handle the requested operation. +With this ticket collection you will be able to find what ticket has been associated with the operation on an order. + +```php +accept('ref3', 'amazon') + ->refuse('ref4', 'amazon') + ->ship('ref5', 'amazon') + ->cancel('ref3', 'amazon'); +$ticketCollection = $orderApi->execute($updateOperation); + +// Tickets to follow all acceptance tasks +$tickets = $ticketCollection->getAccepted(); + +// Ticket ID to follow 'ref3' cancelling task +$ticketId = $ticketCollection->getCanceled('ref3')[0]->getId(); +``` + +### Accept + +The accept operation accept 3 parameters : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [optional] `$reason` : The reason of the acceptance (eq: 'Why we accept the order') + +Example : +```php +accept('ref1', 'amazon') + ->accept('ref2', 'amazon', 'Why we accept it'); +$orderApi->execute($updateOperation); +``` + +### Cancel + +The cancel operation accept 3 parameters : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [optional] `$reason` : The reason of the cancelling (eq: 'Why we cancel the order') + +Example : +```php +cancel('ref1', 'amazon') + ->cancel('ref2', 'amazon', 'Why we accept it'); +$orderApi->execute($updateOperation); +``` + +### Refuse + +The refuse operation accept 3 parameters : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [optional] `$refund` : Item references to refund (eq: `['itemref1', 'itemref2']`) + +Example : +```php +refuse('ref1', 'amazon') + ->refuse('ref2', 'amazon', ['itemref1', 'itemref2']); +$orderApi->execute($updateOperation); +``` + +### Ship + +The ship operation accept 3 parameters : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [optional] `$carrier` : The carrier name used for the shipment (eq: 'ups') +3. [optional] `$trackingNumber` : Tracking number (eq: '01234598abcdef') +3. [optional] `$trackingLink` : Tracking link (eq: 'http://tracking.url/') + +Example : +```php +ship('ref1', 'amazon') + ->ship('ref2', 'amazon', 'ups', '123456789abcdefg', 'http://tracking.url/'); +$orderApi->execute($updateOperation); +``` +### Acknowledge + +To acknowledge the good reception of order : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [mandatory] `$status` : Status of acknowledgment (eq: 'success') +4. [mandatory] `$storeReference` : Store reference (eq: 'store-reference') +5. [optional] `$message` : Acknowledge message (eq: 'Order well acknowledge') + +Example : +```php +acknowledge('reference1', 'amazon', 'success', 'store-reference') + ->acknowledge('reference1', 'amazon', 'error', 'store-reference') + ->acknowledge('reference1', 'amazon', 'error', 'store-reference', 'Order well acknowledged'); +$orderApi->execute($updateOperation); +``` + +### Unacknowledge + +To unacknowledge the good reception of order : +1. [mandatory] `$reference` : Order reference (eg: 'reference1') +2. [mandatory] `$channelName` : The channel where the order is from (eg: 'amazon') +3. [mandatory] `$status` : Status of unacknowledgment (eq: 'success') +4. [mandatory] `$storeReference` : Store reference (eq: 'store-reference') +5. [optional] `$message` : Unacknowledge message (eq: 'Order well unacknowledge') + +Example : +```php +unacknowledge('reference1', 'amazon', 'success', 'store-reference') + ->unacknowledge('reference1', 'amazon', 'error', 'store-reference') + ->unacknowledge('reference1', 'amazon', 'error', 'store-reference', 'Order well unacknowledged'); +$orderApi->execute($updateOperation); +``` diff --git a/phpunit.xml b/phpunit.xml index da50b50..0cdb6c8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,4 +10,4 @@ src - \ No newline at end of file + diff --git a/src/Api/Order/OrderCollection.php b/src/Api/Order/OrderCollection.php new file mode 100644 index 0000000..480d92b --- /dev/null +++ b/src/Api/Order/OrderCollection.php @@ -0,0 +1,9 @@ +execute($this->link); + } +} diff --git a/src/Api/Order/OrderOperation.php b/src/Api/Order/OrderOperation.php new file mode 100644 index 0000000..edb0c68 --- /dev/null +++ b/src/Api/Order/OrderOperation.php @@ -0,0 +1,291 @@ +addOperation( + $reference, + $channelName, + OrderOperation::TYPE_ACCEPT, + compact('reason') + ); + + return $this; + } + + /** + * Notify market place of order cancellation + * + * @param string $reference Order reference + * @param string $channelName Channel to notify + * @param string $reason Optional reason of cancellation + * + * @return OrderOperation + * + * @throws Order\Exception\UnexpectedTypeException + */ + public function cancel($reference, $channelName, $reason = '') + { + $this->addOperation( + $reference, + $channelName, + OrderOperation::TYPE_CANCEL, + compact('reason') + ); + + return $this; + } + + /** + * Notify market place of order shipment sent + * + * @param string $reference Order reference + * @param string $channelName Channel to notify + * @param string $carrier Optional carrier name + * @param string $trackingNumber Optional tracking number + * @param string $trackingLink Optional tracking link + * + * @return OrderOperation + * + * @throws Order\Exception\UnexpectedTypeException + */ + public function ship($reference, $channelName, $carrier = '', $trackingNumber = '', $trackingLink = '') + { + $this->addOperation( + $reference, + $channelName, + OrderOperation::TYPE_SHIP, + compact('carrier', 'trackingNumber', 'trackingLink') + ); + + return $this; + } + + /** + * Notify market place of order refusal + * + * @param string $reference Order reference + * @param string $channelName Channel to notify + * @param array $refund Order item reference that will be refunded + * + * @return OrderOperation + * + * @throws Order\Exception\UnexpectedTypeException + */ + public function refuse($reference, $channelName, $refund = []) + { + $this->addOperation( + $reference, + $channelName, + OrderOperation::TYPE_REFUSE, + compact('refund') + ); + + return $this; + } + + /** + * Acknowledge order reception + * + * @param string $reference + * @param string $channelName + * @param string $status + * @param string $storeReference + * @param string $message + * + * @return OrderOperation + * + * @throws Order\Exception\UnexpectedTypeException + * @throws \Exception + */ + public function acknowledge($reference, $channelName, $status, $storeReference, $message = '') + { + $acknowledgedAt = new \DateTimeImmutable('now'); + $this->addOperation( + $reference, + $channelName, + OrderOperation::TYPE_ACKNOWLEDGE, + compact('status', 'storeReference', 'acknowledgedAt', 'message') + ); + + return $this; + } + + /** + * Unacknowledge order reception + * + * @param string $reference + * @param string $channelName + * @param string $status + * @param string $storeReference + * @param string $message + * + * @return OrderOperation + * + * @throws Order\Exception\UnexpectedTypeException + * @throws \Exception + */ + public function unacknowledge($reference, $channelName, $status, $storeReference, $message = '') + { + $acknowledgedAt = new \DateTimeImmutable('now'); + $this->addOperation( + $reference, + $channelName, + OrderOperation::TYPE_UNACKNOWLEDGE, + compact('status', 'storeReference', 'acknowledgedAt', 'message') + ); + + return $this; + } + + /** + * Execute all declared operations + * + * @param Hal\HalLink $link + * + * @return Api\Order\OrderTicketCollection + */ + public function execute(Hal\HalLink $link) + { + // Create requests per batch + $requests = []; + foreach ($this->allowedOperationTypes as $type) { + $this->eachBatch( + function (array $chunk) use ($type, $link, &$requests) { + $requests[] = $link->createRequest( + 'POST', + ['operation' => $type], + ['order' => $chunk] + ); + }, + $type + ); + } + + // Send requests + $ticketReferences = []; + $resources = []; + $requestIndex = 0; + $link->batchSend( + $requests, + function (Hal\HalResource $resource) use (&$resources, &$ticketReferences, &$requestIndex, $requests) { + $this->associateTicketWithReference( + $resource, + $requests[$requestIndex], + $ticketReferences + ); + + array_push($resources, $resource); + $requestIndex++; + }, + null, + [], + $this->getPoolSize() + ); + + return new Api\Order\OrderTicketCollection($resources, $ticketReferences); + } + + /** + * Extract association between order references, operation and ticket + * + * @param Hal\HalResource $resource + * @param Request $request + * @param $ticketReferences + */ + private function associateTicketWithReference( + Hal\HalResource $resource, + Request $request, + &$ticketReferences + ) + { + $ticketId = $resource->getProperty('id'); + $orders = json_decode($request->getBody())->order; + $uri = $request->getUri()->getPath(); + $operation = substr($uri, strrpos($uri, '/') + 1); + + // Extract reference > ticket association + foreach ($orders as $order) { + if (! isset($ticketReferences[$operation])) { + $ticketReferences[$operation][$ticketId] = []; + } + + if (! in_array($order->reference, $ticketReferences[$operation][$ticketId])) { + $ticketReferences[$operation][$ticketId][] = $order->reference; + } + } + } + + /** + * Add operation to queue + * + * @param string $reference Order reference + * @param string $channelName Channel to notify + * @param string $type Type of operation + * @param array $data Extra data to pass to operation call + * + * @throws Order\Exception\UnexpectedTypeException + */ + public function addOperation($reference, $channelName, $type, $data = []) + { + if (! in_array($type, $this->allowedOperationTypes)) { + throw new Order\Exception\UnexpectedTypeException(sprintf( + 'Only %s operations are accepted', + implode(', ', $this->allowedOperationTypes) + )); + } + + if (! isset($this->operations[$type])) { + $this->operations[$type] = []; + } + + $this->operations[$type][] = array_merge(compact('reference', 'channelName'), $data); + } +} diff --git a/src/Api/Order/OrderResource.php b/src/Api/Order/OrderResource.php new file mode 100644 index 0000000..3ba6d2b --- /dev/null +++ b/src/Api/Order/OrderResource.php @@ -0,0 +1,97 @@ +getProperty('id'); + } + + /** + * @return string + */ + public function getReference() + { + return (string) $this->getProperty('reference'); + } + + /** + * @return null|string + */ + public function getStoreReference() + { + return $this->getProperty('storeReference'); + } + + /** + * @return string + */ + public function getStatus() + { + return (string) $this->getProperty('status'); + } + + /** + * @return null|\DateTimeImmutable + */ + public function getAcknowledgedAt() + { + $dateValue = $this->getProperty('acknowledgedAt'); + return date_create_immutable(is_null($dateValue) ? 'now' : $dateValue); + } + + /** + * @return null|\DateTimeImmutable + */ + public function getUpdateddAt() + { + $dateValue = $this->getProperty('updatedAt'); + return date_create_immutable(is_null($dateValue) ? 'now' : $dateValue); + } + + /** + * @return \DateTimeImmutable + */ + public function getCreatedAt() + { + return date_create_immutable($this->getProperty('createdAt')); + } + + /** + * @return array + */ + public function getShippingAddress() + { + return $this->getProperty('shippingAddress'); + } + + /** + * @return array + */ + public function getBillingAddress() + { + return $this->getProperty('billingAddress'); + } + + /** + * @return array + */ + public function getPaymentInformation() + { + return $this->getProperty('payment'); + } + + /** + * @return array + */ + public function getShipment() + { + return $this->getProperty('shipment'); + } +} diff --git a/src/Api/Order/OrderTicketCollection.php b/src/Api/Order/OrderTicketCollection.php new file mode 100644 index 0000000..06ec40d --- /dev/null +++ b/src/Api/Order/OrderTicketCollection.php @@ -0,0 +1,196 @@ + ['order-ref']] + */ + private $ticketReferences = []; + + public function __construct( + array $resources = [], + array $ticketReferences = [] + ) + { + parent::__construct($resources); + + $this->ticketReferences = $ticketReferences; + } + + /** + * Get ticket for shiped reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getShipped($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_SHIP, + ] + ); + } + + /** + * Get ticket for accepted reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getAccepted($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_ACCEPT, + ] + ); + } + + /** + * Get ticket for accepted reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getRefused($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_REFUSE, + ] + ); + } + + /** + * Get ticket for accepted reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getCanceled($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_CANCEL, + ] + ); + } + + /** + * Get ticket for acknowledged reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getAcknowledge($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_ACKNOWLEDGE, + ] + ); + } + + /** + * Get ticket for unacknowledged reference + * + * @param $reference + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + public function getUnacknowledge($reference = null) + { + return $this->findTickets( + [ + 'reference' => $reference, + 'operation' => OrderOperation::TYPE_UNACKNOWLEDGE, + ] + ); + } + + /** + * Find tickets for an operation on an order + * + * @param array $criteria Criteria to find tickets ['reference' => 'xxx', 'operation" => 'xxx'] + * + * @return Task\TicketResource[] + * + * @throws Order\Exception\TicketNotFoundException + */ + protected function findTickets(array $criteria = []) + { + if (! isset($criteria['operation']) && ! isset($criteria['reference'])) { + return (array) $this->getIterator(); + } + + if (isset($criteria['operation']) && ! isset($this->ticketReferences[$criteria['operation']])) { + return []; + } + + if (isset($criteria['operation']) && ! isset($criteria['reference'])) { + return $this->getTicketsById( + array_keys($this->ticketReferences[$criteria['operation']]) + ); + } + + foreach ($this->ticketReferences[$criteria['operation']] as $ticketId => $orders) { + if (in_array($criteria['reference'], $orders)) { + return $this->getTicketsById([$ticketId]); + } + } + + throw Order\Exception\TicketNotFoundException::forOperationAndOrder( + $criteria['operation'], + $criteria['reference'] + ); + } + + /** + * Find ticket in collection by its ID + * + * @param array $ids + * + * @return Task\TicketResource[] + */ + private function getTicketsById(array $ids) + { + $tickets = []; + foreach ($this->getIterator() as $ticket) { + /** @var Task\TicketResource $ticket */ + if (in_array($ticket->getId(), $ids)) { + $tickets[] = $ticket; + } + } + + return $tickets; + } +} diff --git a/src/Api/Store/StoreResource.php b/src/Api/Store/StoreResource.php index 9530bf4..56c1e18 100644 --- a/src/Api/Store/StoreResource.php +++ b/src/Api/Store/StoreResource.php @@ -2,6 +2,7 @@ namespace ShoppingFeed\Sdk\Api\Store; use ShoppingFeed\Sdk\Api\Catalog\InventoryDomain; +use ShoppingFeed\Sdk\Api\Order\OrderDomain; use ShoppingFeed\Sdk\Resource\AbstractResource; class StoreResource extends AbstractResource @@ -47,4 +48,14 @@ public function getInventory() $this->resource->getLink('inventory') ); } + + /** + * @return OrderDomain + */ + public function getOrderApi() + { + return new OrderDomain( + $this->resource->getLink('order') + ); + } } diff --git a/src/Api/Task/TicketCollection.php b/src/Api/Task/TicketCollection.php new file mode 100644 index 0000000..8c26567 --- /dev/null +++ b/src/Api/Task/TicketCollection.php @@ -0,0 +1,9 @@ +getProperty('id'); + } +} diff --git a/src/Operation/AbstractBulkOperation.php b/src/Operation/AbstractBulkOperation.php index 0a57dab..18f1153 100644 --- a/src/Operation/AbstractBulkOperation.php +++ b/src/Operation/AbstractBulkOperation.php @@ -62,13 +62,53 @@ public function getPoolSize() return $this->poolSize; } + /** + * Count operations + * + * @param string $filter + * + * @return int + */ + public function count($filter = null) + { + return count($this->getOperations($filter)); + + } + /** * @param callable $callback + * @param string $filter Allow to filter operations */ - protected function eachBatch(callable $callback) + protected function eachBatch(callable $callback, $filter = null) { - foreach (array_chunk($this->operations, $this->batchSize) as $chunk) { + foreach (array_chunk($this->getOperations($filter), $this->batchSize) as $chunk) { $callback($chunk); } } + + /** + * Get operations + * If operations are grouped but no filter is asked return operations ungrouped + * + * @param string $filter If operation are grouped get only the group + * + * @return AbstractOperation[] + */ + protected function getOperations($filter = null) + { + if ($filter) { + return isset($this->operations[$filter]) ? $this->operations[$filter] : []; + } + + $operations = (array) $this->operations; + // If operations are grouped but no filter is asked we ungrouped operations + if (is_null($filter) && ! current($operations) instanceof AbstractOperation) { + $operations = []; + foreach ($this->operations as $group => $groupedOperations) { + $operations = array_merge($operations, $groupedOperations); + } + } + + return $operations; + } } diff --git a/src/Order/Exception/TicketNotFoundException.php b/src/Order/Exception/TicketNotFoundException.php new file mode 100644 index 0000000..f908786 --- /dev/null +++ b/src/Order/Exception/TicketNotFoundException.php @@ -0,0 +1,22 @@ +isPartial; } - - /** - * @param string $name - * - * @return Hal\HalLink|null - */ - protected function getLink($name) - { - return $this->resource->getLink($name); - } } diff --git a/tests/unit/Api/Order/OrderDomainTest.php b/tests/unit/Api/Order/OrderDomainTest.php new file mode 100644 index 0000000..8905292 --- /dev/null +++ b/tests/unit/Api/Order/OrderDomainTest.php @@ -0,0 +1,25 @@ +createMock(HalLink::class); + $operations = $this->createMock(OrderOperation::class); + $operations + ->expects($this->once()) + ->method('execute') + ->with($link) + ->willReturn($this->createMock(OrderTicketCollection::class)); + + $instance = new OrderDomain($link); + $instance->execute($operations); + } +} diff --git a/tests/unit/Api/Order/OrderOperationTest.php b/tests/unit/Api/Order/OrderOperationTest.php new file mode 100644 index 0000000..f032078 --- /dev/null +++ b/tests/unit/Api/Order/OrderOperationTest.php @@ -0,0 +1,358 @@ +operationCount; $i++) { + $orderOperation->addOperation( + 'ref' . $i, + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_ACCEPT + ); + } + } + + /** + * @throws \Exception + */ + public function testAddOperation() + { + $orderOperation = new Sdk\Api\Order\OrderOperation(); + $this->generateOperations($orderOperation); + + $this->assertEquals( + $this->operationCount, + $orderOperation->count(Sdk\Api\Order\OrderOperation::TYPE_ACCEPT) + ); + } + + /** + * @throws \Exception + */ + public function testAcceptOperation() + { + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_ACCEPT, + ['reason' => 'noreason'] + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->accept( + 'ref1', + 'amazon', + 'noreason' + ) + ); + } + + /** + * @throws \Exception + */ + public function testCancelOperation() + { + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_CANCEL, + ['reason' => 'noreason'] + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->cancel( + 'ref1', + 'amazon', + 'noreason' + ) + ); + } + + /** + * @throws \Exception + */ + public function testRefuseOperation() + { + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_REFUSE, + ['refund' => ['item1', 'item2']] + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->refuse( + 'ref1', + 'amazon', + ['item1', 'item2'] + ) + ); + } + + /** + * @throws \Exception + */ + public function testShipOperation() + { + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_SHIP, + [ + 'carrier' => 'ups', + 'trackingNumber' => '123654abc', + 'trackingLink' => 'http://tracking.lnk', + ] + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->ship( + 'ref1', + 'amazon', + 'ups', + '123654abc', + 'http://tracking.lnk' + ) + ); + } + + /** + * @throws \Exception + */ + public function testAcknowledgeOperation() + { + $data = [ + 'ref1', + 'amazon', + 'success', + '123654abc', + 'Acknowledged', + ]; + + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref1', + 'amazon', + Sdk\Api\Order\OrderOperation::TYPE_ACKNOWLEDGE, + new \PHPUnit_Framework_Constraint_Callback( + function ($param) use ($data) { + return $param['status'] === $data[2] + && $param['storeReference'] === $data[3] + && $param['message'] === $data[4] + && $param['acknowledgedAt'] instanceof \DateTimeImmutable; + } + ) + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->acknowledge(...$data) + ); + } + + /** + * @throws \Exception + */ + public function testUnacknowledgeOperation() + { + $data = [ + 'ref2', + 'amazon2', + 'success2', + '123654abcd', + 'Unacknowledged', + ]; + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['addOperation']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('addOperation') + ->with( + 'ref2', + 'amazon2', + Sdk\Api\Order\OrderOperation::TYPE_UNACKNOWLEDGE, + new \PHPUnit_Framework_Constraint_Callback( + function ($param) use ($data) { + return $param['status'] === $data[2] + && $param['storeReference'] === $data[3] + && $param['message'] === $data[4] + && $param['acknowledgedAt'] instanceof \DateTimeImmutable; + } + ) + ); + + $this->assertInstanceOf( + Sdk\Api\Order\OrderOperation::class, + $instance->unacknowledge(...$data) + ); + } + + /** + * @throws \Exception + */ + public function testAddWrongOperation() + { + $orderOperation = new Sdk\Api\Order\OrderOperation(); + + $this->expectException(Sdk\Order\Exception\UnexpectedTypeException::class); + + $orderOperation->addOperation( + 'ref', + 'amazon', + 'FakeType' + ); + } + + /** + * @throws \Exception + */ + public function testExecute() + { + $link = $this->createMock(Sdk\Hal\HalLink::class); + $link + ->expects($this->once()) + ->method('createRequest') + ->willReturn( + $this->createMock(RequestInterface::class) + ); + + /** @var Sdk\Api\Order\OrderOperation|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(Sdk\Api\Order\OrderOperation::class) + ->setMethods(['getPoolSize']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('getPoolSize') + ->willReturn(10); + + $this->generateOperations($instance); + + $link + ->expects($this->once()) + ->method('batchSend') + ->with( + [$this->createMock(RequestInterface::class)], + function (Sdk\Hal\HalResource $resource) use (&$resources) { + array_push($resources, ...$resource->getResources('order')); + }, + null, + [], + 10 + ); + + $this->assertInstanceOf( + Sdk\Api\Task\TicketCollection::class, + $instance->execute($link) + ); + } + + public function testAssociationBetweenRefandTicketId() + { + $expected = [ + 'accept' => [ + 'ticket123' => ["abc123", "abc456"], + ], + ]; + + $instance = new Sdk\Api\Order\OrderOperation(); + $reflection = new \ReflectionClass(get_class($instance)); + $method = $reflection->getMethod('associateTicketWithReference'); + $method->setAccessible(true); + + $uri = $this->createMock(Psr7\Uri::class); + /** @var Sdk\Hal\HalResource|\PHPUnit_Framework_MockObject_MockObject $resource */ + $resource = $this->createMock(Sdk\Hal\HalResource::class); + /** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */ + $request = $this->createMock(Psr7\Request::class); + $references = []; + + $resource + ->expects($this->once()) + ->method('getProperty') + ->willReturn('id') + ->willReturn('ticket123'); + + $request + ->expects($this->once()) + ->method('getBody') + ->willReturn('{"order":[{"reference": "abc123"}, {"reference": "abc456"}]}'); + + $uri + ->expects($this->once()) + ->method('getPath') + ->willReturn('/fake/accept'); + + $request + ->expects($this->once()) + ->method('getUri') + ->willReturn($uri); + + $method->invokeArgs($instance, [$resource, $request, &$references]); + + $this->assertEquals($expected, $references); + } +} diff --git a/tests/unit/Api/Order/OrderResourceTest.php b/tests/unit/Api/Order/OrderResourceTest.php new file mode 100644 index 0000000..9090973 --- /dev/null +++ b/tests/unit/Api/Order/OrderResourceTest.php @@ -0,0 +1,88 @@ +props = [ + 'id' => 10, + 'reference' => 'abc123', + 'storeReference' => 'def456', + 'status' => 'active', + 'createdAt' => '2017-12-05', + 'updatedAt' => '2017-12-06', + 'acknowledgedAt' => '2017-12-07', + 'payment' => [ + "carrier" => "Home", + "trackingNumber" => "94718832", + ], + 'shipment' => [ + "shippingAmount" => 58.8, + "productAmount" => 495.24, + "totalAmount" => 554.04, + "currency" => "EUR", + "method" => "", + ], + 'shippingAddress' => [ + "firstName" => "Bill", + "lastName" => "BOQUET", + "company" => "BD", + "street" => "10 RUE RUE DE BOULE", + "additionalDetails" => "", + "postalCode" => "75000", + "city" => "PARIS", + "country" => "FR", + "phone" => "061234579", + "email" => "biletboule@mail.com", + ], + 'billingAddress' => [ + "firstName" => "Bill", + "lastName" => "BOQUET", + "company" => "BD", + "street" => "10 RUE RUE DE BOULE", + "additionalDetails" => "", + "postalCode" => "75000", + "city" => "PARIS", + "country" => "FR", + "phone" => "061234579", + "email" => "biletboule@mail.com", + ], + ]; + } + + public function testPropertiesGetters() + { + $this->initPropertyGetterTester(); + + $instance = new Sdk\Api\Order\OrderResource($this->propertyGetter); + + $this->assertEquals($this->props['id'], $instance->getId()); + $this->assertEquals($this->props['reference'], $instance->getReference()); + $this->assertEquals($this->props['storeReference'], $instance->getStoreReference()); + $this->assertEquals($this->props['status'], $instance->getStatus()); + $this->assertEquals($this->props['payment'], $instance->getPaymentInformation()); + $this->assertEquals($this->props['shipment'], $instance->getShipment()); + $this->assertEquals($this->props['shippingAddress'], $instance->getShippingAddress()); + $this->assertEquals($this->props['billingAddress'], $instance->getBillingAddress()); + $this->assertEquals(date_create_immutable($this->props['createdAt']), $instance->getCreatedAt()); + $this->assertEquals(date_create_immutable($this->props['updatedAt']), $instance->getUpdateddAt()); + $this->assertEquals(date_create_immutable($this->props['acknowledgedAt']), $instance->getAcknowledgedAt()); + } + + public function testNullDates() + { + $this->props = [ + 'updatedAt' => null, + 'acknowledgedAt' => null, + ]; + $this->initPropertyGetterTester(); + + $instance = new Sdk\Api\Order\OrderResource($this->propertyGetter); + + $this->assertInstanceOf(\DateTimeImmutable::class, $instance->getUpdateddAt()); + $this->assertInstanceOf(\DateTimeImmutable::class, $instance->getAcknowledgedAt()); + } +} diff --git a/tests/unit/Api/Order/OrderTicketCollectionMock.php b/tests/unit/Api/Order/OrderTicketCollectionMock.php new file mode 100644 index 0000000..d3548e8 --- /dev/null +++ b/tests/unit/Api/Order/OrderTicketCollectionMock.php @@ -0,0 +1,20 @@ + [ + 'ticketId123' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId456' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + OrderOperation::TYPE_CANCEL => [ + 'ticketId789' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId321' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + OrderOperation::TYPE_SHIP => [ + 'ticketId654' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId987' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + OrderOperation::TYPE_REFUSE => [ + 'ticketId159' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId753' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + OrderOperation::TYPE_ACKNOWLEDGE => [ + 'ticketId147' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId258' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + OrderOperation::TYPE_ACKNOWLEDGE => [ + 'ticketId369' => ['orderRef1', 'orderRef2', 'orderRef3'], + 'ticketId486' => ['orderRef4', 'orderRef5', 'orderRef6'], + ], + ]; + + public function testFindTicket() + { + $this->generateTickets(); + $instance = new OrderTicketCollectionMock($this->tickets, $this->data); + $tickets = $instance->findTickets(['reference' => 'orderRef5', 'operation' => OrderOperation::TYPE_SHIP]); + + $this->assertInstanceOf(TicketResource::class, $tickets[0]); + $this->assertEquals('ticketId987', $tickets[0]->getId()); + } + + public function testFindTicketWithNoOperationNoRef() + { + $this->generateTickets(); + $instance = new OrderTicketCollectionMock($this->tickets, $this->data); + $tickets = $instance->findTickets(); + + $this->assertCount(count($this->tickets), $tickets); + } + + public function testFindTicketForOperation() + { + $this->generateTickets(); + $instance = new OrderTicketCollectionMock($this->tickets, $this->data); + $tickets = $instance->findTickets(['operation' => OrderOperation::TYPE_SHIP]); + + $this->assertCount(count($this->data[OrderOperation::TYPE_SHIP]), $tickets); + } + + public function testGetShippedTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef5', 'operation' => OrderOperation::TYPE_SHIP]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getShipped('orderRef5'); + } + + public function testGetCanceledTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef4', 'operation' => OrderOperation::TYPE_CANCEL]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getCanceled('orderRef4'); + } + + public function testGetRefusedTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef1', 'operation' => OrderOperation::TYPE_REFUSE]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getRefused('orderRef1'); + } + + public function testGetAcceptedTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef3', 'operation' => OrderOperation::TYPE_ACCEPT]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getAccepted('orderRef3'); + } + + public function testGetAcknowledgeTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef6', 'operation' => OrderOperation::TYPE_ACKNOWLEDGE]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getAcknowledge('orderRef6'); + } + + public function testGetUnacknowledgeTicket() + { + /** @var OrderTicketCollection|\PHPUnit_Framework_MockObject_MockObject $instance */ + $instance = $this + ->getMockBuilder(OrderTicketCollection::class) + ->setMethods(['findTickets']) + ->getMock(); + + $instance + ->expects($this->once()) + ->method('findTickets') + ->with(['reference' => 'orderRef7', 'operation' => OrderOperation::TYPE_UNACKNOWLEDGE]) + ->willReturn( + $this->createMock(TicketResource::class) + ); + + $instance->getUnacknowledge('orderRef7'); + } + + public function testFindTicketWrongOperation() + { + $this->generateTickets(); + $instance = new OrderTicketCollection($this->tickets); + + $this->assertEquals([], $instance->getShipped('orderRef5')); + } + + public function testFindTicketWrongReference() + { + $this->generateWrongTickets(); + $instance = new OrderTicketCollection($this->tickets, $this->data); + + $this->expectException(TicketNotFoundException::class); + + $instance->getShipped('orderRef22'); + } + + /** + * Generate tickets based on $this->data + */ + private function generateTickets() + { + foreach ($this->data as $operation => $tickets) { + foreach ($tickets as $ticketId => $orders) { + $ticket = $this->createMock(TicketResource::class); + $ticket + ->method('getId') + ->willReturn($ticketId); + + $this->tickets[] = $ticket; + } + } + } + + /** + * Generate wrong tickets + */ + private function generateWrongTickets() + { + foreach ($this->data as $operation => $tickets) { + foreach ($tickets as $ticketId => $orders) { + $ticket = $this->createMock(TicketResource::class); + $ticket + ->method('getId') + ->willReturn($ticketId . '22'); + + $this->tickets[] = $ticket; + } + } + } +} diff --git a/tests/unit/Api/Store/StoreResourceTest.php b/tests/unit/Api/Store/StoreResourceTest.php index 58484a2..41a762d 100644 --- a/tests/unit/Api/Store/StoreResourceTest.php +++ b/tests/unit/Api/Store/StoreResourceTest.php @@ -27,8 +27,9 @@ public function testPropertiesGetters() $this->assertTrue($instance->isActive()); } - public function testGetInventory() + public function testGetInventoryApi() { + /** @var Sdk\Hal\HalResource|\PHPUnit_Framework_MockObject_MockObject $halResource */ $halResource = $this->createMock(Sdk\Hal\HalResource::class); $halResource ->expects($this->once()) @@ -42,4 +43,21 @@ public function testGetInventory() $this->assertInstanceOf(Sdk\Api\Catalog\InventoryDomain::class, $instance->getInventory()); } + + public function testGetOrderApi() + { + /** @var Sdk\Hal\HalResource|\PHPUnit_Framework_MockObject_MockObject $halResource */ + $halResource = $this->createMock(Sdk\Hal\HalResource::class); + $halResource + ->expects($this->once()) + ->method('getLink') + ->with('order') + ->willReturn( + $this->createMock(Sdk\Hal\HalLink::class) + ); + + $instance = new Sdk\Api\Store\StoreResource($halResource); + + $this->assertInstanceOf(Sdk\Api\Order\OrderDomain::class, $instance->getOrderApi()); + } } diff --git a/tests/unit/Api/Task/TicketResourceTest.php b/tests/unit/Api/Task/TicketResourceTest.php new file mode 100644 index 0000000..89f66d0 --- /dev/null +++ b/tests/unit/Api/Task/TicketResourceTest.php @@ -0,0 +1,24 @@ +props = [ + 'id' => '123abc', + ]; + } + + public function testGetproperty() + { + $this->initPropertyGetterTester(); + + $instance = new TicketResource($this->propertyGetter); + + $this->assertEquals($this->props['id'], $instance->getId()); + } +} diff --git a/tests/unit/Operation/AbstractBulkOperationTest.php b/tests/unit/Operation/AbstractBulkOperationTest.php index a97a952..c049964 100644 --- a/tests/unit/Operation/AbstractBulkOperationTest.php +++ b/tests/unit/Operation/AbstractBulkOperationTest.php @@ -3,6 +3,7 @@ use PHPUnit\Framework\TestCase; use ShoppingFeed\Sdk\Operation\AbstractBulkOperation; +use ShoppingFeed\Sdk\Operation\AbstractOperation; class AbstractBulkOperationTest extends TestCase { @@ -22,7 +23,7 @@ public function testGetterSetters() public function testEachBatchCallback() { $operations = []; - for ($i = 0; $i <= 50; $i++) { + for ($i = 0; $i < 50; $i++) { $operations[] = 'operation' . $i; }; $instance = new BulkOperationMock($operations); @@ -42,4 +43,66 @@ function ($chunck) use ($tester, $opeCount, &$count) { } ); } + + /** + * Test that the batch creation group only handle the requested group + */ + public function testEachBatchGroupedBy() + { + // Create 2 groups of operations : pair > group0 and odd > group1 + $operations = []; + for ($i = 0; $i < 100; $i++) { + $operations['group' . ($i % 2 ? '1' : '0')][] = 'operation' . $i; + }; + + $instance = new BulkOperationMock($operations); + $instance->setBatchSize(10); + + $count = 0; + $instance->eachBatch( + function ($chunk) use (&$count) { + $count += count($chunk); + }, + 'group1' + ); + + // Assert that the sum of all chuncks = number of operation in group1 + $this->assertEquals(count($operations['group1']), $count); + } + + public function testCountOperation() + { + $countOperation = 50; + $operations = []; + for ($i = 0; $i < $countOperation; $i++) { + $operations[] = $this->createMock(AbstractOperation::class); + }; + $instance = new BulkOperationMock($operations); + + $this->assertEquals($countOperation, $instance->count()); + } + + public function testCountOperationWithFilter() + { + $countOperation = 50; + $operations = []; + for ($i = 0; $i < $countOperation; $i++) { + $operations[$i % 2 ? 'group1' : 'group2'][] = $this->createMock(AbstractOperation::class); + }; + $instance = new BulkOperationMock($operations); + + $this->assertEquals(count($operations['group1']), $instance->count('group1')); + } + + public function testCountGroupedOperationWithNoFilter() + { + $countOperation = 50; + $operations = []; + for ($i = 0; $i < $countOperation; $i++) { + $operations[$i % 2 ? 'group1' : 'group2'][] = $this->createMock(AbstractOperation::class); + }; + $instance = new BulkOperationMock($operations); + + $this->assertEquals(50, $instance->count()); + } } diff --git a/tests/unit/Operation/BulkOperationMock.php b/tests/unit/Operation/BulkOperationMock.php index a324905..f97c8f6 100644 --- a/tests/unit/Operation/BulkOperationMock.php +++ b/tests/unit/Operation/BulkOperationMock.php @@ -16,9 +16,9 @@ public function __construct($operations = []) $this->operations = $operations; } - public function eachBatch(callable $callback) + public function eachBatch(callable $callback, $filter = '') { - parent::eachBatch($callback); + parent::eachBatch($callback, $filter); } public function execute(Hal\HalLink $link) diff --git a/tests/unit/Resource/AbstractDomainResourceTest.php b/tests/unit/Resource/AbstractDomainResourceTest.php index 0301e70..b579d2a 100644 --- a/tests/unit/Resource/AbstractDomainResourceTest.php +++ b/tests/unit/Resource/AbstractDomainResourceTest.php @@ -130,6 +130,7 @@ private function getPaginatedResource(&$i = 0, $pageFrom = 1, $perPage = 10, $to ->disableOriginalConstructor() ->setMethods(['getLink']) ->getMock(); + $link = $this ->getMockBuilder(HalLink::class) ->disableOriginalConstructor() diff --git a/tests/unit/Resource/AbstractResourceTest.php b/tests/unit/Resource/AbstractResourceTest.php index 3eb32ba..49fc73f 100644 --- a/tests/unit/Resource/AbstractResourceTest.php +++ b/tests/unit/Resource/AbstractResourceTest.php @@ -166,17 +166,4 @@ public function testInitialize() $instance->initialize(true); $this->assertFalse($instance->isPartial()); } - - public function testGetLink() - { - $this - ->halResource - ->expects($this->once()) - ->method('getLink') - ->with('name'); - - $instance = new ResourceMock($this->halResource); - - $instance->getLink('name'); - } } diff --git a/tests/unit/Resource/ResourceMock.php b/tests/unit/Resource/ResourceMock.php index e29b57a..744026d 100644 --- a/tests/unit/Resource/ResourceMock.php +++ b/tests/unit/Resource/ResourceMock.php @@ -34,9 +34,4 @@ public function isPartial() { return parent::isPartial(); } - - public function getLink($name) - { - return parent::getLink($name); - } }