On this page

Processing an order

An order calculation combines input assumptions, arithmetic, delivery policy and an approval decision. Keep those responsibilities visible instead of hiding them in property setters or a single long expression.

This workflow prepares an order for approval. It does not charge a card or reserve stock. Those external side effects need transaction and idempotency policies of their own.

Order state

The workflow uses the Customer model from customer validation. Quantities and prices belong to lines; the chosen delivery method belongs to the order. Enum zero values describe meaningful initial states.

import Global.System
import Global.System.Collections.Generic

public enum OrderStatus
    Pending = 0
    Approved = 1
    NeedsReview = 2
    Rejected = 3
end enum

public enum DeliveryMethod
    Standard = 0
    Express = 1
    Collection = 2
end enum

public class OrderLine
    public ProductCode as String = ""
    public Quantity as Integer = 0
    public UnitPrice as Decimal = 0
end class

public class Order
    public Id as Guid = Guid.NewGuid()
    public Customer as Customer
    public Lines as List <of OrderLine> = new List <of OrderLine>()
    public Delivery as DeliveryMethod = DeliveryMethod.Standard
    public Status as OrderStatus = OrderStatus.Pending
end class

public class OrderSummary
    public OrderId as Guid = Guid.Empty
    public MerchandiseTotal as Decimal = 0
    public DeliveryFee as Decimal = 0
    public Total as Decimal = 0
    public Status as OrderStatus = OrderStatus.Pending
end class

The public stored members are ordinary .NET properties. Mutable members receive their type defaults; Lines deliberately constructs an empty list. An empty order exists as an input model, but cannot pass the processing contracts.

Calculating and deciding

The processor accepts one pending order and returns an independent summary. The 5000 review threshold and delivery fees are application policy. They are easy to locate and change without changing the model.

import Global.System

public class OrderProcessor
    public action Process(order as Order)
        returns OrderSummary
        require order is not nothing
        require order.Customer is not nothing
        require order.Customer.IsActive
        require order.Status = OrderStatus.Pending otherwise new InvalidOperationException("Order was already processed")
        require order.Lines is not nothing
        require order.Lines.Count > 0
        ensure result.Total >= 0

        try
            let subtotal = CalculateMerchandise(order)
            let deliveryFee as Decimal = select order.Delivery
                case .Collection
                    take 0
                case .Express
                    take 15
                else
                    take 5
            end select

            let summary = new OrderSummary()
            summary.OrderId = order.Id
            summary.MerchandiseTotal = subtotal
            summary.DeliveryFee = deliveryFee
            checked
                summary.Total = subtotal + deliveryFee
            end checked

            if summary.Total > 5000 then
                order.Status = OrderStatus.NeedsReview
            else
                order.Status = OrderStatus.Approved
            end if
            summary.Status = order.Status
            return summary

        catch OverflowException
            order.Status = OrderStatus.Rejected
            throw new InvalidOperationException("Order amount exceeds the supported range", ex)
        end try
    end action

    private action CalculateMerchandise(order as Order)
        returns Decimal
        let total as Decimal = 0
        for_each line in order.Lines
            require line is not nothing otherwise new ArgumentException("Order line is required")
            require line.Quantity > 0 otherwise new ArgumentException("Quantity must be positive")
            require line.UnitPrice >= 0 otherwise new ArgumentException("Price cannot be negative")
            checked
                total += line.Quantity * line.UnitPrice
            end checked
        end for_each
        return total
    end action
end class

The select computes only the delivery fee. Each selected path ends with take, and else makes the expression complete. return later exits Process. The two words have different scopes.

CalculateMerchandise rejects malformed lines rather than silently skipping them. The protected arithmetic reports overflow as a business-operation failure while retaining the underlying exception. Validation contract failures retain their own exception behavior; this catch does not turn every failure into an overflow.

The state transition happens after the amount has been calculated. A successful result agrees with the order's new status. An arithmetic failure marks the order rejected. Because the model is mutable, callers must avoid running Process concurrently on the same order instance.

Preparing input

A caller can construct a line, add it to an order, and invoke the processor without a domain-specific builder API:

public class OrderExample
    public static action Run(customer as Customer)
        returns OrderSummary
        let line = new OrderLine()
        line.ProductCode = "DESK-42"
        line.Quantity = 2
        line.UnitPrice = 149.50

        let order = new Order()
        order.Customer = customer
        order.Delivery = DeliveryMethod.Express
        order.Lines.Add(line)

        let processor = new OrderProcessor()
        return processor.Process(order)
    end action
end class

For an active customer, the merchandise amount is 299 and the total is 314. A higher total may still be a valid order that needs review. An inactive customer fails the processing precondition. Test those outcomes separately from invalid quantities, repeated processing and overflow.