commit 9e8b75234a444e4efa992065890380e585710f8e
Author: Jonathan Sacksick <jonathan.sacksick@gmail.com>
Date:   Sun Jan 8 19:05:08 2017 +0200

    Issue #2841809: Complete the workflow implementation.

diff --git a/commerce_shipping.services.yml b/commerce_shipping.services.yml
index b2cc8fb..2f00ec9 100644
--- a/commerce_shipping.services.yml
+++ b/commerce_shipping.services.yml
@@ -1,14 +1,19 @@
 services:
+  commerce_shipping.order_guard:
+    class: Drupal\commerce_shipping\Guard\OrderGuard
+    tags:
+      - { name: state_machine.guard, group: commerce_order }
+
   commerce_shipping.referenceable_plugin_types_subscriber:
     class: Drupal\commerce_shipping\EventSubscriber\ReferenceablePluginTypesSubscriber
     tags:
       - { name: event_subscriber }
 
-    commerce_shipping.order_subscriber:
-      class: Drupal\commerce_shipping\EventSubscriber\OrderSubscriber
-      arguments: ['@entity_type.manager', '@entity.query']
-      tags:
-        - { name: event_subscriber }
+  commerce_shipping.order_subscriber:
+    class: Drupal\commerce_shipping\EventSubscriber\OrderSubscriber
+    arguments: ['@entity_type.manager', '@entity.query']
+    tags:
+      - { name: event_subscriber }
 
   plugin.manager.commerce_shipping_method:
     class: Drupal\commerce_shipping\ShippingMethodManager
diff --git a/src/EventSubscriber/OrderSubscriber.php b/src/EventSubscriber/OrderSubscriber.php
index 1d5df0f..d51be19 100644
--- a/src/EventSubscriber/OrderSubscriber.php
+++ b/src/EventSubscriber/OrderSubscriber.php
@@ -40,7 +40,10 @@ class OrderSubscriber implements EventSubscriberInterface {
    * {@inheritdoc}
    */
   public static function getSubscribedEvents() {
-    $events = ['commerce_order.cancel.post_transition' => ['onCancel', -100]];
+    $events = [
+      'commerce_order.cancel.post_transition' => ['onCancel', -100],
+      'commerce_order.place.post_transition' => ['onPlaceTransition', -100],
+    ];
     return $events;
   }
 
@@ -58,7 +61,33 @@ class OrderSubscriber implements EventSubscriberInterface {
     if (!empty($result)) {
       $shipments = $this->shipmentStorage->loadMultiple($result);
       foreach ($shipments as $shipment) {
-        $shipment->state = 'canceled';
+        $transition = $shipment->getState()->getWorkflow()->getTransition('cancel');
+        $shipment->getState()->applyTransition($transition);
+        $shipment->save();
+      }
+    }
+  }
+
+  /**
+   * Finalize the order's shipments when the order itself is placed.
+   *
+   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
+   *   The transition event.
+   */
+  public function onPlaceTransition(WorkflowTransitionEvent $event) {
+    if ($event->getToState()->getId() != 'fulfillment') {
+      return;
+    }
+    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+    $order = $event->getEntity();
+    $query = $this->entityQuery->condition('order_id', $order->id());
+    $result = $query->execute();
+    if (!empty($result)) {
+      $shipments = $this->shipmentStorage->loadMultiple($result);
+
+      foreach ($shipments as $shipment) {
+        $transition = $shipment->getState()->getWorkflow()->getTransition('finalize');
+        $shipment->getState()->applyTransition($transition);
         $shipment->save();
       }
     }
diff --git a/src/Guard/OrderGuard.php b/src/Guard/OrderGuard.php
new file mode 100644
index 0000000..d158bf4
--- /dev/null
+++ b/src/Guard/OrderGuard.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\commerce_shipping\Guard;
+
+use Drupal\state_machine\Guard\GuardInterface;
+use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
+use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides workflow guards for orders.
+ */
+class OrderGuard implements GuardInterface {
+
+  /**
+   * @inheritdoc
+   */
+  public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
+    if (!in_array($workflow->getId(), ['order_fulfillment', 'order_fulfillment_validation']) || !in_array($transition->getId(), ['validate', 'fulfill'])) {
+      return;
+    }
+    $query = \Drupal::service('entity.query')->get('commerce_shipment')
+      ->condition('order_id', $entity->id());
+
+    // Do not allow an order to be validated if its shipments are not ready.
+    if ($transition->getId() == 'validate') {
+      $query->condition('state', 'ready', '!=');
+    }
+    // Do not allow an order to be fulfilled if its shipments are not shipped.
+    elseif ($transition->getId() == 'fulfill') {
+      $query->condition('state', 'shipped', '!=');
+    }
+
+    // Transition will be allowed if no shipments were found.
+    return empty($query->execute());
+  }
+
+}
diff --git a/src/Plugin/Field/FieldType/ShipmentItemList.php b/src/Plugin/Field/FieldType/ShipmentItemList.php
index 592d5ce..52d7e05 100644
--- a/src/Plugin/Field/FieldType/ShipmentItemList.php
+++ b/src/Plugin/Field/FieldType/ShipmentItemList.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\commerce_shipping\Plugin\Field\FieldType;
 
-use Drupal\commerce_shipping\ShipmentItem;
+use Drupal\commerce_shipping\ShipmentItem as ShipmentItemValue;
 use Drupal\Core\Field\FieldItemList;
 
 /**
@@ -26,7 +26,7 @@ class ShipmentItemList extends FieldItemList implements ShipmentItemListInterfac
   /**
    * {@inheritdoc}
    */
-  public function removeShipmentItem(ShipmentItem $shipment_item) {
+  public function removeShipmentItem(ShipmentItemValue $shipment_item) {
     /** @var \Drupal\commerce_shipping\Plugin\Field\FieldType\ShipmentItem $field_item */
     foreach ($this->list as $key => $field_item) {
       if ($field_item->value === $shipment_item) {
diff --git a/src/Plugin/Field/FieldType/ShipmentItemListInterface.php b/src/Plugin/Field/FieldType/ShipmentItemListInterface.php
index c3f5cea..e117ea0 100644
--- a/src/Plugin/Field/FieldType/ShipmentItemListInterface.php
+++ b/src/Plugin/Field/FieldType/ShipmentItemListInterface.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\commerce_shipping\Plugin\Field\FieldType;
 
-use Drupal\commerce_shipping\ShipmentItem;
+use Drupal\commerce_shipping\ShipmentItem as ShipmentItemValue;
 use Drupal\Core\Field\FieldItemListInterface;
 
 /**
@@ -26,6 +26,6 @@ interface ShipmentItemListInterface extends FieldItemListInterface {
    *
    * @return $this
    */
-  public function removeShipmentItem(ShipmentItem $shipment_item);
+  public function removeShipmentItem(ShipmentItemValue $shipment_item);
 
 }
diff --git a/tests/src/Kernel/OrderWorkflowTest.php b/tests/src/Kernel/OrderWorkflowTest.php
new file mode 100644
index 0000000..4b1f774
--- /dev/null
+++ b/tests/src/Kernel/OrderWorkflowTest.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\Tests\commerce_shipping\Kernel;
+
+use Drupal\commerce_order\Entity\Order;
+use Drupal\commerce_order\Entity\OrderType;
+use Drupal\commerce_shipping\Entity\Shipment;
+use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
+
+/**
+ * Tests the order workflow.
+ *
+ * @group commerce_shipping
+ */
+class OrderWorkflowTest extends CommerceKernelTestBase {
+
+  /**
+   * A sample order.
+   *
+   * @var \Drupal\commerce_order\Entity\OrderInterface
+   */
+  protected $order;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity_reference_revisions',
+    'physical',
+    'profile',
+    'state_machine',
+    'commerce_order',
+    'commerce_shipping'
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('profile');
+    $this->installEntitySchema('commerce_order');
+    $this->installEntitySchema('commerce_shipment');
+    $this->installConfig([
+     'profile',
+     'commerce_order',
+     'commerce_shipping',
+    ]);
+
+    $user = $this->createUser(['mail' => $this->randomString() . '@example.com']);
+    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+    $order = Order::create([
+      'type' => 'default',
+      'state' => 'draft',
+      'mail' => $user->getEmail(),
+      'uid' => $user->id(),
+      'store_id' => $this->store->id(),
+    ]);
+    $order->save();
+    $this->order = $this->reloadEntity($order);
+  }
+
+  /**
+   * Tests the order cancellation.
+   */
+  public function testOrderCancellation() {
+    $shipment = Shipment::create([
+      'state' => 'draft',
+      'order_id' => $this->order->id(),
+    ]);
+    $shipment->save();
+
+    $transitions = $this->order->getState()->getTransitions();
+    $this->order->getState()->applyTransition($transitions['cancel']);
+    $this->order->save();
+
+    $shipment = $this->reloadEntity($shipment);
+    $this->assertEquals('canceled', $shipment->getState()->value, 'The shipment has been correctly canceled.');
+  }
+
+  /**
+   * Tests the order validation.
+   */
+  public function testOrderValidation() {
+    $order_type = OrderType::load($this->order->bundle());
+    $order_type->setWorkflowId('order_fulfillment_validation');
+    $order_type->save();
+
+    $shipment = Shipment::create([
+      'state' => 'draft',
+      'order_id' => $this->order->id(),
+    ]);
+    $shipment->save();
+
+    $transitions = $this->order->getState()->getTransitions();
+    $this->order->getState()->applyTransition($transitions['place']);
+    $this->order->save();
+
+    $this->assertArrayNotHasKey('validate', $this->order->getState()->getWorkflow()->getAllowedTransitions($this->order->getState()->value, $this->order), 'Order cannot be validated until shipments are ready.');
+
+    $transition = $shipment->getState()->getWorkflow()->getTransition('finalize');
+    $shipment->getState()->applyTransition($transition);
+    $shipment->save();
+
+    $this->assertArrayHasKey('validate', $this->order->getState()->getWorkflow()->getAllowedTransitions($this->order->getState()->value, $this->order), 'Order can be validated');
+  }
+
+  /**
+   * Tests the order fulfillment.
+   */
+  public function testOrderFulfillment() {
+    $order_type = OrderType::load($this->order->bundle());
+    $order_type->setWorkflowId('order_fulfillment');
+    $order_type->save();
+
+    $shipment = Shipment::create([
+      'state' => 'draft',
+      'order_id' => $this->order->id(),
+    ]);
+    $shipment->save();
+
+    $transitions = $this->order->getState()->getTransitions();
+    $this->order->getState()->applyTransition($transitions['place']);
+    $this->order->save();
+
+    $shipment = $this->reloadEntity($shipment);
+    $this->assertEquals('ready', $shipment->getState()->value, 'The shipment has been correctly finalized.');
+
+    $this->assertArrayNotHasKey('fulfill', $this->order->getState()->getWorkflow()->getAllowedTransitions($this->order->getState()->value, $this->order), 'Order cannot be fulfilled until shipments have been shipped.');
+
+    $transition = $shipment->getState()->getWorkflow()->getTransition('ship');
+    $shipment->getState()->applyTransition($transition);
+    $shipment->save();
+
+    $this->assertArrayHasKey('fulfill', $this->order->getState()->getWorkflow()->getAllowedTransitions($this->order->getState()->value, $this->order), 'Order can be fulfilled.');
+  }
+
+}
