Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Zac 238 posts 539 karma points
    Jun 25, 2016 @ 06:11
    Zac
    0

    Digital products checkout flow

    Hi,

    We're using Merchello 2.1.0 and have some simple questions regarding digital products.

    We figured this would be easy but can't seem to get it straight

    1. All of our products are digital / non-shipable items
    2. Because of this, we skip the shipping step
    3. We're trying to automatically fulfill the order (we auth and capture the full amount but can't seem to set a fulfillment status)
    4. We aren't presented with a fulfillment option in the back end

    Our questions

    1. Since these products are digital we do not need to set a shipping method, correct? Or do we need to setup a digital shipment option and automatically set this?
    2. Is there a way to programatically set the fulfillment status to fulfilled?
    3. To mark the product as digital and shippable, we checked the followingn options in Merchello - "this variant has digital goods" and "this variant is available to purchase", we left "this variant is shippable as unchecked and Choose Media as empty - our product isn't a stored digital file. Is anything wrong with this setup?

    Any advise would be greatly appreciated.

    Thanks

  • Greg 8 posts 78 karma points
    Jun 28, 2016 @ 20:21
    Greg
    0

    It seems the CheckoutManager is not fully implemented as changes in the model checkoutstage do not affect it's behavior yet. But there appears to be a need to check for shippables in the basket. If there are no shippables, jumping to the PaymentMethod page appears to be logical. Here is the approach I've implemented. It's hard to tell if this will interfere with the architecture going forward, so I consider this a work around.

    I've implemented this same code for the HandleShippingAddressSave because the default behavior of the client provides a back button on the PaymentMethod page, which will take you back to the ShippingAddressPage if it's not valid ( which is the case when the basket has not shippables and you jump to PaymentMethod the first time). So for quickness sake, I allow the Shipping address to be updated, it wont break anything and I don't have to create a workaround.

    [PluginController("FastTrack")]
    public class CheckoutAddressController : CheckoutAddressControllerBase<FastTrackBillingAddressModel, FastTrackCheckoutAddressModel>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CheckoutAddressController"/> class.
        /// </summary>
        public CheckoutAddressController()
            : base(
                  new FastTrackBillingAddressModelFactory(),
                  new FastTrackShippingAddressModelFactory())
        {
        }
    
        /// <summary>
        /// Overrides the action for a successful billing address save.
        /// </summary>
        /// <param name="model">
        /// The model.
        /// </param>
        /// <returns>
        /// The <see cref="ActionResult"/>.
        /// </returns>
        protected override ActionResult HandleBillingAddressSaveSuccess(FastTrackBillingAddressModel model)
        {
            ActionResult returnValue = null;
            bool hasShippables = BasketHasShippables(Basket);
            CheckoutStage checkOutStage = GetPostBillingCheckOutStage(hasShippables, modelIsValid: true);
    
            // If the customer is logged in save the address to their default customer billing address for the next use
            if (!CurrentCustomer.IsAnonymous)
            {
                // In this implementation we are simply overwritting any previously saved addresses
                // This could can be extended to allow customers to manage multiple addresses of a given
                // type in other implementations.
                var customer = (ICustomer)CurrentCustomer;
                var existing = customer.DefaultCustomerAddress(AddressType.Billing);
                var caddress = BillingAddressFactory.Create(model, (ICustomer)CurrentCustomer, "Billing Address", AddressType.Billing);
                if (existing != null)
                {
                    caddress.CreateDate = existing.CreateDate;
                    caddress.Key = existing.Key;
                }
    
                ((ICustomer)CurrentCustomer).SaveCustomerAddress(caddress);
            }
    
            // NOTE: We need to do a special check here to assert that the country code is valid as it
            // billing addresses by default can be associated any where in the world, whereas shiping
            // destinations are usually constrained to specific locations.  Some implementations may
            // opt to swap the order of the address collection to alleviate the need for this check, but
            // there are also cases, where items may not need to be shipped and the billing address is required
            // to create the invoice.
            if (model.UseForShipping && EnsureBillingAddressIsValidAsShippingAddress(model))
            {
                // we use the billing address factory here since we know the model FastTrackBillingAddressModel
                // and only want Merchello's IAddress
                var address = BillingAddressFactory.Create(model);
    
                // In this implementation, we cannot save the customer shipping address to the customer as it may be a different model here
                // However, it is possible but more work would be required to ensure the model mapping
                CheckoutManager.Customer.SaveShipToAddress(address);
    
                // set the checkout stage
                model.WorkflowMarker = GetNextCheckoutWorkflowMarker(CheckoutStage.ShippingAddress);
            }
    
            if (hasShippables == false)
            {
                returnValue = Redirect("/checkout/payment-method");
            }
            else
            {
                returnValue = !model.SuccessRedirectUrl.IsNullOrWhiteSpace() ?
                Redirect(model.SuccessRedirectUrl) :
                base.HandleShippingAddressSaveSuccess(model);
            }
            return returnValue;
        }
    
          protected bool BasketHasShippables(IBasket basket)
        {
            return (from i in basket.Items where i.ExtendedData.GetValue("merchShippable") == "true" select true).Any();
        }
    
        protected CheckoutStage GetPostBillingCheckOutStage(bool hasShippables, bool modelIsValid = true)
        {
            CheckoutStage returnValue = CheckoutStage.ShippingAddress;
            if (modelIsValid == false)
            {
                returnValue = CheckoutStage.BillingAddress;
            }
            else if (hasShippables == false)
            {
                returnValue = CheckoutStage.PaymentMethod;
            }
    
    
            return returnValue;
        }
    
  • Zac 238 posts 539 karma points
    Jun 29, 2016 @ 14:43
    Zac
    0

    Thanks Greg - we've done this type of thing for other components of our checkout, however we don't have any shippable products. We're trying to figure out if we still need to set a shipping method / shipping address since we don't have these options.

    Rusty, do we still need to set the shipping method / address?

    Is there a way to programatically set the fulfillment status to fulfilled? How is this determined?

  • Rusty Swayne 1655 posts 4993 karma points c-trib
    Jul 07, 2016 @ 16:35
    Rusty Swayne
    0

    Hey Zac,

    You should not need to have a shipping address if you don't have any shippable products.

    It's been a long time since I've been into the digital products bit, but was setup to allow the fulfilled to be set once the customer downloaded the file (which I know is not your use case, but it can be done).

    I'll look for more details this afternoon, but in a nutshell what is happening -

    The checkout generates an invoice only. When the invoice is paid (usually in the last stage of the checkout) an Order is generated. On the order, there is an order status - which needs to be marked to fulfilled.

    A quick fix (untested) could be to grab the invoice from the payment result in the CheckoutManager.Finalizing event, get the order from the Invoice.Orders collection and set it's order status if there are no shippable line items.

    This seems pretty clunky though. Again, I'll have a look.

  • Claudiu Bria 34 posts 146 karma points
    Apr 24, 2017 @ 09:43
    Claudiu Bria
    0

    Hi Rusty,

    What is the status of digital products checkout, please ? Where exactly do we need to skip the shippable for that ?

  • David Brendel 792 posts 2968 karma points MVP c-trib
    Aug 01, 2016 @ 19:45
    David Brendel
    0

    Hi Zac, hi Rusty,

    have you figured out how this works? I also want to implement a shop to sell only digital goods, also without linking them in the media section.

    I tried to skip the shipping address and it kinda worked. I can pay the order but then in the backend I can't fullfill my orders and the shipping address is empty.

    Tried using this code to set it:

    CheckoutManager.Customer.SaveShipToAddress(address);
    

    Note: I don't use FastTrack and tried to implement most of the bits myself by looking at the source.

    Regards David

  • Zac 238 posts 539 karma points
    Aug 01, 2016 @ 23:35
    Zac
    0

    Hey David,

    I didn't need to save the shipping address to get this to work - I was fine leaving the shipping address empty since we aren't using it anyway.

    What's your checkout flow? Our code's pretty custom too - we basically have a three step checkout that's all custom.

    1. Step 1 - show cart - set billing address
    2. Step 2 - apply discounts and pay
    3. Step 3 - show receipt

    Step 2 is the most important and probably where you're running into issues.

    Basically, we post the payment info to a CheckoutPaymentController (code snippets below), attempt to authorize and capture the payment and if that worked, we then create the order, set the status and saved.
    The code after if(attempt.Payment.Success) was what we were missing. This allowed us to generate the order and set it as fulfilled.

    Hope this helps.

    [PluginController("MerchelloCustom")]
    [GatewayMethodUi("AuthorizeNet.CreditCard")]
    public class AuthorizeNetPaymentController : CheckoutPaymentControllerBase<AuthorizeNetPaymentModel>
    {
       ....
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult ProcessPayment(AuthorizeNetPaymentModel model)
        {
                CreditCardFormData ccData = new CreditCardFormData() {
                    // the CC info - removed for brevity
                };
                ProcessorArgumentCollection paymentData = CreditCardInfoExtensions.AsProcessorArgumentCollection(ccData);
    
                var attempt = CheckoutManager.Payment.AuthorizeCapturePayment(paymentMethod.Key, paymentData);
                var resultModel = CheckoutPaymentModelFactory.Create(CurrentCustomer, paymentMethod, attempt);
    
                CustomerContext.SetValue("paymentSuccess", attempt.Payment.Success.ToString().ToLower());
    
                if (attempt.Payment.Success)
                {
    
                    // generate the order, set the fulfillment status, save it
                    var approved = attempt.ApproveOrderCreation;
                    if (approved)
                    {
                        var order = attempt.Invoice.PrepareOrder();
                        order.OrderStatus =  MerchelloCore.MerchelloContext.Current.Services.OrderService.GetOrderStatusByKey(MerchelloCore.Constants.DefaultKeys.OrderStatus.Fulfilled);
                        MerchelloCore.MerchelloContext.Current.Services.OrderService.Save(order);
    
                        .......
    
  • David Brendel 792 posts 2968 karma points MVP c-trib
    Aug 02, 2016 @ 18:49
    David Brendel
    0

    Hi Zac,

    my custom code looks like this.

    Address handling:

    public class CheckoutAddressController : CheckoutAddressControllerBase<StoreAddress, StoreAddress>
    {
        public CheckoutAddressController()
            : base(new BillingAddressModelFactory(), new BillingAddressModelFactory())
        {
        }
        public override ActionResult SaveBillingAddress(StoreAddress model)
        {
            if (!this.ModelState.IsValid) return this.CurrentUmbracoPage();
    
            try
            {
                // Ensure billing address type is billing
                if (model.AddressType != AddressType.Billing) model.AddressType = AddressType.Billing;
    
                var address = BillingAddressFactory.Create(model);
    
                // Temporarily save the address in the checkout manager.
                this.CheckoutManager.Customer.SaveBillToAddress(address);
    
                if (!this.CurrentCustomer.IsAnonymous) this.SaveCustomerBillingAddress(model);
    
                model.WorkflowMarker = new CheckoutWorkflowMarker
                {
                    Previous = CheckoutStage.BillingAddress,
                    Current = CheckoutStage.BillingAddress,
                    Next = CheckoutStage.PaymentMethod
                };
    
                return this.HandleBillingAddressSaveSuccess(model);
            }
            catch (Exception ex)
            {
                return this.HandleBillingAddressSaveException(model, ex);
            }
        }
    
        protected override ActionResult HandleBillingAddressSaveSuccess(StoreAddress model)
        {
            if (!CurrentCustomer.IsAnonymous)
            {
                // In this implementation we are simply overwritting any previously saved addresses
                // This could can be extended to allow customers to manage multiple addresses of a given
                // type in other implementations.
                var customer = (ICustomer)CurrentCustomer;
                var existing = customer.DefaultCustomerAddress(AddressType.Billing);
                var caddress = BillingAddressFactory.Create(model, (ICustomer)CurrentCustomer, "Billing Address", AddressType.Billing);
                if (existing != null)
                {
                    caddress.CreateDate = existing.CreateDate;
                    caddress.Key = existing.Key;
                }
    
                ((ICustomer)CurrentCustomer).SaveCustomerAddress(caddress);
            }
    
            // we use the billing address factory here since we know the model FastTrackBillingAddressModel
            // and only want Merchello's IAddress
            var address = BillingAddressFactory.Create(model);
    
            CheckoutManager.Customer.SaveShipToAddress(address);
    
            return Redirect(model.SuccessRedirectUrl);
        }
    }
    

    Payment method handling:

    public class CheckoutPaymentMethodController : CheckoutPaymentMethodControllerBase<PaymentMethodModel>
    {
        /// <summary>
        /// Overrides the successful set payment operation.
        /// </summary>
        /// <param name="model">
        /// The model.
        /// </param>
        /// <returns>
        /// The <see cref="ActionResult"/>.
        /// </returns>
        protected override ActionResult HandleSetPaymentMethodSuccess(PaymentMethodModel model)
        {
            return !model.SuccessRedirectUrl.IsNullOrWhiteSpace() ?
                Redirect(model.SuccessRedirectUrl) :
                base.HandleSetPaymentMethodSuccess(model);
        }
    }
    

    Basket summary:

    public class CheckoutSummaryController : CheckoutSummaryControllerBase<CheckoutSummaryModel, StoreAddress, StoreAddressModel, StoreLineItemModel>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="CheckoutSummaryController"/> class.
        /// </summary>
        public CheckoutSummaryController()
            : base(
                  new CheckoutSummaryModelFactory(),
                  new CheckoutContextSettingsFactory())
        {
        }
    
        /// <summary>
        /// Renders the Basket Summary.
        /// </summary>
        /// <param name="view">
        /// The optional view.
        /// </param>
        /// <returns>
        /// The <see cref="ActionResult"/>.
        /// </returns>
        [ChildActionOnly]
        public override ActionResult BasketSummary(string view = "")
        {
            var model = CheckoutSummaryFactory.Create(Basket, CheckoutManager);
    
            return view.IsNullOrWhiteSpace() ? this.PartialView(model) : this.PartialView(view, model);
        }
    }
    

    I think what's missing is that I set the order status to fullfilled.

    Not that I am using Merchello 2.1.0.

    Regards David

Please Sign in or register to post replies

Write your reply to:

Draft