Writing an Orchard Webshop Module from scratch - Part 9

webshop-module-from-scratch-part-1">introduction.

In this part, we'll be handling the following:

  • We'll create the OrderService responsible for turning shopping cart items into an actual order;
  • We'll see how to create parent / child (or one to many) relationships
  • We'll create an extensibility point to other modules by leveraging the Event Bus
  • We'll implement a simulated Payment Service Provider that uses the extensibility point

Download the source code

In the previous part, we were left at the Summary screen, which is the final step before the customer is directed to some sort of payment service provider.
We're going to continue and implement the "Place Order" button. 

 

Creating the Order

Since the "Place Order" button will modify some state on the server, we'll wrap it inside a <form> element, which will invoke a Create action on a new OrderController: 

Views/Checkout.Summary.cshtml:

...

 <article>
        <div class="group">
            <div class="align left"><a href="#">Cancel</a></div>
            <div class="align right">
                @using (Html.BeginFormAntiForgeryPost(Url.Action("Create""Order"new { area = "Orchard.Webshop" }))) {
                    <button type="submit">Place Order</button>
                }
            </div>
        </div>
    </article>

...

Next, we'll create a new OrderController class that will handle the request:

Controllers/OrderController.cs:

using System.Web.Mvc;
using Orchard.DisplayManagement;
using Orchard.Mvc;
using Orchard.Themes;
 
namespace Orchard.Webshop.Controllers {
    public class OrderController : Controller {
        private readonly dynamic _shapeFactory;
 
        public OrderController(IShapeFactory shapeFactory) {
            _shapeFactory = shapeFactory;
        }
 
        [Themed, HttpPost]
        public ActionResult Create() {
 
            var shape = _shapeFactory.Order_Created();
            return new ShapeResult(this, shape);
        }
    }
}

The only thing the controller currently does is providing the Create action, which creates a shape called Order_Created. We'll go ahead and create a template for that:

Views/Order.Created.cshtml:

@{
    Style.Require("Webshop.Order");
}
<h2>@T("Order {0} has been created", -1)</h2>
<p>@T("Please find your order details below")</p>
 
<article class="order">
    <header>
        <ul>
            <li>
                <div class="field-label">Order Number</div>
                <div class="field-value">1000</div>
            </li>
            <li>
                <div class="field-label">Created</div>
                <div class="field-value">01/20/2012 04:29 PM</div>
            </li>
        </ul>
    </header>
    <table>
        <thead>
            <tr>
                <td>Article</td>
                <td class="numeric">Unit Price</td>
                <td class="numeric">Quantity</td>
                <td class="numeric">Total Price</td>
            </tr>
        </thead>
        <tbody>
            @for (var i = 0; i < 3; i++) {
                <tr>
                    <td>Article Title @i</td>
                    <td class="numeric">@29m.ToString("c")</td>
                    <td class="numeric">1</td>
                    <td class="numeric">@29m.ToString("c")</td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</td></tr>
            <tr>
                <td class="numeric label" colspan="2">Subtotal:</td>
                <td class="numeric">@87m.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">VAT (19%):</td>
                <td class="numeric">@16.53m.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="3">Total:</td>
                <td class="numeric">@103.53m.ToString("c")</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
</article>
    
<article class="addresses form">
    <div class="invoice-address">
        <h2>Invoice Address</h2>
        <ul class="address-fields">
            <li>Mr. Bauer</li>
            <li>Address line 1</li>
            <li>Address line 2</li>
            <li>Zipcode</li>
            <li>City</li>
            <li>Country</li>
        </ul>
    </div>
    <div class="shipping-address">
        <h2>Shipping Address</h2>
        <ul class="address-fields">
            <li>Mr. Bauer</li>
            <li>Address line 1</li>
            <li>Address line 2</li>
            <li>Zipcode</li>
            <li>City</li>
            <li>Country</li>
        </ul>
    </div>
</article>

Payment

Although we created an Order_Created shape and template, what we really want is to initiate one or another Payment Service Provider (PSP).
We could easily implement one for our own specific needs, but let's decouple the PSP from our webshop module. This will increase reusability, and perhaps some day allow our module to be a generic commerce module that features core commerce services, on which more specific implementations can be built.

One example that uses this approach by the way is the Orchard.Search module; it provides an abstraction which other modules can communicate with, but the actual indexing is implemented by modules such as Lucene. Another example is Orchard.Email, which leverages the core messaging infrastructure.

In the same way, we will delegate the actual payment processing to other modules. However, it might be a good idea to at least provide a simulation PSP so that we can test the entire process from shopping cart to successfull payment (as well as failed payments, cancelled payments, etc.). A simulation PSP will also provide for a good reference implementation of our extensibility point.

We will implement the simulation PSP as a feature of our module, which users can enable or disable from the Modules section.
We'll see how this works in a few moments.

The reason we created the Order_Created screen is that we can at least show something to the user, should there be no PSP enabled or installed.

OrderService, Order and OrderDetail

First of all, we need to be able to store an order, so we'll go ahead and create two classes: OrderRecord, which will hold a list of order details, and OrderDetailRecord, which will represent a single purchased product, including quantity.

Models/OrderDetailRecord.cs:

namespace Orchard.Webshop.Models {
    public class OrderDetailRecord {
        public virtual int Id { getset; }
        public virtual int OrderRecord_Id { getset; }
        public virtual int ProductId { getset; }
        public virtual int Quantity { getset; }
        public virtual decimal UnitPrice { getset; }
        public virtual decimal VatRate { getset; }
 
        public virtual decimal UnitVat
        {
            get { return UnitPrice * VatRate; }
        }
 
        public virtual decimal Vat
        {
            get { return UnitVat * Quantity; }
        }
 
        public virtual decimal SubTotal
        {
            get { return UnitPrice * Quantity; }
        }
 
        public virtual decimal Total
        {
            get { return SubTotal + Vat; }
        }
    }
}

Models/OrderRecord.cs:

using System;
using System.Collections.Generic;
using System.Globalization;
 
namespace Orchard.Webshop.Models
{
    public class OrderRecord
    {
        public virtual int Id { getset; }
        public virtual int CustomerId { getset; }
        public virtual DateTime CreatedAt { getset; }
        public virtual decimal SubTotal { getset; }
        public virtual decimal Vat { getset; }
        public virtual OrderStatus Status { getset; }
        public virtual IList<OrderDetailRecord> Details { getprivate set; }
        public virtual string PaymentServiceProviderResponse { getset; }
        public virtual string PaymentReference { getset; }
        public virtual DateTime? PaidAt { getset; }
        public virtual DateTime? CompletedAt { getset; }
        public virtual DateTime? CancelledAt { getset; }
 
        public virtual decimal Total
        {
            get { return SubTotal + Vat; }
        }
 
        public virtual string Number
        {
            get { return (Id + 1000).ToString(CultureInfo.InvariantCulture); }
        }
        
        public OrderRecord() {
            Details = new List<OrderDetailRecord>();
        }
 
        public virtual void UpdateTotals()
        {
            var subTotal = 0m;
            var vat = 0m;
 
            foreach (var detail in Details) {
                subTotal += detail.SubTotal;
                vat += detail.Vat;
            }
 
            SubTotal = subTotal;
            Vat = vat;
        }
    }
}

OrderStatus is an enum:

Models/OrderStatus.cs:

namespace Orchard.Webshop.Models {
    public enum OrderStatus {
        /// <summary>
        /// The order is new and is yet to be paid for
        /// </summary>
        New,
 
        /// <summary>
        /// The order has been paid for, so it's eligable for shipping
        /// </summary>
        Paid,
 
        /// <summary>
        /// The order has shipped
        /// </summary>
        Completed,
 
        /// <summary>
        /// The order was cancelled
        /// </summary>
        Cancelled
    }
}

Next, we'll update Migrations:

Migrations.cs:

public int UpdateFrom6() {
            SchemaBuilder.CreateTable("OrderRecord", t => t
                .Column<int>("Id", c => c.PrimaryKey().Identity())
                .Column<int>("CustomerId", c => c.NotNull())
                .Column<DateTime>("CreatedAt", c => c.NotNull())
                .Column<decimal>("SubTotal", c => c.NotNull())
                .Column<decimal>("Vat", c => c.NotNull())
                .Column<string>("Status", c => c.WithLength(50).NotNull())
                .Column<string>("PaymentServiceProviderResponse", c => c.WithLength(null))
                .Column<string>("PaymentReference", c => c.WithLength(50))
                .Column<DateTime>("PaidAt", c => c.Nullable())
                .Column<DateTime>("CompletedAt", c => c.Nullable())
                .Column<DateTime>("CancelledAt", c => c.Nullable())
                );
 
            SchemaBuilder.CreateTable("OrderDetailRecord", t => t
                .Column<int>("Id", c => c.PrimaryKey().Identity())
                .Column<int>("OrderRecord_Id", c => c.NotNull())
                .Column<int>("ProductId", c => c.NotNull())
                .Column<int>("Quantity", c => c.NotNull())
                .Column<decimal>("UnitPrice", c => c.NotNull())
                .Column<decimal>("VatRate", c => c.NotNull())
                );
 
            SchemaBuilder.CreateForeignKey("Order_Customer""OrderRecord"new[] {"CustomerId"}, "CustomerRecord"new[] {"Id"});
            SchemaBuilder.CreateForeignKey("OrderDetail_Order""OrderDetailRecord"new[] { "OrderRecord_Id" }, "OrderRecord"new[] { "Id" });
            SchemaBuilder.CreateForeignKey("OrderDetail_Product""OrderDetailRecord"new[] { "ProductId" }, "ProductRecord"new[] { "Id" });
 
            return 7;
        }


Notice that since we don't derive OrderRecord and OrderDetailRecord from the ContentRecord base class, we need to define the primary keys ourselves.

The PrimaryKey method marks the column as the primary key, while the Identity method marks the column to be auto-incrementing using the database server seed mechanism. 

Also note that we are creating a column called "OrderRecord_Id". Why the change in naming convention? Well, to support the Details collection property, Orchard (or NHibernate, the ORM used by Orchard) seems to expect a column name based on the parent type, appended with "_id".
Since we have a IList<OrderDetail> Details property in OrderRecord, to which we may add OrderDetail entities, we need to provide a foreign key column in the OrderDetailRecord table that matches the convention.
Perhaps there's a way to override this convention, but I'm not currently aware of any.

Now that the data entities and schema are in place, we'll create the OrderService which our OrderController can use to actually create an order.

Services/IOrderService.cs:

using System.Collections.Generic;
using Orchard.Webshop.Models;
 
namespace Orchard.Webshop.Services {
    public interface IOrderService : IDependency {
        /// <summary>
        /// Creates a new order based on the specified ShoppingCartItems
        /// </summary>
        OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);
 
        /// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails);
    }
}

Services/OrderService.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Webshop.Models;
 
namespace Orchard.Webshop.Services {
    public class OrderService : IOrderService {
        private readonly IDateTimeService _dateTimeService;
        private readonly IRepository<ProductRecord> _productRepository;
        private readonly IContentManager _contentManager;
        private readonly IRepository<OrderRecord> _orderRepository;
        private readonly IRepository<OrderDetailRecord> _orderDetailRepository;
 
        public OrderService(IDateTimeService dateTimeService, IRepository<ProductRecord> productRepository, IContentManager contentManager, IRepository<OrderRecord> orderRepository , IRepository<OrderDetailRecord> orderDetailRepository ) {
            _dateTimeService = dateTimeService;
            _productRepository = productRepository;
            _contentManager = contentManager;
            _orderRepository = orderRepository;
            _orderDetailRepository = orderDetailRepository;
        }
 
        public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items) {
 
            if(items == null)
                throw new ArgumentNullException("items");
 
            // Convert to an array to avoid re-running the enumerable
            var itemsArray = items.ToArray();
 
            if(!itemsArray.Any())
                throw new ArgumentException("Creating an order with 0 items is not supported""items");
 
            var order = new OrderRecord {
                CreatedAt  = _dateTimeService.Now,
                CustomerId = customerId,
                Status     = OrderStatus.New
            };
 
            _orderRepository.Create(order);
 
            // Get all products in one shot, so we can add the product reference to each order detail
            var productIds = itemsArray.Select(x => x.ProductId).ToArray();
            var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();
 
            // Create an order detail for each item
            foreach (var item in itemsArray) {
                var product = products.Single(x => x.Id == item.ProductId);
                
                var detail      = new OrderDetailRecord {
                    OrderRecord_Id     = order.Id,
                    ProductId   = product.Id,
                    Quantity    = item.Quantity,
                    UnitPrice   = product.Price,
                    VatRate     = .19m
                };
 
                _orderDetailRepository.Create(detail);
                order.Details.Add(detail);
            }
 
            order.UpdateTotals();
            
            return order;
        }
 
        /// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails) {
            var productIds = orderDetails.Select(x => x.ProductId).ToArray();
            return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty);
        }
    }
}

All we're doing here is create a new OrderRecord instance, store it into the database, and then create an OrderDetailRecord for each item in the shopping cart. Easy!

Now we can go back to OrderController and complete the Create action method:

using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Mvc;
using Orchard.Security;
using Orchard.Themes;
using Orchard.Webshop.Models;
using Orchard.Webshop.Services;
 
namespace Orchard.Webshop.Controllers {
    public class OrderController : Controller {
        private readonly dynamic _shapeFactory;
        private readonly IOrderService _orderService;
        private readonly IAuthenticationService _authenticationService;
        private readonly IShoppingCart _shoppingCart;
        private readonly ICustomerService _customerService;
        private readonly Localizer _t;
 
        public OrderController(IShapeFactory shapeFactory, IOrderService orderService, IAuthenticationService authenticationService, IShoppingCart shoppingCart, ICustomerService customerService) {
            _shapeFactory          = shapeFactory;
            _orderService          = orderService;
            _authenticationService = authenticationService;
            _shoppingCart          = shoppingCart;
            _customerService = customerService;
            _t                     = NullLocalizer.Instance;
        }
 
        [Themed, HttpPost]
        public ActionResult Create() {
 
            var user = _authenticationService.GetAuthenticatedUser();
 
            if(user == null)
                throw new OrchardSecurityException(_t("Login required"));
 
            var customer = user.ContentItem.As<CustomerPart>();
 
            if(customer == null)
                throw new InvalidOperationException("The current user is not a customer");
 
            var order = _orderService.CreateOrder(customer.Id, _shoppingCart.Items);
 
            // Todo: Give payment service providers a chance to process payment by sending a event. If no PSP handled the event, we'll just continue by displaying the created order.
            // Raise an OrderCreated event
 
            // If we got here, no PSP handled the OrderCreated event, so we'll just display the order.
            var shape = _shapeFactory.Order_Created(
                Order: order,
                Products: _orderService.GetProducts(order.Details).ToArray(),
                Customer: customer,
                InvoiceAddress: (dynamic)_customerService.GetAddress(user.Id, "InvoiceAddress"),
                ShippingAddress: (dynamic)_customerService.GetAddress(user.Id, "ShippingAddress")
            );
            return new ShapeResult(this, shape);
        }
    }
}

Before we'll introduce the extensibility point for PSP's, we'll update the "Order.Created.cshtml" template to display actual Order values, instead of the hardcoded dummy values:

Views/Order.Created.cshtml:

@using System.Globalization
@using Orchard.ContentManagement
@using Orchard.Core.Routable.Models
@using Orchard.Webshop.Models
@{
    Style.Require("Webshop.Order");
 
    var order = (OrderRecord) Model.Order;
    var productParts = (IList<ProductPart>) Model.Products;
    var invoiceAddress = Model.InvoiceAddress;
    var shippingAddress = Model.ShippingAddress;
}
<h2>@T("Order {0} has been created", order.Number)</h2>
<p>@T("Please find your order details below")</p>
 
<div class="order-wrapper">
    <article class="order">
        <header>
            <ul>
                <li>
                    <div class="field-label">Order Number</div>
                    <div class="field-value">@order.Number</div>
                </li>
                <li>
                    <div class="field-label">Created</div>
                    <div class="field-value">@order.CreatedAt.ToString(CultureInfo.InvariantCulture)</div>
                </li>
            </ul>
        </header>
        <table>
            <thead>
                <tr>
                    <td>Article</td>
                    <td class="numeric">Unit Price</td>
                    <td class="numeric">Quantity</td>
                    <td class="numeric">Total Price</td>
                </tr>
            </thead>
            <tbody>
                @foreach (var detail in order.Details) {
                    var productPart = productParts.Single(x => x.Id == detail.Product.Id);
                    var routePart = productPart.As<RoutePart>();
                    var productTitle = routePart != null ? routePart.Title : "(No RoutePart attached)";
                    <tr>
                        <td>@productTitle</td>
                        <td class="numeric">@detail.UnitPrice.ToString("c")</td>
                        <td class="numeric">@detail.Quantity</td>
                        <td class="numeric">@detail.SubTotal.ToString("c")</td>
                    </tr>
                }
            </tbody>
            <tfoot>
                <tr class="separator"><td colspan="4">&nbsp;</td></tr>
                <tr>
                    <td class="numeric label" colspan="2">Subtotal:</td>
                    <td class="numeric">@order.SubTotal.ToString("c")</td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="2">VAT:</td>
                    <td class="numeric">@order.Vat.ToString("c")</td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="2">Total:</td>
                    <td class="numeric">@order.Total.ToString("c")</td>
                </tr>
            </tfoot>
        </table>
    </article>
    
    <article class="addresses form">
        <div class="invoice-address">
            <h2>Invoice Address</h2>
            <ul class="address-fields">
                <li>@invoiceAddress.Name.Value</li>
                <li>@invoiceAddress.AddressLine1.Value</li>
                <li>@invoiceAddress.AddressLine2.Value</li>
                <li>@invoiceAddress.Zipcode.Value</li>
                <li>@invoiceAddress.City.Value</li>
                <li>@invoiceAddress.Country.Value</li>
            </ul>
        </div>
        <div class="shipping-address">
            <h2>Shipping Address</h2>
            <ul class="address-fields">
                <li>@shippingAddress.Name.Value</li>
                <li>@shippingAddress.AddressLine1.Value</li>
                <li>@shippingAddress.AddressLine2.Value</li>
                <li>@shippingAddress.Zipcode.Value</li>
                <li>@shippingAddress.City.Value</li>
                <li>@shippingAddress.Country.Value</li>
            </ul>
        </div>
    </article>
</div>

ResourceManifest.cs:

...
manifest.DefineStyle("Webshop.Order").SetUrl("order.css").SetDependencies("Webshop.Common");
...

Styles/order.css:

.order-wrapper {
    padding10px;
    border1px dashed #ccc;    
}
 
article.order {
    width100%;
}
 
article.order > header {
    margin-bottom20px;
    padding-bottom10px;
    border-bottom1px dotted #ccc;
}
 
article.order > header ul {
    list-stylenone;
    margin0;
}
 
article.order > header ul li:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.order > header ul li .field-label {
    floatleft;
    width100px;
    font-weightbold;
}
 
article.order > header ul li .field-value {
    floatleft;
}
 
article.order table {
    width100%;   
}
 
article.order td {
    padding7px 3px 4px 4px;
    vertical-alignmiddle;
}
 
article.order table thead td {
    background#f6f6f6;
    font-weightbold;
}
 
article.order table tfoot tr.separator td {
    border-bottom1px solid #ccc;
}
 
article.order table tfoot td {
    font-weightbold;
}
 
article.order footer {
    margin-top20px;
}
 
article.order td.numeric {
    width75px;
    text-alignright;
}
 
article.addresses {
    margin10px 0 10px 0;
    padding0 40px 10px 20px;
}
 
article.addresses:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.addresses .invoice-address{
    floatleft;
}
 
article.addresses .shipping-address{
    floatright;
}
 
ul.address-fields {
    margin0;
    list-stylenone;   
}

When we go ahead and hit the "Place Order" button, we should see the following screen (make sure you refresh the admin so that you can update the module):

We received an order! Now let's see how we can make the customer pay for it.

As mentioned, we are going to send an event message that a new order has been created, so that any listener of that event can take action.
To achieve this, we're going to leverage Orchard's Event Bus.

The Event Bus

Orchard's Event Bus is a powerful mechanism that allows us to provide extensibility points from our module, of which other modules can take advantage of.
Let's see how it works.


First of all, let's define a class that will represent a PaymentRequest. This class will contain the created order and a boolean flag that any event listener can set to let us know whether it will take care of starting the payment process for us. 
We'll also define a class that will represent a PaymentResponse, which will contain parsed feedback from the payment service provider. 

Let's go ahead and create a new class called PaymentRequest in a new folder called Extensibility:

Extensibility/PaymentRequestEventArgs.cs:

using System.Web.Mvc;
using Orchard.Webshop.Models;
 
namespace Orchard.Webshop.Extensibility
{
    public class PaymentRequest
    {
        public OrderRecord Order { getprivate set; }
        public bool WillHandlePayment { getset; }
        public ActionResult ActionResult { getset; }
 
        public PaymentRequest(OrderRecord order) {
            Order = order;
        }
    }
}

And the PaymentResponse class:

using System.Web;
 
namespace Orchard.Webshop.Extensibility
{
    public class PaymentResponse {
        public bool WillHandleResponse { getset; }
        public PaymentResponseStatus Status { getset; }
        public string OrderReference { getset; }
        public string PaymentReference { getset; }
        public string ResponseText { getset; }
        public HttpContextBase HttpContext { getprivate set; }
 
        public PaymentResponse(HttpContextBase httpContext) {
            HttpContext = httpContext;
        }
    }
}

Next, we'll create an interface the derives from IEventHandler called IPaymentServiceProvider. This will act as the contract for any PSP that wants to integrate with our webshop

Extensibility/IPaymentServiceProvider.cs:

using Orchard.Events;
 
namespace Orchard.Webshop.Extensibility {
    public interface IPaymentServiceProvider : IEventHandler {
        void RequestPayment(PaymentRequest e);
        void ProcessResponse(PaymentResponse e);
    }
}

IEventHandler is the key to making use of the Event Bus system, as we'll soon see.

Next, we need to inject an IEnumerable of IPaymentServiceProvider into our OrderController, so that we can invoke each event handler:

Controllers/OrderController.cs:

... 
private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders;
        public OrderController(             ...             IEnumerable<IPaymentServiceProvider> paymentServiceProviders) {
            ...
          _paymentServiceProviders = paymentServiceProviders;
        }

Now we can invoke each handler from the Create action method:

Controllers/OrderController.cs:

[Themed, HttpPost]
        public ActionResult Create() {
 
            ...
 
            // Fire the PaymentRequest event
            var paymentRequest = new PaymentRequest(order);
 
            foreach (var handler in _paymentServiceProviders) {
                handler.RequestPayment(paymentRequest);
 
                // If the handler responded, it will set the action result
                if (paymentRequest.WillHandlePayment) {
                    return paymentRequest.ActionResult;
                }
            }
 
            // If we got here, no PSP handled the PaymentRequest event, so we'll just display the order.
            ...
        }

You may be wondering what objects the _paymentServiceProviders will contain. Well, as it currently is, none whatsoever.

The way the event bus system works is that when it needs to inject an IEnumerable of something that implements IPaymentServiceProvider (which derives from IEventHandler), it will inject a list of any instance of any class that implements that interface.
Currently there is no such class, so let's create one.
 

Implementing the Simulated Payment Service Provider.

Let's go ahead and create a simulated payment service provider in the form of a class that implements IPaymentServiceProvider.
Because we ultimately want other modules to provide payment services, we should implement our simulated version as a feature so that our module users can manually enable or disable it.

To introduce a new feature, we first add it to our module's manifest:

Module.txt:

Name: Orchard.WebShop
AntiForgery: enabled
Author: Sipke Schoorstra
Website: http://skywalkersoftwaredevelopment.net
Version: 1.0
OrchardVersion: 1.3.10
Description: Orchard Webshop Module Demo
Category: Webshop
Dependencies: Orchard.jQuery, Orchard.Widgets, Orchard.Users
Features: Orchard.Webshop.SimulatedPSP
Name: Simulated Payment Service Provider
Description: Provides a simulated Payment Service Provider for testing purposes only.
Category: Webshop

 

Next, we'll create a new class called SimulatedPaymentServiceProvider in the Services folder:

Services/SimulatedPaymentServiceProvider.cs:

using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Environment.Extensions;
using Orchard.Webshop.Extensibility;
 
namespace Orchard.Webshop.Services {
    [OrchardFeature("Orchard.Webshop.SimulatedPSP")]
    public class SimulatedPaymentServiceProvider : IPaymentServiceProvider
    {
        public void RequestPayment(PaymentRequest e) {
 
            e.ActionResult = new RedirectToRouteResult(new RouteValueDictionary {
                {"action""Index"},
                {"controller""SimulatedPaymentServiceProvider"},
                {"area""Orchard.Webshop"},
                {"orderReference", e.Order.Number},
                {"amount", (int)(e.Order.Total * 100)}
            });
 
            e.WillHandlePayment = true;
        }
 
        public void ProcessResponse(PaymentResponse e) {
            var result = e.HttpContext.Request.QueryString["result"];
 
            e.OrderReference     = e.HttpContext.Request.QueryString["orderReference"];
            e.PaymentReference   = e.HttpContext.Request.QueryString["paymentId"];
            e.ResponseText       = e.HttpContext.Request.QueryString.ToString();
            
            switch (result) {
                case "Success":
                    e.Status = PaymentResponseStatus.Success;
                    break;
                case "Failure":
                    e.Status = PaymentResponseStatus.Failed;
                    break;
                case "Cancelled":
                    e.Status = PaymentResponseStatus.Cancelled;
                    break;
                default:
                    e.Status = PaymentResponseStatus.Exception;
                    break;
            }
 
            e.WillHandleResponse = true;
        }
    }
}

Orchard will add an instance of this class into the IEnumerable<IPaymentServiceProvider> constructor argument of our OrderController. That is, if we enabled the feature named "Orchard.Webshop.SimulatedPSP". Notice the OrchardFeatureAttribute: Orchard will only instantiate dependencies if the feature they are decorated with is enabled. That's some nifty feature we've got there!

Let's enable the "Simulated Payment Service Provider" feature:

 

Next, we'll create the view of our simulated PSP. Notice that this time, we're returning a plain old MVC PartialViewResult instead of a ShapeResult. That's because the simulated PSP is not intended to be overridden in a theme.

Views/SimulatedPaymentServiceProvider/Index.cshtml:

@{
    var orderReference = (string) Model.OrderReference;
    var amount = (decimal)((int) Model.Amount) / 100;
    var commands = new[] { "Success""Failure""Cancelled""Exception" };
 
    Style.Require("Webshop.SimulatedPSP");
}
 
<h2>Payment Service Provider Simulation</h2>
<p>
    Received a payment request with order reference <strong>@orderReference</strong><br/>
    Amount: <strong>@amount.ToString("c")</strong>
</p>
@using (Html.BeginFormAntiForgeryPost(Url.Action("Command""SimulatedPaymentServiceProvider"new {area = "Orchard.Webshop"}))) {
    <article class="form">
        <input type="hidden" name="orderReference" value="@orderReference"/>
        <ul class="commands">
            @foreach (var command in commands) {
                <li><button type="submit" name="command" value="@command">@command</button></li>
            }
        </ul>
    </article>
}

ResourceManifest.cs:

manifest.DefineStyle("Webshop.SimulatedPSP").SetUrl("simulated-psp.css").SetDependencies("Webshop.Common");

Styles/simulated-psp.css:

ul.commands {
    list-stylenone;
}
 
ul.commands button {
    width100px;
}

The simulation screen should look something like this:

We'll implement the Command action method as follows:

Controllers/SimulatedPaymentServiceProviderController.cs:

[HttpPost]
        public ActionResult Command(string command, string orderReference) {
 
            // Generate a fake payment ID
            var paymentId = new Random(Guid.NewGuid().GetHashCode()).Next(1000, 9999);
 
            // Redirect back to the webshop
            return RedirectToAction("PaymentResponse""Order"new {area = "Orchard.Webshop", paymentId = paymentId, result = command, orderReference});
        }

And that's sufficient four our simulation PSP. 
Notice that we "configured" the PSP to redirect to a PaymentResponse action method on the OrderController, so let's implement that:

Controllers/OrderController.cs:

[Themed]
        public ActionResult PaymentResponse() {
 
            var args = new PaymentResponse(HttpContext);
 
            foreach (var handler in _paymentServiceProviders) {
                handler.ProcessResponse(args);
 
                if (args.WillHandleResponse)
                    break;
            }
 
            if(!args.WillHandleResponse)
                throw new OrchardException(_t("Such things mean trouble"));
 
            var order = _orderService.GetOrderByNumber(args.OrderReference);
            _orderService.UpdateOrderStatus(order, args);
 
            if (order.Status == OrderStatus.Paid) {
                // Send some notification mail message to the customer that the order was paid.
                // We may also initiate the shipping process from here
            }
 
            return new ShapeResult(this, _shapeFactory.Order_PaymentResponse(Order: order, PaymentResponse: args));
        }


Here, we once again leveraged the power of the Event Bus to delegate the task of interpreting the response for us! We defined a simple protocol that each PSP implementation must adhere to in order to provide us with a data that we know how to work with. If that sounds a bit like the Adapter pattern at work to you, you're right, given the fact that IPaymentServiceProvider will adapt the input to a specific, wellknown output.

As you may have noticed, we introduced two new methods on IOrderServiceGetOrderByNumber and UpdateOrderStatus:

Services/IOrderService.cs:

OrderRecord GetOrderByNumber(string orderNumber);
void UpdateOrderStatus(OrderRecord order, PaymentResponseEventArgs paymentResponse);

Services/OrderService.cs:

...
namespace Orchard.Webshop.Services {
    public class OrderService : IOrderService {
        ...
 
        public OrderRecord GetOrderByNumber(string orderNumber) {
            var orderId = int.Parse(orderNumber) - 1000;
            return _orderRepository.Get(orderId);
        }
 
        public void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse) {
            OrderStatus orderStatus;
 
            switch (paymentResponse.Status) {
                case PaymentResponseStatus.Success:
                    orderStatus = OrderStatus.Paid;
                    break;
                default:
                    orderStatus = OrderStatus.Cancelled;
                    break;
            }
            
            if (order.Status == orderStatus)
                return;
 
            order.Status = orderStatus;
            order.PaymentServiceProviderResponse = paymentResponse.ResponseText;
            order.PaymentReference = paymentResponse.PaymentReference;
 
            switch(order.Status) {
                case OrderStatus.Paid:
                    order.PaidAt = _dateTimeService.Now;
                    break;
                case OrderStatus.Completed:
                    order.CompletedAt = _dateTimeService.Now;
                    break;
                case OrderStatus.Cancelled:
                    order.CancelledAt = _dateTimeService.Now;
                    break;
            }
        }
    }
}

Next, we'll implement the PaymentResponse action:

Controllers/OrderController.cs:

[Themed]
        public ActionResult PaymentResponse() {
 
            var args = new PaymentResponse(HttpContext);
 
            foreach (var handler in _paymentServiceProviders) {
                handler.ProcessResponse(args);
 
                if (args.WillHandleResponse)
                    break;
            }
 
            if(!args.WillHandleResponse)
                throw new OrchardException(_t("Such things mean trouble"));
 
            var order = _orderService.GetOrderByNumber(args.OrderReference);
            _orderService.UpdateOrderStatus(order, args);
 
            if (order.Status == OrderStatus.Paid) {
                // Send some notification mail message to the customer that the order was paid.
                // We may also initiate the shipping process from here
            }
 
            return new ShapeResult(this, _shapeFactory.Order_PaymentResponse(Order: order, PaymentResponse: args));
        }

Once again, we are iterating over a list of IPaymentResponseEventHandler instances, and invoke the ProcessResponse method, which will take care of adapting the received data to our needs.

The last thing we need to do is create a template for the Order_PaymentResponse shape, which we returned at the end.
We know how to do that:

Views/Order.PaymentResponse.cshtml: 

@using Orchard.Webshop.Extensibility
@using Orchard.Webshop.Models
@{
    var order = (OrderRecord) Model.Order;
    var paymentResponse = (PaymentResponse) Model.PaymentResponse;  
}
@if (paymentResponse.Status == PaymentResponseStatus.Success)
{
    <h2>@T("Payment was succesful")</h2>
    <p>Thanks! We succesfully received payment for order @order.Number with payment ID @paymentResponse.PaymentReference</p>
    <p>Enjoy your products and come again!</p>
}
else {
    <h2>@T("Order cancelled")</h2>
    <p>Your order (@order.Number) has been cancelled</p>
}

When we complete our order by simulating a successful payment, we should see something like this:

Providing extensibility points using the Event Bus can be very powerful. In the same we, we could implement some sort of IShippingProvider contract, which other modules will implement. For example, if you're selling virtual goods, one implementation could take care of actually delivering the virtual product to your customer.

In the next part, we're gonna have a look on how to extend the Orchard Admin UI so that our users can manage their customers and orders.

Part 10 -> Managing the Customers and Orders from the backend

45 comments

  • ... Posted by Katy Posted 11/07/2014 08:18 AM

    Hi! I'm following along and I'm in Part 9 where I'm ready to enable the "Simulated Payment Service Provider" feature, but when I build the project, I'm getting an error that the type or namespace "PaymentResponseStatus" could not be found. Where is this being defined? I tried to look at the source code, but it looks like I have everything that is in the source code.

    Thanks, Katy

  • ... Posted by Katy Posted 11/07/2014 08:19 AM

    I found the file that I was missing, it was Extensibility/PaymentResponseStatus.cs. I added that file and then re-built the project and once the page opened, I was missing the Shopping Cart Widget, the price and sku on the books and the "Add to shopping cart" buttons. The code is all still in my project, but can you tell me how to get the pages to show the buttons again?

    Thanks, Katy

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:19 AM [http://www.ideliverable.com]

    Hi Katy, I'm not sure why the buttons aren't showing, but perhaps you could check the log files in the App_Data/Logs folder to see if any exceptions are being thrown. It happened to me more than once that an error occurred during the execution of certain drivers. These exceptions are swallowed, so that the page itself gets rendered, except for the shapes created by the faulting drivers.

  • ... Posted by Alexander To Posted 11/07/2014 08:19 AM

    Hi May I know how does the constructor of ShoppingCartController work? I can't relate it to how Orchard know which class to instantiate and inject into the constructor. If I create another class called ShoppingCart2 which implements IShoppingCart, which one will be injected?? Thanks

  • ... Posted by Alexander To Posted 11/07/2014 08:20 AM

    Nevermind, I trace the code and roughly know how it works now

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:20 AM [http://www.ideliverable.com]

    Great. Although I didn't test it you will probably just get one or the other implementation, based on which implementation AutoFac will find first. Shoudl you ever have multiple implementations of a certain interface, you could inject them via the constructor by injecting an IEnumerable of the interface definition, e.g. "IEnumerable<ishoppingcart> shoppingCarts". That would give you all implementations that exist. Then, you can provide your own logic to determine which implementation to use.

  • ... Posted by Nick Bratym Posted 11/07/2014 08:20 AM

    Hi, it's a great job you've done. I have some thoughts about security, since this aspect is important in payments domain model. I think it's too easy to fake the amount.

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:20 AM [http://www.ideliverable.com]

    Hi Nick, glad you like it! I am curious about your thoughts, could you please share them? Thanks.

  • ... Posted by Nick Bratym Posted 11/07/2014 08:21 AM

    Hi, again. I have one more question for you. It seems, when a product part displayed as content item in a content items list, in the dashboard, it's displayed by the product driver, and that why we got add to shopingcart button in the dashboard. Could you explain, how to avoid that?

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:21 AM [http://www.ideliverable.com]

    Hi Nick,

    You're correct: when Orchard renders a list of Content Items, it will invoke all of the content part drivers of each content item. To avoid displaying the "ShoppingCart" button from appearing in the back end, you should create a so called "alternate" shape and template. By convention, Orchard supplies the "SummaryAdmin" displaytype when invoking the Display method on the drivers, which you can use to determine of you should create different shape specifically for being rendered in the admin.

    I will update update the post and demonstrate how this is done.

    Thanks for your question.

  • ... Posted by Nick Bratym Posted 11/07/2014 08:21 AM

    Hi Spike, I fixed it with the placement . info

  • ... Posted by Brent Arias Posted 11/07/2014 08:21 AM

    You have the requirement "Each catalog will show one type of product (e.g. Books, DVDs, CDs)" and you say "There is no technical restriction that prevents other product types to show up." But it seems as if it is a technical limitation. The admin drop-down to specify what the content type "contains" only allows one containable type to be selected. How then would "any/every containable" type, having the product part, be specified in a single product catalog?

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:22 AM [http://www.ideliverable.com]

    Hi Brent, as it currently is, the Container part allows you to either allow all content types, or just a specific content type. Should I want to show different types of content items in my catalog, I would have to set the drop-down to "all types". That would work, although I agree that it would be much nicer if we could specify to only display content items that have a specific content part attached to it (ProductPart in this case). Doing so is quite possible, but it would require us to create a customized ContainerPart of sorts. However, I would then rather have a look at the Projector module that's coming with the next release of Orchard, where we could specify a query that selects all content items that have the content part attached.

  • ... Posted by nxcong Posted 11/07/2014 08:22 AM

    I want to ask how to set up the permission for users that they just can post on my site but can’t log in admin page to create their own product. I tried to log in permission to allow users can publish,unpublish,edit,delete product but they still can log in admin page.???Sorry about my english.

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:22 AM [http://www.ideliverable.com]

    Hi nxcong, as long as the user does not have the "AccessAdminPanel" permission, then the user will not be able to login into the admin. So, of you create a new user, be sure that he is in none of the following groups: Administrator, Editor, Moderator, Author or Contributor. Both Anonymous and Authenticated users will have permission to add comments on the front end.

  • ... Posted by Jean-Noël Gourdol Posted 11/07/2014 08:23 AM

    A truly excellent tutorial. The only thing I disagree with is the use of the globalization javascript plug-in, as Orchard has pretty good built-in localization capabilities. Basically, you just have to write @T(myString) in any view to have it localized. You may have to work a little to have it accomodate numeric and date formats, but not a whole lot. I encourage you to try it if you have not already.

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:23 AM [http://www.ideliverable.com]

    I agree that Orchard has great built-in localization capabilities, but do they also have client side scripts that are able to format numbers using a variety of cultures? The reason that I included the javascript Globalization plugin is because the ShoppingCart updates the price values clientside (by multiplying with the quantity). Since there's no servercode involved, I needed a way to format the calculated prices on the client. Hence the Globalization plugin. But if you know how to do it using Orchard javascripts, I'd be happy to hear it.

    Thanks for the comment.

  • ... Posted by Markus Erlacher Posted 11/07/2014 08:23 AM

    Hi Skywalker,

    I look for a way to add a CustomerPart and AddressPart to an existing User. Any tip? Regards Markus

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:23 AM [http://www.ideliverable.com]

    Hi Markus,

    Sure, just create a class that derives from ContentHandler and add an ActivatingFilter to the Filters collection. It goes like this:

    public class CustomerHandler : ContentHandler { public CustomerHandler() { Filters.Add(new ActivatingFilter<customerpart>("User")); Filters.Add(new ActivatingFilter<addresspart>("User")); }

    }

    Normally you could attach parts to any content type using the Migrations file, but there doesn;t seem to be a User content type in the database. That's why you need to attach the parts every time a "User" content type is being requested (I believe that's the purpose of the ActivatingFilter).

    If you look at the Orchard.Users module, you'll see that it's done exactly the same to attach the UserPart to the User content type.

    Regards, Sipke

  • ... Posted by Валентин Гушан Posted 11/07/2014 08:24 AM

    Thank you for this awesome guide! I walk from beginning to this chapter, step by step, but got exception, please help!

    Exception details: when I try choos items for my Product catalog:

    Object reference not set to an instance of an object. Source Error:

    Line 7: Layout.Title = T("Choose Items"); Line 8: Line 9: var targetContainers = ((IEnumerable<contentitem>)Model.Containers).Select( Line 10: contentItem => new SelectListItem { Line 11: Text = T("Move to {0}", contentItem.ContentManager.GetItemMetadata(contentItem).DisplayText ?? contentItem.ContentType).ToString()...

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:24 AM [http://www.ideliverable.com]

    My mistake; I forgot to attach the CommonPart to one or the other content type. I don't remember off the top of my head which content type it was exactly, but just go through all of the content types you are creating in your Migrations file, and make sure that all content types have the CommonPart attached. You can also attach the CommonPart after the fact using the Admin.

    I believe a few guys on this thread were experiencing the same issue.

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:25 AM [http://www.ideliverable.com]

    After reading some of the other comments I noticed that more people were having the same issue. To avoid the exception, you need to attach the CommonPart to the ProductCatalog content type. It should have bene included in the UpdateFrom2 method of the Migrations class. I updated Part 5 so that you can see how it looks. Again, you can also use the Admin to attach the CommonPart still.

  • ... Posted by Fernando Sonego Posted 11/07/2014 08:25 AM

    Hi, in the part 4 you didn't choose Admin Menu as part of a book, then when you do. WithPart (typeof (AdminMenuPart). Name) shows the error.

    sorry for my horrible English. :P

  • ... Posted by Dave Posted 11/07/2014 08:25 AM

    Thanks immensely for this excellent tutorial. It is the single best resource for learning Orchard (and you should have written it ages ago!). Please provide a blog search facility so I can more easily find stuff when I want to refer back. Many thanks, Dave.

  • ... Posted by Jack Richmond Posted 11/07/2014 08:26 AM

    You have really done a fabulous job by providing such great tutorials to us. As yet I am working on such application, my work has become much easier by following each and every steps of your tutorial. Little bit modification is required according to client priority, but that I will manage to handle it.

  • ... Posted by Game_beta2003 Posted 11/07/2014 08:26 AM

    For anyone getting errors, when creating "ProductCatalog" using the AlterTypeDefinition method, add the following using statements.

    using Orchard.Core.Common.Models; using Orchard.Core.Containers.Models; using Orchard.Core.Routable.Models; using Orchard.Core.Navigation.Models;

  • ... Posted by GadgetMadGeek Posted 11/07/2014 08:26 AM

    I don't seem to get the drop down admin menu. I only ever get the top item. Any snippets/clarification would be appreciated!

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:27 AM [http://www.ideliverable.com]

    The only snippet required is in this post: http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-10

    Did you copy the exact same code?

  • ... Posted by Game_beta2003 Posted 11/07/2014 08:27 AM

    What do you mean by drop down admin menu?

  • ... Posted by Demetrios Seferlis Posted 11/07/2014 08:28 AM

    One of the best tutorials ever seen for orchard module development. We all thank you Sipke.

  • ... Posted by Coach James Posted 11/07/2014 08:28 AM [http://www.crcdata.net]

    The force is strong within you Skywalker...

    After 15 years of classic asp and object javascripting, I almost started supporting Wordpress and Drupal (back into Unix/Perl/PHP) because of the lack any proper ASP/CMS package, and almost built my own, until I started looking for any open source projects with ASP.NET, and finally found my first acceptable ASP.NET CMS package and its Orchard!

    Then came the fun part of migrating my classic asp and json's into ASP.NET MVC 3 structure USING C# and utilizing the nice architectural rendering engine and other other great benefits by Orchard designers by the way of "razor", etc.

    But it was taking way too long to figure out where everything goes, whats happening behing the curtains, and what options and limits I need to deal with when converting my database driven applications.

    Thanks to Skywalker "Orchard can and will be a Big Hit this year!" But don't let David Hyden know you let the cat out of the bag, cause he's been keeping it a secret.

    Coach James

  • ... Posted by alaki Posted 11/07/2014 08:29 AM

    thanks for your awesome tutorial. i have a question, how can i build hole my query and projection in code? because i want build a module that user just install and start using it!

  • ... Posted by stalker1982 Posted 11/07/2014 08:29 AM

    Thanks a lot! This is one of the most useful tutorials I've read! Thank you!

  • ... Posted by J3ffb Posted 11/07/2014 08:30 AM

    Brilliant series of blog posts, great work :)

    Just wondering if it would be better to cast the ContentItem as a ITitleAspect so that it can support ContentTypes that don't use the RoutePart?

  • ... Posted by Brian Posted 11/07/2014 08:30 AM

    This is a great article and has given me a much better idea of how to deal with a customer checkout implementation I have been working on. One thing that is unclear to me. What happens if someone is not yet a customer but already has a login? I'm not seeing anything that adds a CustomerPart to an existing user.

  • ... Posted by Ryan Posted 11/07/2014 08:31 AM

    Fantastic tutorials. I am learning Orchard as part of my new job and there is so little information out there, with what does exist being very patchy. This series has been extremely helpful, so thanks!

  • ... Posted by Caskia Liu Posted 11/07/2014 08:31 AM

    Thanks a lots,I learned a lot from you.

  • ... Posted by Noro Posted 11/07/2014 08:33 AM

    Incredibly thorough tutorial. A look inside the core Orchard architecture with such an ease. Can't thank you enough.

  • ... Posted by Keighley Josiah Posted 11/07/2014 08:33 AM

    WOOHOO!jackpot...i was looking for the orchard module for some time now....

  • ... Posted by LarryBS Posted 11/07/2014 08:33 AM

    Great! Great!

    But only one thing, how to make the "Customer" ContentType to appear in the Content Item list ?

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 08:34 AM [http://www.ideliverable.com]

    Set its "Creatable" property to true.

  • ... Posted by Elijah Posted 03/07/2015 11:52 PM

    Hi. This is a greate article and I'm really grateful for your effort in sharing this knowledge. I'm new to orchard and NHibernate - and I've got a little problem after the step where are I created the Order.css file. I try to run the app and I get a 404 error. When I view the logs I get this information: 2015-03-07 23:37:46,231 [7] Orchard.Environment.DefaultOrchardHost - (null) - A tenant could not be started: DefaultNHibernate.PropertyNotFoundException: Could not find a setter for property 'UnitVat' in class 'Orchard.Webshop.Models.OrderDetailRecord' Any ideas?

    Thanks

  • ... Posted by Ahmad Khudairy Posted 03/29/2015 05:19 PM

    I was following this great tutorial (many thanks) ... i got a shock reaching to this part9. its like this is a different project now!, first of all the migration file has skipped updatefrom5!! in part 8 the last update we did was updatefrom4() and here it is updatefrom6(). Not just that, the namespace also changed! we were using "Skywalker.Webshop" and now suddenly it is "Orchard.Webshop". I tried to follow along any way until I reached the update to Order.Created.cshtml, I just don't know where to get "routPart" reference from, and it seems this is actually the wrong way to do things as I did a bit search for it. Order service is also referencing many classes that I didn't know how to get IDateTimeService, ProductRecord

  • ... Posted by Shahin Posted 05/30/2015 08:43 PM

    Hi Sipke First of all thanks for sharing your knowledge and not hiding the details like Other guys --He is not David Hayden ;) -- But I got the same issue as Elijah points to and that's the problem with get only properties of our OrderRecord and OrderDetailRecord Models what should I do ? It's so confusing I think somthing happen with the mapper of Nhibernate do you have any suggestion? I'm on 1.8.1 Regards

  • ... Posted by Roor Posted 07/05/2015 08:27 PM

    Well, the thing with the setters can be resolved (although it's not a nice way, there must be something better). Instead of

    public virtual decimal Total {
        get{ return SubTotal + Vat}
    }
    

    you can write

    public virtual decimal Total {
        get{ return SubTotal + Vat}
        set {}
    }
    

    That means that NHibernate is happy although we don't use the setter.

    In any case: I would be really pleased if the author could update his awesome tutorial! I am interested in using the admin module and the search feature for orders. But this does not work anymore, either. Some things seem to have changed in Orchard and I am a bit lost, too.

Leave a comment