Adding a new test client that supports the Order-based booking flow
diff --git a/utils/utils.go b/utils/utils.go
index ac9f064..7e2bf7c 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -18,12 +18,14 @@
 package utils
 
 import (
+	"crypto/md5"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
 	"math/rand"
 	"path"
+	"sort"
 	"strings"
 
 	"github.com/golang/protobuf/jsonpb"
@@ -47,6 +49,116 @@
 	log.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " "))
 }
 
+func parseServiceFeed(serviceFeed string) ([]*fpb.Service, error) {
+	var feed fpb.ServiceFeed
+	content, err := ioutil.ReadFile(serviceFeed)
+	if err != nil {
+		return nil, fmt.Errorf("unable to read input file: %v", err)
+	}
+	if path.Ext(serviceFeed) == ".json" {
+		if err := jsonpb.UnmarshalString(string(content), &feed); err != nil {
+			return nil, fmt.Errorf("unable to parse feed as json: %v", err)
+		}
+	}
+	if path.Ext(serviceFeed) == ".pb3" {
+		if err := proto.Unmarshal(content, &feed); err != nil {
+			return nil, fmt.Errorf("unable to parse feed as pb3: %v", err)
+		}
+	}
+
+	if services := feed.GetService(); len(services) != 0 {
+		return services, nil
+	}
+	return nil, errors.New("service feed is empty. At least one service must be present in feed")
+}
+
+func merchantService(merchantID, serviceID string) string {
+	return strings.Join([]string{merchantID, serviceID}, "||")
+}
+
+func buildLineItem(availability *fpb.Availability, tickets []*fpb.TicketType) *mpb.LineItem {
+	lineItem := &mpb.LineItem{
+		ServiceId:   availability.GetServiceId(),
+		StartSec:    availability.GetStartSec(),
+		DurationSec: availability.GetDurationSec(),
+		Price:       &mpb.Price{},
+	}
+
+	// If not ticket types return nil as there is nothing to build.
+	if len(tickets) == 0 {
+		return nil
+	}
+
+	for i := 0; i < int(availability.GetSpotsOpen()); i++ {
+		// This deterministic which is fine given that we just want to get a mix of ticket types.
+		ticketTypeIndex := rand.Intn(len(tickets))
+		// Calculate price of line item.
+		if lineItem.GetPrice().GetCurrencyCode() == "" && tickets[ticketTypeIndex].GetPrice().GetCurrencyCode() != "" {
+			lineItem.Price.CurrencyCode = tickets[ticketTypeIndex].GetPrice().GetCurrencyCode()
+		}
+		lineItem.Price.PriceMicros += tickets[ticketTypeIndex].GetPrice().GetPriceMicros()
+
+		// Add ticket to line item.
+		lineItem.Tickets = append(lineItem.Tickets, &mpb.LineItem_OrderedTickets{
+			TicketId: tickets[ticketTypeIndex].GetTicketTypeId(),
+			Count:    1,
+		})
+	}
+
+	return lineItem
+}
+
+// MerchantLineItemMapFrom attempts to build a collection of LineItems from a service and availability feed.
+func MerchantLineItemMapFrom(serviceFeed, availabilityFeed string, testSlots int) (map[string][]*mpb.LineItem, error) {
+	services, err := parseServiceFeed(serviceFeed)
+	if err != nil {
+		return nil, err
+	}
+
+	feedHasTicketType := false
+	serviceTicketTypeMap := make(map[string][]*fpb.TicketType)
+	for _, service := range services {
+		merchantServiceID := merchantService(service.GetMerchantId(), service.GetServiceId())
+		for _, ticket := range service.GetTicketType() {
+			// TicketType can't have an empty price message or ticket_type_id. If it does it's excluded from map.
+			diff := cmp.Diff(&fpb.Price{}, ticket.GetPrice(), cmp.Comparer(proto.Equal))
+			if len(ticket.GetTicketTypeId()) == 0 || diff == "" {
+				continue
+			}
+
+			if _, ok := serviceTicketTypeMap[merchantServiceID]; !ok {
+				serviceTicketTypeMap[merchantServiceID] = []*fpb.TicketType{}
+			}
+			feedHasTicketType = true
+			serviceTicketTypeMap[merchantServiceID] = append(serviceTicketTypeMap[merchantServiceID], ticket)
+		}
+	}
+
+	if !feedHasTicketType {
+		return nil, errors.New("no valid ticket types found in service feed, please update service feed and retry")
+	}
+
+	availabilities, err := AvailabilityFrom(availabilityFeed, testSlots)
+	if err != nil {
+		return nil, err
+	}
+
+	merchantLineItemMap := make(map[string][]*mpb.LineItem)
+	for _, availability := range availabilities {
+		merchantServiceID := merchantService(availability.GetMerchantId(), availability.GetServiceId())
+		if tickets, ok := serviceTicketTypeMap[merchantServiceID]; ok {
+			lineItem := buildLineItem(availability, tickets)
+			// If lineItem can't be built, don't include in map
+			if lineItem == nil {
+				continue
+			}
+			merchantLineItemMap[availability.GetMerchantId()] = append(merchantLineItemMap[availability.GetMerchantId()], lineItem)
+		}
+	}
+
+	return merchantLineItemMap, nil
+}
+
 // AvailabilityFrom parses the file specified in availabilityFeed, returning a random permutation of availability data, maximum entries specified in testSlots
 func AvailabilityFrom(availabilityFeed string, testSlots int) ([]*fpb.Availability, error) {
 	LogFlow("Parse Input Feed", "Start")
@@ -89,7 +201,7 @@
 }
 
 // ValidateBooking performs granular comparisons between all got and want Bookings.
-func ValidateBooking(got *mpb.Booking, want *mpb.Booking) error {
+func ValidateBooking(got, want *mpb.Booking) error {
 	if got.GetBookingId() == "" {
 		return errors.New("booking_id is empty")
 	}
@@ -114,6 +226,136 @@
 	return nil
 }
 
+// ValidateLineItems performs granular comparisons between got and want LineItem arrays.
+func ValidateLineItems(got, want []*mpb.LineItem, confirmStatus bool) error {
+	if len(got) != len(want) {
+		return fmt.Errorf("number of LineItems differ got %d want %d", len(got), len(want))
+	}
+
+	for _, lineItem := range got {
+		orderTickets := OrderedTickets(lineItem.GetTickets())
+		sort.Sort(orderTickets)
+		lineItem.Tickets = orderTickets
+	}
+	for _, lineItem := range want {
+		orderTickets := OrderedTickets(lineItem.GetTickets())
+		sort.Sort(orderTickets)
+		lineItem.Tickets = orderTickets
+
+		if confirmStatus {
+			lineItem.Status = mpb.BookingStatus_CONFIRMED
+		}
+	}
+	sort.Sort(LineItems(got))
+	sort.Sort(LineItems(want))
+
+	if diff := cmp.Diff(got, want, cmp.Comparer(proto.Equal)); diff != "" {
+		return fmt.Errorf("LineItems differ (-got +want)\n%s", diff)
+	}
+
+	return nil
+}
+
+// ValidateOrder performs granular comparisons between got and want Order messages.
+// Params are purposely copied.
+func ValidateOrder(got, want mpb.Order) error {
+	if got.GetOrderId() == "" {
+		return fmt.Errorf("no order id provided for Order %v", got)
+	}
+	want.OrderId = got.GetOrderId()
+
+	if err := ValidateLineItems(got.GetItem(), want.GetItem(), true); err != nil {
+		return err
+	}
+
+	// LineItems okay. Remove, free memory, and compare rest of proto.
+	want.Item = nil
+	got.Item = nil
+	if diff := cmp.Diff(got, want, cmp.Comparer(proto.Equal)); diff != "" {
+		return fmt.Errorf("order protos differ. LineItems excluded, already validated. (-got +want)\n%s", diff)
+	}
+
+	return nil
+}
+
+// ValidateOrders performs simple comparisons and set up before forwarding orders
+// individually to ValidateOrder.
+func ValidateOrders(got, want Orders) error {
+	if len(got) != len(want) {
+		return fmt.Errorf("number of Orders differ got %d want %d", len(got), len(want))
+	}
+	sort.Sort(got)
+	sort.Sort(want)
+
+	var errorStrings []string
+	for i := 0; i < len(got); i++ {
+		if err := ValidateOrder(*got[i], *want[i]); err != nil {
+			errorStrings = append(errorStrings, err.Error())
+		}
+	}
+
+	if len(errorStrings) != 0 {
+		return errors.New(strings.Join(errorStrings, "\n"))
+	}
+	return nil
+}
+
+func hashLineItemByTicketIds(l *mpb.LineItem) string {
+	var uID []string
+	for _, ticket := range l.GetTickets() {
+		uID = append(uID, ticket.GetTicketId())
+	}
+	return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(uID, `|`))))
+}
+
+// Orders is a convenience type for an Orders array
+type Orders []*mpb.Order
+
+func (o Orders) Len() int {
+	return len(o)
+}
+
+func (o Orders) Less(i, j int) bool {
+	return o[i].GetOrderId() < o[j].GetOrderId()
+}
+
+func (o Orders) Swap(i, j int) {
+	o[i], o[j] = o[j], o[i]
+}
+
+// OrderedTickets is a convenience type for an OrderedTickets array
+type OrderedTickets []*mpb.LineItem_OrderedTickets
+
+func (ot OrderedTickets) Len() int {
+	return len(ot)
+}
+
+func (ot OrderedTickets) Less(i, j int) bool {
+	return ot[i].GetTicketId() < ot[j].GetTicketId()
+}
+
+func (ot OrderedTickets) Swap(i, j int) {
+	ot[i], ot[j] = ot[j], ot[i]
+}
+
+// LineItems is a convenience type for a LineItem array
+// This sort interface defined below should be used iff
+// OrderedTickets have already been sorted AND
+// ticket ids are UIDs
+type LineItems []*mpb.LineItem
+
+func (l LineItems) Len() int {
+	return len(l)
+}
+
+func (l LineItems) Less(i, j int) bool {
+	return hashLineItemByTicketIds(l[i]) < hashLineItemByTicketIds(l[j])
+}
+
+func (l LineItems) Swap(i, j int) {
+	l[i], l[j] = l[j], l[i]
+}
+
 // BuildSlotFrom creates a bookingservice slot from an feed availability record.
 func BuildSlotFrom(a *fpb.Availability) (*mpb.Slot, error) {
 	r := a.GetResources()