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
All of our products are digital / non-shipable items
Because of this, we skip the shipping step
We're trying to automatically fulfill the order (we auth and capture the full amount but can't seem to set a fulfillment status)
We aren't presented with a fulfillment option in the back end
Our questions
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?
Is there a way to programatically set the fulfillment status to fulfilled?
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?
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;
}
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?
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.
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.
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.
Step 1 - show cart - set billing address
Step 2 - apply discounts and pay
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);
.......
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.
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
Our questions
Any advise would be greatly appreciated.
Thanks
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.
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?
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.
Hi Rusty,
What is the status of digital products checkout, please ? Where exactly do we need to skip the shippable for that ?
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:
Note: I don't use FastTrack and tried to implement most of the bits myself by looking at the source.
Regards David
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.
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.
Hi Zac,
my custom code looks like this.
Address handling:
Payment method handling:
Basket summary:
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
is working on a reply...