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.