RestBucks on .Net; canceling an order

Previous posts

In this post we are going to implement a new use case; the customer cancels an order.

Before we go to the REST operation itself it is important to introduce some modifications to our domain class:

public class Order : Entity
{
    //more properties and methods....
    
    public OrderStatus Status { get; private set; }
    public string CancelReason { get; private set; }

    public virtual void Cancel(string cancelReason)
    {
        if(Status != OrderStatus.Unpaid)
        {
            throw new InvalidOrderOperationException(string.Format("The order can not be canceled because it is {0}.",
                Status.ToString().ToLower()));
        }
        CancelReason = cancelReason;
        Status = OrderStatus.Canceled;
    }
}

The method is simple, before changing the state of the order to Canceled we have to validate if the order is at the Unpaid state (remember this graph?), once the order has been Paid or it is at any other stage of the process; the Cancel method will throw an exception.

I’ve tested the happy path as follows:

[TestFixture]
public class GivenAnUnpaidOrder
{
    private Order order;

    [SetUp]
    public void SetUp()
    {
        order = new Order();
    }

    [Test]
    public void CancelShouldWork()
    {
        order.Cancel("error");
        order.Status.Should().Be.EqualTo(OrderStatus.Canceled);
    }
}

And then the not so happy path:

[TestFixture]
public class GivenAPayedOrder
{
    private Order order;

    [SetUp]
    public void SetUp()
    {
        order = new Order();
        order.Pay("123", "jose");
    }

    [Test]
    public void CancelShouldThrow()
    {
        order.Executing(o => o.Cancel("error"))
            .Throws<InvalidOrderOperationException>()
            .And
            .Exception.Message.Should().Be.EqualTo("The order can not be canceled because it is paid.");
    }
}

I choose to use different classes much like as per-context fixtures, because I needed to test several other things when the order is Unpaid/Paid.

The next thing is to handle the REST stuff in the handler:

[WebInvoke(UriTemplate = "{orderId}", Method = "DELETE")]
public HttpResponseMessage Cancel(int orderId)
{
    var order = orderRepository.GetById(orderId);
    if(order == null) return Responses.NotFound();
    order.Cancel("canceled from the rest interface");
    return Responses.NoContent();
}

This handler validate first if the order exists, if it doesn't exists; return 404. If the order exists it calls the Cancel operation and returns 204. You can see the tests here.

Since we are doing something like "soft deletes" the next question is what will happen if somebody makes a GET requests to order/id with the id of a canceled order. In my mind the above Cancel method moves the resource from one collection to another one (the trash) so we can modify our GET to work as follows:

[Test]
public void ACallToGet_ShouldReturnMovedPermanentlyAndNewLocation()
{
    var order = new Order { Id = 123 };
    var handler = new OrderResourceHandler(new RepositoryStub<Order>(order), resourceLinker);
    handler.Cancel(123);

    
    var responseToGet = handler.Get(123, null);

    var expected = "http://restbuckson.net/trash/order/123";
    responseToGet.Satisfy(r => r.StatusCode == HttpStatusCode.MovedPermanently
                            && r.Headers.Location.ToString() == expected);
}

The implementation:

[Test]
public void ACallToGet_ShouldReturnMovedPermanentlyAndNewLocation()
{
    var order = new Order { Id = 123 };
    var handler = new OrderResourceHandler(new RepositoryStub<Order>(order), resourceLinker);
    handler.Cancel(123);

    
    var responseToGet = handler.Get(123, null);

    var expected = "http://restbuckson.net/trash/order/123";
    responseToGet.Satisfy(r => r.StatusCode == HttpStatusCode.MovedPermanently
                            && r.Headers.Location.ToString() == expected);
}

And finally we need a TrashHandler (nice name!):

[ServiceContract, WithUriPrefix("trash")]
public class TrashHandler
{
    private readonly IRepository<Order> orderRepository;

    public TrashHandler(IRepository<Order> orderRepository)
    {
        this.orderRepository = orderRepository;
    }
    
    [WebGet(UriTemplate = "/order/{orderId}")]
    public HttpResponseMessage GetCanceled(int orderId)
    {
        var order = orderRepository.Retrieve(o => o.Id == orderId && o.Status == OrderStatus.Canceled)
                                   .FirstOrDefault();

        return order == null ? Responses.NotFound()
            : new HttpResponseMessage<OrderRepresentation>(OrderRepresentationMapper.Map(order));
    }
}

This is all for now! I will talk about the Pay operation in my next article, thanks for reading!


blog comments powered by Disqus
  • Categories

  • Archives