Implement split shipping UI controls

It is standard e-commerce practice that shipping selections are implemented as part of the checkout flow.

Commerce already implements single shipping this way, so for the purposes of continuity, it is recommended that split shipping is also implemented in checkout. A non-standard implementation (such as on the cart page), although possible, would require more custom coding and may have additional side effects that require mitigation.

Split shipping toggle

A split shipping toggle button allows users to activate or deactivate split shipping for the current order. The button toggles the state of the isSplitShipping property. Also, it may be necessary to toggle the visibility of split shipping/single shipping UI elements.

The following sequence diagrams shows how you might choose to activate and deactivate the split shipping toggle on your storefront.

Activate

split shipping toggle

In this diagram, the following happens:

  • The shopper clicks a Use Split Shipping button on the UI, which calls the Checkout Address Book widget’s toggleSplitShipping() method.
  • The toggleSplitShipping() method sets the CartViewModel.isSplitShipping property to true.
  • The Checkout Address Book widget shows the Ship To Multiple Addresses UI and hides the Ship To Single Address UI.

Deactivate

deactivate split shipping

In this diagram, the following happens:

  • The shopper clicks the Use Single Shipping button on the UI, which calls the Checkout Address Book widget’s toggleSplitShipping() method.
  • The toggleSplitShipping() method sets the CartViewModel.isSplitShipping property to false.
  • The Checkout Address Book widget shows the Ship To Single Address UI and hides the Ship To Multiple Addresses UI.

Split shipping web form

A split shipping web form allows users to populate shipping options for each cart item. The shippingGroupRelationships property (which is an observable array) captures the split shipping options for each cart item. Each instance of a ShippingGroupRelationships object associates a quantity of cart item with a given shipping address and shipping method. A single cart item can have several ShippingGroupRelationships instances, allowing the cart item to be split across several shipping groups.

split shipping web form

Quantity field

The Quantity field allows the shopper to specify the portion of cart item to be associated with a given shipping group. The following code shows a sample UI binding pattern for this feature:

<!-- ko foreach: cart().items -->
  <!-- ko foreach: shippingGroupRelationships -->
    <input type="number" name="quantity" class="form-control" data-bind="
      value: quantity,
      event: {change: $parents[1].priceSplitShippingCartForCheckout}">
  <!-- /ko -->
<!-- /ko -->

Note: A change of Quantity will cause the widget to make a pricing call, provided the split shipping form is complete and valid.

Shipping Address field

The Shipping Address field allows the shopper to select a shipping address for the specified quantity of the cart item. The following code shows a sample UI binding pattern for this feature:

<!-- ko foreach: cart().items -->
  <!-- ko foreach: shippingGroupRelationships -->
    <select
      class="form-control"
      name="shippingAddress"
      data-bind="options: $parents[1].user().shippingAddressBook(),
                 optionsText: $parents[1].getOptionTextForAddress,
                 value: shippingAddress,
                 optionsCaption: 'Select shipping address',
                 event: {change: $parents[1].lookupShippingOptions}">
    </select>
  <!-- /ko -->
<!-- /ko -->

Shipping Address options should be retrieved from the profile’s shipping address book so that updates to the shipping address book will automatically be reflected in the options list. A change of Shipping Address must trigger a method in your widget that makes an AJAX service call to retrieve the valid shipping options for the selected address and product. The product ID must be passed in this call because some products may have a shipping surcharge and not all shipping methods can be used for products with surcharges.

Shipping Method field

The Shipping Method field allows the shopper to select a shipping method for the specified quantity of the cart item. The following code shows a sample UI binding pattern for this feature:

<!-- ko foreach: cart().items -->
  <!-- ko foreach: shippingGroupRelationships -->
    <select
      class="form-control"
       name="shippingMethod"
       data-bind="options: shippingOptions,
                  optionsText: 'displayName',
                  value: shippingMethod,
                  optionsCaption: 'Select shipping method',
                  enable: shippingAddress,
                  event: {change: $parents[1].priceSplitShippingCartForCheckout}">
    </select>
  <!-- /ko -->
<!-- /ko -->

The Shipping Method options displayed to the shopper must be populated by a method in your widget that makes an AJAX service call to retrieve the valid shipping options for the selected address. A change of Shipping Address should trigger this method.

A change of Shipping Method should cause the widget to make a pricing call, provided the split shipping form is complete and valid.

Split Items button

The Split Items button creates another shipping group relationship instance for this cart item, allowing the same cart item to be associated with more than one shipping group. The following code shows a sample UI binding pattern for this feature:

<!-- ko foreach: cart().items -->
  <!-- ko if: $parent.canAddShippingGroupRelationship($parent) -->
    <button class="btn btn-link" data-bind="click: addShippingGroupRelationship">
      Split items
    </button>
  <!-- /ko -->
<!-- /ko -->

It is only possible to split a cart item if the cart item quantity is greater than shippingGroupRelartionships.length.

Remove Item (X) button

The Remove Item button, shown as an X in the sample UI displayed in this section, removes a shipping group relationship instance. The following code shows a sample UI binding pattern for this feature:

<!-- ko foreach: cart().items -->
  <!-- ko foreach: shippingGroupRelationships -->
    <button class="btn btn-sm btn-link" data-bind="click:
       parents[1].removeShippingGroupRelationship.bind($parent)">
      <span class="glyphicon glyphicon-remove"></span>
    </button>
  <!-- /ko -->
<!-- /ko -->

The removeShippingGroupRelationship method, used in the click binding above, is a widget method and not the CartItem.removeShippingGroupRelationship method. It does, however, delegate to CartItem.removeShippingGroupRelationship, and also makes a pricing call, provided the split shipping form is complete and valid.

Add Address button

The Add Address button opens an address form where a shopper can save a new address to his profile address book. Once created, the new address will automatically appear in the Shipping Address options in the split shipping form. Apart from the inclusion of an alias field, no new address management APIs are required for split shipping. Re-using existing address management functionality is wholly sufficient to for this purpose.

The following illustration shows what an Add Address form might look like with fields for name, address, and phone number information. Note the addition of the Alias field.

add address

ShippingGroupsRelationships array validation

The shippingGroupRelationships property has two predefined custom Knockout validators:

  • Quantity of item allocated to shipping groups exceeds quantity of item in cart: Checks that the sum of the shipping group quantities is not greater than the cart item quantity.
  • Cart item quantity not fully allocated to shipping groups: Checks that the sum of the shipping group quantities is not less than the cart item quantity.

The above validators are computed automatically. The illustration below shows an error message that indicates to the shopper when a validation has failed.

validation

To output the error message on screen, use the validationMessage binding shown below:

<!-- ko foreach: cart().items -->
          <div class="text-danger" data-bind="validationMessage:
           shippingGroupRelationships" role="alert"></div> <!-- /ko -->

Price order

The pricing method CartItem.priceCartForCheckout handles both single and split shipping pricing. There is no change to the API for the split shipping.

As you create your widgets, you should consider when pricing is called. For example, you should call pricing when:

  • The Quantity field changes.
  • The Shipping Method field changes.
  • A shipping group relationship is removed (clicking the X button in the sample UI shown in this section).

The priceCartForCheckout method uses the isSplitShipping property to determine which pricing request to make. The priceCartForCheckout method will only make a pricing request if the split shipping form is complete and valid.

Order summary

Your storefront may need to show a pricing breakdown by shipping group in an Order Summary section, as shown in the following example which displays the shipping group name, subtotal before tax and shipping, shipping costs, sales tax, and total cost for each group.

order summary

The following binding pattern outputs the price info per shipping group in the widget shown above.

<!-- ko if: cart().isSplitShipping() -->
          <!-- ko foreach: cart().orderShippingGroups -->
            <!-- ko if: $data.hasOwnProperty("priceInfo") -->
              <div class="well well-sm small">         <strong>
                  Shipping Group
                  <span data-bind="text: ($index() + 1)"></span>
                  (<span data-bind="text: shippingAddress.alias"></span> -
                   <span data-bind="text:
                       shippingMethod.shippingMethodDescription"></span>)
                </strong>         <div class="row">
                  <div class="col-xs-7">Subtoal</div>
                  <div class="col-xs-5 text-right">
                    <span data-bind="currency: {
                       price: priceInfo.subTotal,
                       currencyObj:
                            $parent.site().selectedPriceListGroup().currency}">
                    </span>           </div>
                </div>         <div class="row">
                  <div class="col-xs-7">
                    Shipping (<span data-bind="text:
                        shippingMethod.shippingMethodDescription"></span>)
                  </div>           <div class="col-xs-5 text-right">
                    <span data-bind="currency: {
                       price: priceInfo.shipping,
                       currencyObj:
                            $parent.site().selectedPriceListGroup().currency}">
                    </span>           </div>
                </div>
                <!-- ko if: $data.hasOwnProperty("discountInfo") -->
                  <!-- ko if: discountInfo.shippingDiscount !== 0 -->
                    <div class="row">
                      <div class="col-xs-7">Shipping Discount </div>
                      <div class="col-xs-5 text-right">
                        <span data-bind="currency: {
                            price: -discountInfo.shippingDiscount,
                            currencyObj:
                                $parent.site().selectedPriceListGroup().currency}">
                        </span>               </div>
                    </div>           <!-- /ko -->
                <!-- /ko -->
                <!-- ko if: priceInfo.shippingSurchargeValue &&
                     priceInfo.shippingSurchargeValue !== 0 -->
                  <div class="row">
                    <div class="col-xs-7">Shipping Surcharge</div>
                    <div class="col-xs-5 text-right">
                      <span data-bind="currency: {
                         price: priceInfo.shippingSurchargeValue,
                         currencyObj:
                            $parent.site().selectedPriceListGroup().currency}">
                      </span>             </div>
                  </div>         <!-- /ko -->
                <!-- ko if: $parent.cart().showTaxSummary -->
                  <div class="row">
                    <div class="col-xs-7">Sales Tax</div>
                    <div class="col-xs-5 text-right">
                      <span data-bind="currency: {
                         price: priceInfo.tax,
                         currencyObj:
                              $parent.site().selectedPriceListGroup().currency}">
                      </span>             </div>
                  </div>         <!-- /ko -->
                <!-- ko if: (taxPriceInfo.isTaxIncluded &&
                             $parent.cart().showTaxSummary) -->
                  <div class="row">
                    <div class="col-xs-7">Group Total (excluding tax)</div>
                    <div class="col-xs-5 text-right">
                      <span data-bind="currency: {
                         price: priceInfo.totalWithoutTax,
                         currencyObj: $parent.site().priceListGroup.currency}">
                      </span>             </div>
                  </div>         <!-- /ko -->
                <div class="row">           <div class="col-xs-7">
                    Group Total
                    <!-- ko if: (taxPriceInfo.isTaxIncluded &&
                                 $parent.cart().showTaxSummary) -->
                      <span data-bind="widgetLocaleText: 'includingTaxText'"></span>
                    <!-- /ko -->           </div>
                  <div class="col-xs-5 text-right">
                    <span data-bind="currency: {
                       price: priceInfo.total,
                       currencyObj:
                            $parent.site().selectedPriceListGroup().currency}">
                    </span>           </div>         </div>       </div>     <!-- /ko -->   <!-- /ko -->
        <!-- /ko -->

Place order

The OrderViewModel.handlePlaceOrder method handles placing both single and split shipping orders. There is no change to the API for split shipping. The handlePlaceOrder method should be called when the Place Order button is clicked.

Order confirmation

Order confirmation should display each shipping group and its relevant information such as the addressee, the shipping method, the items in the shipping group, the subtotal before tax and shipping, shipping costs, sales tax, and a total cost for each group.

order confirmation

The following binding pattern iterates over the shippingGroups array in the widget shown above.

<!-- ko with: confirmation -->   <!-- ko foreach: shippingGroups -->
            Mark-up for shipping group here...   <!-- /ko -->
        <!-- /ko -->

Order details

Order details, like order confirmation, should display each shipping group and its relevant information such as the addressee, the shipping method, the items in the shipping group, the subtotal before tax and shipping, shipping costs, sales tax, and a total cost for each group.

my order

The following binding pattern iterates over the shippingGroups array in the widget shown above.

<!-- ko with: orderDetails-->
   <!-- ko foreach: shippingGroups -->
   Mark-up for shipping group here...
   <!-- /ko -->
<!-- /ko -->