diff --git a/.editorconfig b/.editorconfig index d2f3002c12b..25886cc34df 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_size = 4 # Trim trailing whitespace, limited support. # https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces -trim_trailing_whitespace = true +trim_trailing_whitespace = false [*.{cs,vb}] dotnet_style_predefined_type_for_locals_parameters_members = true:error diff --git a/15/umbraco-commerce/SUMMARY.md b/15/umbraco-commerce/SUMMARY.md index 97e8d1ba8c0..37321aaf9a9 100644 --- a/15/umbraco-commerce/SUMMARY.md +++ b/15/umbraco-commerce/SUMMARY.md @@ -27,17 +27,36 @@ * [Migrate Umbraco Commerce Checkout](upgrading/migrate-from-vendr-to-umbraco-commerce/migrate-umbraco-commerce-checkout.md) * [Migrate custom Payment Providers](upgrading/migrate-from-vendr-to-umbraco-commerce/migrate-custom-payment-providers.md) +## Tutorials + +* [Overview](tutorials/overview.md) +* [Build a Store in Umbraco using Umbraco Commerce](tutorials/build-a-store/overview.md) + * [Installation](tutorials/build-a-store/installation.md) + * [Creating a Store](tutorials/build-a-store/create-store.md) + * [Configuring your Store](tutorials/build-a-store/configure-store.md) + * [Creating your first Product](tutorials/build-a-store/create-product.md) + * [Implementing a Shopping Cart](tutorials/build-a-store/cart.md) + * [Using the Umbraco.Commerce.Cart Drop-in Shopping Cart](https://docs.umbraco.com/umbraco-commerce-packages/cart/cart) + * [Creating a Custom Shopping Cart](tutorials/build-a-store/custom-cart.md) + * [Implementing a Checkout Flow](tutorials/build-a-store/checkout.md) + * [Using the Umbraco.Commerce.Checkout Drop-in Checkout Flow](https://docs.umbraco.com/umbraco-commerce-packages/checkout/checkout) + * [Creating a Custom Checkout Flow](tutorials/build-a-store/custom-checkout.md) + * [Configuring Store Access Permissions](tutorials/build-a-store/permissions.md) + ## How-To Guides * [Overview](how-to-guides/overview.md) * [Configure SQLite support](how-to-guides/configure-sqlite-support.md) -* [Limit Order Line Quantity](how-to-guides/limit-orderline-quantity.md) * [Use an Alternative Database for Umbraco Commerce Tables](how-to-guides/use-an-alternative-database-for-umbraco-commerce-tables.md) -* [Add item to Cart](how-to-guides/add-item.md) -* [Update Cart](how-to-guides/update-cart.md) -* [Delete item from Cart](how-to-guides/delete-item.md) * [Customizing Templates](how-to-guides/customizing-templates.md) -* [Configuring Store Cleanup](how-to-guides/configuring-cart-cleanup.md) +* [Configuring Cart Cleanup](how-to-guides/configuring-cart-cleanup.md) +* [Limit Order Line Quantity](how-to-guides/limit-orderline-quantity.md) +* [Implementing Product Bundles](how-to-guides/product-bundles.md) +* [Implementing Member Based Pricing](how-to-guides/member-based-pricing.md) +* [Implementing Dynamically Priced Products](how-to-guides/dynamically-priced-products.md) +* [Implementing Personalized Products](how-to-guides/personalized-products.md) +* [Implementing a Currency Switcher](how-to-guides/currency-switching.md) +* [Building a Members Portal](how-to-guides/member-portal.md) ## Key Concepts @@ -108,8 +127,3 @@ * [Management API](reference/management-api/README.md) * [Go behind the scenes](reference/go-behind-the-scenes.md) * [Telemetry](reference/telemetry.md) - -## Tutorials - -* [Overview](tutorials/overview.md) -* [Getting started with Umbraco Commerce: The Backoffice](tutorials/getting-started-with-commerce.md) diff --git a/15/umbraco-commerce/how-to-guides/add-item.md b/15/umbraco-commerce/how-to-guides/add-item.md deleted file mode 100644 index ab0b9282f76..00000000000 --- a/15/umbraco-commerce/how-to-guides/add-item.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -description: How-To Guide to add an item to your cart. ---- - -# Add item to Cart - -To add an item to the cart, configure Umbraco with a store and add the necessary properties for interaction. Learn more by following the [Getting started with Umbraco Commerce: The Backoffice tutorial](../tutorials/getting-started-with-commerce.md). - -You will need the front end to be set up to allow an item to be added to the cart. This can be done by adding a button to the front end to call the Action to add the item to the cart. - -Create a new Document Type with the template. Call it **Product Page** with the following property aliases: `productTitle`, `productDescription`, `price`, `stock`. - -The following property editors are recommended to be used for the above: - -* `productTitle`: TextString -* `productDescription`: TextArea -* `price`: Umbraco Commerce Price -* `stock`: Umbraco Commerce Stock - -The Product Page template can be implemented as shown below. - -```csharp -@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage -@{ -var store = Model.Value("store", fallback: Fallback.ToAncestors); -var product = CommerceApi.Instance.GetProduct(store.Id, Model.Key.ToString(), "en-GB"); -var price = product.TryCalculatePrice().ResultOrThrow("Unable to calculate product price"); -} -``` - -The code above does the following: - -- You need to access the store to access the relevant properties for your product, such as price. The store has a fallback property allowing you to traverse the tree to find the store. -- You retrieve the product based on the store and a reference for the product. The 'productReference' comes from the Model which is a single product. -- The Product is returned as a ProductSnapshot which is Umbraco Commerce obtaining the page ID and carrying out necessary processes to bring in the data for further processing. -- Finally, you need to calculate the price which is then displayed without VAT. This can also be displayed with VAT. - -To display this you need to add some markup or at least amend it to include a button to add an item. Add the following to the same file: - -```csharp -@using (Html.BeginUmbracoForm("AddToCart", "CartSurface")) -{ - @Html.Hidden("productReference", Model.Key.ToString()) -

@Model.Value("productTitle")

-

@Model.Value("productDescription")

- -

Our price excluding VAT @price.WithoutTax.ToString("C0")

- - if (@Model.Value("stock") == 0) - { -

Sorry, out of stock

- } - else - { - - } - -} -``` - -The hidden field uses the `productReference` to be passed across to the Controller. - -## Adding the Controller - -For the button to work, you need to implement a controller. An example of this is shown below. - -Create a new Controller called `CartSurfaceController.cs`. - -{% hint style="warning" %} - -The namespaces used in this Controller are important and need to be included. - -``` -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.Website.Controllers; -using Umbraco.Commerce.Common.Validation; -using Umbraco.Commerce.Core.Api; -using Umbraco.Commerce.Core.Models; -using Umbraco.Commerce.Extensions; -using Umbraco.Extensions; -``` - -{% endhint %} - -```csharp -public class CartSurfaceController : SurfaceController -{ - public CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoCommerceApi commerceApi) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) - { - _commerceApi = commerceApi; - } -} -``` - -Below you can see the equivalent code for having this as a Primary Constructor: - -```csharp -public class CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider) - : SurfaceController(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) -{ -} -``` - -The CartDto class below is used to pass the `productReference` across to the Controller. This class has only one property for the `productReference`. - -```csharp -public class CartDto -{ - public string ProductReference { get; set; } -} -``` - -We now need to add the Action to add the item to the cart. This action will be called when the button is clicked. - -```csharp -[HttpPost] -public IActionResult AddToBasket(CartDto cart) -{ - commerceApi.Uow.Execute(uow => - { - var store = CurrentPage.Value("store", fallback: Fallback.ToAncestors); - - if (store == null) return; - - try - { - var order = commerceApi.GetOrCreateCurrentOrder(store.Id) - .AsWritable(uow) - .AddProduct(cart.ProductReference, 1); - - commerceApi.SaveOrder(order); - - uow.Complete(); - - TempData["SuccessFeedback"] = "Product added to cart"; - return RedirectToCurrentUmbracoPage(); - } - catch (ValidationException ve) - { - throw new ValidationException(ve.Errors); - } - catch (Exception ex) - { - logger.Error(ex, "An error occurred."); - } - }); -} -``` - -The code above does the following: - -- The `store` variable is used to access the store to get the store ID. -- A try-catch block captures any errors that may occur when adding the item to the cart, including any validation errors. -- `order` is used to retrieve the current order if one exists or create a new order against the store found. In the Commerce API, everything is read-only for performance so you need to make it writable to add the product. -- `AddProduct` is called and `productReference` is passed along with the quantity. -- `SaveOrder` is called to save the order. -- `TempData` stores a message to be displayed to the user if the product has been added to the cart. - -{% hint style="warning" %} -Umbraco Commerce uses the Unit of Work pattern to complete saving the item (`uow.Complete`). When retrieving or saving data ideally you would want the entire transaction to be committed. However, if there is an error nothing is changed on the database. -{% endhint %} - -Finally, you need to add the `TempData` to tell the user that the product has been added to the cart. - -## Add a partial view to display the message - -Create a new partial view called `Feedback.cshtml`. - -```csharp -@Html.ValidationSummary(true, "", new { @class = "danger" }) - -@{ - var success = TempData["SuccessFeedback"]?.ToString(); - - if (!string.IsNullOrWhiteSpace(success)) - { -
@success
- } -} -``` - -You can now run the application, click the button, and see the product added to the cart with a message displayed to the user. diff --git a/15/umbraco-commerce/how-to-guides/currency-switching.md b/15/umbraco-commerce/how-to-guides/currency-switching.md new file mode 100644 index 00000000000..338370139ee --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/currency-switching.md @@ -0,0 +1,159 @@ +--- +description: Learn how to implement a currency switcher in Umbraco Commerce. +--- + +# Implementing a Currency Switcher + +In a globalized world, it is essential to provide users with the ability to switch between different currencies. This feature is especially important for e-commerce websites that cater to customers from different countries. + +In this guide, you can learn how to implement a currency switcher in Umbraco Commerce. + +{% hint style="info" %} +In this guide, it is assumed that each country has a single currency. If your store supports multiple currencies per country, you must adjust the implementation accordingly. +{% endhint %} + +## Configure Countries and Currencies + +1. Define the countries and currencies you want to support, in the Umbraco backoffice. + +![Countries](images/localization/store-countries.png) + +![Currencies](images/localization/store-currencies.png) + +2. Navigate to the Content section. +3. Populate the product prices for each currency. + +![Product Prices](images/localization/product-prices.png) + +## Create a Currency Switcher Component + +A partial view is used on the frontend to allow users to toggle between existing currencies. + +![Currency Switcher](images/localization/country-switch.png) + +This is done by creating a `CurerrencySwitcher.cshtml` partial with the following implementation: + +{% code title="CurerrencySwitcher.cshtml" %} + +````csharp +@using Umbraco.Commerce.Core.Api; +@using Umbraco.Commerce.SwiftShop.Extensions; +@inject IUmbracoCommerceApi UmbracoCommerceApi +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage + +@{ + var store = Model.GetStore(); + var countries = await UmbracoCommerceApi.GetCountriesAsync(store.Id); + var currencies = await UmbracoCommerceApi.GetCurrenciesAsync(store.Id); + var currentCountry = await UmbracoCommerceApi.GetDefaultShippingCountryAsync(store.Id); +} + +@if (countries.Count() > 1) +{ + @using (Html.BeginUmbracoForm("ChangeCountry", "Culture", FormMethod.Post, new { @name = "changeCountryForm" })) + { + @Html.DropDownList("countryIsoCode", countries.Select(x + => new SelectListItem(currencies.First(y => y.Id == x.DefaultCurrencyId!.Value).Code, x.Code, x.Code == currentCountry.Code)), + new + { + @class = "form-select form-select-sm", + @onchange = "document.forms['changeCountryForm'].submit()" + }) + } +} +```` + +{% endcode %} + +This can then be placed in your sites base template by adding the following: + +{% code title="Layout.cshtml" %} + +```csharp +@(await Html.PartialAsync("CurerrencySwitcher")) +``` + +{% endcode %} + +## Handle Switching Currencies + +Switching the culture is handled by a Surface controller. + +Create a new Surface controller called `CultureSurfaceController` and add the following code: + +{% code title="CultureSurfaceController.cs" %} + +````csharp +public class CultureSurfaceController : SurfaceController +{ + private readonly IUmbracoCommerceApi _commerceApi; + + public CultureSurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoCommerceApi commerceApi) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + _commerceApi = commerceApi; + } + + [HttpPost] + public async Task ChangeCountry(ChangeCountryDto changeCountryDto) + { + var store = CurrentPage.GetStore(); + var country = await _commerceApi.GetCountryAsync(store.Id, changeCountryDto.CountryIsoCode); + var currency = await _commerceApi.GetCurrencyAsync(country.DefaultCurrencyId.Value); + + await _commerceApi.SetDefaultPaymentCountryAsync(store.Id, country); + await _commerceApi.SetDefaultShippingCountryAsync(store.Id, country); + await _commerceApi.SetDefaultCurrencyAsync(store.Id, currency); + + var currentOrder = await _commerceApi.GetCurrentOrderAsync(store.Id); + if (currentOrder != null) + { + await _commerceApi.Uow.ExecuteAsync(async uow => + { + var writableOrder = await currentOrder.AsWritableAsync(uow) + .ClearPaymentCountryRegionAsync() + .ClearShippingCountryRegionAsync() + .SetCurrencyAsync(currency.Id); + + await _commerceApi.SaveOrderAsync(writableOrder); + + uow.Complete(); + }); + } + + return RedirectToCurrentUmbracoPage(); + } +} +```` + +{% endcode %} + +The `ChangeCountryDto` class binds the country ISO code from the form. + +{% code title="ChangeCountryDto.cs" %} + +````csharp +public class ChangeCountryDto +{ + public string CountryIsoCode { get; set; } +} +```` + +{% endcode %} + +## Result + +With the currency switcher implemented, users can switch between countries/currencies on your website. + +The changes are reflected on the product details pages. + +![product-gb](images/localization/product-gb.png) + +![product-dk](images/localization/product-dk.png) diff --git a/15/umbraco-commerce/how-to-guides/delete-item.md b/15/umbraco-commerce/how-to-guides/delete-item.md deleted file mode 100644 index ec7ac680cef..00000000000 --- a/15/umbraco-commerce/how-to-guides/delete-item.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -description: Learn how to remove items added to the shopping cart. ---- - -# Delete item from cart - -{% hint style="info" %} -This guide builds on the [Update Cart](update-cart.md) guide. It is recommended to follow that guide before starting this one. -{% endhint %} - -This will teach you how to delete an item from the cart. - -Your view for the `cart.cshtml` page will be similar to the example below. - -```csharp -@inherits UmbracoViewPage -@{ - var store = Model.Value("store", fallback: Fallback.ToAncestors); - var currentOrder = CommerceApi.Instance.GetCurrentOrder(store!.Id); - if (currentOrder == null) return; - - @using (Html.BeginUmbracoForm("UpdateCart", "CartSurface")) - { - @foreach (var item in currentOrder.OrderLines.Select((ol, i) => new - { - OrderLine = ol, - Index = i - })) - { -

- @Html.Hidden($"orderLines[{item.Index}].Id", item.OrderLine.Id) - @item.OrderLine.Name @Html.Hidden($"orderLines[{item.Index}].Quantity", (int)item.OrderLine.Quantity, new { @type = "number" }) - @Html.Hidden($"orderLines[{item.Index}].ProductReference", item.OrderLine.ProductReference) - Remove Item -

- - } - - - - var success = TempData["SuccessMessage"]?.ToString(); - - if (!string.IsNullOrWhiteSpace(success)) - { -
@success
- } - } -} -``` - -The code below allows the Umbraco `SurfaceAction` to call `RemoveFromCart` when the link is clicked. It will also pass the `OrderLineId`. - -```csharp -Remove -``` - -## Adding the Controller - -For the button to work, you need to add some functionality via a Controller. - -Create a new Controller called `CartSurfaceController.cs` - -{% hint style="warning" %} - -The namespaces used in this Controller are important and need to be included. - -```cs -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.Website.Controllers; -using Umbraco.Commerce.Common.Validation; -using Umbraco.Commerce.Core.Api; -using Umbraco.Commerce.Core.Models; -using Umbraco.Commerce.Extensions; -using Umbraco.Extensions; -``` - -{% endhint %} - -```csharp -public class CartSurfaceController : SurfaceController -{ - public CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoCommerceApi commerceApi) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) - { - _commerceApi = commerceApi; - } -} -``` - -The example below is the equivalent code for having this as a Primary Constructor: - -```csharp -public class CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoCommerceApi commerceApi) - : SurfaceController(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) -{ -} -``` - -The `CartDto` is a class used to pass data to the Controller. In this instance, it passes over the `OrderLineId`. - -```csharp - public class CartDto - { - public Guid OrderLineId { get; set; } - } -``` - -You need to add the `Action` to delete the item from the cart. This will be called when the button is clicked. - -```csharp -[HttpGet] -public IActionResult RemoveFromCart(CartDto cart) -{ - try - { - _commerceApi.Uow.Execute(uow => - { - var store = CurrentPage?.Value("store", fallback: Fallback.ToAncestors); - - if (store == null) return; - - var order = _commerceApi.GetOrCreateCurrentOrder(store.Id) - .AsWritable(uow) - .RemoveOrderLine(cart.OrderLineId); - - _commerceApi.SaveOrder(order); - - uow.Complete(); - }); - } - catch (ValidationException) - { - ModelState.AddModelError(string.Empty, "Failed to remove product from cart"); - - return CurrentUmbracoPage(); - } - - TempData["SuccessMessage"] = "Item removed"; - - return RedirectToCurrentUmbracoPage(); -} -``` - -- A `try-catch` block captures any validation errors that may occur when updating items in the cart. -- The `store` variable is used to access the store to retrieve the store ID. -- `order` is used to retrieve the current order. In the Commerce API, everything is read-only for performance so you need to make it writable to add the product. -- `SaveOrder` is called to save the order. -- If there are any validation errors, they are added to a `ModelState` error, and the user is redirected back to the current page. -- `TempData` stores a message to be displayed to the user if the product has been successfully updated. - -{% hint style="warning" %} -Umbraco Commerce uses the Unit of Work pattern to complete saving the item (`uow.Complete`). When retrieving or saving data ideally you would want the entire transaction to be committed. However, if there is an error nothing is changed on the database. -{% endhint %} - -If you have followed the [Add item to cart](add-item.md) article, run the application, add an item to your cart, and navigate to your `cart.cshtml` page. Clicking the `Remove Item` button will delete the item in your cart and display a message. diff --git a/15/umbraco-commerce/how-to-guides/dynamically-priced-products.md b/15/umbraco-commerce/how-to-guides/dynamically-priced-products.md new file mode 100644 index 00000000000..571d69bda31 --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/dynamically-priced-products.md @@ -0,0 +1,208 @@ +--- +description: Learn how to implement dynamically priced products in Umbraco Commerce. +--- + +# Implementing Dynamically Priced Products + +Sometimes products do not have a fixed price. Depending on the customer's requirements, the price of the product can change. Examples could be window blinds that a priced based on the size of the window, or wallpaper that is priced by the meter. + +This guide shows you how to implement dynamically priced products in Umbraco Commerce. + +{% hint style="info" %} +This guide is not a direct follow-on from the [getting started tutorial](../tutorials/build-a-store/overview.md). It is assumed that your store is set up in a similar structure. +{% endhint %} + +## Capturing User Input + +Add a new field on the product's frontend page, to capture the desired length we want to purchase. + +![Length Input](images/dynamic-price/length-input.png) + +The selected length will reflect on the cart value. + +![Calculated Cart Values](images/dynamic-price/cart-with-length.png) + +To provide the correct calculations for an order, the captured data will need to go through two different processes behind the scenes: + +* Store the user input against the order line property. +* Implement a custom order line calculator to calculate the prices based on the user input. + +## Storing the User Input + +1. Add a new property to the `AddToCartDto` class to capture the length. + +{% code title="AddToCartDto.cs" %} + +```csharp +public class AddToCartDto +{ + ... + public string? Length { get; set; } +} +``` + +{% endcode %} + +2. Update the `AddToCart` method of the `CartSurfaceController` to store the length against the order line as a [property](../key-concepts/umbraco-properties.md). + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpPost] +public async Task AddToCart(AddToCartDto postModel) +{ + try + { + await _commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage.GetStore(); + var order = await _commerceApi.GetOrCreateCurrentOrderAsync(store.Id) + .AsWritableAsync(uow) + .AddProductAsync(postModel.ProductReference, decimal.Parse(postModel.Quantity), new Dictionary{ + { "length", postModel.Length.ToString() } + }); + + await _commerceApi.SaveOrderAsync(order); + + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ... + } + ... +} +``` + +{% endcode %} + +## Calculating the Order Line Price + +We will calculate the price/tax rate of a given order line by multiplying the specified length by the unit price. This is done using a custom order line calculator. + +1. Create a new class that implements the `IOrderLineCalculator` interface. + +{% code title="SwiftOrderLineCalculator.cs" %} + +```csharp +public class SwiftOrderLineCalculator : IOrderLineCalculator +{ + private ITaxService _taxService; + private IProductPriceFreezerService _productPriceFreezerService; + + public SwiftOrderLineCalculator( + ITaxService taxService, + IProductPriceFreezerService productPriceFreezerService) + { + _taxService = taxService; + _productPriceFreezerService = productPriceFreezerService; + } + + public async Task> TryCalculateOrderLineTaxRateAsync(OrderReadOnly order, OrderLineReadOnly orderLine, TaxSource taxSource, TaxRate fallbackTaxRate, OrderLineCalculatorContext context = null, CancellationToken cancellationToken = default) + { + order.MustNotBeNull(nameof(order)); + orderLine.MustNotBeNull(nameof(orderLine)); + + TaxRate taxRate = fallbackTaxRate; + + if (orderLine.TaxClassId != null) + { + taxRate = (await _taxService.GetTaxClassAsync(orderLine.TaxClassId.Value)).GetTaxRate(taxSource); + } + + return Attempt.Succeed(taxRate); + } + + public async Task> TryCalculateOrderLineUnitPriceAsync(OrderReadOnly order, OrderLineReadOnly orderLine, Guid currencyId, TaxRate taxRate, OrderLineCalculatorContext context = null, CancellationToken cancellationToken = default) + { + order.MustNotBeNull(nameof(order)); + orderLine.MustNotBeNull(nameof(orderLine)); + + var numberOfMeters = order.Properties.TryGetValue(Constants.OrderProperties.Length, out var propertyValue) + ? propertyValue.Value + : string.Empty; + + var unitPrice = order.IsNew + ? (await _productPriceFreezerService.GetProductPriceAsync(order.StoreId, order.Id, orderLine.ProductReference, orderLine.ProductVariantReference, currencyId)).ProductPrice.Value + : (await _productPriceFreezerService.GetOrCreateFrozenProductPriceAsync(order.StoreId, order.Id, orderLine.ProductReference, orderLine.ProductVariantReference, currencyId)).Value; + + var price = !string.IsNullOrEmpty(numberOfMeters) && int.TryParse(numberOfMeters, out int result) + ? result * unitPrice + : orderLine.UnitPrice; + + var x = Price.Calculate(price, taxRate.Value, currencyId); + + return Attempt.Succeed(Price.Calculate(price, taxRate.Value, currencyId)); + } +} +``` + +{% endcode %} + +2. Register the custom calculator in the `Startup.cs` file or in an `IComposer`. + +{% code title="SwiftShopComposer.cs" %} + +```csharp +internal class SwiftShopComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` + +{% endcode %} + +## Backoffice UI + +A useful extra step is to expose the captured data in the order's details in the Backoffice. + +This is implemented as a custom [UI Extension](https://docs.umbraco.com/umbraco-commerce/key-concepts/ui-extensions/order-line-properties). + +Create a new `umbraco-package.json` file in a folder in the `App_Plugins` directory in the root of your project and add the following code: + +{% code title="umbraco-package.json" %} + +```json +{ + "name": "SwiftShop", + "extensions": [ + { + "type": "ucOrderLineProperty", + "alias": "Uc.OrderLineProperty.ProductLength", + "name": "Product Length", + "weight": 400, + "meta": { + "propertyAlias": "productLength", + "showInOrderLineSummary": true, + "summaryStyle": "inline", + "editorUiAlias": "Umb.PropertyEditorUi.TextBox", + "labelUiAlias": "Umb.PropertyEditorUi.Label" + } + }, + { + "type": "localization", + "alias": "Uc.OrderLineProperty.ProductLength.EnUS", + "name": "English", + "meta": { + "culture": "en", + "localizations": { + "section": { + "ucProperties_productLengthLabel": "Length", + "ucProperties_productLengthDescription": "Customer product ordered length" + } + } + } + } + ] +} +``` + +{% endcode %} + +The length property is now displayed in the order details in the Backoffice. + +![Order Details](images/dynamic-price/order-editor-property.png) diff --git a/15/umbraco-commerce/how-to-guides/images/dynamic-price/cart-with-length.png b/15/umbraco-commerce/how-to-guides/images/dynamic-price/cart-with-length.png new file mode 100644 index 00000000000..38d9cad0594 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/dynamic-price/cart-with-length.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/dynamic-price/length-input.png b/15/umbraco-commerce/how-to-guides/images/dynamic-price/length-input.png new file mode 100644 index 00000000000..1cbc6d9698c Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/dynamic-price/length-input.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/dynamic-price/order-editor-property.png b/15/umbraco-commerce/how-to-guides/images/dynamic-price/order-editor-property.png new file mode 100644 index 00000000000..49e7f88ed22 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/dynamic-price/order-editor-property.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/cart-dk.png b/15/umbraco-commerce/how-to-guides/images/localization/cart-dk.png new file mode 100644 index 00000000000..890f11c1439 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/cart-dk.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/cart-gb.png b/15/umbraco-commerce/how-to-guides/images/localization/cart-gb.png new file mode 100644 index 00000000000..a39a5acca2b Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/cart-gb.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/country-switch.png b/15/umbraco-commerce/how-to-guides/images/localization/country-switch.png new file mode 100644 index 00000000000..e2216386a6f Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/country-switch.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/product-dk.png b/15/umbraco-commerce/how-to-guides/images/localization/product-dk.png new file mode 100644 index 00000000000..0bdf79de682 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/product-dk.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/product-gb.png b/15/umbraco-commerce/how-to-guides/images/localization/product-gb.png new file mode 100644 index 00000000000..95a6830bb0f Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/product-gb.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/product-prices.png b/15/umbraco-commerce/how-to-guides/images/localization/product-prices.png new file mode 100644 index 00000000000..21398f52a52 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/product-prices.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/store-countries.png b/15/umbraco-commerce/how-to-guides/images/localization/store-countries.png new file mode 100644 index 00000000000..34795a8cedc Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/store-countries.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/localization/store-currencies.png b/15/umbraco-commerce/how-to-guides/images/localization/store-currencies.png new file mode 100644 index 00000000000..5da2002b6ae Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/localization/store-currencies.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/default-product-page.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/default-product-page.png new file mode 100644 index 00000000000..ffa83f9cda2 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/default-product-page.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/gold-product-page.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/gold-product-page.png new file mode 100644 index 00000000000..a166635ac83 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/gold-product-page.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-groups.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-groups.png new file mode 100644 index 00000000000..f88b51aefa5 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-groups.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-block-list.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-block-list.png new file mode 100644 index 00000000000..8745ed31db3 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-block-list.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-content.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-content.png new file mode 100644 index 00000000000..63c5623b01a Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-content.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-element.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-element.png new file mode 100644 index 00000000000..60e32ee00ae Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/member-price-element.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/members.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/members.png new file mode 100644 index 00000000000..df2ae3c3f4a Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/members.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-based-pricing/platinum-product-page.png b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/platinum-product-page.png new file mode 100644 index 00000000000..89818a94277 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-based-pricing/platinum-product-page.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/allowed-children.png b/15/umbraco-commerce/how-to-guides/images/member-portal/allowed-children.png new file mode 100644 index 00000000000..e241ccd0743 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/allowed-children.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/content-structure.png b/15/umbraco-commerce/how-to-guides/images/member-portal/content-structure.png new file mode 100644 index 00000000000..c079fc7dee8 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/content-structure.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/customer-assign-member-group.png b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-assign-member-group.png new file mode 100644 index 00000000000..bd25e5449d2 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-assign-member-group.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-group.png b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-group.png new file mode 100644 index 00000000000..9ebedefee9f Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-group.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-type.png b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-type.png new file mode 100644 index 00000000000..f5f05e543d6 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/customer-member-type.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/customers.png b/15/umbraco-commerce/how-to-guides/images/member-portal/customers.png new file mode 100644 index 00000000000..aa2126213ec Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/customers.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/document-types.png b/15/umbraco-commerce/how-to-guides/images/member-portal/document-types.png new file mode 100644 index 00000000000..e2c5256b3ea Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/document-types.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/logged-in.png b/15/umbraco-commerce/how-to-guides/images/member-portal/logged-in.png new file mode 100644 index 00000000000..d93f6d8fb52 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/logged-in.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/logged-out.png b/15/umbraco-commerce/how-to-guides/images/member-portal/logged-out.png new file mode 100644 index 00000000000..af0e6df5b80 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/logged-out.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/login-page.png b/15/umbraco-commerce/how-to-guides/images/member-portal/login-page.png new file mode 100644 index 00000000000..27079e729af Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/login-page.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/member-portal.png b/15/umbraco-commerce/how-to-guides/images/member-portal/member-portal.png new file mode 100644 index 00000000000..f4d55ba2d13 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/member-portal.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/order-history.png b/15/umbraco-commerce/how-to-guides/images/member-portal/order-history.png new file mode 100644 index 00000000000..99d0ae39bc9 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/order-history.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/public-access.png b/15/umbraco-commerce/how-to-guides/images/member-portal/public-access.png new file mode 100644 index 00000000000..d07226f0884 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/public-access.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/member-portal/register-page.png b/15/umbraco-commerce/how-to-guides/images/member-portal/register-page.png new file mode 100644 index 00000000000..f55f4e934e8 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/member-portal/register-page.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-collapsed.png b/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-collapsed.png new file mode 100644 index 00000000000..17c5a0b9c1a Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-collapsed.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-default.png b/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-default.png new file mode 100644 index 00000000000..4f8a41abe48 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/personalized-products/observations-default.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/personalized-products/order-line-property.png b/15/umbraco-commerce/how-to-guides/images/personalized-products/order-line-property.png new file mode 100644 index 00000000000..7207a8d3789 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/personalized-products/order-line-property.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type-structure.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type-structure.png new file mode 100644 index 00000000000..b58b0f60492 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type-structure.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type.png new file mode 100644 index 00000000000..1403aadf7c5 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/bundle-document-type.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/order-editor.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/order-editor.png new file mode 100644 index 00000000000..57320e1e240 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/order-editor.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/product-bundles.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-bundles.png new file mode 100644 index 00000000000..3bbd3b70c62 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-bundles.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-children.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-children.png new file mode 100644 index 00000000000..1d686d1692c Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-children.png differ diff --git a/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-details.png b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-details.png new file mode 100644 index 00000000000..bef1e3fae11 Binary files /dev/null and b/15/umbraco-commerce/how-to-guides/images/product-bundles/product-variant-details.png differ diff --git a/15/umbraco-commerce/how-to-guides/member-based-pricing.md b/15/umbraco-commerce/how-to-guides/member-based-pricing.md new file mode 100644 index 00000000000..aff0292e6f4 --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/member-based-pricing.md @@ -0,0 +1,146 @@ +--- +description: Learn how to implement member-based pricing in Umbraco Commerce. +--- + +# Implementing Member Based Pricing + +By default, Umbraco Commerce uses a single price for a product. However, in some cases, you may want to have different prices for different customers. In this guide, you learn how to implement member-based pricing in Umbraco Commerce. + +## Member Configuration + +1. Creating the Member Groups to use for the member-based pricing. In this example two member groups are created: _Platinum_ and _Gold_. + +![Member Groups](images/member-based-pricing/member-groups.png) + +2. Create one Member for each group: + +![Members](images/member-based-pricing/members.png) + +## Property Editor Configuration + +Next, you will create a new property editor for the member-based pricing. The in-built Block List Editor is used for this. + +1. Create a `Member Price` element type with a `Price` and `Member Group` property. +2. Use the default Umbraco Commerce `Price` property editor for the `Price` property. +3. Use the in-built `Member Group Picker` property editor for the `Member Group` property. + +![Member Price Element](images/member-based-pricing/member-price-element.png) + +4. Open the **Product** Document Type. +5. Add a new `Member Price` property using a new Block List Property editor configuration. +6. Select the `Member Price` element type as the only allowed block type. + +![Member Price Block List Configuration](images/member-based-pricing/member-price-block-list.png) + +7. Navigate to the Content section. +8. Assign member-based pricing for any product you wish. +9. Populate the `Member Price` field with the required Member Group and price combination. + +![Member Group Price](images/member-based-pricing/member-price-content.png) + +## Product Adapter + +With the prices defined, it's time to configure Umbraco Commerce to select the correct price based on the logged-in Member. This is done by creating a custom product adapter to override the default product adapter and select the correct price. + +{% code title="MemberPricingProductAdapter.cs" %} + +```csharp +public class MemberPricingProductAdapter : UmbracoProductAdapter +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMemberService _memberService; + private readonly IMemberGroupService _memberGroupService; + private readonly UmbracoCommerceContext _umbracoCommerce; + + public MemberPricingProductAdapter( + IUmbracoContextFactory umbracoContextFactory, + IContentService contentService, + PublishedContentWrapperFactory publishedContentWrapperFactory, + IExamineManager examineManager, + PublishedContentHelper publishedContentHelper, + IUmbracoProductNameExtractor umbracoProductNameExtractor, + UmbracoCommerceServiceContext services, + IHttpContextAccessor httpContextAccessor, + IMemberService memberService, + IMemberGroupService memberGroupService, + UmbracoCommerceContext umbracoCommerce) + : base(umbracoContextFactory, contentService, publishedContentWrapperFactory, examineManager, publishedContentHelper, umbracoProductNameExtractor, services) + { + _httpContextAccessor = httpContextAccessor; + _memberService = memberService; + _memberGroupService = memberGroupService; + _umbracoCommerce = umbracoCommerce; + } + + public override async Task GetProductSnapshotAsync(Guid storeId, string productReference, string productVariantReference, string languageIsoCode, CancellationToken cancellationToken = default) + { + var baseSnapshot = (UmbracoProductSnapshot)await base.GetProductSnapshotAsync(storeId, productReference, productVariantReference, languageIsoCode, cancellationToken); + + if (_httpContextAccessor.HttpContext?.User.Identity is { IsAuthenticated: true } + && baseSnapshot is { Content: Product { MemberPrice: not null } productPage } + && productPage.MemberPrice.Any()) + { + var memberId = _httpContextAccessor.HttpContext.User.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; + var memberGroupName = _memberService.GetAllRoles(int.Parse(memberId)).First(); + var memberGroupId = (await _memberGroupService.GetByNameAsync(memberGroupName))!.Id; + + var memberPrice = productPage.MemberPrice + .Select(x => x.Content as MemberPrice) + .FirstOrDefault(x => int.Parse(x.MemberGroup) == memberGroupId); + + if (memberPrice != null) + { + var list2 = new List(); + + var currencies = await _umbracoCommerce.Services.CurrencyService.GetCurrenciesAsync(baseSnapshot.StoreId); + foreach (var currency in currencies) + { + var productPrice = memberPrice.Price!.TryGetPriceFor(currency.Id); + if (memberPrice.Price != null && productPrice.Success) + { + list2.Add(new ProductPrice(productPrice.Result!.Value, productPrice.Result.CurrencyId)); + } + } + + baseSnapshot.Prices = list2; + } + } + + return baseSnapshot; + } +} +``` +{% endcode %} + + +Add the following to a `Composer` file to register the custom product adapter: + +{% code title="SwiftShopComposer.cs" %} + +```csharp +internal class SwiftShopComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + } +} +``` + +{% endcode %} + +## Results + +With all this implemented, the product page will display the correct price based on the logged-in Member. + +The expected result for the standard product page: + +![Default Product Page](images/member-based-pricing/default-product-page.png) + +The expected result for a _Gold_ Member: + +![Gold Product Page](images/member-based-pricing/gold-product-page.png) + +The expected result for a _Platinum_ Member: + +![Platinum Product Page](images/member-based-pricing/platinum-product-page.png) diff --git a/15/umbraco-commerce/how-to-guides/member-portal.md b/15/umbraco-commerce/how-to-guides/member-portal.md new file mode 100644 index 00000000000..3d068c71880 --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/member-portal.md @@ -0,0 +1,251 @@ +--- +description: Learn how to build a members portal in Umbraco Commerce. +--- + +# Building a Members Portal + +A members portal is a private area of your website where customers can access their order history, manage their account details, and view personalized content. This guide will show you how to build a members portal in Umbraco Commerce. + +## Setting Up the Members + +The first step in building a members portal is to create a Member Type for your customers. This Member Type will define the properties that customers can have, such as their name, email address, and password. + +### Creating a Member Group + +1. Navigate to the **Members** section of the backoffice. +2. Click the **+** button next to the **Member Groups** heading in the navigation to create a new Member Type. +3. Enter a name for the Member Group, such as `Customer`. + +![Customer Member Group](images/member-portal/customer-member-group.png) + +4. Click the **Save** button to create the Member Group. + +### Assigning Members to the Customer Member Group + +1. Navigate to the **Members** section of the backoffice. +2. Click on the **Members** tab in the navigation. +3. Click on the Member you want to assign to the `Customer` Member Group. +4. Select the `Customer` Member Group in the **Member Group** property. + +![Example Customer](images/member-portal/customer-assign-member-group.png) + +5. Click the **Save** button to assign the Member to the `Customer` Member Group. + +## Setting Up the Member Area + +The next step in building a members portal is to create the pages and templates that will make up the members area of your website. + +### Document Type Setup + +1. Navigate to the **Settings** section of the backoffice. +2. Create two new Document Types: `Customer Portal` and `Login`. + +![Customer Portal Document Types](images/member-portal/document-types.png) + +3. Update your site root Document Type to include the `Customer Portal` and `Login` Document Types as child-pages. + +![Allowed Children Configuration](images/member-portal/allowed-children.png) + +### Content Setup + +1. Navigate to the **Content** section of the backoffice. +2. Create a new page using the `Customer Portal` Document Type and name it `Customer Portal`. +3. Create a new page using the `Login` Document Type and name it `Login`. + +![Customer Portal Content Structure](images/member-portal/content-structure.png) + +4. Expand the context menu for the `Customer Portal` node by clicking the three dots. +5. Click on the **Public Access** option. +6. Choose the **Group based protection** option in the **Public Access** dialog and select **Next**. +7. Select the `Customer` Member Group for the group option. +8. Select the `Login` node for the login and error page options. + +![Public Access Configuration](images/member-portal/public-access.png) + +9. Click **Save** to apply the public access settings. + +## Implementing a Member Login + +To access the members portal, customers need to log in. Through the following steps a login form allowing customers to enter their username and password to access the portal is created. + +1. Open the `Login.cshtml` template file. +2. Add the following code to create a login form: + +{% code title="Login.cshtml" %} + +```csharp +@using (Html.BeginUmbracoForm("HandleLogin", new { RedirectUrl = "/customer-portal" })) +{ +
+ +
+ + + +
+ +
+ + + +
+ + +} +``` + +{% endcode %} + +{% hint style="info" %} +The `UmbLoginController` class comes pre-installed with Umbraco. It handles the login process, so you don't need to create a custom controller. +{% endhint %} + +On the frontend, customers can enter their username and password and click the **Login** button to access the members portal. + +![Login Page](images/member-portal/login-page.png) + +## Displaying Member Order History + +Now that members can log in, update the `Customer Portal` page to display the order history for the logged-in member. + +1. Open the `CustomerPortal.cshtml` template file. +2. Add the following code to display the order history: + +{% code title="CustomerPortal.cshtml" %} + +```csharp +@inject IMemberManager memberManager +@inject IUmbracoCommerceApi commerceApi +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + + var currentMember = await memberManager.GetCurrentMemberAsync(); + var orders = currentMember != null + ? await commerceApi.GetFinalizedOrdersForCustomerAsync(store.Id, currentMember.Email) + : Enumerable.Empty(); +} + +@if (orders.Count() > 0) +{ + + + + + + + + @foreach (var order in orders) + { + + + + + + } + +
Order NumberDateTotal
@order.OrderNumber@order.FinalizedDate?.ToString("MMM, d yyyy")@(await order.TotalPrice.Value.FormattedAsync())
+} +else +{ +

You haven't placed any orders yet

+} +``` + +{% endcode %} + +The `Customer Portal` page will now display a table of the member's order history, including the order number, date, and total price. + +![Order History](images/member-portal/order-history.png) + +### Assigning Orders to a Customer + +The order history will display all orders that have been finalized for the logged-in member. Orders created whilst the member is logged in will automatically be associated with the member. If you wish to assign an order to a member at any point, you can use the API method: + +```csharp +writableOrder.AssignToCustomer(member.Key.ToString()); +``` + +## Extras + +### Displaying Member Login Status + +In your site header, add the following code to display the member login status: + +{% code title="Header.cshtml" %} + +```csharp +@{ + var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false; + if (isLoggedIn) + { + @Context.User?.Identity?.Name + @using (Html.BeginUmbracoForm("HandleLogout", new { RedirectUrl = rootPage.Url() }, new { @name = "logoutForm" })) + { + + } + } + else + { + Login + } +} +``` + +{% endcode %} + +![Logged Out Status](images/member-portal/logged-out.png) + +![Logged In Status](images/member-portal/logged-in.png) + +### Registering a Member + +To allow customers to register as members, you can create a registration form allowing customers to enter their name, email address, and password. + +1. Implement a registration Document Type and page in the same way as the login page. +2. Open the `Register.cshtml` template file and add the following code to create a registration form: + +{% code title="Register.cshtml" %} + +```csharp +@using (Html.BeginUmbracoForm("HandleRegisterMember", new { RedirectUrl = "/customer-portal", UsernameIsEmail = true })) +{ +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +} +``` + +{% endcode %} + +{% hint style="info" %} +The `UmbRegisterController` class comes pre-installed with Umbraco. It handles the login process, so you don't need to create a custom controller. +{% endhint %} + +On the frontend, customers can enter their name, email address, and password to register as a member. + +![Register Page](images/member-portal/register-page.png) diff --git a/15/umbraco-commerce/how-to-guides/personalized-products.md b/15/umbraco-commerce/how-to-guides/personalized-products.md new file mode 100644 index 00000000000..083eb09217b --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/personalized-products.md @@ -0,0 +1,125 @@ +--- +description: Learn how to implement personalized products in Umbraco Commerce. +--- + +# Implementing Personalized Products + +Personalized products can be customized by the customer. This customization can be adding a message or selecting different options for the product. This guide will show you how to implement personalized products in Umbraco Commerce. + +This will be broken down into the following steps: + +* Add a message field on the product page in the add-to-cart form +* Save the details in an order line [property](../key-concepts/properties.md) +* Register a UI extension to display the value in the Backoffice. + +{% hint style="info" %} +This guide is not a direct follow-on from the [getting started tutorial](../tutorials/build-a-store/overview.md). It is assumed that your store is set up in a similar structure. +{% endhint %} + +## Capturing a Message + +On the frontend, add a text area to the product page where the customer can enter their message. + +![Customer Message Field](images/personalized-products/observations-collapsed.png) + +## Saving the Message as an Order Line Property + +When the customer adds the product to the cart, the message will be saved in an order line property. + +1. Add an `Observations` property of the `AddToCartDto` to capture the message. + +{% code title="AddToCartDto.cs" %} + +```csharp +public class AddToCartDto +{ + ... + + public string? Observations { get; set; } +} +``` + +{% endcode %} + +2. Locate the `AddToCart` of the `CartSurfaceController`. +2. Set a property on the order line if a value has been sent with the request. + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpPost] +public async Task AddToCart(AddToCartDto postModel) +{ + try + { + await _commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage.GetStore(); + var order = await _commerceApi.GetOrCreateCurrentOrderAsync(store.Id) + .AsWritableAsync(uow) + .AddProductAsync(postModel.ProductReference, decimal.Parse(postModel.Quantity), new Dictionary{ + { "productObservations", postModel.Observations } + }); + + await _commerceApi.SaveOrderAsync(order); + + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ... + } +} +``` + +{% endcode %} + +## Accessing the Property in the Backoffice + +To view the data in the Backoffice order editor, you need to register an `ucOrderProperty` extension along with the relevant label localizations as sampled below. + +Create a new `umbraco-package.json` file in a folder in the `App_Plugins` directory in the root of your project and add the following code: + +{% code title="umbraco-package.json" %} + +````csharp +{ + "name": "SwiftShop", + "extensions": [ + { + "type": "ucOrderLineProperty", + "alias": "Uc.OrderLineProperty.ProductObservations", + "name": "Product Observations", + "weight": 400, + "meta": { + "propertyAlias": "productObservations", + "showInOrderLineSummary": true, + "summaryStyle": "inline", + "editorUiAlias": "Umb.PropertyEditorUi.TextBox", + "labelUiAlias": "Umb.PropertyEditorUi.Label" + } + }, + { + "type": "localization", + "alias": "Uc.OrderLineProperty.ProductObservations.EnUS", + "name": "English", + "meta": { + "culture": "en", + "localizations": { + "section": { + "ucProperties_productObservationsLabel": "Observations", + "ucProperties_productObservationsDescription": "Customer product observations" + } + } + } + } + ] +} +```` + +{% endcode %} + +The property is displayed in the Backoffice order editor. + +![Backoffice Order Line Property](images/personalized-products/order-line-property.png) diff --git a/15/umbraco-commerce/how-to-guides/product-bundles.md b/15/umbraco-commerce/how-to-guides/product-bundles.md new file mode 100644 index 00000000000..88429b3671e --- /dev/null +++ b/15/umbraco-commerce/how-to-guides/product-bundles.md @@ -0,0 +1,170 @@ +--- +description: Learn how to implement product bundles in Umbraco Commerce. +--- + +# Implementing Product Bundles + +Product bundles are Umbraco Commerces' way of creating composite products. This feature allows you to create a product that consists of multiple sub-products. The sub-products can be optional or mandatory, and you can define the quantity of each sub-product. The final order line will be a composite order line of the selected primary product and its sub-product options. + +{% hint style="info" %} +This guide is not a direct follow-on from the [getting started tutorial](../tutorials/build-a-store/overview.md). It is assumed that your store is set up in a similar structure. +{% endhint %} + +## Product Setup + +To create a product bundle, you need to create a primary product with multiple sub-products. The most common approach is to create a product page with a structure that allows child nodes of a product variant type. + +1. Navigate to the Settings section in the Umbraco backoffice. +2. Create a Document Type for your primary product. + +![Bundle Document Type](images/product-bundles/bundle-document-type.png) + +2. Create a Document Type for your sub-products. + +![Product Variant Details](images/product-bundles/product-variant-details.png) + +3. Update your primary product Document Type to allow child nodes of the sub-product type. + +![Product Variant Children](images/product-bundles/bundle-document-type-structure.png) + +## Content Configuration + +The bundle content tree will contain a bundle page with variant elements as children. + +1. Navigate to the Content section. +2. Create a new product page with the primary product Document Type. +3. Add variant elements as children. + +![Product Variant Children](images/product-bundles/product-variant-children.png) + +## Frontend Configuration + +The base product page will display the product details with a list of variants that can be used as add-ons. + +Add the following to your product page template. + +{% code title="ProductPage.cshtml" %} + +```csharp +@using (Html.BeginUmbracoForm("AddToCart", "CartSurface", FormMethod.Post)) +{ + @Html.Hidden("bundleProductReference", Model.GetProductReference()) + @Html.Hidden("quantity", 1) + +
+ +

@Model.Headline

+

@Model.Description

+ +
+ @Model.Name +

@Model.Title

+

Base Price: @(await Model.GetFormattedPriceAsync())

+
+ +
    + @foreach (var item in Model.Children()) + { +
  • + +
  • + } +
+ + + +
+} +``` + +{% endcode %} + +![Product with Bundle](images/product-bundles/product-bundles.png) + +## Add to Cart Updates + +With the frontend setup, you must update the add-to-cart functionality to handle the bundle product and its sub-products. + +1. Update the `AddToCartDto` object to include the bundle product reference and an array of variant product references. + +{% code title="AddToCartDto.cs" %} + +```csharp +public class AddToCartDto +{ + ... + public string? BundleProductReference { get; set; } + public string[] BundleItemReferences { get; set; } +} +``` + +{% endcode %} + +2. Update the `AddToCart` method on your `CartSurfaceController` to handle the bundle product and its sub-products. + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpPost] +public async Task AddToCart(AddToCartDto postModel) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + StoreReadOnly store = CurrentPage!.GetStore()!; + Order? order = await commerceApi.GetOrCreateCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow); + + if (postModel.BundleProductReference is null) + { + // Not a bundle so add the product directly + await order.AddProductAsync(postModel.ProductReference, postModel.Quantity); + } + else // Bundle product so add the bundle and its items + { + // Create a unique bundle id + var bundleId = Guid.NewGuid().ToString(); + + // Add the bundle product + await order.AddProductAsync(postModel.BundleProductReference, 1, bundleId); + + // Add the bundle items to the bundle + foreach (var itemRef in postModel.BundleItemReferences) + { + await order.AddProductToBundleAsync(bundleId, itemRef, 1); + } + } + + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("productReference", "Failed to add product to cart"); + + return CurrentUmbracoPage(); + } + + TempData["successMessage"] = "Product added to cart"; + + return RedirectToCurrentUmbracoPage(); +} +``` + +{% endcode %} + +When a user adds a product including variants to the cart, the order is created with the primary product and its sub-products combined. + +## Order Editor View + +When an order includes a bundled product, the order editor will display the primary product and its sub-products as a composite order line. + +![Order Editor](images/product-bundles/order-editor.png) diff --git a/15/umbraco-commerce/how-to-guides/update-cart.md b/15/umbraco-commerce/how-to-guides/update-cart.md deleted file mode 100644 index acc5f71ee2f..00000000000 --- a/15/umbraco-commerce/how-to-guides/update-cart.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -description: Learn how to update your cart when one or more quantities have changed. ---- - -# Update Cart - -Functionality is needed to update the cart once an item has been added. In this guide, you can learn how to add this functionality. - -You need a new page to summarize the items in the cart and allow users to update each item. - -Create a new Document With a Template. Call it "Cart Page" and update the template with the following code: - -```csharp -@inherits UmbracoViewPage -@{ - var store = Model.Value("store", fallback: Fallback.ToAncestors); - var currentOrder = CommerceApi.Instance.GetCurrentOrder(store!.Id); - if (currentOrder == null) return; -} -``` - -- You need to access the store to see the relevant data for the current cart/order. The store has a `fallback` property allowing you to traverse the tree to find the store. -- `currentOrder` is used to get the current order for the store. If the current order is null then there is nothing to display. - -To display the default layout when an order does exist, you need to add some markup or amend it to include the desired functionality. Add the following code to the template: - -```csharp -@using (Html.BeginUmbracoForm("UpdateCart", "CartSurface")) -{ - @foreach (var item in currentOrder.OrderLines.Select((ol, i) => new - { - OrderLine = ol, - Index = i - })) - { -

- @Html.Hidden($"orderLines[{item.Index}].Id", item.OrderLine.Id) - @item.OrderLine.Name | @Html.TextBox($"orderLines[{item.Index}].Quantity", (int)item.OrderLine.Quantity, new { @type = "number" }) - @Html.Hidden($"orderLines[{item.Index}].ProductReference", item.OrderLine.ProductReference) - Remove -

- - } - - - - var success = TempData["SuccessMessage"]?.ToString(); - - if (!string.IsNullOrWhiteSpace(success)) - { -
@success
- } -} -``` - -You first loop through each item in the `cart/order` and display the product name and quantity. - -A hidden input is added for the order ID, quantity, and product reference. This is so you can update the cart with the new number. - -```csharp - @Html.Hidden($"orderLines[{item.Index}].OrderId", item.OrderLine.Id) -``` - -The line below sets the ID of the order line (or the item in the current cart/order). - -```csharp - @item.OrderLine.Name @Html.Hidden($"orderLines[{item.Index}].Quantity", (int)item.OrderLine.Quantity, new { @type = "number" }) -``` - -As well as setting the product name, the line below sets the quantity of the product in the cart/order. Finally, the number is set to a number input type. - -```csharp - @Html.Hidden($"orderLines[{item.Index}].ProductReference", item.OrderLine.ProductReference) -``` - -This is setting the product reference in the cart/order so there is a way to distinguish between products. This is hidden as it does not need to be displayed to the user. - -{% hint style="warning" %} - -The `remove` button is added here but is not covered in this guide. Learn more in the [Delete item from Cart](delete-item.md) article. - -{% endhint %} - -Finally, a button is added to submit the form to update the cart. This will call the `UpdateCart` action in the `CartSurfaceController` which will then show a success message to the user. - -## Adding the Controller - -Create a new Controller called `CartSurfaceController.cs` - -{% hint style="warning" %} - -The namespaces used in this Controller are important and need to be included. - -```cs -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Routing; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.Website.Controllers; -using Umbraco.Commerce.Common.Validation; -using Umbraco.Commerce.Core.Api; -using Umbraco.Commerce.Core.Models; -using Umbraco.Commerce.Extensions; -using Umbraco.Extensions; -``` - -{% endhint %} - -```csharp -public class CartSurfaceController : SurfaceController -{ - public CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoCommerceApi commerceApi) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) - { - _commerceApi = commerceApi; - } -} -``` - -The following is the equivalent code for having this as a Primary Constructor: - -```csharp -public class CartSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IUmbracoCommerceApi commerceApi) - : SurfaceController(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) -{ -} -``` - -The `CartDto` is a class that passes data to the Controller. This is a class that has a property for the `productReference` and an array of `OrderLineQuantityDto[]`. - -```csharp - public class CartDto - { - public string ProductReference { get; set; } - public OrderLineQuantityDto[] OrderLines { get; set; } - } - - public class OrderLineQuantityDto - { - public Guid Id { get; set; } - public decimal Quantity { get; set; } - } -``` - -{% hint style="warning" %} -The code example above adds the `ProductReference` but it is not used in this guide. - -It is an example of passing the product reference to the controller for similar tasks. -{% endhint %} - -You need to add the `Action` to update the items in the cart. This will be called when the button is clicked. - -```csharp -[HttpPost] - public IActionResult UpdateCart(CartDto cart) - { - try - { - _commerceApi.Uow.Execute(uow => - { - var store = CurrentPage?.Value("store", fallback: Fallback.ToAncestors); - - if (store == null) return; - - var order = _commerceApi.GetCurrentOrder(store.Id) - .AsWritable(uow); - - foreach (var orderLine in cart.OrderLines) - { - order.WithOrderLine(orderLine.Id) - .SetQuantity(orderLine.Quantity); - } - - _commerceApi.SaveOrder(order); - - uow.Complete(); - }); - } - catch (ValidationException) - { - ModelState.AddModelError(string.Empty, "Failed to update cart"); - - return CurrentUmbracoPage(); - } - - TempData["SuccessMessage"] = "Cart updated"; - - return RedirectToCurrentUmbracoPage(); - } -``` - -- A `try-catch` block captures any validation errors that may occur when updating items in the cart. -- The `store` variable is used to access the store to retrieve the store ID. -- `order` is used to retrieve the current order. In the Commerce API, everything is read-only for performance so you need to make it writable to add the product. -- You loop through all the `orderLines(items)` in the cart, set the new quantity amount set in the View, and pass it to the CartDto model. -- `SaveOrder` is called to save the order. -- If there are any validation errors, they are added to `ModelState` error, and the user is redirected back to the current page. -- `TempData` stores a message to be displayed to the user if the product has been successfully updated. - -{% hint style="warning" %} -Umbraco Commerce uses the Unit of Work pattern to complete saving the item (`uow.Complete`). When retrieving or saving data you want the entire transaction to be committed. However, if there is an error nothing is changed on the database. -{% endhint %} - -If you have followed the [Add item to cart](add-item.md) article then run the application, add an item to your cart, and navigate to your `cart.cshtml` page. Enter a new quantity, click the Update Cart button, and the item(s) in your cart will tell you the values have been updated. diff --git a/15/umbraco-commerce/tutorials/images/property-editors.png b/15/umbraco-commerce/key-concepts/images/property-editors.png similarity index 100% rename from 15/umbraco-commerce/tutorials/images/property-editors.png rename to 15/umbraco-commerce/key-concepts/images/property-editors.png diff --git a/15/umbraco-commerce/key-concepts/properties.md b/15/umbraco-commerce/key-concepts/properties.md index 08ad1347caf..2303d32bdda 100644 --- a/15/umbraco-commerce/key-concepts/properties.md +++ b/15/umbraco-commerce/key-concepts/properties.md @@ -62,7 +62,7 @@ On occasions where Umbraco Commerce needs to capture some information about an O Umbraco Commerce has a built-in mechanism that can be configured to automatically copy properties from a Product information source to the Order Line automatically. This is done by using the **Product Property Aliases** field on the Store settings screen. -![Product Property Aliases Configuration](../media/product\_property\_aliases.png) +![Product Property Aliases Configuration](../media/product_property_aliases.png) When a Product is added to the Order containing a comma-separated list of property aliases, the property values are automatically copied to the Order Lines Properties collection. @@ -81,7 +81,7 @@ A good example of this is when you have configurable products, such as customiza Product uniqueness is configured via the **Product Uniqueness Property Aliases** field on the Store setting screen. -![Product Uniqueness Property Aliases Configuration](../media/product\_uniqueness\_property\_aliases.png) +![Product Uniqueness Property Aliases Configuration](../media/product_uniqueness_property_aliases.png) When set to a comma-separated list of property aliases and a Product is added to an Order, the properties are compared against all pre-existing Order Lines for that Product. Should their values be different, then a unique Order Line will be created for that Product. @@ -143,4 +143,4 @@ builder.WithOrderPropertyConfigs() //Notes .For(x => x.Notes.CustomerNotes).MapFrom("customerNotes") .For(x => x.Notes.InternalNotes).MapFrom("internalNotes")); -``` \ No newline at end of file +``` diff --git a/15/umbraco-commerce/key-concepts/umbraco-properties.md b/15/umbraco-commerce/key-concepts/umbraco-properties.md index f46b4936cbd..ff9d8b42825 100644 --- a/15/umbraco-commerce/key-concepts/umbraco-properties.md +++ b/15/umbraco-commerce/key-concepts/umbraco-properties.md @@ -93,3 +93,18 @@ Umbraco Commerce uses Umbraco nodes as its source of information. In order for U + +## Property Editors + +Umbraco Commerce includes default property editors that help manage and configure eCommerce functionalities within the Umbraco backoffice. + +![Built-in Property Editors](images/property-editors.png) + +The available property editors include: + +* **Price:** Used to manage and define product pricing. +* **Store Picker:** Allows selection of a specific store for products or configurations. +* **Store Entity Picker:** Used for selecting store entities such as currencies, locations and payment / shipping methods. +* **Stock:** Helps manage stock levels for products. +* **Measurements:** Allows the configuration of product dimensions and weight. +* **Variants Editor:** Used for managing [complex product variants](product-variants/complex-variants.md), such as sizes or colors. diff --git a/15/umbraco-commerce/tutorials/build-a-store/cart.md b/15/umbraco-commerce/tutorials/build-a-store/cart.md new file mode 100644 index 00000000000..d949f4a07b4 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/cart.md @@ -0,0 +1,19 @@ +--- +description: Learn how to implement a shopping cart in Umbraco Commerce. +--- + +# Implementing a Shopping Cart + +When it comes to implementing a shopping cart in Umbraco Commerce, there are two options available to you. + +## [Using the Umbraco.Commerce.Cart Drop-in Shopping Cart](https://docs.umbraco.com/umbraco-commerce-packages/cart/cart) + +The [Umbraco.Commerce.Cart](https://docs.umbraco.com/umbraco-commerce-packages/cart/cart) add-on is a great starting point for most stores. It provides a ready-to-use shopping cart experience. + +For details on how to install and configure the Umbraco.Commerce.Cart add-on, see the [Umbraco.Commerce.Cart](https://docs.umbraco.com/umbraco-commerce-packages/cart/cart) documentation. + +## Creating a Custom Shopping Cart + +If you need a more custom shopping cart experience, you can build your own shopping cart using the Umbraco Commerce API. This approach gives you full control over the shopping cart experience, allowing you to tailor it to your specific requirements. + +For details on how to build a custom shopping cart, see the [Creating a Custom Shopping Cart](custom-cart.md) article. diff --git a/15/umbraco-commerce/tutorials/build-a-store/checkout.md b/15/umbraco-commerce/tutorials/build-a-store/checkout.md new file mode 100644 index 00000000000..9a63e6d866d --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/checkout.md @@ -0,0 +1,19 @@ +--- +description: Learn how to implement a checkout flow in Umbraco Commerce. +--- + +# Implementing a Checkout Flow + +When it comes to implementing a checkout in Umbraco Commerce, there are two options available to you. + +## [Using the Umbraco.Commerce.Checkout Drop-in Checkout Flow](https://docs.umbraco.com/umbraco-commerce-packages/checkout/checkout) + +The [Umbraco.Commerce.Checkout](https://docs.umbraco.com/umbraco-commerce-packages/checkout/checkout) add-on is a great starting point for most stores. It provides a ready-to-use checkout experience. + +For details on how to install and configure the `Umbraco.Commerce.Checkout` add-on, see the [Umbraco.Commerce.Checkout](https://docs.umbraco.com/umbraco-commerce-packages/checkout/checkout) documentation. + +## Creating a Custom Shopping Cart + +If you need a more custom checkout experience, you can build your own checkout flow using the Umbraco Commerce API. This approach gives you full control over the checkout experience, allowing you to tailor it to your specific requirements. + +For details on how to build a custom checkout flow, see the [Creating a Custom Checkout Flow](custom-checkout.md) article. diff --git a/15/umbraco-commerce/tutorials/build-a-store/configure-store.md b/15/umbraco-commerce/tutorials/build-a-store/configure-store.md new file mode 100644 index 00000000000..4217eb10775 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/configure-store.md @@ -0,0 +1,156 @@ +--- +description: Learn how to configure your store in Umbraco Commerce. +--- + +# Configuring your Store + +Each store comes with a set of predefined configurations that you can extend, covering: + +* [Locations](#setting-up-a-location) +* [Order Statuses](#setting-up-order-statuses) +* [Payment Methods](#setting-up-payment-methods) +* [Shipping Methods](#setting-up-shipping-methods) +* [Countries](#setting-up-a-country) +* [Currencies](#setting-up-a-currency) +* [Taxes](#setting-up-taxes) +* [Templates](#setting-up-templates) + +## Setting up a Location + +If your business operates in multiple regions, setting up locations helps: + +* Configure stores for different locations with separate languages, shipping addresses, regional offers, local regulations, and payment gateways. +* Ship products from different locations. The system can be set up to route orders to the nearest warehouse based on the customer’s location. + +### Steps to set up a location: + +1. Select your store from the **Stores** menu in the **Settings** section. In this case, *Umbraco Swag Store*. +2. Go to **Locations** under the Store. +3. Click **Create Location**. +4. Enter the **Name** for the Location. For example: *Denmark* +5. Provide the necessary address details. + +![Create Location](../images/create-location.png) + +6. **Save** the changes. + +## Setting up Order Statuses + +Order Status tracks the progression of an order. It helps both the store owner and customers track the order's progress from the moment it is placed until it is delivered (or returned). + +When you first set up Umbraco Commerce, it comes with predefined order statuses to help manage the order lifecycle. These statuses include _New_, _Completed_, _Cancelled_, and _Error_. The statuses can be customized based on your specific business requirements. + +### Steps to create an order status: + +1. Go to **Order Statuses** under the Store. +2. Click **Create Order Status**. +3. Enter a **Name** for the order status. For Example: *Processing* +4. Select a **Color** for the order status. + +![Create Order Status](../images/create-order-status.png) + +5. **Save** the changes. + +## Setting up Payment Methods + +Payment Methods define the payment options available in the store. By default, Umbraco Commerce includes basic providers like **Invoicing** and **Zero Value** to get started. + +Umbraco Commerce also supports the integration of different third-party payment gateways. For more information, see the [Umbraco Commerce Payment Providers Documentation](../../../../commerce-add-ons/payment-providers/README.md). + +### Steps to set up a payment method: + +1. Go to **Payment Methods** under the Store. +2. Click **Create Payment Method**. +3. Select a payment provider from the list. For example: *Zero Value*. +4. Enter a **Name** for the payment method. For example: *Zero Payment*. +5. Configure the payment method as per your requirements. + +![Create Payment Method](../images/create-payment-methods.png) + +6. **Save** the changes. + +## Setting up Shipping Methods + +Shipping methods determine how customers receive their orders. Setting up shipping methods effectively is crucial, as it impacts customer satisfaction, fulfillment costs, and overall operational efficiency. + +By default, Umbraco Commerce comes with the basic Pickup option. For more information on the integration for different providers, see the [Umbraco Commerce Shipping providers Documentation](../../../../commerce-add-ons/shipping-providers/README.md). + +### Steps to create a shipping method: + +1. Go to **Shipping Methods** under the Store. +2. Click **Create Shipping Method**. +3. Choose the shipping provider from the list. For Example: *DHL*. +4. Enter a **Name** for the shipping method. For example: *DHL*. +5. Configure the shipping method as per your requirements. + +![Create Shipping Method](../images/create-shipping-method.png) + +6. **Save** the changes. + +## Setting up a Country + +Setting up a country involves configuring settings related to shipping, payment methods, tax rates, localization, legal compliance requirements, and so on for that specific country. + +### Steps to set up a country: + +1. Go to **Countries** under the Store. +2. Click **Create Country**. +3. Choose an item from the list. For Example: *Create Country from ISO 3166 preset*. +4. Select a country from the list. For example: *Denmark*. +5. Configure the country details as per your requirements. + +![Create Country](../images/create-country.png) + +6. **Save** the changes. + +## Setting up a Currency + +Setting up currency is essential for ensuring that prices are displayed and transactions are processed accurately. For information on configuring an exchange rate service, see the [Currency Exchange Rate Service Provider](../key-concepts/currency-exchange-rate-service-providers.md) article. + +### Steps to set up a currency: + +1. Go to **Currencies** under the Store. +2. Click **Create Currency**. +3. Enter a **Name** for the currency. For Example: `DKK`. +4. Configure the currency details as per your requirements. + +![Create Currency](../images/create-currency.png) + +5. **Save** the changes. + +## Setting up Taxes + +Tax setup is crucial for compliance with local regulations and for ensuring that your pricing is accurate and transparent. You can set up tax rates for each jurisdiction where you must collect tax. For more information, see the [Tax Sources](../key-concepts/tax-sources.md) article. + +### Steps to set up taxes: + +1. Go to **Taxes** under the Store. +2. Click **Create Tax Class**. +3. Enter a **Name** for the tax class. For Example: *Custom*. +4. Configure the tax rates as per your requirements. + +![Create Tax Class Rate](../images/create-tax-rate.png) + +5. **Save** to changes. + +## Setting up Templates + +Defines the different **Email**, **Print**, and **Export** templates available for the store. These templates help maintain consistency and professionalism in communication with customers and facilitate data handling. + +### Steps to create an Email Template: + +1. Expand the **Templates** folder under the Store. +2. Go to **Email Templates**. +3. Click **Create Email Template**. +4. Enter a **Name** for the Email template. For Example: *Shipping Notification*. +5. Configure the email details as per your requirements. + +![Create Email Template](../images/create-email-template.png) + +6. **Save** the changes. + +Similarly, you can create custom **Print** and **Export** Templates. + +## Setting up Store Defaults + +In addition to the above settings, you can configure a series of default settings on a store from the store editor. See the [Stores reference](../../reference/stores/README.md) article for more information. diff --git a/15/umbraco-commerce/tutorials/build-a-store/create-product.md b/15/umbraco-commerce/tutorials/build-a-store/create-product.md new file mode 100644 index 00000000000..928e8131c71 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/create-product.md @@ -0,0 +1,69 @@ +--- +description: Learn how to create your first product in Umbraco Commerce. +--- + +# Creating your first Product + +By default, products in Umbraco Commerce are regular content nodes. These can consist of any number of properties depending on the site design. To be considered a product, however, a Document Type must contain the fields `Sku` and `Price`, with their corresponding aliases `sku` and `price`. + +## Create a Product Composition + +Because any node in Umbraco can be a product, the first step is to create a **Product** Composition. This composition can be applied to any Document Type to make it a product. + +1. Navigate to the **Settings** section of the backoffice. +2. Create a new Element Type called **Product**. +3. Add an SKU property with the alias `sku` and select the `Text Box Editor`. +4. Add a Price property with the alias `price` and select the `Price Property Editor` that comes with Umbraco Commerce. + +![Umbraco Commerce Property Editors](../images/blendid/commerce_property_editors.png) + +![Product Composition](../images/blendid/product_composition.png) + +6. Click **Save** to create the Element Type. + +## Create a Product Document Type + +With the product composition created, you can now create a Document Tyoe for a product. + +1. Navigate to the **Settings** section of the backoffice. +2. Create or open a Document Type that you want to use as a product. +3. Click the **Compositions** button. +4. Select the `Product` composition you created earlier and **Submit**. + +![Umbraco Commerce Property Editors](../images/blendid/product_pick_composition.png) + +5. Add any other properties to your Document Type as needed for your product. + +![Umbraco Commerce Property Editors](../images/blendid/product_page_doctype.png) + +6. Click **Save** to save the changes. + +## Allow Creating Product Pages + +If you haven't already, you'll need to allow the product Document Type to be a child node of the store root or product container Document Type. + +1. Open your store root or product container Document Type to edit. +2. Navigate to the **Structure** tab. +3. Add our new Document Type to the **Allowed child node types** property. + +![Allow Product as Child Node](../images/blendid/product_allowed_child_node.png) + +4. Click **Save** to save the changes. + +## Create a Product + +1. Navigate to the **Content** section of the backoffice. +2. Create a new content node somewhere beneath the store root using the Document Type you created earlier. + +![Create Product](../images/blendid/create_product.png) + +3. Fill in the details of the product, including the SKU and Price properties. + +![Product Page Editor](../images/blendid/product_page_editor.png) + +4. Click **Save and Publish** to save the product. +5. Navigate to the frontend of the site to view the product page. + +![Product Page](../images/blendid/product_page.png) + +With the product created, you can now move on to [Implementing a Shopping Cart](cart-management/overview.md). diff --git a/15/umbraco-commerce/tutorials/build-a-store/create-store.md b/15/umbraco-commerce/tutorials/build-a-store/create-store.md new file mode 100644 index 00000000000..9ede52006a2 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/create-store.md @@ -0,0 +1,48 @@ +--- +description: Learn how to create a store in Umbraco Commerce. +--- + +# Creating a Store + +With Umbraco setup and Umbraco Commerce installed, the next step is to create a store. + +A store is an online platform where products or services are listed for customers to browse, purchase, and complete transactions online. + +Setting up a store allows you to manage both the content and commerce aspects of your site. It allows you to create a custom and scalable online shopping experience. For more information, see the [Stores](../../reference/stores/README.md) article. + +## Create a Store + +1. Navigate to the **Settings** section of the backoffice +2. Click the "+" button next to the Stores heading in the navigation area to launch the Create Store dialog. +3. Enter a unique name for your store and click **Create**. + +![Create a store](../images/blendid/create_store.png) + +4. Click **Save** to create the store and auto-populate it with the default configuration. + +![Store editor](../images/blendid/store_settings.png) + +The store is now created and ready to be used. + +In this tutorial, the default configuration is used. For more information on configuring the different aspects of your store, see the [Configuring your Store](configure-store.md) article. + +## Connecting the Store to your website + +With the store defined, the next step is to link the store to the website. This is done by associating the store with the root content node of the website. To do this, you need to add a property to the root content node that allows for selecting the store. + +1. Navigate to the **Settings** section of the backoffice. +2. Edit the root content Document Type. +3. Add a new property to the Document Type using the `Store Picker Property Editor` that comes with Umbraco Commerce. +4. Give the property the alias `store`. + +![Configure Document Type](../images/blendid/homepage_doctype_store_setting.png) + +5. **Save** the changes. +6. Navigate to the root content node in the **Content** section. +7. Select your store via the store property. + +![Umbraco Commerce Store Picker](../images/blendid/homepage_editor_pick_store.png) + +8. **Save and Publish** the changes. + +With the store linked to the website, you can start adding products. For more information, see the [Creating your first Product](create-product.md) article. diff --git a/15/umbraco-commerce/tutorials/build-a-store/custom-cart.md b/15/umbraco-commerce/tutorials/build-a-store/custom-cart.md new file mode 100644 index 00000000000..216b4c6f223 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/custom-cart.md @@ -0,0 +1,486 @@ +--- +description: Learn how to build a custom shopping cart in Umbraco Commerce. +--- + +# Creating a Custom Shopping Cart + +If you need a more custom shopping cart experience, you can build a custom shopping cart using the Umbraco Commerce API. This approach gives you full control over the shopping cart experience, allowing you to tailor it to your requirements. + +## Create a Cart Surface Controller + +Before adding any functionality to the custom shopping cart, you must create a Surface Controller to handle the cart actions. + +1. Create a new class in your project and inherit from `Umbraco.Cms.Web.Website.Controllers.SurfaceController`. +2. Name the class `CartSurfaceController`. +3. Add the following code to the class: + +{% code title="CartSurfaceController.cs" %} + +```csharp +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Commerce.Core.Api; + +namespace Umbraco.Commerce.DemoStore.Controllers; + +public class CheckoutSurfaceController : SurfaceController +{ + private readonly IUmbracoCommerceApi _commerceApi; + + public CheckoutSurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoCommerceApi commerceApi) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + _commerceApi = commerceApi; + } + + // Add your cart actions here +} +``` + +{% endcode %} + +## Adding a Product to the Cart + +To add a product to the cart, create an action method in the `CartSurfaceController`. The action should accept a `productReference` or `productVariantReference` and add the product to the cart. The properties will be wrapped in a DTO class to pass on to the controller. + +1. Create a new class named `AddToCartDto` using the following properties. + +{% code title="AddToCartDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Dtos; + +public class AddToCartDto +{ + public string ProductReference { get; set; } + public string ProductVariantReference { get; set; } + public decimal Quantity { get; set; } = 1; +} +``` + +{% endcode %} + +2. Add the following action method to your `CartSurfaceController`: + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpPost] +public async Task AddToCart(AddToCartDto postModel) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + StoreReadOnly store = CurrentPage!.GetStore()!; + Order? order = await commerceApi.GetOrCreateCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .AddProductAsync(postModel.ProductReference, postModel.ProductVariantReference, postModel.Quantity); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("productReference", "Failed to add product to cart"); + + return CurrentUmbracoPage(); + } + + TempData["successMessage"] = "Product added to cart"; + + return RedirectToCurrentUmbracoPage(); +} +``` + +{% endcode %} + +3. Open the view where you want to add a product to the cart. +4. Create a form that posts to the `AddToCart` action of your `CartSurfaceController`. + +{% code title="ProductPage.cshtml" %} + +```html +
+ @using (Html.BeginUmbracoForm("AddToCart", "CartSurface")) + { + @Html.Hidden("productReference", Model.GetProductReference()) + @Html.Hidden("quantity", 1) + +
+

@Model.Title

+

@Model.Description

+

@(await Model.CalculatePriceAsync().FormattedAsync())

+ +
+ } +
+``` + +{% endcode %} + +On the front end, when the user clicks the "Add to Basket" button, the product will be added to the cart. + +![Add to Cart Success](../images/blendid/product_page_with_notification.png) + +### Showing a Cart Count + +The total cart quantity is managed through a [view component](https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-9.0) that displays a counter near the shopping cart icon. + +1. Create a new view component named `CartCountViewComponent`. + +{% code title="CartCountViewComponent.cs" %} + +````csharp +[ViewComponent] +public class CartCountViewComponent : ViewComponent +{ + public async Task InvokeAsync(IPublishedContent currentPage) + { + var store = currentPage.GetStore(); + var order = await currentPage.GetCurrentOrderAsync(); + + return View("CartCount", (int)(order?.TotalQuantity ?? 0)); + } +} +```` + +{% endcode %} + +2. Create a partial view named `CartCount.cshtml` to display the cart count. + +{% code title="CartCount.cshtml" %} + +```html +@model int +(@Model) +``` + +{% endcode %} + +3. Include the view component in your layout or view to display the cart count. + +{% code title="Layout.cshtml" %} + +```html +Cart @(await Component.InvokeAsync("CartCount", new { currentPage = Model })) +``` + +{% endcode %} + +### Showing Cart Notifications + +1. Create a new class named `NotificationModel`. + +{% code title="NotificationModel.cs" %} + +```csharp +public class NotificationModel +{ + public string Text { get; set; } + public NotificationType Type { get; set; } +} + +public enum NotificationType +{ + Success, + Error +} +``` + +{% endcode %} + +2. Create a partial view named `Notification.cshtml`. + +{% code title="Notification.cshtml" %} + +```csharp +@model Umbraco.Commerce.DemoStore.Models.NotificationModel + + + + +``` + +{% endcode %} + +3. Reference the partial view in your layout or view to display cart notifications. + +{% code title="Layout.cshtml" %} + +```csharp +@{ + var message = TempData["successMessage"] ?? TempData["errorMessage"]; + var type = TempData["successMessage"] != null ? NotificationType.Success : NotificationType.Error; + + if (message != null) + { + await Html.PartialAsync("Notification", new Umbraco.Commerce.DemoStore.Models.NotificationModel + { + Text = message, + Type = type + }) + } +} +``` + +{% endcode %} + +## Displaying the Cart + +You can create a new view that lists the items in the cart and allows the user to update or remove items. + +1. Create a new `Cart` Document Type and add it as a content node beneath the store root. +7. Open the `Cart.cshtml` template created by your Document Type and add the following. + +{% code title="Cart.cshtml" %} + +```csharp +@{ + var order = await Model.GetCurrentOrderAsync(); + + if (order != null && order.OrderLines.Count > 0) + { + using (Html.BeginUmbracoForm("UpdateCart", "CartSurface")) + { + + + + + + + + + + + @foreach (var item in order.OrderLines.Select((ol, i) => new { OrderLine = ol, Index = i })) + { + @Html.Hidden($"orderLines[{item.Index}].Id", orderLine.Id) + + var product = Umbraco.Content(Guid.Parse(item.OrderLine.ProductReference)); + var image = product.Value(nameof(Product.Image)); + + + + + + + + } + + + + + + + + + + + + + +
ProductPriceQuantityTotal
+

@product.Name

+

Remove

+
@(await item.OrderLine.UnitPrice.Value.FormattedAsync()) + @Html.TextBox($"orderLines[{item.Index}].Quantity", (int)item.OrderLine.Quantity, new { @type = "number", @class = "form-control text-center quantity-amount" }) + @(await orderLine.TotalPrice.Value.FormattedAsync())
Subtotal@(await order.SubtotalPrice.WithoutAdjustments.FormattedAsync())
Discounts and Shipping calculated at checkout
+
+ + Checkout +
+
+ } + } + else + { +

Your cart is empty

+ } +} +``` + +{% endcode %} + +3. Access the frontend of the website. +4. Navigate to the cart page to view the items in the cart. + +![Cart Page](../images/blendid/cart.png) + +## Updating a Cart Item + +In the [Cart view above](#displaying-the-cart) you have wrapped the cart markup in a form that posts to an `UpdateCart` action on the `CartSurfaceController`. Additionally, for each order line, you render a hidden input for the `Id` of the order line and a numeric input for it's `Quantity`. When the **Update Cart** button is clicked, the form will post all the order lines and their quantities to the `UpdateCart` action to be updated. + +1. Create a new class named `UpdateCartDto` using the following properties. + +{% code title="UpdateCartDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Dtos; + +public class UpdateCartDto +{ + public OrderLineQuantityDto[] OrderLines { get; set; } +} + +public class OrderLineQuantityDto +{ + public Guid Id { get; set; } + + public decimal Quantity { get; set; } +} + +``` + +{% endcode %} + +2. Add the following action method to your `CartSurfaceController`: + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpPost] +public async Task UpdateCart(UpdateCartDto postModel) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + StoreReadOnly store = CurrentPage!.GetStore()!; + Order order = await commerceApi.GetOrCreateCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow); + + foreach (OrderLineQuantityDto orderLine in postModel.OrderLines) + { + await order.WithOrderLine(orderLine.Id) + .SetQuantityAsync(orderLine.Quantity); + } + + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + TempData["errorMessage"] = "Failed to update cart"; + + return CurrentUmbracoPage(); + } + + TempData["successMessage"] = "Cart updated"; + + return RedirectToCurrentUmbracoPage(); +} +``` + +{% endcode %} + +3. Access the frontend of the website. +4. Update the quantity of an item in the cart and click the **Update Cart** button. + +## Removing a Cart Item + +In the [Cart view above](#displaying-the-cart) for each order line you render a remove link that triggers a `RemoveFromCart` action on the `CartSurfaceController`. This uses the `Url.SurfaceAction` helper to call a surface action as a GET request instead of a POST request. When the **Remove** link is clicked, the order line will be removed from the cart. + +To hook up the remove link, perform the following steps: + +1. Create a new class named `RemoveFromCartDto` using the following properties. + +{% code title="RemoveFromCartDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Dtos; + +public class RemoveFromCartDto +{ + public Guid OrderLineId { get; set; } +} +``` + +{% endcode %} + +2. Add the following action method to your `CartSurfaceController`: + +{% code title="CartSurfaceController.cs" %} + +```csharp +[HttpGet] +public async Task RemoveFromCart(RemoveFromCartDto postModel) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + StoreReadOnly store = CurrentPage!.GetStore()!; + Order order = await commerceApi.GetOrCreateCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .RemoveOrderLineAsync(postModel.OrderLineId); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + TempData["errorMessage"] = "Failed to remove cart item"; + + return CurrentUmbracoPage(); + } + + return RedirectToCurrentUmbracoPage(); +} +``` + +{% endcode %} + +3. Access the frontend of the website. +4. Click the **Remove** link on the cart item to remove it from the cart. + +## Useful Extension Methods + +In the examples above, you used several `IPublishedContent` extension methods to simplify the code. Here are some of the most useful extension methods: + +{% code title="PublishedContentExtensions.cs" %} + +```csharp +public static StoreReadOnly? GetStore(this IPublishedContent content) +{ + return content.AncestorOrSelf()?.Store; +} + +public static async Task GetCurrentOrderAsync(this IPublishedContent content) +{ + var store = content.GetStore(); + + if (store == null) + { + return null; + } + + return await UmbracoCommerceApi.Instance.GetCurrentOrderAsync(store.Id); +} +``` + +{% endcode %} diff --git a/15/umbraco-commerce/tutorials/build-a-store/custom-checkout.md b/15/umbraco-commerce/tutorials/build-a-store/custom-checkout.md new file mode 100644 index 00000000000..4353c021f46 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/custom-checkout.md @@ -0,0 +1,682 @@ +--- +description: Learn how to build a custom checkout flow in Umbraco Commerce. +--- + +# Creating a Custom Checkout Flow + +If you need a more custom checkout experience, you can build a custom checkout flow using the Umbraco Commerce API. This approach gives you full control over the checkout experience, allowing you to tailor it to your specific requirements. + +## Create a Checkout Surface Controller + +To create a custom checkout flow, add a custom Surface Controller to handle the checkout process. + +1. Create a new class and inherit from `Umbraco.Cms.Web.Website.Controllers.SurfaceController`. +2. Name the class `CheckoutSurfaceController`. +3. Add the following code to the class: + +{% code title="CheckoutSurfaceController.cs" %} + +```csharp +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Commerce.Core.Api; + +namespace Umbraco.Commerce.SwiftShop.Controllers; + +public class CheckoutSurfaceController : SurfaceController +{ + private readonly IUmbracoCommerceApi _commerceApi; + + public CheckoutSurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoCommerceApi commerceApi) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + _commerceApi = commerceApi; + } + + // Add your checkout actions here +} +``` + +{% endcode %} + +## Define the Checkout Steps + +Before you can start building the checkout flow, you must define the steps the customer goes through. A typical checkout flow consists of the following steps: + +* Collecting Customer Information. +* Selecting a Shipping Method. +* Selecting a Payment Method. +* Reviewing Order Details. +* Showing an Order Confirmation. + +To accommodate these steps, you must create a few new Document Types for each step (or one with multiple templates, one per step). Each Document Type will represent a step in the checkout flow. + +1. Create Document Types for each of the steps above, adding only properties that make sense for your setup. + +![Checkout Steps](../images/blendid/checkout_structure.png) + +## Collecting Customer Information + +To collect customer information, you need to create an action method in our `CheckoutSurfaceController`. This should accept the details and update the order accordingly. The properties will be wrapped in a DTO class to pass it to the controller. + +1. Create a new class and name it `UpdateOrderInformationDto` using the following properties. + +{% code title="UpdateOrderInformationDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Dtos; + +public class UpdateOrderInformationDto +{ + public string Email { get; set; } + public bool MarketingOptIn { get; set; } + public OrderAddressDto BillingAddress { get; set; } + public OrderAddressDto ShippingAddress { get; set; } + public bool ShippingSameAsBilling { get; set; } + public string Comments { get; set; } +} + +public class OrderAddressDto +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public string Line1 { get; set; } + public string Line2 { get; set; } + public string ZipCode { get; set; } + public string City { get; set; } + public Guid Country { get; set; } + public string Telephone { get; set; } +} + +``` + +{% endcode %} + +2. Add the following action method to your `CheckoutSurfaceController`. + +{% code title="CheckoutSurfaceController.cs" %} + +```csharp +public async Task UpdateOrderInformation(UpdateOrderInformationDto model) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage!.GetStore()!; + var order = await commerceApi.GetCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .SetPropertiesAsync(new Dictionary + { + { Constants.Properties.Customer.EmailPropertyAlias, model.Email }, + { "marketingOptIn", model.MarketingOptIn ? "1" : "0" }, + + { Constants.Properties.Customer.FirstNamePropertyAlias, model.BillingAddress.FirstName }, + { Constants.Properties.Customer.LastNamePropertyAlias, model.BillingAddress.LastName }, + { "billingAddressLine1", model.BillingAddress.Line1 }, + { "billingAddressLine2", model.BillingAddress.Line2 }, + { "billingCity", model.BillingAddress.City }, + { "billingZipCode", model.BillingAddress.ZipCode }, + { "billingTelephone", model.BillingAddress.Telephone }, + + { "shippingSameAsBilling", model.ShippingSameAsBilling ? "1" : "0" }, + { "shippingFirstName", model.ShippingSameAsBilling ? model.BillingAddress.FirstName : model.ShippingAddress.FirstName }, + { "shippingLastName", model.ShippingSameAsBilling ? model.BillingAddress.LastName : model.ShippingAddress.LastName }, + { "shippingAddressLine1", model.ShippingSameAsBilling ? model.BillingAddress.Line1 : model.ShippingAddress.Line1 }, + { "shippingAddressLine2", model.ShippingSameAsBilling ? model.BillingAddress.Line2 : model.ShippingAddress.Line2 }, + { "shippingCity", model.ShippingSameAsBilling ? model.BillingAddress.City : model.ShippingAddress.City }, + { "shippingZipCode", model.ShippingSameAsBilling ? model.BillingAddress.ZipCode : model.ShippingAddress.ZipCode }, + { "shippingTelephone", model.ShippingSameAsBilling ? model.BillingAddress.Telephone : model.ShippingAddress.Telephone }, + + { "comments", model.Comments } + }) + .SetPaymentCountryRegionAsync(model.BillingAddress.Country, null) + .SetShippingCountryRegionAsync(model.ShippingSameAsBilling ? model.BillingAddress.Country : model.ShippingAddress.Country, null); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("", "Failed to update information"); + + return CurrentUmbracoPage(); + } + + return Redirect("/checkout/shipping-method"); +} +``` + +{% endcode %} + +3. Open the view for the `Collecting Customer Information` step. +4. Create a form that posts to the `UpdateOrderInformation` action method of the `CheckoutSurfaceController`, passing the customer information. + +{% code title="CheckoutInformation.cshtml" %} + +```csharp +@inject IUmbracoCommerceApi UmbracoCommerceApi +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + var order = await UmbracoCommerceApi.GetCurrentOrderAsync(store!.Id); + var countries = await UmbracoCommerceApi.GetCountriesAsync(store!.Id) +} + + + +@using (Html.BeginUmbracoForm("UpdateOrderInformation", "CheckoutSurface")) +{ +

Contact Information

+ + + +

Billing Address

+ + + + + + + + + + + + + +

Comments

+ + +
+ Return to Cart + +
+} +``` + +{% endcode %} + +The customer can fill out their details and proceed to the next step in the checkout flow. + +![Collecting Customer Information](../images/blendid/checkout_information.png) + +## Selecting a Shipping Method + +1. Create a new class and name it `UpdateShippingMethodDto` using the following properties. + +{% code title="UpdateShippingMethodDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Dtos; + +public class UpdateOrderShippingMethodDto +{ + public Guid ShippingMethod { get; set; } + public string ShippingOptionId { get; set; } +} +``` + +{% endcode %} + +2. Add the following action method to your `CheckoutSurfaceController`. + +{% code title="CheckoutSurfaceController.cs" %} + +```csharp +public async Task UpdateOrderShippingMethod(UpdateOrderShippingMethodDto model) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage!.GetStore()!; + var order = await commerceApi.GetCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow); + + if (!string.IsNullOrWhiteSpace(model.ShippingOptionId)) + { + var shippingMethod = await commerceApi.GetShippingMethodAsync(model.ShippingMethod); + Attempt shippingRateAttempt = await shippingMethod.TryCalculateRateAsync(model.ShippingOptionId, order); + await order.SetShippingMethodAsync(model.ShippingMethod, shippingRateAttempt.Result!.Option); + } + else + { + await order.SetShippingMethodAsync(model.ShippingMethod); + } + + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("", "Failed to set order shipping method"); + + return CurrentUmbracoPage(); + } + + return RedirectToUmbracoPage("/checkout/payment-method"); +} +``` + +{% endcode %} + +3. Open the view for the `Selecting a Shipping Method` step. +4. Create a form that posts to the `UpdateOrderShippingMethod` action method of the `CheckoutSurfaceController`, passing the selected shipping method. + +{% code title="CheckoutShippingMethod.cshtml" %} + +```csharp +@inject IUmbracoCommerceApi UmbracoCommerceApi +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + var order = await UmbracoCommerceApi.GetCurrentOrderAsync(store!.Id); + var shippingMethods = await UmbracoCommerceApi.GetShippingMethodsAllowedInAsync(order!.ShippingInfo.CountryId!.Value).ToListAsync(); + var shippingMethodsRates = await Task.WhenAll(shippingMethods.Select(sm => sm.TryCalculateRatesAsync())); +} + + + +@using (Html.BeginUmbracoForm("UpdateOrderShippingMethod", "CheckoutSurface")) +{ +

Shipping Method

+
    + @foreach (var item in shippingMethods.Select((sm, i) => new { ShippingMethod = sm, Index = i })) + { + var rates = shippingMethodsRates[item.Index]; + if (rates.Success) + { + foreach (var rate in rates.Result!) + { +
  • + +
  • + } + } + } +
+ + + +
+ Return to Customer Information + +
+} +``` + +{% endcode %} + +The customer can select a shipping method and proceed to the next step in the checkout flow. + +![Selecting a Shipping Method](../images/blendid/checkout_shipping_method.png) + +## Selecting a Payment Method + +1. Create a new class and name it `UpdatePaymentMethodDto` using the following properties. + +{% code title="UpdatePaymentMethodDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Web.Dtos; + +public class UpdateOrderPaymentMethodDto +{ + public Guid PaymentMethod { get; set; } +} +``` + +{% endcode %} + +2. Add the following action method to your `CheckoutSurfaceController`. + +{% code title="CheckoutSurfaceController.cs" %} + +```csharp +public async Task UpdateOrderPaymentMethod(UpdateOrderPaymentMethodDto model) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage!.GetStore()!; + var order = await commerceApi.GetCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .SetPaymentMethodAsync(model.PaymentMethod); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("", "Failed to set order payment method"); + + return CurrentUmbracoPage(); + } + + return Redirect("/checkout/review-order"); +} +``` + +{% endcode %} + +3. Open the view for the `Selecting a Payment Method` step +4. Create a form that posts to the `UpdateOrderPaymentMethod` action method of the `CheckoutSurfaceController`, passing the selected payment method. + +{% code title="CheckoutPaymentMethod.cshtml" %} + +```csharp +@inject IUmbracoCommerceApi UmbracoCommerceApi +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + var order = await UmbracoCommerceApi.GetCurrentOrderAsync(store!.Id); + var paymentMethods = await UmbracoCommerceApi.GetPaymentMethodsAllowedInAsync(order!.PaymentInfo.CountryId!.Value).ToListAsync(); + var paymentMethodFees = await Task.WhenAll(paymentMethods.Select(x => x.TryCalculateFeeAsync())); +} +@using (Html.BeginUmbracoForm("UpdateOrderPaymentMethod", "CheckoutSurface")) +{ +

Payment Method

+
    + @foreach (var item in paymentMethods.Select((pm, i) => new { PaymentMethod = pm, Index = i })) + { + var fee = paymentMethodFees[item.Index]; +
  • + +
  • + } +
+ +
+ Return to Shipping Method + +
+} + +``` + +{% endcode %} + +The customer can select a payment method and proceed to the next step in the checkout flow. + +![Selecting a Payment Method](../images/blendid/checkout_payment_method.png) + +## Reviewing Order Details + +1. Open the view for the `Reviewing Order Details` step. +2. Display the order details and provide a button to trigger capturing payment. + +{% code title="CheckoutReview.cshtml" %} + +```csharp +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + var order = await UmbracoCommerceApi.GetCurrentOrderAsync(store!.Id); +} + +// Ommitted for brevity, but displays the order details from the order object +@await Html.PartialAsync("OrderInformationSummary") + +@using (await Html.BeginPaymentFormAsync(order)) +{ + + +
+ Return to Payment Method + +
+} +``` + +{% endcode %} + +This is a unique step in the checkout flow as it doesn't post back to the `CheckoutSurfaceController`. Instead, it uses the `BeginPaymentFormAsync` method of the `Html` helper to render a payment method-specific form that triggers the payment capturing process. + +{% hint style="info" %} +It is within the `BeginPaymentFormAsync` method that an order number is assigned. No modifications must be made to the order after this point as it may result in the order number getting reset and the payment failing. +{% endhint %} + +The customer can review their order details and proceed to the payment gateway. + +![Reviewing Order Details](../images/blendid/checkout_review.png) + +## Capturing Payment + +The payment capture screen is entirely dependent on the payment method being used. It is the responsibility of the associated payment provider to redirect and handle the payment process. The payment provider will redirect back to the order confirmation page on success, or to the cart page with an error if there is a problem. + +For more information on how to implement a payment provider, see the [Payment Providers](../../key-concepts/payment-providers.md) documentation. + +## Showing an Order Confirmation + +1. Open the view for the `Showing an Order Confirmation` step. +2. Display the order confirmation details. + +{% code title="CheckoutConfirmation.cshtml" %} + +```csharp +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + var store = Model.GetStore(); + var order = await UmbracoCommerceApi.GetCurrentFinalizedOrderAsync(store!.Id); +} + +
+

Thank you for your order #@(order!.OrderNumber)

+

A confirmation email has been sent to @(order!.CustomerInfo.Email)

+

Return to Store

+
+ +// Ommitted for brevity, but same partial as used on the review step +@await Html.PartialAsync("OrderInformationSummary") +``` + +{% endcode %} + +In the order confirmation, you must use `GetCurrentFinalizedOrderAsync` instead of the previously used `GetCurrentOrderAsync`. This is because the order will have been finalized during the payment processing step and the current cart order will be cleared. + +The customer can view their order confirmation details. + +![Showing an Order Confirmation](../images/blendid/checkout_confirmation.png) + +At this stage, the customer should receive an email confirmation of their order. + +![Order Confirmation Email](../images/blendid/order_confirmation_email.png) + +Umbraco Commerce comes with a default order confirmation email template. This can be customized to suit your store's branding. See the [Customizing Templates](../../how-to-guides/customizing-templates.md) documentation for more information. + +## Extras + +### Redeeming a Coupon / Gift Card + +The checkout flow is a common place to allow customers to redeem coupons or gift cards. This can be done by adding another step to the checkout flow where the customer can enter the code and apply it to their order. + +1. Create a new class and name it `DiscountOrGiftCardCodeDto` using the following properties. + +{% code title="DiscountOrGiftCardCodeDto.cs" %} + +```csharp +namespace Umbraco.Commerce.DemoStore.Web.Dtos; + +public class DiscountOrGiftCardCodeDto +{ + public string Code { get; set; } +} +``` + +{% endcode %} + +2. Add the following action methods to your `CheckoutSurfaceController`. + +{% code title="CheckoutSurfaceController.cs" %} + +```csharp +public async Task ApplyDiscountOrGiftCardCode(DiscountOrGiftCardCodeDto model) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage!.GetStore()!; + var order = await commerceApi.GetCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .RedeemAsync(model.Code); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("", "Failed to redeem discount code"); + + return CurrentUmbracoPage(); + } + + return RedirectToCurrentUmbracoPage(); +} + +public async Task RemoveDiscountOrGiftCardCode(DiscountOrGiftCardCodeDto model) +{ + try + { + await commerceApi.Uow.ExecuteAsync(async uow => + { + var store = CurrentPage!.GetStore()!; + var order = await commerceApi.GetCurrentOrderAsync(store.Id)! + .AsWritableAsync(uow) + .UnredeemAsync(model.Code); + await commerceApi.SaveOrderAsync(order); + uow.Complete(); + }); + } + catch (ValidationException ex) + { + ModelState.AddModelError("", "Failed to redeem discount code"); + + return CurrentUmbracoPage(); + } + + return RedirectToCurrentUmbracoPage(); +} +``` + +{% endcode %} + +2. Open the base view for your checkout flow. +3. Create a form that posts to the `ApplyDiscountOrGiftCardCode` action method of the `CheckoutSurfaceController`, passing the code to redeem. + +{% code title="CheckoutPage.cshtml" %} + +```csharp +@using (Html.BeginUmbracoForm("ApplyDiscountOrGiftCardCode", "CheckoutSurface")) +{ + + +} +``` + +{% endcode %} + +4. Show a list of applied discounts and gift cards with a link to remove them. + +{% code title="CheckoutPage.cshtml" %} + +```csharp +@if (order.DiscountCodes.Count > 0 || order.GiftCards.Count > 0) +{ + +} +``` + +{% endcode %} + +The customers can enter a discount or gift card code and apply it to their order. + +![Redeeming a Coupon / Gift Card](../images/blendid/discount.png) diff --git a/15/umbraco-commerce/tutorials/build-a-store/installation.md b/15/umbraco-commerce/tutorials/build-a-store/installation.md new file mode 100644 index 00000000000..8c9dad9ada4 --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/installation.md @@ -0,0 +1,29 @@ +--- +description: Installing Umbraco Commerce on your Umbraco site. +--- + +# Installation + +The first thing you need to do is set up an Umbraco site with Umbraco Commerce installed. + +## Prerequisites + +To follow this tutorial, you'll need the following: + +* Umbraco.CMS version 15.1.0+ +* Umbraco.Commerce version 15.0.0 +* SQL Database (LocalDB or any SQL server) +* Visual Studio or your preferred IDE +* [Umraco CMS Requirements](../../../umbraco-cms/fundamentals/setup/requirements.md) + +## Setup your Umbraco Project + +To set up your Umbraco project, see the [Installation](../../../umbraco-cms/fundamentals/setup/install/README.md) article. + +## Installing Umbraco Commerce + +Once your Umbraco site is up and running, you can install the [Umbraco Commerce package](../../getting-started/install.md). + +{% hint style="info" %} +If you installed Umbraco CMS using a SQLite database, you will need to configure [SQlite support](../../how-to-guides/configure-sqlite-support.md). +{% endhint %} diff --git a/15/umbraco-commerce/tutorials/build-a-store/overview.md b/15/umbraco-commerce/tutorials/build-a-store/overview.md new file mode 100644 index 00000000000..5798f3705eb --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/overview.md @@ -0,0 +1,24 @@ +--- +description: A Step-by-Step Tutorial on how to build a store in Umbraco using Umbraco Commerce. +--- + +# Overview + +This tutorial covers the process of setting up an Umbraco Commerce store in Umbraco from beginning to end. You will set up a store, create products, and configure a complete customer checkout flow. + +This tutorial will be based around the official **Blendid Demo Store** solution, a fictional tea supplier web store. The source code for which can be found on [GitHub](https://github.com/umbraco/Umbraco.Commerce.DemoStore). + +![Blendid Store Homepage](../images/blendid/homepage.png) + +{% hint style="info" %} +This tutorial assumes that you already have the relevant site structure and content views in place for your store. The tutorial will focus on adding the commerce functionality to your existing site. +{% endhint %} + +## Steps + +{% content-ref url="installation.md" %} Installation {% endcontent-ref %} +{% content-ref url="create-store.md" %} Creating a Store {% endcontent-ref %} +{% content-ref url="create-product.md" %} Creating your first Product {% endcontent-ref %} +{% content-ref url="cart.md" %} Implementing a Shopping Cart {% endcontent-ref %} +{% content-ref url="checkout.md" %} Implementing a Checkout Flow {% endcontent-ref %} +{% content-ref url="permissions.md" %} Configuring Store Permissions {% endcontent-ref %} diff --git a/15/umbraco-commerce/tutorials/build-a-store/permissions.md b/15/umbraco-commerce/tutorials/build-a-store/permissions.md new file mode 100644 index 00000000000..5be77f3ad0a --- /dev/null +++ b/15/umbraco-commerce/tutorials/build-a-store/permissions.md @@ -0,0 +1,61 @@ +--- +description: Configuring store permissions in Umbraco Commerce. +--- + +# Configuring Store Permissions + +Configuring store permissions gives you control over who can access the store's management interface. + +The setup of store permissions is split into two parts: + +* [Allowing access to the `Commerce` section](#allow-access-to-the-commerce-section) +* [Allowing access to manage a store](#allow-access-to-manage-a-store) + +## Allow Access to the Commerce Section + +Store management is done through the `Commerce` section in the backoffice. To allow access to this section, you need to either create or update a user group and assign access to the `Commerce` section. + +1. Navigate to the **Users** section of the backoffice. +2. Create a new user group or edit an existing one. +3. Find the **Allowed Sections** property. +4. Select the `Commerce` section. + +![Assign Commerce Allowed Section](../images/users/user-group.png ) + +4. **Save** the changes. + +### Assign Users to the User Group + +After creating the user group, you can assign users to it allowing them access to the `Commerce` section. + +1. Navigate to the **Users** section of the backoffice. +2. Edit the user you want to assign to the user group. +3. Select the new user group in the **Groups** property. + +![Assign Commerce User Group to User](../images/users/assign-user.png) + +4. **Save** the changes. + +After assigning the user to the user group, they have the `Commerce` section in the backoffice. + +![Commerce Section](../images/blendid/commerce_no_stores_highlight.png) + +At this point the user has access to the Commerce section, they will not yet be able to manage any stores. To give access to managing a store, you need to [allow access to manage a store](#allow-access-to-manage-a-store). + +## Allow Access to Manage a Store + +To allow a user to manage a store, you must assign store permissions from the store's settings. + +1. Navigate to the **Settings** section of the backoffice +2. Open the **Stores** area. +3. Select the store you want to assign permissions to. +4. Click the **Permissions** tab in the Store editor. + +![Store Permissions](../images/blendid/store_permissions.png) + +5. Select the new user group in the **User Roles** property. +6. **Save** the changes. + +After assigning the user group to the store, users in that group can manage the store. + +![Commerce Section](../images/blendid/commerce_with_store.png) diff --git a/15/umbraco-commerce/tutorials/getting-started-with-commerce.md b/15/umbraco-commerce/tutorials/getting-started-with-commerce.md deleted file mode 100644 index c094af02552..00000000000 --- a/15/umbraco-commerce/tutorials/getting-started-with-commerce.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -description: "A guide to setting up Umbraco Commerce, including an introduction to basic concepts and configuration." ---- -# Getting started with Umbraco Commerce: The Backoffice - -This tutorial focuses exclusively on setting up Umbraco Commerce, introducing key concepts, and the configuration process in the Backoffice. It has been tested on the latest releases of **Umbraco CMS version 14** and **Umbraco Commerce version 14**. - -{% hint style="info" %} -This tutorial does not provide instructions for building a complete eCommerce website. It focuses solely on the setup through the Umbraco Backoffice. -{% endhint %} - -## Introduction - -Umbraco Commerce is an eCommerce platform that integrates seamlessly with Umbraco CMS. It provides features such as product management, order processing, and payment integrations. This guide will walk you through the setup of Umbraco Commerce so you can hit the ground running. - -## Prerequisites - -To follow this tutorial, you'll need the following: - -* Visual Studio Code or your preferred IDE -* SQL Database (LocalDB or any SQL server) -* [Umraco CMS Requirements](../../umbraco-cms/fundamentals/setup/requirements.md) -* [Umbraco CMS Installation Guide](../../umbraco-cms/fundamentals/setup/install/README.md) -* [Umbraco Commerce package](../getting-started/install.md) - -## Setting Up an Umbraco Project - -To set up your Umbraco project, see the [Installation](../../umbraco-cms/fundamentals/setup/install/README.md) article. - -## Installing Umbraco Commerce - -Once your Umbraco site is up and running, you can install the [Umbraco Commerce package](../getting-started/install.md). - -{% hint style="info" %} -If you installed Umbraco CMS using an SQLite database, you may encounter errors after installing Umbraco Commerce. To resolve these, follow the instructions in the [Configure SQLite support](../how-to-guides/configure-sqlite-support.md) article. -{% endhint %} - -## Configuring Umbraco Commerce - -To access the **Commerce** section, additional configuration is needed. For detailed steps, see the [Configuration](../getting-started/umbraco-configuration.md) article. - -## Accessing the Umbraco Backoffice - -You can access the backoffice by adding `/umbraco` at the end of your website URL. For example: `https://mywebsite.com/umbraco`. - -## Setting Up a Store - -A store is an online platform where products or services are listed for customers to browse, purchase, and complete transactions over the internet. - -Setting up a store allows you to manage both the content and commerce aspects of your site. It allows you to create a custom and scalable online shopping experience. For more information, see the [Stores](../reference/stores/README.md) article. - -To set up a store: - -1. Navigate to the **Settings** section. -2. Click **+** next to **Stores**. -3. Enter a **Name** for the Store (For example: *Umbraco Swag Store*). - -![Create Store](images/create-store.png) - -4. Click **Create**. -5. Click **Save**. - -### Setting up a Location - -If your business operates in multiple regions, setting up locations helps: - -* Configure stores for different locations with their own language, shipping addresses, regional offers, local regulations, and payment gateways. -* Ship products from different locations. The system can be set up to route orders to the nearest warehouse based on the customer’s location. - -To set up a location: - -1. Select your store from the **Stores** menu in the **Settings** section. In this case, *Umbraco Swag Store*. -2. Go to **Locations** under the Store. -3. Click **Create Location**. -4. Enter the **Name** for the Location. For example: *Denmark* -5. Provide the necessary address details. - -![Create Location](images/create-location.png) - -6. Click **Save**. - -### Setting up Order Status - -Order status tracks the progression of an order. It helps both the store owner and customers track the progress of the order from the moment it is placed until itis delivered (or returned). - -When you first set up Umbraco Commerce, it comes with predefined order statuses to help manage the order lifecycle. These statuses include New, Completed, Cancelled, and Error. These statuses can be customized based on your specific business requirements. - -To create an order status: - -1. Go to **Order Statuses** under the Store. -2. Click **Create Order Status**. -3. Enter a **Name** for the order status. For Example: *Processing* -4. Select a **Color** for the order status. - -![Create Order Status](images/create-order-status.png) - -5. Click **Save**. - -### Setting up Payment Methods - -Payment Methods define the payment options available in the store. By default, Umbraco Commerce includes basic providers like **Invoicing** and **Zero Value** to get started. - -Umbraco Commerce also supports the integration of different third-party payment gateways. For more information, see the [Umbraco Commerce Payment Providers Documentation](../../../commerce-add-ons/payment-providers/README.md). - -To set up a payment method: - -1. Go to **Payment Methods** under the Store. -2. Click **Create Payment Method**. -3. Select a payment provider from the list. For example: *Zero Value*. -4. Enter a **Name** for the payment method. For example: *Zero Payment*. -5. Configure the payment method as per your requirements. - -![Create Payment Method](images/create-payment-methods.png) - -6. Click **Save**. - -### Setting up Shipping Methods - -Shipping methods determine how customers receive their orders. Setting up shipping methods effectively is crucial, as it impacts customer satisfaction, fulfillment costs, and overall operational efficiency. - -By default, Umbraco Commerce comes with the basic Pickup option. For more information on the integration for different providers, see the [Umbraco Commerce Shipping providers Documentation](../../../commerce-add-ons/shipping-providers/README.md). - -To create a shipping method: - -1. Go to **Shipping Methods** under the Store. -2. Click **Create Shipping Method**. -3. Choose the shipping provider from the list. For Example: *DHL*. -4. Enter a **Name** for the shipping method. For example: *DHL*. -5. Configure the shipping method as per your requirements. - -![Create Shipping Method](images/create-shipping-method.png) - -6. Click **Save**. - -### Setting up a Country - -Setting up a country involves configuring settings related to shipping, payment methods, tax rates, localization, legal compliance requirements, and so on for that specific country. - -To set up a country: - -1. Go to **Countries** under the Store. -2. Click **Create Country**. -3. Choose an item from the list. For Example: *Create Country from ISO 3166 preset*. -4. Select a country from the list. For example: *Denmark*. -5. Configure the country details as per your requirements. - -![Create Country](images/create-country.png) - -6. Click **Save**. - -### Setting up a Currency - -Setting up currency is essential for ensuring that prices are displayed and transactions are processed accurately. For information on configuring an exchange rate service, see the [Currency Exchange Rate Service Provider](../key-concepts/currency-exchange-rate-service-providers.md) article. - -To set up a currency: - -1. Go to **Currencies** under the Store. -2. Click **Create Currency**. -3. Enter a **Name** for the currency. For Example: *DKK*. -4. Configure the currency details as per your requirements. - -![Create Currency](images/create-currency.png) - -5. Click **Save**. - -### Setting up Taxes - -Tax setup is crucial for compliance with local regulations and for ensuring that your pricing is accurate and transparent. You can set up tax rates for each jurisdiction where you need to collect tax. For more information, see the [Tax Sources](../key-concepts/tax-sources.md) article. - -To set up taxes: - -1. Go to **Taxes** under the Store. -2. Click **Create Tax Class**. -3. Enter a **Name** for the tax class. For Example: *Custom*. -4. Configure the tax rates as per your requirements. - -![Create Tax Class Rate](images/create-tax-rate.png) - -5. Click **Save**. - -### Setting up Templates - -Defines the different **Email**, **Print**, and **Export** templates available for the store. These templates help maintain consistency and professionalism in communication with customers and facilitate data handling. - -To create an Email Template: - -1. Expand the **Templates** folder under the Store. -2. Go to **Email Templates**. -3. Click **Create Email Template**. -4. Enter a **Name** for the Email template. For Example: *Shipping Notification*. -5. Configure the email details as per your requirements. - -![Create Email Template](images/create-email-template.png) - -6. Click **Save**. - -Similarly, you can create custom **Print** and **Export** Templates. - -## Configuring the Store Settings - -You can customize your Store's currencies, countries, locations, shipping, tax calculations, and so on. Additionally, you can set Notification, Order, Product, and Gift Card Settings. For detailed settings, see the [Stores](../reference/stores/README.md) article. - -## Built-in Property Editors - -Umbraco Commerce includes default property editors that help manage and configure eCommerce functionalities within the Umbraco backoffice. - -![Built-in Property Editors](images/property-editors.png) - -The available property editors include: - -* **Price:** Used to manage and define product pricing. -* **Store Picker:** Allows selection of a specific store for products or configurations. -* **Store Entity Picker:** Used for selecting entities like products or categories within a store. -* **Stock:** Helps manage stock levels for products. -* **Measurements:** Allows the configuration of product dimensions and weight. -* **Variants Editor:** Used for managing product variants, such as sizes or colors. - -## Accessing Store Permissions in Umbraco Commerce - -When editing a store in Umbraco Commerce, the **Permissions** tab allows you to control who can access the store's management interface. This ensures that only authorized individuals or user groups can make changes or view store data. - -![Store Permissions](images/store-permissions.png) - -The Permissions tab contain the following options: - -* **User Groups:** You can assign permissions to entire user groups. A toggle is provided to either allow or deny access to specific user groups. This is useful for assigning store management roles to groups like "Store Managers" or "Editors" without setting permissions for individual users. - -* **Users:** In addition to groups, you can assign permissions to individual users. This feature lets you grant or deny store access to specific individuals based on their role in the organization. Like the user group settings, toggle allows you to control what each user can access. - -{% hint style="info" %} -If both a user group and an individual user have conflicting permissions, the "Allow" control will always take priority. For example: "Deny" at the group level and "Allow" at the user level. This ensures that users explicitly granted access will not be denied by group-level settings. -{% endhint %} - -## Conclusion - -We have now covered the essential steps to set up Umbraco Commerce - from installation to store configuration. - -Umbraco Commerce provides a flexible solution for creating and managing eCommerce websites directly within Umbraco CMS. While this tutorial helps you get started, feel free to explore advanced configurations to extend your store's functionality based on your business needs. diff --git a/15/umbraco-commerce/tutorials/images/blendid/cart.png b/15/umbraco-commerce/tutorials/images/blendid/cart.png new file mode 100644 index 00000000000..9cf6c8f4b02 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/cart.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/cart_with_notification.png b/15/umbraco-commerce/tutorials/images/blendid/cart_with_notification.png new file mode 100644 index 00000000000..2b63f107bce Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/cart_with_notification.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_confirmation.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_confirmation.png new file mode 100644 index 00000000000..6429fcf09bb Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_confirmation.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_information.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_information.png new file mode 100644 index 00000000000..4625bd5b6fa Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_information.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_payment_method.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_payment_method.png new file mode 100644 index 00000000000..d43144dbe9d Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_payment_method.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_review.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_review.png new file mode 100644 index 00000000000..daf53531624 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_review.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_shipping_method.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_shipping_method.png new file mode 100644 index 00000000000..a361daa7f6f Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_shipping_method.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/checkout_structure.png b/15/umbraco-commerce/tutorials/images/blendid/checkout_structure.png new file mode 100644 index 00000000000..496a826166a Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/checkout_structure.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores.png b/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores.png new file mode 100644 index 00000000000..2d0669b02c5 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores_highlight.png b/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores_highlight.png new file mode 100644 index 00000000000..b88f6978372 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/commerce_no_stores_highlight.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/commerce_property_editors.png b/15/umbraco-commerce/tutorials/images/blendid/commerce_property_editors.png new file mode 100644 index 00000000000..e7a45907449 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/commerce_property_editors.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/commerce_with_store.png b/15/umbraco-commerce/tutorials/images/blendid/commerce_with_store.png new file mode 100644 index 00000000000..8af1e456d5a Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/commerce_with_store.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/create_product.png b/15/umbraco-commerce/tutorials/images/blendid/create_product.png new file mode 100644 index 00000000000..b92da01ae56 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/create_product.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/create_store.png b/15/umbraco-commerce/tutorials/images/blendid/create_store.png new file mode 100644 index 00000000000..8bb14c583c1 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/create_store.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/discount.png b/15/umbraco-commerce/tutorials/images/blendid/discount.png new file mode 100644 index 00000000000..99033ff8ef5 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/discount.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/homepage.png b/15/umbraco-commerce/tutorials/images/blendid/homepage.png new file mode 100644 index 00000000000..7384e70e746 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/homepage.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/homepage_doctype_store_setting.png b/15/umbraco-commerce/tutorials/images/blendid/homepage_doctype_store_setting.png new file mode 100644 index 00000000000..0a9a473118d Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/homepage_doctype_store_setting.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/homepage_editor_pick_store.png b/15/umbraco-commerce/tutorials/images/blendid/homepage_editor_pick_store.png new file mode 100644 index 00000000000..e5c1ee3cd86 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/homepage_editor_pick_store.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/order.png b/15/umbraco-commerce/tutorials/images/blendid/order.png new file mode 100644 index 00000000000..96aded5a6f0 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/order.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/order_confirmation_email.png b/15/umbraco-commerce/tutorials/images/blendid/order_confirmation_email.png new file mode 100644 index 00000000000..87996c72663 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/order_confirmation_email.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_allowed_child_node.png b/15/umbraco-commerce/tutorials/images/blendid/product_allowed_child_node.png new file mode 100644 index 00000000000..b7585397817 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_allowed_child_node.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_composition.png b/15/umbraco-commerce/tutorials/images/blendid/product_composition.png new file mode 100644 index 00000000000..fc85e0ddfdf Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_composition.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_page.png b/15/umbraco-commerce/tutorials/images/blendid/product_page.png new file mode 100644 index 00000000000..6901c5c1320 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_page.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_page_doctype.png b/15/umbraco-commerce/tutorials/images/blendid/product_page_doctype.png new file mode 100644 index 00000000000..dff069193ec Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_page_doctype.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_page_editor.png b/15/umbraco-commerce/tutorials/images/blendid/product_page_editor.png new file mode 100644 index 00000000000..289eca6b1c1 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_page_editor.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_page_with_notification.png b/15/umbraco-commerce/tutorials/images/blendid/product_page_with_notification.png new file mode 100644 index 00000000000..e32323990c2 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_page_with_notification.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/product_pick_composition.png b/15/umbraco-commerce/tutorials/images/blendid/product_pick_composition.png new file mode 100644 index 00000000000..1c0d2295cd2 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/product_pick_composition.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/store_permissions.png b/15/umbraco-commerce/tutorials/images/blendid/store_permissions.png new file mode 100644 index 00000000000..45d1fa25a44 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/store_permissions.png differ diff --git a/15/umbraco-commerce/tutorials/images/blendid/store_settings.png b/15/umbraco-commerce/tutorials/images/blendid/store_settings.png new file mode 100644 index 00000000000..37cf3f59870 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/blendid/store_settings.png differ diff --git a/15/umbraco-commerce/tutorials/images/create-store.png b/15/umbraco-commerce/tutorials/images/create-store.png deleted file mode 100644 index 66326b6f8b1..00000000000 Binary files a/15/umbraco-commerce/tutorials/images/create-store.png and /dev/null differ diff --git a/15/umbraco-commerce/tutorials/images/homepage.png b/15/umbraco-commerce/tutorials/images/homepage.png new file mode 100644 index 00000000000..690e68f2745 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/homepage.png differ diff --git a/15/umbraco-commerce/tutorials/images/sqlite-error.png b/15/umbraco-commerce/tutorials/images/sqlite-error.png deleted file mode 100644 index f553f55344c..00000000000 Binary files a/15/umbraco-commerce/tutorials/images/sqlite-error.png and /dev/null differ diff --git a/15/umbraco-commerce/tutorials/images/store-permissions.png b/15/umbraco-commerce/tutorials/images/store-permissions.png deleted file mode 100644 index 0d5a9b12573..00000000000 Binary files a/15/umbraco-commerce/tutorials/images/store-permissions.png and /dev/null differ diff --git a/15/umbraco-commerce/tutorials/images/users/assign-user.png b/15/umbraco-commerce/tutorials/images/users/assign-user.png new file mode 100644 index 00000000000..0a00f88a5db Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/users/assign-user.png differ diff --git a/15/umbraco-commerce/tutorials/images/users/user-group.png b/15/umbraco-commerce/tutorials/images/users/user-group.png new file mode 100644 index 00000000000..a24aecc4371 Binary files /dev/null and b/15/umbraco-commerce/tutorials/images/users/user-group.png differ diff --git a/15/umbraco-commerce/tutorials/overview.md b/15/umbraco-commerce/tutorials/overview.md index f331a744ec0..98c42d4fe6d 100644 --- a/15/umbraco-commerce/tutorials/overview.md +++ b/15/umbraco-commerce/tutorials/overview.md @@ -8,10 +8,6 @@ In this section, we offer a series of step-by-step tutorials to help you impleme Once you are comfortable with the basics of Umbraco Commerce, use the documentation to explore advanced features and deepen your understanding of Umbraco Commerce. -{% hint style="warning" %} -We are crafting new tutorials to guide you through Umbraco Commerce. Stay tuned for some practical guides coming your way. If you have any suggestions, share them using the **Feedback** option. -{% endhint %} +## Tutorials -## Getting Started with Umbraco Commerce - -* [Getting started with Umbraco Commerce: The Backoffice](getting-started-with-commerce.md) - This tutorial will take you through the process of setting up Umbraco Commerce. +* [Build a Store in Umbraco using Umbraco Commerce](build-a-store/overview.md) - This tutorial will help you setup a basic store in Umbraco, and get started with Commerce specific workflows.