/// <summary> /// Creates a collection of shipments for the current basket /// </summary> public override IEnumerable<IShipment> PackageShipments() { // filter basket items for shippable items var shippableVisitor = new ShippableProductVisitor(); LineItemCollection.Accept(shippableVisitor); if(!shippableVisitor.ShippableItems.Any()) return new List<IShipment>(); // the origin address will be the default warehouse // For the initial version we are only exposing a single warehouse var warehouse = MerchelloContext.Services.WarehouseService.GetDefaultWarehouse(); var origin = warehouse.AsAddress(); //For the initial version we are only exposing a single shipment var shipment = new Shipment(origin, Destination) { VersionKey = VersionKey // this is used in cache keys }; // get the variants for each of the shippable line items var variants = MerchelloContext.Services.ProductVariantService.GetByKeys( shippableVisitor.ShippableItems.Select(x => x.ExtendedData.GetProductVariantKey()).Where(x => !Guid.Empty.Equals(x)) ).ToArray(); foreach (var lineItem in shippableVisitor.ShippableItems) { // We need to know what Warehouse Catalog this product is associated with for shipping and inventory var variant = variants.FirstOrDefault(x => x.Key.Equals(lineItem.ExtendedData.GetProductVariantKey())); if(variant == null) throw new InvalidOperationException("This packaging strategy cannot handle null ProductVariants"); if (variant.CatalogInventories.FirstOrDefault() == null) { LogHelper.Error<ShippableProductVisitor>("ProductVariant marked as shippable was not assoicated with a WarehouseCatalog. Product was: " + variant.Key.ToString() + " - " + variant.Name, new InvalidDataException()); } else { lineItem.ExtendedData.SetValue("merchWarehouseCatalogKey", variant.CatalogInventories.First().CatalogKey.ToString()); shipment.Items.Add(lineItem); } } return new List<IShipment> { shipment }; }
public void Can_Create_A_Customer_Invoice_And_Order() { // Adding the shipmethod is typically done in the back office through the UI. // Interested in the use case to dynamically add theses? var key = Constants.ProviderKeys.Shipping.FixedRateShippingProviderKey; var defaultCatalogKey = Constants.DefaultKeys.Warehouse.DefaultWarehouseCatalogKey; // this would have to be done through the back office as it uses an internal service var us = MerchelloContext.Current.Services.StoreSettingService.GetCountryByCode("US"); var usCountry = new ShipCountry(defaultCatalogKey, us); ((ServiceContext)MerchelloContext.Current.Services).ShipCountryService.Save(usCountry); // we can use this later. var rateTableProvider = (FixedRateShippingGatewayProvider)MerchelloContext.Current.Gateways.Shipping.GetProviderByKey(key); // again usually done in the back office if (!rateTableProvider.ShipMethods.Any()) { // creates the rate table for ship rate quotes var gwShipmeMethod = (FixedRateShippingGatewayMethod)rateTableProvider.CreateShipMethod(FixedRateShippingGatewayMethod.QuoteType.VaryByWeight, usCountry, "Ground (Vary by Weight)"); gwShipmeMethod.RateTable.AddRow(0, 10, 5); gwShipmeMethod.RateTable.AddRow(10, 15, 10); // total weight should be 10M so we should hit this tier gwShipmeMethod.RateTable.AddRow(15, 25, 25); gwShipmeMethod.RateTable.AddRow(25, 10000, 100); rateTableProvider.SaveShippingGatewayMethod(gwShipmeMethod); } // Get the persisted customer const string loginName = "rusty"; var customerService = MerchelloContext.Current.Services.CustomerService; var customer = customerService.GetByLoginName(loginName) ?? customerService.CreateCustomerWithKey(loginName, "Rusty", "Swayne", "*****@*****.**"); // I'll use this for billing and shipping var billingAddress = new Address() { Name = "Mindfly Web Design Studio", Address1 = "114 W. Magnolia St. Suite 300", Locality = "Bellingham", Region = "WA", PostalCode = "98225", CountryCode = "US" }; // Most of the time this information is brought in from the IProductVariant - but the idea is you can // describe things on the fly var extendedData = new ExtendedDataCollection(); // this is used to determine where a shipment originates extendedData.SetValue(Constants.ExtendedDataKeys.WarehouseCatalogKey, defaultCatalogKey.ToString()); // items set to shippable extendedData.SetValue(Constants.ExtendedDataKeys.TrackInventory, "false"); extendedData.SetValue(Constants.ExtendedDataKeys.Shippable, "true"); extendedData.SetValue(Constants.ExtendedDataKeys.Weight, "1.25"); extendedData.SetValue(Constants.ExtendedDataKeys.CurrencyCode, "USD"); var item = new InvoiceLineItem(LineItemType.Product, "My product", "mySku", 2, 10M, extendedData); var invoiceService = MerchelloContext.Current.Services.InvoiceService; var invoice = invoiceService.CreateInvoice(Constants.DefaultKeys.InvoiceStatus.Unpaid); // I'd say we need to add a parameter to the service so we don't have to do this // http://issues.merchello.com/youtrack/issue/M-434 ((Invoice)invoice).CustomerKey = customer.Key; // The version key is useful in some cases to invalidate shipping quotes or taxation calculations invoice.VersionKey = Guid.NewGuid(); invoice.Items.Add(item); // at this point the invoice is not saved and we don't have an invoice number // however, we may want to quote shipping so we need a shipment // Shipment Statuses are new in 1.5.0 var warehouse = MerchelloContext.Current.Services.WarehouseService.GetDefaultWarehouse(); var shipmentStatus = MerchelloContext.Current.Services.ShipmentService.GetShipmentStatusByKey( Constants.DefaultKeys.ShipmentStatus.Quoted); // since we know all the items in the invoice will be shipped we don't need to filter var shipment = new Shipment(shipmentStatus, warehouse.AsAddress(), billingAddress, invoice.Items); // since we already know the shipping provider we want from above we can do var quotes = rateTableProvider.QuoteShippingGatewayMethodsForShipment(shipment); // if we wanted Merchello to get quotes from all shipping providers we'd do the following // var quotes = shipment.ShipmentRateQuotes(); if (quotes.Any()) { // this check makes certain a quote was returned. For example if the collection of items was outside the allowable // weight range, the provider would not return a quote. // Add the first quote to the invoice. invoice.Items.Add(quotes.FirstOrDefault().AsLineItemOf<InvoiceLineItem>()); } // you do need to update the total ... this is usually done in the InvoiceBuilder in // instantiated by a SalesPreparation sub class var charges = invoice.Items.Where(x => x.LineItemType != LineItemType.Discount).Sum(x => x.TotalPrice); var discounts = invoice.Items.Where(x => x.LineItemType == LineItemType.Discount).Sum(x => x.TotalPrice); // total the invoice decimal converted; invoice.Total = decimal.TryParse((charges - discounts).ToString(CultureInfo.InvariantCulture), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture.NumberFormat, out converted) ? converted : 0; // Now we save the invoice since we have to have a real record of something to collect a payment against // This also generates the invoice number invoiceService.Save(invoice); Console.WriteLine(invoice.InvoiceNumber); // cash payment method var cashProvider = MerchelloContext.Current.Gateways.Payment.GetProviderByKey(Constants.ProviderKeys.Payment.CashPaymentProviderKey); if (cashProvider != null) { var cash = cashProvider.PaymentMethods.FirstOrDefault(); // default install has a single payment method "Cash" // I usually Authorize a cash payment if taken online since we don't really see the money. Capture is used // when the money comes in. In your inquiry, it looks like you are assuming the money is in hand at the // time of the purchase, so we'll use AuthorizeCapture straight away. var attempt = invoice.AuthorizeCapturePayment(cash.Key); if (! attempt.Payment.Success) { // handle the error } // otherwise you'll notice var approved = attempt.ApproveOrderCreation; // equals true // the order will be automatically created by the event handler in Merchello.Core.Gateways.GatewayEvents // however in this test I don't have the event wired up so I have to do it manuall if (approved) { var order = invoice.PrepareOrder(); MerchelloContext.Current.Services.OrderService.Save(order); var items = order.Items; } } // Cash provider is not active }
/// <summary> /// Creates a collection of shipments for the current basket /// </summary> /// <returns> /// A collection of <see cref="IShipment"/>. /// </returns> public override IEnumerable<IShipment> PackageShipments() { // All packaged shipments will start with a shipment status of "Quoted" as these are being used for the Shipment Rate Quote // NOTE: the "Packaging" status to indicate the shipment is physically being packaged/boxed up. var quoted = MerchelloContext.Services.ShipmentService.GetShipmentStatusByKey(Constants.DefaultKeys.ShipmentStatus.Quoted); // filter basket items for shippable items var shippableVisitor = new ShippableProductVisitor(); LineItemCollection.Accept(shippableVisitor); if (!shippableVisitor.ShippableItems.Any()) return new List<IShipment>(); // the origin address will be the default warehouse // For the initial version we are only exposing a single warehouse var warehouse = MerchelloContext.Services.WarehouseService.GetDefaultWarehouse(); var origin = warehouse.AsAddress(); ////For the initial version we are only exposing a single shipment var shipment = new Shipment(quoted, origin, Destination) { VersionKey = VersionKey // this is used in cache keys }; // get the variants for each of the shippable line items var variants = MerchelloContext.Services.ProductVariantService.GetByKeys( shippableVisitor.ShippableItems .Select(x => x.ExtendedData.GetProductVariantKey()) .Where(x => !Guid.Empty.Equals(x))).ToArray(); foreach (var lineItem in shippableVisitor.ShippableItems) { // We need to know what Warehouse Catalog this product is associated with for shipping and inventory var variant = variants.FirstOrDefault(x => x.Key.Equals(lineItem.ExtendedData.GetProductVariantKey())); if (variant == null) throw new InvalidOperationException("This packaging strategy cannot handle null ProductVariants"); if (variant.CatalogInventories.FirstOrDefault() == null) { LogHelper.Error<ShippableProductVisitor>( "ProductVariant marked as shippable was not assoicated with a WarehouseCatalog. Product was: " + variant.Key.ToString() + " - " + variant.Name, new InvalidDataException()); } else { // TODO this needs to be refactored to look at the entire shipment // since products could be in multiple catalogs which could have // opposing shippng rules and we have the destination address. lineItem.ExtendedData.SetValue( Constants.ExtendedDataKeys.WarehouseCatalogKey, variant.CatalogInventories.First().CatalogKey.ToString()); shipment.Items.Add(lineItem); } } return new List<IShipment> { shipment }; }