Multiple shipments with different shipping methods on the same order

We need to handle orders where different order rows will be shipped in different ways. We have both digital and physical products that can be placed on the same order.
The shipping option for the physical products will be the one the customer selects on the checkout page since there could be different options for that (Standard or Express and so on). There’s really no need for the customer to select anything for the digital products since there’s just one option.
Also, it’s just possible to set one shipping option on the cart context before the order is placed using: cartContext.SelectShippingOptionAsync(selectShippingOptionArgs);.
I’m guessing that this is what makes up for the shipping option on ShippingInfo when the order is placed.

However, ShippingInfo on a sales order is a collection so it should be possible to have more than one ShippingInfo on an order. Also, to be able to create a shipment with a certain ShippingMethod that method needs to be a shipping option of a ShippingInfo in the shipping info collection on the order. If not you get an error creating the shipment.

So… How can we have more than one ShippingInfo on an order? How do we create that after the order has been placed? Or is there any other solution to this?

I can also add this. Digital shipments will be created and shipped from the Litium solution. Physical shipments will be created and shipped by our ERP and communicated to Litium with ERP Connect.

Litium version: 8.12

Anyone with a suggestion for this?

//See following code

using Litium.Sales;
using Litium.Events;
using Litium.Runtime;
using Litium.Sales.Events;
using Litium.Security;

namespace Litium.Accelerator.SampleCode
{
[Autostart]
public class AddCustomShippingInfo : IAsyncAutostart
{
private readonly IApplicationLifetime _applicationLifetime;
private readonly ShippingProviderService _shippingProviderService;
private readonly SecurityContextService _securityContextService;
private readonly EventBroker _eventBroker;
private readonly Sales.OrderService _orderService;

    public AddCustomShippingInfo(Sales.OrderService orderService, EventBroker eventBroker, IApplicationLifetime applicationLifetime, ShippingProviderService shippingProviderService, SecurityContextService securityContextService)
    {
        _eventBroker = eventBroker;
        _applicationLifetime = applicationLifetime;
        _shippingProviderService = shippingProviderService;
        _securityContextService = securityContextService;
        _orderService = orderService;
    }

    public ValueTask StartAsync(CancellationToken cancellationToken)
    {
        var subscription = _eventBroker.Subscribe<SalesOrderConfirmed>(async args =>
        {
            try
            {
                var order = args.Item;
                var productRows = order.Rows.Where(x => x.OrderRowType == OrderRowType.Product);
                if (productRows.Count() > 1) //TODO: Find whether there is any digital goods, here we assume the last row is a digital good.
                {
                    var shippingProvider = _shippingProviderService.GetAll().FirstOrDefault();//TODO: use your own digital shipping provider.
                    if (shippingProvider != null)
                    {
                        var digitalOption = shippingProvider.Options.LastOrDefault(); //TODO: find the digital shipping option.
                        if (digitalOption != null)
                        {
                            //for this example, we change the last row to the last shipping option.
                            var salesOrder = order.MakeWritableClone();
                            var digitalGoodShippingOption = salesOrder.ShippingInfo.FirstOrDefault(x=>x.Id == "DigitalGoods");
                            if (digitalGoodShippingOption == null)
                            {
                                digitalGoodShippingOption = new ShippingInfo()
                                {
                                    SystemId = Guid.NewGuid(),
                                    Id = "DigitalGoods",
                                    ShippingOption = new ProviderOptionIdentifier(shippingProvider.Id, digitalOption.Id)
                                };
                                salesOrder.ShippingInfo.Add(digitalGoodShippingOption);
                            }

                            //TODO: following would need to be set for every row where there is a digital good.
                            var lastRow = salesOrder.Rows.Where(x => x.OrderRowType == OrderRowType.Product).Last(); //todo: Find digital goods.
                            lastRow.ShippingInfoSystemId = digitalGoodShippingOption.SystemId;

                            using (_securityContextService.ActAsSystem("OrderUpdation"))
                            {
                                _orderService.Update(salesOrder);
                            }
                        }
                    }

                }
            }
            catch (Exception ex)
            {
               //TODO: handle errors.
            }
        });

        _applicationLifetime.ApplicationStopping.Register(() =>
        {
            // Application is about to shutdown, unregister the event listener
            subscription.Dispose();
        });
        return ValueTask.CompletedTask;
    }
}

}

@anusha.ganegoda I’ve never seen an example before that disposes a subscription. Is this something you recommend doing? And for what reason? Does it hold any other resources other than memory?

This worked well for us. Thanks @anusha.ganegoda!

One thing I need to mention though is that when creating the new ShippingInfo the Id needs to be a unique value or not set at all. Otherwise you’ll get a SQL duplicate key exception when updating the sales order. In the code example it’s set to a hard coded string “DigitalGoods”, doing it that way won’t work.

Now I’ve had some time to test this a bit more and I’m running in to trouble when I create the first shipment if I have more than one.

This is what happens:
When the order is placed it is created with one ShippingInfo, let’s call it A. At this time all order rows are connected to this ShippingInfo.
When the order becomes confirmed I create a new ShippingInfo (B) and some of the order rows are then connected to the new ShippingInfo (row.ShippingInfoSystemId).
Everything looks good so far. Now it’s time to create the first shipment.
I’m using the OrderFulfilmentService.CreateShipment to create the shipment and I add the rows connected to ShippingInfo B as shipment rows.
The Shipment is created but on the shipment, on the order in backoffice, it’s says that the shipping option is the one set for OrderInfo A though the only shipment rows added when creating the shipment was the ones connected to OrderInfo B. Also, the shipping option that is set on OrderInfo A has a fee. That fee is added to the amount of the partial payment capture. Looking in the database I can confirm that the shipping fee order row is added to the shipment, although I did not add as a shipment row it when creating the shipment.

@anusha.ganegoda How can I create a shipment for just the order rows connected to ShippingInfo B that also gets the shipping option set on ShippingInfo B?

In our case ShippingInfo B is a digital delivery that will be delivered instantly after the order is confirmed. ShippingInfo A is a standard delivery of physical goods that will happen from a warehouse.

This is my code:

var digitalGoodsRows = x.Item.Rows.Where(salesOrderRow => salesOrderRow.OrderRowType == Sales.OrderRowType.Product && salesOrderRow.ProductType == ProductType.DigitalGoods);
if (digitalGoodsRows?.Any() ?? false)
{
    var salesOrder = x.Item.MakeWritableClone();

    // Check if there's a DigitalDelivery ShippingInfo on the order
    var digitalShippingOption = new ProviderOptionIdentifier(ShippingProviderConstants.DirectShipment, ShippingMethodConstants.DigitalDelivery);
    var digitalDeliveryShippingInfo = salesOrder.ShippingInfo.FirstOrDefault(x => x.ShippingOption == digitalShippingOption);
    // If not - create one
    if (digitalDeliveryShippingInfo == null)
    {
        digitalDeliveryShippingInfo = new Sales.ShippingInfo()
        {
            SystemId = Guid.NewGuid(),
            ShippingOption = digitalShippingOption,
            ShippingAddress = x.Item.ShippingInfo.First().ShippingAddress.Clone()
        };
        salesOrder.ShippingInfo.Add(digitalDeliveryShippingInfo);
    }

    // Make sure that all digital goods rows are connected to the DigitalDelivery ShippingInfo
    foreach (var row in salesOrder.Rows.Where(salesOrderRow => salesOrderRow.OrderRowType == Sales.OrderRowType.Product && salesOrderRow.ProductType == ProductType.DigitalGoods))
    {
        row.ShippingInfoSystemId = digitalDeliveryShippingInfo.SystemId;
    }

    using (_securityContextService.ActAsSystem())
    {
        _orderService.Update(salesOrder);
    }

    // Create the digital shipment 
    _orderFulfilmentService.CreateShipment(new Connect.Erp.Shipment
    {
        OrderId = salesOrder.Id,
        Id = $"{salesOrder.Id.Replace("LS", "LSS")}-1",
        ShippingMethod = ShippingMethodConstants.DigitalDelivery,
        Address = digitalDeliveryShippingInfo.ShippingAddress.ToConnectErpAddress(),
        Rows = digitalGoodsRows.Select(r => new Connect.Erp.ShipmentRow
        {
            ArticleNumber = r.ArticleNumber,
            OrderRowId = r.Id,
            Quantity = r.Quantity
        }).ToList(),
    });
}
});

return ValueTask.CompletedTask;

would you report this as a bug please.

OK. Bug created.

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.