Syncing between source head and git
diff --git a/api/api.go b/api/api.go
index d419c40..61e0129 100644
--- a/api/api.go
+++ b/api/api.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Google Inc.
+Copyright 2019 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
@@ -33,33 +34,27 @@
 	"strings"
 	"time"
 
+	epb "github.com/golang/protobuf/ptypes/empty"
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/golang/protobuf/proto"
-	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/google/go-cmp/cmp"
+
 	"github.com/maps-booking-v3/utils"
 
 	fpb "github.com/maps-booking-v3/feeds"
 	mpb "github.com/maps-booking-v3/v3"
-	wpb "github.com/maps-booking-v3/waitlist"
+	wpb "github.com/maps-booking-v3/v3waitlist"
 )
 
 const (
-	userID    = "0"
-	firstName = "Jane"
-	lastName  = "Doe"
-	telephone = "+1 800-789-7890"
-	email     = "test@example.com"
+	userID     = "0"
+	firstName  = "Jane"
+	lastName   = "Doe"
+	telephone  = "+1 800-789-7890"
+	email      = "test@example.com"
+	reqTimeout = 10 * time.Second
 )
 
-// HTTPConnection is a convenience struct for holding connection-related objects.
-type HTTPConnection struct {
-	client      *http.Client
-	credentials string
-	marshaler   *jsonpb.Marshaler
-	baseURL     string
-}
-
 func setupCertConfig(caFile string, fullServerName string) (*tls.Config, error) {
 	if caFile == "" {
 		return nil, nil
@@ -78,9 +73,9 @@
 	}, nil
 }
 
-// InitHTTPConnection creates and returns a new HTTPConnection object
+// InitHTTPConnection creates and returns a new utils.HTTPConnection object
 // with a given server address and username/password.
-func InitHTTPConnection(serverAddr string, credentialsFile string, caFile string, fullServerName string) (*HTTPConnection, error) {
+func InitHTTPConnection(serverAddr string, credentialsFile string, caFile string, fullServerName string) (*utils.HTTPConnection, error) {
 	// Set up username/password.
 	var credentials string
 	if credentialsFile != "" {
@@ -98,24 +93,16 @@
 	if config != nil {
 		protocol = "https"
 	}
-	return &HTTPConnection{
-		client: &http.Client{
-			Timeout:   10 * time.Second,
+	return &utils.HTTPConnection{
+		Client: &http.Client{
 			Transport: &http.Transport{TLSClientConfig: config},
 		},
-		credentials: credentials,
-		marshaler:   &jsonpb.Marshaler{OrigName: true},
-		baseURL:     protocol + "://" + serverAddr,
+		Credentials: credentials,
+		Marshaler:   &jsonpb.Marshaler{OrigName: true},
+		BaseURL:     protocol + "://" + serverAddr,
 	}, nil
 }
 
-func (h HTTPConnection) getURL(rpcName string) string {
-	if rpcName != "" {
-		return h.baseURL + "/v3/" + rpcName
-	}
-	return h.baseURL
-}
-
 // Bookings is a convenience type for a booking array.
 type Bookings []*mpb.Booking
 
@@ -132,32 +119,58 @@
 }
 
 // HealthCheck performs a health check.
-func HealthCheck(conn *HTTPConnection) error {
-	utils.LogFlow("Health Check", "Start")
-	defer utils.LogFlow("Health Check", "End")
+func HealthCheck(ctx context.Context, logger *log.Logger, conn *utils.HTTPConnection) error {
+	utils.LogFlow(logger, "Health Check", "Start")
+	defer utils.LogFlow(logger, "Health Check", "End")
 
-	httpReq, err := http.NewRequest("GET", conn.getURL("HealthCheck"), nil)
-	httpReq.Header.Set("Authorization", conn.credentials)
+	fmt.Println(conn.GetURL("HealthCheck"))
+	httpReq, err := http.NewRequest("GET", conn.GetURL("HealthCheck"), nil)
+	reqCtx, cancel := context.WithTimeout(ctx, reqTimeout)
+	defer cancel()
+	httpReq = httpReq.WithContext(reqCtx)
+	httpReq.Header.Set("Authorization", conn.Credentials)
 
 	// See if we get a response.
-	resp, err := conn.client.Do(httpReq)
+	resp, err := conn.Client.Do(httpReq)
+	if err := ctx.Err(); err != nil {
+		panic(fmt.Sprintf("Encountered context error: %s", err.Error()))
+	}
 	if err != nil {
 		return fmt.Errorf("Health check failed to connect to server: %v", err)
 	} else if resp.StatusCode != 200 {
 		return fmt.Errorf("Health check returned unhealthy status: %s", resp.Status)
 	}
-	log.Printf("health check success! Got status: %s", resp.Status)
+	logger.Printf("health check success! Got status: %s", resp.Status)
 	return nil
 }
 
-// sendRequest sets up and sends the relevant HTTP request to the server and returns the HTTP response.
-func sendRequest(rpcName string, req string, conn *HTTPConnection) (string, error) {
-	httpReq, err := http.NewRequest("POST", conn.getURL(rpcName), bytes.NewBuffer([]byte(req)))
-	httpReq.Header.Set("Content-Type", "application/json")
-	httpReq.Header.Set("Authorization", conn.credentials)
-	log.Printf("%v Request. Sent(unix): %s, Url: %v, Method: %v, Header: %v, Body: %v\n", rpcName, time.Now().UTC().Format(time.RFC850), httpReq.URL, httpReq.Method, httpReq.Header, httpReq.Body)
+func getLogSafeHeader(header http.Header) http.Header {
+	logHeader := make(http.Header)
+	for k, v := range header {
+		if k == "Authorization" {
+			logHeader.Set("Authorization", "VALUE REDACTED.")
+			continue
+		}
+		logHeader[k] = v
+	}
+	return logHeader
+}
 
-	httpResp, err := conn.client.Do(httpReq)
+// sendRequest sets up and sends the relevant HTTP request to the server and returns the HTTP response.
+func sendRequest(ctx context.Context, logger *log.Logger, rpcName string, req string, conn *utils.HTTPConnection) (string, error) {
+	httpReq, err := http.NewRequest("POST", conn.GetURL(rpcName), bytes.NewBuffer([]byte(req)))
+	reqCtx, cancel := context.WithTimeout(ctx, reqTimeout)
+	defer cancel()
+	httpReq = httpReq.WithContext(reqCtx)
+	httpReq.Header.Set("Content-Type", "application/json")
+	httpReq.Header.Set("Authorization", conn.Credentials)
+	logHeader := getLogSafeHeader(httpReq.Header)
+	logger.Printf("%v Request. Sent(unix): %s, Url: %v, Method: %v, Header: %v, Body: %v\n", rpcName, time.Now().UTC().Format(time.RFC850), httpReq.URL, httpReq.Method, logHeader, httpReq.Body)
+
+	httpResp, err := conn.Client.Do(httpReq)
+	if err := ctx.Err(); err != nil {
+		panic(fmt.Sprintf("Encountered context error: %s", err.Error()))
+	}
 	if err != nil {
 		return "", fmt.Errorf("invalid response. %s yielded error: %v", rpcName, err)
 	}
@@ -167,13 +180,13 @@
 		return "", fmt.Errorf("Could not read http response body")
 	}
 	bodyString := string(bodyBytes)
-	log.Printf("%v Response. Received(unix): %s, Response %v\n", rpcName, time.Now().UTC().Format(time.RFC850), bodyString)
+	logger.Printf("%v Response. Received(unix): %s, Response %v\n", rpcName, time.Now().UTC().Format(time.RFC850), bodyString)
 	return bodyString, nil
 }
 
 // CheckAvailability performs a maps booking availability check on all supplied availability slots. This function
 // returns any errors when trying to call the CheckAvailability RPC.
-func CheckAvailability(a *fpb.Availability, conn *HTTPConnection) error {
+func CheckAvailability(ctx context.Context, logger *log.Logger, a *fpb.Availability, conn *utils.HTTPConnection) error {
 	slot, err := utils.BuildSlotFrom(a)
 	if err != nil {
 		return fmt.Errorf("unable to build request for check availability flow. err: %v, availability record: %v", err, a.String())
@@ -181,11 +194,11 @@
 	reqPB := &mpb.CheckAvailabilityRequest{
 		Slot: slot,
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("CheckAvailability", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "CheckAvailability", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. CheckAvailability yielded error: %v", err)
 	}
@@ -207,16 +220,16 @@
 
 // BatchAvailabilityLookup performs a maps booking batch availability lookup on all supplied availability slots. This function
 // returns any errors when trying to call the BatchAvailabilityLookup RPC.
-func BatchAvailabilityLookup(av []*fpb.Availability, conn *HTTPConnection) error {
+func BatchAvailabilityLookup(ctx context.Context, logger *log.Logger, av []*fpb.Availability, conn *utils.HTTPConnection) error {
 	reqPB, err := utils.BuildBatchAvailabilityLookupRequestFrom(av)
 	if err != nil {
 		return fmt.Errorf("unable to build request for batch availability lookup flow. err: %v, availability records: %v", err, av)
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("BatchAvailabilityLookup", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "BatchAvailabilityLookup", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. BatchAvailabilityLookup yielded error: %v", err)
 	}
@@ -232,7 +245,7 @@
 		slotTimeReq := reqPB.GetSlotTime()[i]
 		slotTimeResp := resp.GetSlotTimeAvailability()[i].GetSlotTime()
 		if diff := cmp.Diff(slotTimeReq, slotTimeResp, cmp.Comparer(proto.Equal)); diff != "" {
-			log.Printf("Slot %v differs: req=%v, resp=%v", i, slotTimeReq, slotTimeResp)
+			logger.Printf("Slot %v differs: req=%v, resp=%v", i, slotTimeReq, slotTimeResp)
 			diffCount++
 		}
 	}
@@ -243,7 +256,7 @@
 }
 
 // CreateBooking attempts to create bookings from availability slots.
-func CreateBooking(a *fpb.Availability, conn *HTTPConnection) (*mpb.Booking, error) {
+func CreateBooking(ctx context.Context, logger *log.Logger, a *fpb.Availability, conn *utils.HTTPConnection) (*mpb.Booking, error) {
 	slot, err := utils.BuildSlotFrom(a)
 	if err != nil {
 		return nil, fmt.Errorf("unable to build request for check availability flow. err: %v, availability record: %v", err, a.String())
@@ -265,12 +278,12 @@
 		},
 		IdempotencyToken: strconv.Itoa(gen.Intn(1000000)),
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
 
-	httpResp, err := sendRequest("CreateBooking", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "CreateBooking", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. CreateBooking  yielded error: %v", err)
 	}
@@ -293,8 +306,8 @@
 	}
 
 	// Perform idempotency test.
-	log.Printf("Idempotency check -- CreateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String())
-	idemHTTPResp, err := sendRequest("CreateBooking", req, conn)
+	logger.Printf("Idempotency check -- CreateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String())
+	idemHTTPResp, err := sendRequest(ctx, logger, "CreateBooking", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err)
 	}
@@ -311,15 +324,15 @@
 }
 
 // ListBookings calls the maps booking ListBookings rpc and compares the return with all input bookings.
-func ListBookings(tB Bookings, conn *HTTPConnection) (Bookings, error) {
+func ListBookings(ctx context.Context, logger *log.Logger, tB Bookings, conn *utils.HTTPConnection) (Bookings, error) {
 	reqPB := &mpb.ListBookingsRequest{
 		UserId: userID,
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("ListBookings", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "ListBookings", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. ListBookings yielded error: %v. Abandoning all booking from this flow", err)
 	}
@@ -330,37 +343,37 @@
 
 	gB := Bookings(resp.GetBookings())
 	if len(tB) == 0 {
-		log.Printf("ListBookings returning %d found bookings", len(gB))
+		logger.Printf("ListBookings returning %d found bookings", len(gB))
 		return gB, nil
 	}
 	if len(gB) != len(tB) {
-		log.Printf("ListBookings number of bookings differed unexpectedly. Got: %d, Want: %d.", len(gB), len(tB))
+		logger.Printf("ListBookings number of bookings differed unexpectedly. Got: %d, Want: %d.", len(gB), len(tB))
 	}
 	sort.Sort(gB)
 	sort.Sort(tB)
 	var out Bookings
 	for i := 0; i < len(tB); i++ {
 		if iE := utils.ValidateBooking(gB[i], tB[i]); iE != nil {
-			log.Printf("ListBookings invalid, %s, abandoning slot %d/%d", iE.Error(), i, len(tB))
+			logger.Printf("ListBookings invalid, %s, abandoning slot %d/%d", iE.Error(), i, len(tB))
 			continue
 		}
 		out = append(out, tB[i])
 	}
-	log.Printf("ListBookings returning %d bookings", len(out))
+	logger.Printf("ListBookings returning %d bookings", len(out))
 	return out, nil
 }
 
 // GetBookingStatus checks that all input bookings are in an acceptable state.
-func GetBookingStatus(b *mpb.Booking, conn *HTTPConnection) error {
+func GetBookingStatus(ctx context.Context, logger *log.Logger, b *mpb.Booking, conn *utils.HTTPConnection) error {
 	reqPB := &mpb.GetBookingStatusRequest{
 		BookingId: b.GetBookingId(),
 	}
 
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("GetBookingStatus", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "GetBookingStatus", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. GetBookingStatus yielded error: %v", err)
 	}
@@ -377,18 +390,18 @@
 }
 
 // CancelBooking is a clean up method that cancels all supplied bookings.
-func CancelBooking(bookingID string, conn *HTTPConnection) error {
+func CancelBooking(ctx context.Context, logger *log.Logger, bookingID string, conn *utils.HTTPConnection) error {
 	reqPB := &mpb.UpdateBookingRequest{
 		Booking: &mpb.Booking{
 			BookingId: bookingID,
 			Status:    mpb.BookingStatus_CANCELED,
 		},
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("UpdateBooking", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "UpdateBooking", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err)
 	}
@@ -397,14 +410,20 @@
 		return fmt.Errorf("CancelBooking: Could not parse HTTP response to pb3: %v", err)
 	}
 
-	if iE := utils.ValidateBooking(resp.GetBooking(), reqPB.GetBooking()); iE != nil {
+	// We only care that the correct booking was cancelled. The rest of the response content
+	// is not relevant.
+	compareBooking := &mpb.Booking{
+		BookingId: resp.GetBooking().GetBookingId(),
+		Status:    resp.GetBooking().GetStatus(),
+	}
+	if iE := utils.ValidateBooking(compareBooking, reqPB.GetBooking()); iE != nil {
 		return fmt.Errorf("invalid response. UpdateBooking: %s", iE.Error())
 	}
 	return nil
 }
 
 // Rescheduling will attempt to create a booking, update the booking, then cancel.
-func Rescheduling(av []*fpb.Availability, conn *HTTPConnection) error {
+func Rescheduling(ctx context.Context, logger *log.Logger, av []*fpb.Availability, conn *utils.HTTPConnection) error {
 	var slots []*fpb.Availability
 	for _, v := range utils.BuildMerchantServiceMap(av) {
 		// Need at least two slots for reschedule.
@@ -419,7 +438,7 @@
 		return errors.New("no suitable availability for rescheduling flow. exiting")
 	}
 	// Book first slot.
-	newBooking, err := CreateBooking(slots[0], conn)
+	newBooking, err := CreateBooking(ctx, logger, slots[0], conn)
 	if err != nil {
 		return fmt.Errorf("could not complete booking, abandoning rescheduling flow: %v", err)
 	}
@@ -436,11 +455,11 @@
 			},
 		},
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Rescheduling UpdateBooking: Could not convert pb3 to json: %v", reqPB)
 	}
-	updateHTTPResp, err := sendRequest("UpdateBooking", req, conn)
+	updateHTTPResp, err := sendRequest(ctx, logger, "UpdateBooking", req, conn)
 	var resp mpb.CreateBookingResponse
 	if err := jsonpb.UnmarshalString(updateHTTPResp, &resp); err != nil {
 		return fmt.Errorf("Rescheduling UpdateBooking: Could not parse HTTP response to pb3: %v", err)
@@ -453,21 +472,21 @@
 	if iE := utils.ValidateBooking(resp.GetBooking(), newBooking); iE != nil {
 		return fmt.Errorf("invalid response. UpdateBooking: %s, abandoning slot 1/1", iE.Error())
 	}
-	return CancelBooking(resp.GetBooking().GetBookingId(), conn)
+	return CancelBooking(ctx, logger, resp.GetBooking().GetBookingId(), conn)
 }
 
 // CheckOrderFulfillability attempts to send a CheckOrderFulfillabilityRequest
 // to the connection endpoint and diff the results with what are expected.
-func CheckOrderFulfillability(merchantID string, lineItems []*mpb.LineItem, conn *HTTPConnection) error {
+func CheckOrderFulfillability(ctx context.Context, logger *log.Logger, merchantID string, lineItems []*mpb.LineItem, conn *utils.HTTPConnection) error {
 	reqPB := &mpb.CheckOrderFulfillabilityRequest{
 		MerchantId: merchantID,
 		Item:       lineItems,
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("CheckOrderFulfillability", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "CheckOrderFulfillability", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. CheckOrderFulfillability yielded error: %v", err)
 	}
@@ -477,7 +496,6 @@
 	}
 
 	orderFulfillability := resp.GetFulfillability()
-	// TODO(ccawdrey): Add validation cases for other OrderFulFillability enums.
 	if diff := cmp.Diff(orderFulfillability.GetResult(), mpb.OrderFulfillability_CAN_FULFILL); diff != "" {
 		return fmt.Errorf("invalid response. CheckOrderFulfillability.Fulfillability.OrderFulfillabilityResult differ (-got +want)\n%s", diff)
 	}
@@ -505,7 +523,7 @@
 }
 
 // CreateOrder will attempt to build an order from a merchant id and array of line orders.
-func CreateOrder(merchantID string, lineItems []*mpb.LineItem, conn *HTTPConnection) (*mpb.Order, error) {
+func CreateOrder(ctx context.Context, logger *log.Logger, merchantID string, lineItems []*mpb.LineItem, conn *utils.HTTPConnection) (*mpb.Order, error) {
 	gen := rand.New(rand.NewSource(time.Now().UnixNano()))
 
 	reqOrder := &mpb.Order{
@@ -527,11 +545,11 @@
 		IdempotencyToken: strconv.Itoa(gen.Intn(1000000)),
 	}
 
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("CreateOrder", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "CreateOrder", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. CreateOrder yielded error: %v", err)
 	}
@@ -549,8 +567,8 @@
 	}
 
 	// Perform idempotency test.
-	log.Printf("Idempotency check")
-	idemHTTPResp, err := sendRequest("CreateOrder", req, conn)
+	logger.Printf("Idempotency check")
+	idemHTTPResp, err := sendRequest(ctx, logger, "CreateOrder", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err)
 	}
@@ -569,12 +587,12 @@
 	return resp.GetOrder(), nil
 }
 
-func sendListOrdersRequest(reqPB *mpb.ListOrdersRequest, conn *HTTPConnection) (mpb.ListOrdersResponse, error) {
-	req, err := conn.marshaler.MarshalToString(reqPB)
+func sendListOrdersRequest(ctx context.Context, logger *log.Logger, reqPB *mpb.ListOrdersRequest, conn *utils.HTTPConnection) (mpb.ListOrdersResponse, error) {
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return mpb.ListOrdersResponse{}, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("ListOrders", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "ListOrders", req, conn)
 	if err != nil {
 		return mpb.ListOrdersResponse{}, fmt.Errorf("invalid response. ListOrders yielded error: %v", err)
 	}
@@ -588,7 +606,7 @@
 
 // ListOrders first checks that the number and contents of the server's order state
 // are consistent with what the test client assumes is present.
-func ListOrders(orders []*mpb.Order, conn *HTTPConnection) error {
+func ListOrders(ctx context.Context, logger *log.Logger, orders []*mpb.Order, conn *utils.HTTPConnection) error {
 	if len(orders) == 0 {
 		return errors.New("at least one order must be present for ListOrders to succeed")
 	}
@@ -597,7 +615,7 @@
 	reqPB := &mpb.ListOrdersRequest{
 		Ids: &mpb.ListOrdersRequest_UserId{userID},
 	}
-	respUser, err := sendListOrdersRequest(reqPB, conn)
+	respUser, err := sendListOrdersRequest(ctx, logger, reqPB, conn)
 	if err != nil {
 		return err
 	}
@@ -613,7 +631,7 @@
 	}
 	reqPB.Ids = &mpb.ListOrdersRequest_OrderIds_{&orderIDs}
 
-	respOrder, err := sendListOrdersRequest(reqPB, conn)
+	respOrder, err := sendListOrdersRequest(ctx, logger, reqPB, conn)
 	if err != nil {
 		return err
 	}
@@ -625,7 +643,7 @@
 }
 
 // BatchGetWaitEstimates calls the partners API and verifies the returned WaitEstimates.
-func BatchGetWaitEstimates(s *fpb.Service, conn *HTTPConnection) error {
+func BatchGetWaitEstimates(ctx context.Context, logger *log.Logger, s *fpb.Service, conn *utils.HTTPConnection) error {
 	rules := s.GetWaitlistRules()
 
 	ps := make([]int32, rules.GetMaxPartySize()-rules.GetMinPartySize())
@@ -637,11 +655,11 @@
 		ServiceId:  s.GetServiceId(),
 		PartySize:  ps,
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("BatchGetWaitEstimates", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "BatchGetWaitEstimates", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. BatchGetWaitEstimates yielded error: %v", err)
 	}
@@ -677,7 +695,7 @@
 // CreateWaitlistEntry attempts to create waitlist entries from a service.
 // The max party size listed for the service's waitlist rules is used for
 // the waitlist entry's party size.
-func CreateWaitlistEntry(s *fpb.Service, conn *HTTPConnection) (string, error) {
+func CreateWaitlistEntry(ctx context.Context, logger *log.Logger, s *fpb.Service, conn *utils.HTTPConnection) (string, error) {
 	gen := rand.New(rand.NewSource(time.Now().UnixNano()))
 
 	reqPB := &wpb.CreateWaitlistEntryRequest{
@@ -696,12 +714,12 @@
 	if s.GetWaitlistRules().GetSupportsAdditionalRequest() {
 		reqPB.AdditionalRequest = "test additional request"
 	}
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return "", fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
 
-	httpResp, err := sendRequest("CreateWaitlistEntry", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "CreateWaitlistEntry", req, conn)
 	if err != nil {
 		return "", fmt.Errorf("invalid response. CreateWaitlistEntry  yielded error: %v", err)
 	}
@@ -721,8 +739,8 @@
 	}
 
 	// Perform idempotency test.
-	log.Printf("Idempotency check -- CreateWaitlistEntry Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String())
-	idemHTTPResp, err := sendRequest("CreateWaitlistEntry", req, conn)
+	logger.Printf("Idempotency check -- CreateWaitlistEntry Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String())
+	idemHTTPResp, err := sendRequest(ctx, logger, "CreateWaitlistEntry", req, conn)
 	if err != nil {
 		return "", fmt.Errorf("invalid response. Idempotency check yielded error: %v", err)
 	}
@@ -740,16 +758,16 @@
 
 // GetWaitlistEntry retrieves and validates the booking for the specified
 // waitlist entry id.
-func GetWaitlistEntry(id string, conn *HTTPConnection) (*wpb.WaitlistEntry, error) {
+func GetWaitlistEntry(ctx context.Context, logger *log.Logger, id string, conn *utils.HTTPConnection) (*wpb.WaitlistEntry, error) {
 	reqPB := &wpb.GetWaitlistEntryRequest{
 		WaitlistEntryId: id,
 	}
 
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
-	httpResp, err := sendRequest("GetWaitlistEntry", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "GetWaitlistEntry", req, conn)
 	if err != nil {
 		return nil, fmt.Errorf("invalid response. GetWaitlistEntry yielded error: %v", err)
 	}
@@ -766,22 +784,22 @@
 }
 
 // DeleteWaitlistEntry makes a request to delete the waitlist entry.
-func DeleteWaitlistEntry(id string, conn *HTTPConnection) error {
+func DeleteWaitlistEntry(ctx context.Context, logger *log.Logger, id string, conn *utils.HTTPConnection) error {
 	reqPB := &wpb.DeleteWaitlistEntryRequest{
 		WaitlistEntryId: id,
 	}
 
-	req, err := conn.marshaler.MarshalToString(reqPB)
+	req, err := conn.Marshaler.MarshalToString(reqPB)
 	if err != nil {
 		return fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
 	}
 
-	httpResp, err := sendRequest("DeleteWaitlistEntry", req, conn)
+	httpResp, err := sendRequest(ctx, logger, "DeleteWaitlistEntry", req, conn)
 	if err != nil {
 		return fmt.Errorf("invalid response. DeleteWaitlistEntry yielded error: %v", err)
 	}
 
-	var resp empty.Empty
+	var resp epb.Empty
 	if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil {
 		return fmt.Errorf("DeleteWaitlistEntry: Could not parse HTTP response to pb3: %v. Body should be: {}", err)
 	}
diff --git a/booking/bookingTests.go b/booking/bookingTests.go
new file mode 100644
index 0000000..b44ec05
--- /dev/null
+++ b/booking/bookingTests.go
@@ -0,0 +1,270 @@
+/*
+Copyright 2019 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package booking contains test logic for booking related endpoints.
+package booking
+
+import (
+	"context"
+	"log"
+
+	"github.com/maps-booking-v3/api"
+	"github.com/maps-booking-v3/utils"
+
+	fpb "github.com/maps-booking-v3/feeds"
+	mpb "github.com/maps-booking-v3/v3"
+)
+
+// GenerateBookings creates bookings from an availability feed.
+func GenerateBookings(ctx context.Context, logger *log.Logger, av []*fpb.Availability, stats *utils.TestSummary, conn *utils.HTTPConnection, config *utils.Config) api.Bookings {
+	logger.Println("no previous bookings to use, acquiring new inventory")
+	utils.LogFlow(logger, "Generate Fresh Inventory", "Start")
+	defer utils.LogFlow(logger, "Generate Fresh Inventory", "End")
+
+	var out api.Bookings
+	totalSlots := len(av)
+	for i, a := range av {
+		if config.BookingUseBal {
+			if err := api.BatchAvailabilityLookup(ctx, logger, []*fpb.Availability{a}, conn); err != nil {
+				logger.Printf("BAL error: %s. skipping slot %d/%d", err.Error(), i, totalSlots)
+				stats.BookingBatchAvailabilityLookupErrors++
+				continue
+			}
+			stats.BookingBatchAvailabilityLookupSuccess++
+		} else {
+			if err := api.CheckAvailability(ctx, logger, a, conn); err != nil {
+				logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
+				stats.BookingCheckAvailabilityErrors++
+				continue
+			}
+			stats.BookingCheckAvailabilitySuccess++
+		}
+
+		booking, err := api.CreateBooking(ctx, logger, a, conn)
+		if err != nil {
+			logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
+			stats.BookingCreateBookingErrors++
+			continue
+		}
+		out = append(out, booking)
+		stats.BookingCreateBookingSuccess++
+	}
+	return out
+}
+
+func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) {
+	logger.Println("\n************* Begin Stats *************\n")
+	var totalErrors int
+	if config.BookingHealthFlow || config.BookingAllFlows {
+		if stats.BookingHealthCheckSuccess {
+			logger.Println("HealthCheck Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("HealthCheck Failed")
+		}
+	}
+	if config.BookingCheckFlow || config.BookingAllFlows {
+		if config.BookingUseBal {
+			totalErrors += stats.BookingBatchAvailabilityLookupErrors
+			logger.Printf("BatchAvailabilityLookup Errors: %d/%d", stats.BookingBatchAvailabilityLookupErrors, stats.BookingBatchAvailabilityLookupErrors+stats.BookingBatchAvailabilityLookupSuccess)
+		} else {
+			totalErrors += stats.BookingCheckAvailabilityErrors
+			logger.Printf("CheckAvailability Errors: %d/%d", stats.BookingCheckAvailabilityErrors, stats.BookingCheckAvailabilityErrors+stats.BookingCheckAvailabilitySuccess)
+		}
+	}
+	if config.BookingBookFlow || config.BookingAllFlows {
+		totalErrors += stats.BookingCreateBookingErrors
+		logger.Printf("CreateBooking Errors: %d/%d", stats.BookingCreateBookingErrors, stats.BookingCreateBookingErrors+stats.BookingCreateBookingSuccess)
+	}
+	if config.BookingListFlow || config.BookingAllFlows || config.BookingCancelAllBookings {
+		if stats.BookingListBookingsSuccess {
+			logger.Println("ListBookings Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("ListBookings Failed")
+		}
+	}
+	if config.BookingStatusFlow || config.BookingAllFlows {
+		totalErrors += stats.BookingGetBookingStatusErrors
+		logger.Printf("GetBookingStatus Errors: %d/%d", stats.BookingGetBookingStatusErrors, stats.BookingGetBookingStatusErrors+stats.BookingGetBookingStatusSuccess)
+	}
+	if config.BookingRescheduleFlow || config.BookingAllFlows {
+		if stats.BookingReschedulingSuccess {
+			logger.Println("Rescheduling Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("Rescheduling Failed")
+		}
+	}
+
+	logger.Println("\n\n\n")
+	if totalErrors == 0 {
+		logger.Println("All Tests Pass!")
+	} else {
+		logger.Printf("Found %d Errors", totalErrors)
+	}
+
+	logger.Println("\n************* End Stats *************\n")
+}
+
+// RunTests runs booking tests.
+func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, av []*fpb.Availability, avForRescheduling []*fpb.Availability, stats *utils.TestSummary) {
+	conn := config.Conn
+	// HealthCheck Flow
+	if config.BookingHealthFlow || config.BookingAllFlows {
+		stats.BookingHealthCheckSuccess = true
+		if err := api.HealthCheck(ctx, logger, conn); err != nil {
+			stats.BookingHealthCheckSuccess = false
+			logger.Println(err.Error())
+		}
+		stats.BookingHealthCheckCompleted = true
+		if !config.BookingAllFlows && !config.BookingCheckFlow && !config.BookingBookFlow &&
+			!config.BookingListFlow && !config.BookingStatusFlow && !config.BookingRescheduleFlow {
+			logStats(stats, logger, config)
+			return
+		}
+	}
+	if config.BookingCheckFlow || config.BookingAllFlows {
+		if config.BookingUseBal {
+			utils.LogFlow(logger, "Batch Availability Lookup", "Start")
+			for _, a := range utils.SplitAvailabilityByMerchant(av) {
+				if err := api.BatchAvailabilityLookup(ctx, logger, a, conn); err != nil {
+					logger.Printf("BatchAvailabilityLookup returned error: %v", err)
+					stats.BookingBatchAvailabilityLookupErrors++
+				} else {
+					stats.BookingBatchAvailabilityLookupSuccess++
+				}
+			}
+			utils.LogFlow(logger, "Batch Availability Lookup", "End")
+			stats.BookingBatchAvailabilityLookupCompleted = true
+		} else {
+			// AvailabilityCheck Flow
+			utils.LogFlow(logger, "Availability Check", "Start")
+			totalSlots := len(av)
+
+			for i, a := range av {
+				if err := api.CheckAvailability(ctx, logger, a, conn); err != nil {
+					logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
+					stats.BookingCheckAvailabilityErrors++
+					continue
+				}
+				stats.BookingCheckAvailabilitySuccess++
+			}
+			utils.LogFlow(logger, "Availability Check", "End")
+			stats.BookingCheckAvailabilityCompleted = true
+		}
+	}
+
+	// CreateBooking Flow.
+	var b []*mpb.Booking
+	if config.BookingBookFlow || config.BookingAllFlows {
+		utils.LogFlow(logger, "Booking", "Start")
+		totalSlots := len(av)
+		logger.Printf("total slots %d", totalSlots)
+		for i, a := range av {
+			logger.Printf("creating booking")
+			booking, err := api.CreateBooking(ctx, logger, a, conn)
+			if err != nil {
+				logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
+				stats.BookingCreateBookingErrors++
+				continue
+			}
+			b = append(b, booking)
+			stats.BookingCreateBookingSuccess++
+		}
+		utils.LogFlow(logger, "Booking", "End")
+		stats.BookingCreateBookingCompleted = true
+	}
+	// ListBookings Flow
+	if config.BookingListFlow || config.BookingAllFlows || config.BookingCancelAllBookings {
+		if len(b) == 0 && !config.BookingCancelAllBookings {
+			b = GenerateBookings(ctx, logger, av, stats, conn, config)
+		}
+		utils.LogFlow(logger, "List Bookings", "Start")
+		if len(b) > 0 || config.BookingCancelAllBookings {
+			var err error
+			b, err = api.ListBookings(ctx, logger, b, conn)
+			stats.BookingListBookingsSuccess = true
+			if err != nil {
+				stats.BookingListBookingsSuccess = false
+				logger.Println(err.Error())
+			}
+		} else {
+			logger.Println("Could not create bookings to test ListBookings flow with.")
+			stats.BookingListBookingsSuccess = false
+		}
+		utils.LogFlow(logger, "List Bookings", "End")
+		stats.BookingListBookingsCompleted = true
+	}
+
+	// GetBookingStatus Flow
+	if config.BookingStatusFlow || config.BookingAllFlows {
+		if len(b) == 0 {
+			b = GenerateBookings(ctx, logger, av, stats, conn, config)
+		}
+
+		utils.LogFlow(logger, "BookingStatus", "Start")
+		totalBookings := len(b)
+
+		if totalBookings > 0 {
+			j := 0
+			for i, booking := range b {
+				if err := api.GetBookingStatus(ctx, logger, booking, conn); err != nil {
+					logger.Printf("%s. abandoning booking %d/%d", err.Error(), i, totalBookings)
+					stats.BookingGetBookingStatusErrors++
+					continue
+				}
+				stats.BookingGetBookingStatusSuccess++
+				b[j] = booking
+				j++
+			}
+			b = b[:j]
+		} else {
+			logger.Println("Could not create bookings to test GetBookingStatus flow with.")
+		}
+		utils.LogFlow(logger, "BookingStatus", "End")
+		stats.BookingGetBookingStatusCompleted = true
+	}
+	// CancelBooking Flow
+	if len(b) > 0 {
+		utils.LogFlow(logger, "Cancel Booking", "Start")
+		for i, booking := range b {
+			if err := api.CancelBooking(ctx, logger, booking.GetBookingId(), conn); err != nil {
+				logger.Printf("%s. abandoning booking %d/%d", err.Error(), i, len(b))
+				stats.BookingCancelBookingsErrors++
+				continue
+			}
+			stats.BookingCancelBookingsSuccess++
+		}
+		utils.LogFlow(logger, "Cancel Booking", "End")
+		stats.BookingCancelBookingsCompleted = true
+	}
+
+	// Rescheduling is nuanced and can be isolated
+	// from the rest of the tests.
+	if config.BookingRescheduleFlow || config.BookingAllFlows {
+		utils.LogFlow(logger, "Rescheduling", "Start")
+		stats.BookingReschedulingSuccess = true
+		if err := api.Rescheduling(ctx, logger, avForRescheduling, conn); err != nil {
+			logger.Println(err.Error())
+			stats.BookingReschedulingSuccess = false
+		}
+		utils.LogFlow(logger, "Rescheduling", "End")
+		stats.BookingReschedulingCompleted = true
+	}
+
+	logStats(stats, logger, config)
+}
diff --git a/order/orderTests.go b/order/orderTests.go
new file mode 100644
index 0000000..23c689d
--- /dev/null
+++ b/order/orderTests.go
@@ -0,0 +1,155 @@
+/*
+Copyright 2019 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package order contains test logic for order related endpoints.
+package order
+
+import (
+	"context"
+	"log"
+
+	fpb "github.com/maps-booking-v3/feeds"
+	"github.com/maps-booking-v3/api"
+	mpb "github.com/maps-booking-v3/v3"
+	"github.com/maps-booking-v3/utils"
+)
+
+func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) {
+	logger.Println("\n************* Begin Stats *************\n")
+	var totalErrors int
+	if config.OrderHealthFlow || config.OrderAllFlows {
+		if stats.OrderHealthCheckSuccess {
+			logger.Println("HealthCheck Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("HealthCheck Failed")
+		}
+		stats.OrderHealthCheckCompleted = true
+	}
+	if config.OrderCheckFlow || config.OrderAllFlows {
+		totalErrors += stats.OrderCheckOrderFulfillabilityErrors
+		logger.Printf("CheckOrderFulfillability Errors: %d/%d", stats.OrderCheckOrderFulfillabilityErrors, stats.OrderCheckOrderFulfillabilityErrors+stats.OrderCheckOrderFulfillabilitySuccess)
+	}
+	if config.OrderOrderFlow || config.OrderAllFlows {
+		totalErrors += stats.OrderCreateOrderErrors
+		logger.Printf("CreateOrder Errors: %d/%d", stats.OrderCreateOrderErrors, stats.OrderCreateOrderErrors+stats.OrderCreateOrderSuccess)
+	}
+	if config.OrderAllFlows {
+		if stats.OrderListOrdersSuccess {
+			logger.Println("ListOrders Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("ListOrders Failed")
+		}
+	}
+	if config.OrderCheckFlow || config.OrderOrderFlow || config.OrderAllFlows {
+		logger.Printf("Total Slots Processed: %d", stats.OrderTotalSlotsProcessed)
+	}
+
+	logger.Println("\n\n\n")
+	if totalErrors == 0 {
+		logger.Println("All Tests Pass!")
+	} else {
+		logger.Printf("Found %d Errors", totalErrors)
+	}
+
+	logger.Println("\n************* End Stats *************\n")
+}
+
+// RunTests runs order tests.
+func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, av []*fpb.Availability, services []*fpb.Service, stats *utils.TestSummary) {
+	conn := config.Conn
+	// HealthCheck Flow
+	if config.OrderHealthFlow || config.OrderAllFlows {
+		stats.OrderHealthCheckSuccess = true
+		if err := api.HealthCheck(ctx, logger, conn); err != nil {
+			stats.OrderHealthCheckSuccess = false
+			logger.Println(err.Error())
+		}
+		stats.OrderHealthCheckCompleted = true
+		if !config.OrderAllFlows && !config.OrderCheckFlow && !config.OrderOrderFlow {
+			logStats(stats, logger, config)
+			return
+		}
+	}
+	testInventory, err := utils.BuildLineItemMap(services, av)
+	if err != nil {
+		logger.Printf("Remaining tests cannot run due to error building line items: %s\n", err)
+		return
+	}
+
+	// CheckOrderFulfillability Flow
+	if config.OrderCheckFlow || config.OrderAllFlows {
+		utils.LogFlow(logger, "CheckOrderFulfillability", "Start")
+		for _, value := range testInventory {
+			stats.OrderTotalSlotsProcessed += len(value)
+		}
+
+		i := 0
+		for merchantID, lineItems := range testInventory {
+			if err := api.CheckOrderFulfillability(ctx, logger, merchantID, lineItems, conn); err != nil {
+				logger.Printf("%s. skipping slots %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.OrderTotalSlotsProcessed)
+				stats.OrderCheckOrderFulfillabilityErrors += len(lineItems)
+				delete(testInventory, merchantID)
+				i += len(lineItems)
+				continue
+			}
+			stats.OrderCheckOrderFulfillabilitySuccess += len(lineItems)
+			i += len(lineItems)
+		}
+		stats.OrderCheckOrderFulfillabilityCompleted = true
+		utils.LogFlow(logger, "CheckOrderFulfillability", "End")
+	}
+	// CreateOrder Flow.
+	var orders []*mpb.Order
+	if config.OrderOrderFlow || config.OrderAllFlows {
+		utils.LogFlow(logger, "CreateOrder", "Start")
+		if stats.OrderTotalSlotsProcessed == 0 {
+			for _, value := range testInventory {
+				stats.OrderTotalSlotsProcessed += len(value)
+			}
+		}
+
+		i := 0
+		for merchantID, lineItems := range testInventory {
+			order, err := api.CreateOrder(ctx, logger, merchantID, lineItems, conn)
+			if err != nil {
+				logger.Printf("%s. skipping slot %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.OrderTotalSlotsProcessed)
+				stats.OrderCreateOrderErrors += len(lineItems)
+				delete(testInventory, merchantID)
+				i += len(lineItems)
+				continue
+			}
+			orders = append(orders, order)
+			stats.OrderCreateOrderSuccess += len(lineItems)
+			i += len(lineItems)
+		}
+		stats.OrderCreateOrderCompleted = true
+		utils.LogFlow(logger, "CreateOrder", "End")
+	}
+	// ListOrders Flow
+	if config.OrderAllFlows || config.OrderListFlow {
+		utils.LogFlow(logger, "ListOrders", "Start")
+		stats.OrderListOrdersSuccess = true
+		if err := api.ListOrders(ctx, logger, orders, conn); err != nil {
+			stats.OrderListOrdersSuccess = false
+			logger.Println(err.Error())
+		}
+		stats.OrderListOrdersCompleted = true
+		utils.LogFlow(logger, "ListOrders", "End")
+	}
+	logStats(stats, logger, config)
+}
diff --git a/proto/waitlist.proto b/proto/waitlist.proto
index 218e868..d432edc 100644
--- a/proto/waitlist.proto
+++ b/proto/waitlist.proto
@@ -100,6 +100,17 @@
   EstimatedSeatTimeRange estimated_seat_time_range = 2;
 }
 
+// The confirmation modes used when joining the waitlist.
+enum WaitlistConfirmationMode {
+  // The confirmation mode was not specified.
+  // Synchronous confirmation will be assumed.
+  WAITLIST_CONFIRMATION_MODE_UNSPECIFIED = 0;
+  // Waitlist entries will be confirmed synchronously.
+  WAITLIST_CONFIRMATION_MODE_SYNCHRONOUS = 1;
+  // Waitlist entries will be confirmed asynchronously.
+  WAITLIST_CONFIRMATION_MODE_ASYNCHRONOUS = 2;
+}
+
 // The wait estimate for a particular party size, merchant and service.
 message WaitEstimate {
   // Required. The party size this wait estimate applies to.
@@ -108,6 +119,11 @@
   // Required. Contains fields measuring how long (in time or # of people) until
   // the user is ready to leave the waitlist and be seated.
   WaitLength wait_length = 2;
+
+  // Required. Indicates whether waitlist entries for this wait estimate will be
+  // confirmed synchronously or asynchronously. An UNSPECIFIED value will be
+  // interpreted as synchronous.
+  WaitlistConfirmationMode waitlist_confirmation_mode = 3;
 }
 
 // CreateWaitlistEntry method
@@ -186,6 +202,8 @@
   // The waitlist entry was created and the user is currently waiting in the
   // waitlist.
   WAITING = 1;
+  // The waitlist entry is awaiting confirmation by the merchant.
+  PENDING_MERCHANT_CONFIRMATION = 8;
   // The waitlist entry has been canceled by the user. Cancellation for no-shows
   // should use the NO_SHOW state.
   CANCELED = 2;
diff --git a/testclient/.bookingClient.go.swp b/testclient/.bookingClient.go.swp
deleted file mode 100644
index b3ae8ec..0000000
--- a/testclient/.bookingClient.go.swp
+++ /dev/null
Binary files differ
diff --git a/testclient/bookingClient.go b/testclient/bookingClient.go
index eb23a84..8462716 100644
--- a/testclient/bookingClient.go
+++ b/testclient/bookingClient.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Google Inc.
+Copyright 2019 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,29 +16,23 @@
 package main
 
 import (
+	"context"
 	"flag"
-	"fmt"
 	"log"
-	"os"
-	"path/filepath"
-	"time"
 
 	"github.com/maps-booking-v3/api"
-	"github.com/maps-booking-v3/utils"
-
+	"github.com/maps-booking-v3/booking"
 	fpb "github.com/maps-booking-v3/feeds"
-	mpb "github.com/maps-booking-v3/v3"
+	"github.com/maps-booking-v3/utils"
 )
 
-const logFile = "http_test_client_log_"
-
 var (
 	serverAddr        = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port")
 	credentialsFile   = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.")
 	testSlots         = flag.Int("num_test_slots", 10, "Maximum number of slots to test from availability_feed. Slots will be selected randomly")
 	allFlows          = flag.Bool("all_tests", false, "Whether to test all endpoints.")
 	healthFlow        = flag.Bool("health_check_test", false, "Whether to test the Health endpoint.")
-	checkFlow         = flag.Bool("check_availability_test", false, "Whether to test the CheckAvailability endpoint.")
+	checkFlow         = flag.Bool("check_availability_test", false, "Whether to test availability lookup. Will use BatchAvailabilityLookup or CheckAvailability endpoint depending on value of the use_batch_availability_lookup flag.")
 	bookFlow          = flag.Bool("booking_test", false, "Whether to test the CreateBooking endpoint.")
 	listFlow          = flag.Bool("list_bookings_test", false, "Whether to test the ListBookings endpoint")
 	statusFlow        = flag.Bool("booking_status_test", false, "Whether to test the GetBookingStatus endpoint.")
@@ -52,297 +46,61 @@
 	useBal            = flag.Bool("use_batch_availability_lookup", false, "Whether to use the BatchAvailabilityLookup RPC (as opposed to the deprecated CheckAvailability)")
 )
 
-type counters struct {
-	TotalSlotsProcessed            int
-	HealthCheckSuccess             bool
-	BatchAvailabilityLookupErrors  int
-	BatchAvailabilityLookupSuccess int
-	CheckAvailabilitySuccess       int
-	CheckAvailabilityErrors        int
-	CreateBookingSuccess           int
-	CreateBookingErrors            int
-	ListBookingsSuccess            bool
-	GetBookingStatusSuccess        int
-	GetBookingStatusErrors         int
-	CancelBookingsSuccess          int
-	CancelBookingsErrors           int
-	ReschedulingSuccess            bool
-}
-
-// GenerateBookings creates bookings from an availability feed.
-func GenerateBookings(av []*fpb.Availability, stats *counters, conn *api.HTTPConnection) api.Bookings {
-	log.Println("no previous bookings to use, acquiring new inventory")
-	utils.LogFlow("Generate Fresh Inventory", "Start")
-	defer utils.LogFlow("Generate Fresh Inventory", "End")
-
-	var out api.Bookings
-	totalSlots := len(av)
-	for i, a := range av {
-		if *useBal {
-			if err := api.BatchAvailabilityLookup([]*fpb.Availability{a}, conn); err != nil {
-				log.Printf("BAL error: %s. skipping slot %d/%d", err.Error(), i, totalSlots)
-				stats.BatchAvailabilityLookupErrors++
-				continue
-			}
-			stats.BatchAvailabilityLookupSuccess++
-		} else {
-			if err := api.CheckAvailability(a, conn); err != nil {
-				log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
-				stats.CheckAvailabilityErrors++
-				continue
-			}
-			stats.CheckAvailabilitySuccess++
-		}
-
-		booking, err := api.CreateBooking(a, conn)
-		if err != nil {
-			log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
-			stats.CreateBookingErrors++
-			continue
-		}
-		out = append(out, booking)
-		stats.CreateBookingSuccess++
+func makeConfig(logger *log.Logger) *utils.Config {
+	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
+	if err != nil {
+		logger.Fatalf("Failed to init http connection %v", err)
 	}
-	return out
-}
-
-func createLogFile() (*os.File, error) {
-	var err error
-	outPath := *outputDir
-	if outPath == "" {
-		outPath, err = os.Getwd()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	now := time.Now().UTC()
-	nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
-	outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString))
-
-	return os.Create(outFile)
-}
-
-func logStats(stats counters) {
-	log.Println("\n************* Begin Stats *************\n")
-	var totalErrors int
-	if *healthFlow || *allFlows {
-		if stats.HealthCheckSuccess {
-			log.Println("HealthCheck Succeeded")
-		} else {
-			totalErrors++
-			log.Println("HealthCheck Failed")
-		}
-	}
-	if *useBal {
-		totalErrors += stats.BatchAvailabilityLookupErrors
-		log.Printf("BatchAvailabilityLookup Errors: %d/%d", stats.BatchAvailabilityLookupErrors, stats.BatchAvailabilityLookupErrors+stats.BatchAvailabilityLookupSuccess)
-	} else if *checkFlow || *allFlows {
-		totalErrors += stats.CheckAvailabilityErrors
-		log.Printf("CheckAvailability Errors: %d/%d", stats.CheckAvailabilityErrors, stats.CheckAvailabilityErrors+stats.CheckAvailabilitySuccess)
-	}
-	if *bookFlow || *allFlows {
-		totalErrors += stats.CreateBookingErrors
-		log.Printf("CreateBooking Errors: %d/%d", stats.CreateBookingErrors, stats.CreateBookingErrors+stats.CreateBookingSuccess)
-	}
-	if *listFlow || *allFlows || *cancelAllBookings {
-		if stats.ListBookingsSuccess {
-			log.Println("ListBookings Succeeded")
-		} else {
-			totalErrors++
-			log.Println("ListBookings Failed")
-		}
-	}
-	if *statusFlow || *allFlows {
-		totalErrors += stats.GetBookingStatusErrors
-		log.Printf("GetBookingStatus Errors: %d/%d", stats.GetBookingStatusErrors, stats.GetBookingStatusErrors+stats.GetBookingStatusSuccess)
-	}
-	if *rescheduleFlow || *allFlows {
-		if stats.ReschedulingSuccess {
-			log.Println("Rescheduling Succeeded")
-		} else {
-			totalErrors++
-			log.Println("Rescheduling Failed")
-		}
-	}
-
-	log.Println("\n\n\n")
-	if totalErrors == 0 {
-		log.Println("All Tests Pass!")
-	} else {
-		log.Printf("Found %d Errors", totalErrors)
-	}
-
-	log.Println("\n************* End Stats *************\n")
-	os.Exit(totalErrors)
+	return &utils.Config{Conn: conn,
+		BookingAllFlows:          *allFlows,
+		BookingHealthFlow:        *healthFlow,
+		BookingCheckFlow:         *checkFlow,
+		BookingBookFlow:          *bookFlow,
+		BookingListFlow:          *listFlow,
+		BookingStatusFlow:        *statusFlow,
+		BookingRescheduleFlow:    *rescheduleFlow,
+		BookingCancelAllBookings: *cancelAllBookings,
+		BookingUseBal:            *useBal}
 }
 
 func main() {
 	flag.Parse()
-	var stats counters
 
-	if !*outputToTerminal {
-		// Set up logging before continuing with flows
-		f, err := createLogFile()
-		if err != nil {
-			log.Fatalf("Failed to create log file %v", err)
-		}
-		defer f.Close()
-		log.SetOutput(f)
-	}
-
-	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
+	logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir)
 	if err != nil {
-		log.Fatalf("Failed to init http connection %v", err)
+		log.Fatal("Could not create logger: ", err)
 	}
+	if f != nil {
+		defer f.Close()
+	}
+
+	config := makeConfig(logger)
 
 	// Health check doesn't affect the cancel booking flow so we let it through.
 	if *cancelAllBookings && (*allFlows || *checkFlow || *bookFlow || *listFlow || *statusFlow || *rescheduleFlow) {
-		log.Fatal("cancel_all_bookings is not supported with other test flows")
+		logger.Fatal("cancel_all_bookings is not supported with other test flows")
 	}
 
-	// HealthCheck Flow
-	if *healthFlow || *allFlows {
-		stats.HealthCheckSuccess = true
-		if err := api.HealthCheck(conn); err != nil {
-			stats.HealthCheckSuccess = false
-			log.Println(err.Error())
-		}
-		if !*allFlows && !*checkFlow && !*bookFlow &&
-			!*listFlow && !*statusFlow && !*rescheduleFlow {
-			logStats(stats)
-		}
-	}
-
+	var summary utils.TestSummary
 	var av []*fpb.Availability
 	var avForRescheduling []*fpb.Availability
-	if !*cancelAllBookings {
+
+	needAvailability := !*cancelAllBookings || *allFlows || *checkFlow || *bookFlow || *listFlow || *statusFlow || *rescheduleFlow
+
+	if needAvailability {
 		// Build availablility records.
 		if *availabilityFeed == "" {
-			log.Fatal("please set availability_feed flag if you wish to test additional flows")
+			logger.Fatal("Please set availability_feed flag if you wish to test anything except health check.")
 		}
-		av, err = utils.AvailabilityFrom(*availabilityFeed, *testSlots, false)
+		av, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, false)
 		if err != nil {
-			log.Fatalf("Failed to get availability: %v", err.Error())
+			logger.Fatalf("Failed to get availability: %v", err.Error())
 		}
-		stats.TotalSlotsProcessed += len(av)
-
-		avForRescheduling, err = utils.AvailabilityFrom(*availabilityFeed, *testSlots, true)
+		summary.BookingTotalSlotsProcessed += len(av)
+		avForRescheduling, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, true)
 		if err != nil {
-			log.Fatalf("Failed to get availability for rescheduling test: %v", err.Error())
+			logger.Fatalf("Failed to get availability for rescheduling test: %v", err.Error())
 		}
 	}
-
-	if *useBal {
-		utils.LogFlow("Batch Availability Lookup", "Start")
-		for _, a := range utils.SplitAvailabilityByMerchant(av) {
-			if err = api.BatchAvailabilityLookup(a, conn); err != nil {
-				log.Printf("BatchAvailabilityLookup returned error: %v", err)
-				stats.BatchAvailabilityLookupErrors++
-			} else {
-				stats.BatchAvailabilityLookupSuccess++
-			}
-		}
-		utils.LogFlow("Batch Availability Lookup", "End")
-	} else if *checkFlow || *allFlows {
-		// AvailabilityCheck Flow
-		utils.LogFlow("Availability Check", "Start")
-		totalSlots := len(av)
-
-		j := 0
-		for i, a := range av {
-			if err = api.CheckAvailability(a, conn); err != nil {
-				log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
-				stats.CheckAvailabilityErrors++
-				continue
-			}
-			stats.CheckAvailabilitySuccess++
-			av[j] = a
-			j++
-		}
-		av = av[:j]
-		utils.LogFlow("Availability Check", "End")
-	}
-	// CreateBooking Flow.
-	var b []*mpb.Booking
-	if *bookFlow || *allFlows {
-		utils.LogFlow("Booking", "Start")
-		totalSlots := len(av)
-		for i, a := range av {
-			booking, err := api.CreateBooking(a, conn)
-			if err != nil {
-				log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots)
-				stats.CreateBookingErrors++
-				continue
-			}
-			b = append(b, booking)
-			stats.CreateBookingSuccess++
-		}
-		utils.LogFlow("Booking", "End")
-	}
-	// ListBookings Flow
-	if *listFlow || *allFlows || *cancelAllBookings {
-		if len(b) == 0 && !*cancelAllBookings {
-			b = GenerateBookings(av, &stats, conn)
-		}
-		utils.LogFlow("List Bookings", "Start")
-		b, err = api.ListBookings(b, conn)
-		stats.ListBookingsSuccess = true
-		if err != nil {
-			stats.ListBookingsSuccess = false
-			log.Println(err.Error())
-		}
-		utils.LogFlow("List Bookings", "End")
-	}
-
-	// GetBookingStatus Flow
-	if *statusFlow || *allFlows {
-		if len(b) == 0 {
-			b = GenerateBookings(av, &stats, conn)
-		}
-
-		utils.LogFlow("BookingStatus", "Start")
-		totalBookings := len(b)
-
-		j := 0
-		for i, booking := range b {
-			if err = api.GetBookingStatus(booking, conn); err != nil {
-				log.Printf("%s. abandoning booking %d/%d", err.Error(), i, totalBookings)
-				stats.GetBookingStatusErrors++
-				continue
-			}
-			stats.GetBookingStatusSuccess++
-			b[j] = booking
-			j++
-		}
-		b = b[:j]
-		utils.LogFlow("BookingStatus", "End")
-	}
-	// CancelBooking Flow
-	if len(b) > 0 {
-		utils.LogFlow("Cancel Booking", "Start")
-		for i, booking := range b {
-			if err = api.CancelBooking(booking.GetBookingId(), conn); err != nil {
-				log.Printf("%s. abandoning booking %d/%d", err.Error(), i, len(b))
-				stats.CancelBookingsErrors++
-				continue
-			}
-			stats.CancelBookingsSuccess++
-		}
-		utils.LogFlow("Cancel Booking", "End")
-	}
-
-	// Rescheduling is nuanced and can be isolated
-	// from the rest of the tests.
-	if *rescheduleFlow || *allFlows {
-		utils.LogFlow("Rescheduling", "Start")
-		stats.ReschedulingSuccess = true
-		if err = api.Rescheduling(avForRescheduling, conn); err != nil {
-			log.Println(err.Error())
-			stats.ReschedulingSuccess = false
-		}
-		utils.LogFlow("Rescheduling", "End")
-	}
-
-	logStats(stats)
+	booking.RunTests(context.Background(), logger, config, av, avForRescheduling, &summary)
 }
diff --git a/testclient/orderClient.go b/testclient/orderClient.go
index 718ff23..a3ee63d 100644
--- a/testclient/orderClient.go
+++ b/testclient/orderClient.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Google Inc.
+Copyright 2019 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,21 +16,17 @@
 package main
 
 import (
+	"context"
 	"flag"
-	"fmt"
 	"log"
-	"os"
-	"path/filepath"
-	"time"
+
+	fpb "github.com/maps-booking-v3/feeds"
 
 	"github.com/maps-booking-v3/api"
+	"github.com/maps-booking-v3/order"
 	"github.com/maps-booking-v3/utils"
-
-	mpb "github.com/maps-booking-v3/v3"
 )
 
-const logFile = "http_test_client_log_"
-
 var (
 	serverAddr       = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port")
 	credentialsFile  = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.")
@@ -47,172 +43,48 @@
 	outputToTerminal = flag.Bool("output_to_terminal", false, "Output to terminal rather than a file.")
 )
 
-type counters struct {
-	TotalSlotsProcessed             int
-	HealthCheckSuccess              bool
-	CheckOrderFulfillabilitySuccess int
-	CheckOrderFulfillabilityErrors  int
-	CreateOrderSuccess              int
-	CreateOrderErrors               int
-	ListOrdersSuccess               bool
-}
-
-func createLogFile() (*os.File, error) {
-	var err error
-	outPath := *outputDir
-	if outPath == "" {
-		outPath, err = os.Getwd()
-		if err != nil {
-			return nil, err
-		}
+func makeConfig(logger *log.Logger) *utils.Config {
+	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
+	if err != nil {
+		logger.Fatalf("Failed to init http connection %v", err)
 	}
-
-	now := time.Now().UTC()
-	nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
-	outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString))
-
-	return os.Create(outFile)
-}
-
-func logStats(stats counters) {
-	log.Println("\n************* Begin Stats *************\n")
-	var totalErrors int
-	if *healthFlow || *allFlows {
-		if stats.HealthCheckSuccess {
-			log.Println("HealthCheck Succeeded")
-		} else {
-			totalErrors++
-			log.Println("HealthCheck Failed")
-		}
+	return &utils.Config{Conn: conn,
+		OrderAllFlows:   *allFlows,
+		OrderHealthFlow: *healthFlow,
+		OrderCheckFlow:  *checkFlow,
+		OrderOrderFlow:  *orderFlow,
 	}
-	if *checkFlow || *allFlows {
-		totalErrors += stats.CheckOrderFulfillabilityErrors
-		log.Printf("CheckOrderFulfillability Errors: %d/%d", stats.CheckOrderFulfillabilityErrors, stats.CheckOrderFulfillabilityErrors+stats.CheckOrderFulfillabilitySuccess)
-	}
-	if *orderFlow || *allFlows {
-		totalErrors += stats.CreateOrderErrors
-		log.Printf("CreateOrder Errors: %d/%d", stats.CreateOrderErrors, stats.CreateOrderErrors+stats.CreateOrderSuccess)
-	}
-	if *allFlows {
-		if stats.ListOrdersSuccess {
-			log.Println("ListOrders Succeeded")
-		} else {
-			totalErrors++
-			log.Println("ListOrders Failed")
-		}
-	}
-	if *checkFlow || *orderFlow || *allFlows {
-		log.Printf("Total Slots Processed: %d", stats.TotalSlotsProcessed)
-	}
-
-	log.Println("\n\n\n")
-	if totalErrors == 0 {
-		log.Println("All Tests Pass!")
-	} else {
-		log.Printf("Found %d Errors", totalErrors)
-	}
-
-	log.Println("\n************* End Stats *************\n")
-	os.Exit(totalErrors)
 }
 
 func main() {
 	flag.Parse()
-	var stats counters
 
-	if !*outputToTerminal {
-		// Set up logging before continuing with flows
-		f, err := createLogFile()
-		if err != nil {
-			log.Fatalf("Failed to create log file %v", err)
-		}
+	logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir)
+	if err != nil {
+		log.Fatal("Could not create logger: ", err)
+	}
+	if f != nil {
 		defer f.Close()
-		log.SetOutput(f)
 	}
 
-	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
-	if err != nil {
-		log.Fatalf("Failed to init http connection %v", err)
-	}
-
-	// HealthCheck Flow
-	if *healthFlow || *allFlows {
-		stats.HealthCheckSuccess = true
-		if err := api.HealthCheck(conn); err != nil {
-			stats.HealthCheckSuccess = false
-			log.Println(err.Error())
-		}
-		if !*allFlows && !*checkFlow && !*orderFlow {
-			logStats(stats)
-		}
-	}
-
-	if *availabilityFeed == "" || *serviceFeed == "" {
-		log.Fatal("please set both availability_feed and service_feed flags")
-	}
-
-	testInventory, err := utils.MerchantLineItemMapFrom(*serviceFeed, *availabilityFeed, *testSlots)
-	if err != nil {
-		log.Fatal(err.Error())
-	}
-
-	// CheckOrderFulfillability Flow
-	if *checkFlow || *allFlows {
-		utils.LogFlow("CheckOrderFulfillability", "Start")
-		for _, value := range testInventory {
-			stats.TotalSlotsProcessed += len(value)
+	config := makeConfig(logger)
+	var summary utils.TestSummary
+	var services []*fpb.Service
+	var av []*fpb.Availability
+	if config.OrderAllFlows || config.OrderCheckFlow || config.OrderOrderFlow {
+		if *availabilityFeed == "" || *serviceFeed == "" {
+			log.Fatal("please set both availability_feed and service_feed flags")
 		}
 
-		i := 0
-		for merchantID, lineItems := range testInventory {
-			if err = api.CheckOrderFulfillability(merchantID, lineItems, conn); err != nil {
-				log.Printf("%s. skipping slots %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.TotalSlotsProcessed)
-				stats.CheckOrderFulfillabilityErrors += len(lineItems)
-				delete(testInventory, merchantID)
-				i += len(lineItems)
-				continue
-			}
-			stats.CheckOrderFulfillabilitySuccess += len(lineItems)
-			i += len(lineItems)
-		}
-		utils.LogFlow("CheckOrderFulfillability", "End")
-	}
-	// CreateOrder Flow.
-	var orders []*mpb.Order
-	if *orderFlow || *allFlows {
-		utils.LogFlow("CreateOrder", "Start")
-		if stats.TotalSlotsProcessed == 0 {
-			for _, value := range testInventory {
-				stats.TotalSlotsProcessed += len(value)
-			}
+		services, err = utils.ParseServiceFeed(*serviceFeed)
+		if err != nil {
+			log.Fatal(err.Error())
 		}
 
-		i := 0
-		for merchantID, lineItems := range testInventory {
-			order, err := api.CreateOrder(merchantID, lineItems, conn)
-			if err != nil {
-				log.Printf("%s. skipping slot %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.TotalSlotsProcessed)
-				stats.CreateOrderErrors += len(lineItems)
-				delete(testInventory, merchantID)
-				i += len(lineItems)
-				continue
-			}
-			orders = append(orders, order)
-			stats.CreateOrderSuccess += len(lineItems)
-			i += len(lineItems)
+		av, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, false)
+		if err != nil {
+			log.Fatal(err.Error())
 		}
-		utils.LogFlow("CreateOrder", "End")
 	}
-	// ListOrders Flow
-	if *allFlows {
-		utils.LogFlow("ListOrders", "Start")
-		stats.ListOrdersSuccess = true
-		if err = api.ListOrders(orders, conn); err != nil {
-			stats.ListOrdersSuccess = false
-			log.Println(err.Error())
-		}
-		utils.LogFlow("ListOrders", "End")
-	}
-
-	logStats(stats)
+	order.RunTests(context.Background(), logger, config, av, services, &summary)
 }
diff --git a/testclient/waitlistClient.go b/testclient/waitlistClient.go
index 4f673e7..5260a74 100644
--- a/testclient/waitlistClient.go
+++ b/testclient/waitlistClient.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Google Inc.
+Copyright 2019 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,21 +16,16 @@
 package main
 
 import (
+	"context"
 	"flag"
-	"fmt"
 	"log"
-	"os"
-	"path/filepath"
-	"time"
 
 	"github.com/maps-booking-v3/api"
-	"github.com/maps-booking-v3/utils"
-
 	fpb "github.com/maps-booking-v3/feeds"
+	"github.com/maps-booking-v3/utils"
+	"github.com/maps-booking-v3/waitlist"
 )
 
-const logFile = "http_test_client_log_"
-
 var (
 	serverAddr                = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port")
 	credentialsFile           = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.")
@@ -48,206 +43,64 @@
 	outputToTerminal          = flag.Bool("output_to_terminal", false, "Output to terminal rather than a file.")
 )
 
-type counters struct {
-	TotalServicesProcessed       int
-	HealthCheckSuccess           bool
-	BatchGetWaitEstimatesSuccess int
-	BatchGetWaitEstimatesErrors  int
-	CreateWaitlistEntrySuccess   int
-	CreateWaitlistEntryErrors    int
-	GetWaitlistEntrySuccess      int
-	GetWaitlistEntryErrors       int
-	DeleteWaitlistEntrySuccess   int
-	DeleteWaitlistEntryErrors    int
-}
-
-// GenerateWaitlistEntries creates a waitlist entry for each provided service.
-func GenerateWaitlistEntries(services []*fpb.Service, stats *counters, conn *api.HTTPConnection) []string {
-	log.Println("no previous waitlist entries to use, acquiring new inventory")
-	utils.LogFlow("Generate Fresh Entries", "Start")
-	defer utils.LogFlow("Generate Fresh Entries", "End")
-
-	var out []string
-	totalServices := len(services)
-	for i, s := range services {
-		id, err := api.CreateWaitlistEntry(s, conn)
-		if err != nil {
-			log.Printf("%s. skipping waitlistEntry %d/%d, serviceID: %s",
-				err.Error(), i, totalServices, s.GetServiceId())
-			stats.CreateWaitlistEntryErrors++
-			continue
-		}
-		out = append(out, id)
-		stats.CreateWaitlistEntrySuccess++
+func makeConfig(logger *log.Logger) *utils.Config {
+	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
+	if err != nil {
+		logger.Fatalf("Failed to init http connection %v", err)
 	}
-	return out
-}
-
-func createLogFile() (*os.File, error) {
-	var err error
-	outPath := *outputDir
-	if outPath == "" {
-		outPath, err = os.Getwd()
-		if err != nil {
-			return nil, err
-		}
+	return &utils.Config{Conn: conn,
+		WaitlistAllFlows:                  *allFlows,
+		WaitlistHealthFlow:                *healthFlow,
+		WaitlistBatchGetWaitEstimatesFlow: *batchGetWaitEstimatesFlow,
+		WaitlistCreateWaitlistEntryFlow:   *createWaitlistEntryFlow,
+		WaitlistGetWaitlistEntryFlow:      *getWaitlistEntryFlow,
+		WaitlistDeleteWaitlistEntryFlow:   *deleteWaitlistEntryFlow,
 	}
-
-	now := time.Now().UTC()
-	nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
-	outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString))
-
-	return os.Create(outFile)
-}
-
-func logStats(stats counters) {
-	log.Println("\n************* Begin Stats *************\n")
-	var totalErrors int
-	if *healthFlow || *allFlows {
-		if stats.HealthCheckSuccess {
-			log.Println("HealthCheck Succeeded")
-		} else {
-			totalErrors++
-			log.Println("HealthCheck Failed")
-		}
-	}
-	if *batchGetWaitEstimatesFlow || *allFlows {
-		totalErrors += stats.BatchGetWaitEstimatesErrors
-		log.Printf("BatchGetWaitEstimates Errors: %d/%d", stats.BatchGetWaitEstimatesErrors, stats.BatchGetWaitEstimatesErrors+stats.BatchGetWaitEstimatesSuccess)
-	}
-	if *createWaitlistEntryFlow || *allFlows {
-		totalErrors += stats.CreateWaitlistEntryErrors
-		log.Printf("CreateWaitlistEntry Errors: %d/%d", stats.CreateWaitlistEntryErrors, stats.CreateWaitlistEntryErrors+stats.CreateWaitlistEntrySuccess)
-	}
-	if *getWaitlistEntryFlow || *allFlows {
-		totalErrors += stats.GetWaitlistEntryErrors
-		log.Printf("GetWaitlistEntry Errors: %d/%d", stats.GetWaitlistEntryErrors, stats.GetWaitlistEntryErrors+stats.GetWaitlistEntrySuccess)
-	}
-	if *deleteWaitlistEntryFlow || *allFlows {
-		totalErrors += stats.DeleteWaitlistEntryErrors
-		log.Printf("DeleteWaitlistEntry Errors: %d/%d", stats.DeleteWaitlistEntryErrors, stats.DeleteWaitlistEntryErrors+stats.DeleteWaitlistEntrySuccess)
-	}
-
-	log.Println("\n\n\n")
-	if totalErrors == 0 {
-		log.Println("All Tests Pass!")
-	} else {
-		log.Printf("Found %d Errors", totalErrors)
-	}
-
-	log.Println("\n************* End Stats *************\n")
-	os.Exit(totalErrors)
 }
 
 func main() {
 	flag.Parse()
-	var stats counters
 
-	if !*outputToTerminal {
-		// Set up logging before continuing with flows
-		f, err := createLogFile()
-		if err != nil {
-			log.Fatalf("Failed to create log file %v", err)
-		}
-		defer f.Close()
-		log.SetOutput(f)
-	}
-
-	conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName)
+	logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir)
 	if err != nil {
-		log.Fatalf("Failed to init http connection %v", err)
+		log.Fatal("Could not create logger: ", err)
 	}
+	if f != nil {
+		defer f.Close()
+	}
+	config := makeConfig(logger)
+	var summary utils.TestSummary
 
-	// HealthCheck Flow
-	if *healthFlow || *allFlows {
-		stats.HealthCheckSuccess = true
-		if err := api.HealthCheck(conn); err != nil {
-			stats.HealthCheckSuccess = false
-			log.Println(err.Error())
-		}
-		if !*allFlows && !*batchGetWaitEstimatesFlow && !*createWaitlistEntryFlow &&
-			!*getWaitlistEntryFlow && !*deleteWaitlistEntryFlow {
-			logStats(stats)
-		}
-	}
+	needService := config.WaitlistAllFlows || config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistCreateWaitlistEntryFlow ||
+		config.WaitlistGetWaitlistEntryFlow || config.WaitlistDeleteWaitlistEntryFlow
 
 	// Build services.
-	if *serviceFeed == "" {
-		log.Fatal("please set service_feed flag if you wish to test additional flows")
-	}
-
-	var services []*fpb.Service
-	services, err = utils.ParseServiceFeed(*serviceFeed)
-	if err != nil {
-		log.Fatalf("Failed to get services: %v", err.Error())
-	}
-	// Remove services without waitlist rules.
-	waitlistServices := services[:0]
-	for _, s := range services {
-		if s.GetWaitlistRules() != nil {
-			waitlistServices = append(waitlistServices, s)
+	var reducedServices []*fpb.Service
+	if needService {
+		if *serviceFeed == "" {
+			logger.Fatal("please set service_feed flag if you wish to test additional flows")
 		}
-	}
 
-	if len(waitlistServices) == 0 {
-		log.Fatal("no services have waitlist rules")
-	}
-	reducedServices := utils.ReduceServices(waitlistServices, *numTestServices)
-	stats.TotalServicesProcessed += len(reducedServices)
-
-	// BatchGetWaitEstimates Flow
-	if *batchGetWaitEstimatesFlow || *allFlows {
-		utils.LogFlow("BatchGetWaitEstimates", "Start")
-
-		for i, s := range reducedServices {
-			if err = api.BatchGetWaitEstimates(s, conn); err != nil {
-				log.Printf("%s. BatchGerWaitEstimates failed for service %d/%d. Service_id:",
-					err.Error(), i, stats.TotalServicesProcessed, s.GetServiceId())
-				stats.BatchGetWaitEstimatesErrors++
-				continue
+		var services []*fpb.Service
+		services, err = utils.ParseServiceFeed(*serviceFeed)
+		if err != nil {
+			logger.Fatalf("Failed to get services: %v", err.Error())
+		}
+		// Remove services without waitlist rules.
+		waitlistServices := services[:0]
+		for _, s := range services {
+			if s.GetWaitlistRules() != nil {
+				waitlistServices = append(waitlistServices, s)
 			}
-			stats.BatchGetWaitEstimatesSuccess++
 		}
-		utils.LogFlow("BatchGetWaitEstimates", "End")
-	}
-	// CreateWaitlistEntry Flow.
-	var ids []string
-	if *createWaitlistEntryFlow || *getWaitlistEntryFlow ||
-		*deleteWaitlistEntryFlow || *allFlows {
-		utils.LogFlow("CreateWaitlistEntry", "Start")
-		ids = GenerateWaitlistEntries(reducedServices, &stats, conn)
-		utils.LogFlow("CreateWaitlistEntry", "End")
-	}
-	// GetWaitlistEntry Flow
-	if *getWaitlistEntryFlow || *allFlows {
-		utils.LogFlow("GetWaitlistEntry", "Start")
-		for _, id := range ids {
-			if _, err = api.GetWaitlistEntry(id, conn); err != nil {
-				log.Printf("%s. get waitlist entry failed for waitlist entry id: %s",
-					err.Error(), id)
-				stats.GetWaitlistEntryErrors++
-				continue
-			}
-			stats.GetWaitlistEntrySuccess++
+
+		if len(waitlistServices) == 0 {
+			logger.Fatal("no services have waitlist rules")
 		}
-		utils.LogFlow("GetWaitlistEntry", "End")
+		reducedServices = utils.ReduceServices(logger, waitlistServices, *numTestServices)
+	} else {
+		reducedServices = []*fpb.Service{}
 	}
-
-	// DeleteWaitlistentry Flow
-	if *deleteWaitlistEntryFlow || *allFlows {
-		utils.LogFlow("DeleteWaitlistEntry", "Start")
-
-		for _, id := range ids {
-			if err = api.DeleteWaitlistEntry(id, conn); err != nil {
-				log.Printf("%s. Delete waitlist entry failed for waitlist entry id: %s",
-					err.Error(), id)
-				stats.DeleteWaitlistEntryErrors++
-				continue
-			}
-			stats.DeleteWaitlistEntrySuccess++
-		}
-		utils.LogFlow("DeleteWaitlistEntry", "End")
-	}
-
-	logStats(stats)
+	summary.WaitlistTotalServicesProcessed += len(reducedServices)
+	waitlist.RunTests(context.Background(), logger, config, reducedServices, &summary)
 }
diff --git a/utils/structs.go b/utils/structs.go
new file mode 100644
index 0000000..689c35e
--- /dev/null
+++ b/utils/structs.go
@@ -0,0 +1,152 @@
+/*
+Copyright 2019 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+	"net/http"
+
+	"github.com/golang/protobuf/jsonpb"
+)
+
+// Config holds all test configuration options.
+type Config struct {
+	BookingAllFlows          bool
+	BookingHealthFlow        bool
+	BookingCheckFlow         bool
+	BookingBookFlow          bool
+	BookingListFlow          bool
+	BookingStatusFlow        bool
+	BookingRescheduleFlow    bool
+	BookingCancelAllBookings bool
+	BookingUseBal            bool
+
+	OrderAllFlows   bool
+	OrderHealthFlow bool
+	OrderCheckFlow  bool
+	OrderOrderFlow  bool
+	OrderListFlow   bool
+
+	WaitlistAllFlows                  bool
+	WaitlistHealthFlow                bool
+	WaitlistBatchGetWaitEstimatesFlow bool
+	WaitlistCreateWaitlistEntryFlow   bool
+	WaitlistGetWaitlistEntryFlow      bool
+	WaitlistDeleteWaitlistEntryFlow   bool
+
+	Conn *HTTPConnection
+}
+
+// ShouldTestBookings determines whether any booking tests need to be run.
+func (c *Config) ShouldTestBookings() bool {
+	return c.BookingAllFlows ||
+		c.BookingHealthFlow ||
+		c.BookingCheckFlow ||
+		c.BookingBookFlow ||
+		c.BookingListFlow ||
+		c.BookingStatusFlow ||
+		c.BookingRescheduleFlow ||
+		c.BookingCancelAllBookings
+}
+
+// ShouldTestOrders determines whether any orders tests need to be run.
+func (c *Config) ShouldTestOrders() bool {
+	return c.OrderAllFlows ||
+		c.OrderHealthFlow ||
+		c.OrderCheckFlow ||
+		c.OrderOrderFlow ||
+		c.OrderListFlow
+}
+
+// ShouldTestWaitlists determines whether any waitlist tests need to be run.
+func (c *Config) ShouldTestWaitlists() bool {
+	return c.WaitlistAllFlows ||
+		c.WaitlistHealthFlow ||
+		c.WaitlistBatchGetWaitEstimatesFlow ||
+		c.WaitlistCreateWaitlistEntryFlow ||
+		c.WaitlistGetWaitlistEntryFlow ||
+		c.WaitlistDeleteWaitlistEntryFlow
+}
+
+// TestSummary contains a summary of all test results.
+type TestSummary struct {
+	BookingTotalSlotsProcessed              int
+	BookingHealthCheckSuccess               bool
+	BookingHealthCheckCompleted             bool
+	BookingBatchAvailabilityLookupErrors    int
+	BookingBatchAvailabilityLookupSuccess   int
+	BookingBatchAvailabilityLookupCompleted bool
+	BookingCheckAvailabilitySuccess         int
+	BookingCheckAvailabilityErrors          int
+	BookingCheckAvailabilityCompleted       bool
+	BookingCreateBookingSuccess             int
+	BookingCreateBookingErrors              int
+	BookingCreateBookingCompleted           bool
+	BookingListBookingsSuccess              bool
+	BookingListBookingsCompleted            bool
+	BookingGetBookingStatusSuccess          int
+	BookingGetBookingStatusErrors           int
+	BookingGetBookingStatusCompleted        bool
+	BookingCancelBookingsSuccess            int
+	BookingCancelBookingsErrors             int
+	BookingCancelBookingsCompleted          bool
+	BookingReschedulingSuccess              bool
+	BookingReschedulingCompleted            bool
+
+	OrderTotalSlotsProcessed               int
+	OrderHealthCheckSuccess                bool
+	OrderHealthCheckCompleted              bool
+	OrderCheckOrderFulfillabilitySuccess   int
+	OrderCheckOrderFulfillabilityErrors    int
+	OrderCheckOrderFulfillabilityCompleted bool
+	OrderCreateOrderSuccess                int
+	OrderCreateOrderErrors                 int
+	OrderCreateOrderCompleted              bool
+	OrderListOrdersSuccess                 bool
+	OrderListOrdersCompleted               bool
+
+	WaitlistTotalServicesProcessed         int
+	WaitlistHealthCheckSuccess             bool
+	WaitlistHealthCheckCompleted           bool
+	WaitlistBatchGetWaitEstimatesSuccess   int
+	WaitlistBatchGetWaitEstimatesErrors    int
+	WaitlistBatchGetWaitEstimatesCompleted bool
+	WaitlistCreateWaitlistEntrySuccess     int
+	WaitlistCreateWaitlistEntryErrors      int
+	WaitlistCreateWaitlistEntryCompleted   bool
+	WaitlistGetWaitlistEntrySuccess        int
+	WaitlistGetWaitlistEntryErrors         int
+	WaitlistGetWaitlistEntryCompleted      bool
+	WaitlistDeleteWaitlistEntrySuccess     int
+	WaitlistDeleteWaitlistEntryErrors      int
+	WaitlistDeleteWaitlistEntryCompleted   bool
+}
+
+// HTTPConnection is a convenience struct for holding connection-related objects.
+type HTTPConnection struct {
+	Client      *http.Client
+	Credentials string
+	Marshaler   *jsonpb.Marshaler
+	BaseURL     string
+}
+
+// GetURL computes the URL for an RPC.
+func (h HTTPConnection) GetURL(rpcName string) string {
+	if rpcName != "" {
+		return h.BaseURL + "/v3/" + rpcName
+	}
+	return h.BaseURL
+}
diff --git a/utils/utils.go b/utils/utils.go
index c5e102f..03984b2 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Google Inc.
+Copyright 2019 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,25 +18,32 @@
 package utils
 
 import (
-	"crypto/md5"
+	"crypto/sha256"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
 	"math/rand"
+	"os"
 	"path"
+	"path/filepath"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/golang/protobuf/proto"
 	"github.com/google/go-cmp/cmp"
 
+	
 	fpb "github.com/maps-booking-v3/feeds"
+	
 	mpb "github.com/maps-booking-v3/v3"
-	wpb "github.com/maps-booking-v3/waitlist"
+	wpb "github.com/maps-booking-v3/v3waitlist"
 )
 
+const logFile = "http_test_client_log_"
+
 // SlotKey is a struct representing a unique service.
 type SlotKey struct {
 	MerchantID string
@@ -45,13 +52,45 @@
 	RoomID     string
 }
 
+func createLogFile(outPath string) (*os.File, error) {
+	var err error
+	if outPath == "" {
+		outPath, err = os.Getwd()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	now := time.Now().UTC()
+	nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
+	outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString))
+
+	return os.Create(outFile)
+}
+
+// MakeLogger creates a logger that either logs to terminal or to a file. If outputToTerminal
+// is true, outputDir will be ignored.
+func MakeLogger(outputToTerminal bool, outputDir string) (*log.Logger, *os.File, error) {
+	var logger *log.Logger
+	if outputToTerminal {
+		logger = log.New(os.Stderr, "", log.Flags())
+		return logger, nil, nil
+	}
+	f, err := createLogFile(outputDir)
+	if err != nil {
+		return nil, nil, err
+	}
+	logger = log.New(f, "", log.Flags())
+	return logger, f, nil
+}
+
 // LogFlow is a convenience function for logging common flows..
-func LogFlow(f string, status string) {
-	log.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " "))
+func LogFlow(logger *log.Logger, f string, status string) {
+	logger.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " "))
 }
 
 // ReduceServices randomly selects and returns numTestServices from the provided services.
-func ReduceServices(allServices []*fpb.Service, numTestServices int) []*fpb.Service {
+func ReduceServices(logger *log.Logger, allServices []*fpb.Service, numTestServices int) []*fpb.Service {
 	reducedServices := make([]*fpb.Service, 0, numTestServices)
 
 	if len(allServices) <= numTestServices {
@@ -61,7 +100,7 @@
 			reducedServices = append(reducedServices, allServices[n])
 		}
 	}
-	log.Printf("Selected %d services out of a possible %d", len(reducedServices), len(allServices))
+	logger.Printf("Selected %d services out of a possible %d", len(reducedServices), len(allServices))
 	return reducedServices
 }
 
@@ -152,13 +191,8 @@
 	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
-	}
-
+// BuildLineItemMap creats a collection of LineItems from slices of services and availabilities.
+func BuildLineItemMap(services []*fpb.Service, availabilities []*fpb.Availability) (map[string][]*mpb.LineItem, error) {
 	feedHasTicketType := false
 	serviceTicketTypeMap := make(map[string][]*fpb.TicketType)
 	for _, service := range services {
@@ -178,12 +212,7 @@
 	}
 
 	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, false)
-	if err != nil {
-		return nil, err
+		return nil, errors.New("no valid ticket types found in services, please update services and retry")
 	}
 
 	merchantLineItemMap := make(map[string][]*mpb.LineItem)
@@ -198,14 +227,16 @@
 			merchantLineItemMap[availability.GetMerchantId()] = append(merchantLineItemMap[availability.GetMerchantId()], lineItem)
 		}
 	}
-
+	if len(merchantLineItemMap) == 0 {
+		return nil, errors.New("no availability slots with ticket type IDs matching those in the service were found")
+	}
 	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, forRescheduling bool) ([]*fpb.Availability, error) {
-	LogFlow("Parse Input Feed", "Start")
-	defer LogFlow("Parse Input Feed", "End")
+func AvailabilityFrom(logger *log.Logger, availabilityFeed string, testSlots int, forRescheduling bool) ([]*fpb.Availability, error) {
+	LogFlow(logger, "Parse Input Feed", "Start")
+	defer LogFlow(logger, "Parse Input Feed", "End")
 
 	var feed fpb.AvailabilityFeed
 	content, err := ioutil.ReadFile(availabilityFeed)
@@ -248,16 +279,40 @@
 			}
 		}
 	}
-	log.Printf("Selected %d slots out of a possible %d", len(finalAvailability), len(rawAvailability))
+	logger.Printf("Selected %d slots out of a possible %d", len(finalAvailability), len(rawAvailability))
 	return finalAvailability, nil
 }
 
+func slotDiff(got, want *mpb.Slot) string {
+	// CONFIRMATION_MODE_SYNCHRONOUS and CONFIRMATION_MODE_UNSPECIFIED should be treated as equivalent.
+	var gotConfirmationMode, wantConfirmationMode mpb.ConfirmationMode
+	var gotComp, wantComp mpb.Slot
+	if got != nil {
+		gotComp = *got
+		gotConfirmationMode = got.GetConfirmationMode()
+		gotComp.ConfirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_UNSPECIFIED
+	}
+	if want != nil {
+		wantComp = *want
+		wantConfirmationMode = want.GetConfirmationMode()
+		wantComp.ConfirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_UNSPECIFIED
+	}
+	if diff := cmp.Diff(gotComp, wantComp, cmp.Comparer(proto.Equal)); diff != "" {
+		return diff
+	}
+	if (wantConfirmationMode == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS) !=
+		(gotConfirmationMode == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS) {
+		return cmp.Diff(gotConfirmationMode, wantConfirmationMode)
+	}
+	return ""
+}
+
 // ValidateBooking performs granular comparisons between all got and want Bookings.
 func ValidateBooking(got, want *mpb.Booking) error {
 	if got.GetBookingId() == "" {
 		return errors.New("booking_id is empty")
 	}
-	if diff := cmp.Diff(got.GetSlot(), want.GetSlot(), cmp.Comparer(proto.Equal)); diff != "" {
+	if diff := slotDiff(got.GetSlot(), want.GetSlot()); diff != "" {
 		return fmt.Errorf("slots differ (-got +want)\n%s", diff)
 	}
 	// UserId is the only required field for the partner to return.
@@ -362,7 +417,7 @@
 	for _, ticket := range l.GetTickets() {
 		uID = append(uID, ticket.GetTicketId())
 	}
-	return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(uID, `|`))))
+	return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(uID, `|`))))
 }
 
 // Orders is a convenience type for an Orders array
@@ -500,20 +555,20 @@
 	}
 	waitLength := waitEstimate.GetWaitLength()
 	if waitLength == nil {
-		return errors.New("wait estimate not specified")
+		return errors.New("wait length not specified")
 	}
 	if waitLength.GetPartiesAheadCount() < 0 {
 		return errors.New("parties ahead count < 0")
 	}
 	estimatedSeatTimeRange := waitLength.GetEstimatedSeatTimeRange()
 	if estimatedSeatTimeRange == nil {
-		return errors.New("estimated seat time range not specified")
+		return nil
 	}
 	if estimatedSeatTimeRange.GetStartSeconds() <= 0 {
-		return errors.New("start seconds <= 0")
+		return errors.New("estimated seat time range provided and start seconds <= 0")
 	}
 	if estimatedSeatTimeRange.GetEndSeconds() <= 0 {
-		return errors.New("end seconds <= 0")
+		return errors.New("estimated seat time range provided and end seconds <= 0")
 	}
 	return nil
 }
diff --git a/waitlist/waitlist.pb.go b/v3waitlist/v3waitlist.pb.go
similarity index 100%
rename from waitlist/waitlist.pb.go
rename to v3waitlist/v3waitlist.pb.go
diff --git a/waitlist/waitlistTests.go b/waitlist/waitlistTests.go
new file mode 100644
index 0000000..d67c6ef
--- /dev/null
+++ b/waitlist/waitlistTests.go
@@ -0,0 +1,165 @@
+/*
+Copyright 2019 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package waitlist contains test logic for waitlist related endpoints.
+package waitlist
+
+import (
+	"context"
+	"log"
+
+	fpb "github.com/maps-booking-v3/feeds"
+	"github.com/maps-booking-v3/api"
+	"github.com/maps-booking-v3/utils"
+)
+
+func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) {
+	logger.Println("\n************* Begin Stats *************\n")
+	var totalErrors int
+	if config.WaitlistHealthFlow || config.WaitlistAllFlows {
+		if stats.WaitlistHealthCheckSuccess {
+			logger.Println("HealthCheck Succeeded")
+		} else {
+			totalErrors++
+			logger.Println("HealthCheck Failed")
+		}
+	}
+	if config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistAllFlows {
+		totalErrors += stats.WaitlistBatchGetWaitEstimatesErrors
+		logger.Printf("BatchGetWaitEstimates Errors: %d/%d", stats.WaitlistBatchGetWaitEstimatesErrors, stats.WaitlistBatchGetWaitEstimatesErrors+stats.WaitlistBatchGetWaitEstimatesSuccess)
+	}
+	if config.WaitlistCreateWaitlistEntryFlow || config.WaitlistAllFlows {
+		totalErrors += stats.WaitlistCreateWaitlistEntryErrors
+		logger.Printf("CreateWaitlistEntry Errors: %d/%d", stats.WaitlistCreateWaitlistEntryErrors, stats.WaitlistCreateWaitlistEntryErrors+stats.WaitlistCreateWaitlistEntrySuccess)
+	}
+	if config.WaitlistGetWaitlistEntryFlow || config.WaitlistAllFlows {
+		totalErrors += stats.WaitlistGetWaitlistEntryErrors
+		logger.Printf("GetWaitlistEntry Errors: %d/%d", stats.WaitlistGetWaitlistEntryErrors, stats.WaitlistGetWaitlistEntryErrors+stats.WaitlistGetWaitlistEntrySuccess)
+	}
+	if config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows {
+		totalErrors += stats.WaitlistDeleteWaitlistEntryErrors
+		logger.Printf("DeleteWaitlistEntry Errors: %d/%d", stats.WaitlistDeleteWaitlistEntryErrors, stats.WaitlistDeleteWaitlistEntryErrors+stats.WaitlistDeleteWaitlistEntrySuccess)
+	}
+
+	logger.Println("\n\n\n")
+	if totalErrors == 0 {
+		logger.Println("All Tests Pass!")
+	} else {
+		logger.Printf("Found %d Errors", totalErrors)
+	}
+
+	logger.Println("\n************* End Stats *************\n")
+}
+
+// generateWaitlistEntries creates a waitlist entry for each provided service.
+func generateWaitlistEntries(ctx context.Context, logger *log.Logger, services []*fpb.Service, stats *utils.TestSummary, conn *utils.HTTPConnection) []string {
+	logger.Println("no previous waitlist entries to use, acquiring new inventory")
+	utils.LogFlow(logger, "Generate Fresh Entries", "Start")
+	defer utils.LogFlow(logger, "Generate Fresh Entries", "End")
+
+	var out []string
+	totalServices := len(services)
+	for i, s := range services {
+		id, err := api.CreateWaitlistEntry(ctx, logger, s, conn)
+		if err != nil {
+			logger.Printf("%s. skipping waitlistEntry %d/%d, serviceID: %s",
+				err.Error(), i, totalServices, s.GetServiceId())
+			stats.WaitlistCreateWaitlistEntryErrors++
+			continue
+		}
+		out = append(out, id)
+		stats.WaitlistCreateWaitlistEntrySuccess++
+	}
+	return out
+}
+
+// RunTests runs waitlist tests.
+func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, reducedServices []*fpb.Service, stats *utils.TestSummary) {
+	// HealthCheck Flow
+	conn := config.Conn
+	if config.WaitlistHealthFlow || config.WaitlistAllFlows {
+		stats.WaitlistHealthCheckSuccess = true
+		if err := api.HealthCheck(ctx, logger, conn); err != nil {
+			stats.WaitlistHealthCheckSuccess = false
+			logger.Println(err.Error())
+		}
+		stats.WaitlistHealthCheckCompleted = true
+		if !config.WaitlistAllFlows && !config.WaitlistBatchGetWaitEstimatesFlow && !config.WaitlistCreateWaitlistEntryFlow &&
+			!config.WaitlistGetWaitlistEntryFlow && !config.WaitlistDeleteWaitlistEntryFlow {
+			logStats(stats, logger, config)
+			return
+		}
+	}
+
+	// BatchGetWaitEstimates Flow
+	if config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistAllFlows {
+		utils.LogFlow(logger, "BatchGetWaitEstimates", "Start")
+
+		for i, s := range reducedServices {
+			if err := api.BatchGetWaitEstimates(ctx, logger, s, conn); err != nil {
+				logger.Printf("%s. BatchGerWaitEstimates failed for service %d/%d. Service_id: %s",
+					err.Error(), i, stats.WaitlistTotalServicesProcessed, s.GetServiceId())
+				stats.WaitlistBatchGetWaitEstimatesErrors++
+				continue
+			}
+			stats.WaitlistBatchGetWaitEstimatesSuccess++
+		}
+		stats.WaitlistBatchGetWaitEstimatesCompleted = true
+		utils.LogFlow(logger, "BatchGetWaitEstimates", "End")
+	}
+	// CreateWaitlistEntry Flow.
+	var ids []string
+	if config.WaitlistCreateWaitlistEntryFlow || config.WaitlistGetWaitlistEntryFlow ||
+		config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows {
+		utils.LogFlow(logger, "CreateWaitlistEntry", "Start")
+		ids = generateWaitlistEntries(ctx, logger, reducedServices, stats, conn)
+		stats.WaitlistCreateWaitlistEntryCompleted = true
+		utils.LogFlow(logger, "CreateWaitlistEntry", "End")
+	}
+	// GetWaitlistEntry Flow
+	if config.WaitlistGetWaitlistEntryFlow || config.WaitlistAllFlows {
+		utils.LogFlow(logger, "GetWaitlistEntry", "Start")
+		for _, id := range ids {
+			if _, err := api.GetWaitlistEntry(ctx, logger, id, conn); err != nil {
+				logger.Printf("%s. get waitlist entry failed for waitlist entry id: %s",
+					err.Error(), id)
+				stats.WaitlistGetWaitlistEntryErrors++
+				continue
+			}
+			stats.WaitlistGetWaitlistEntrySuccess++
+		}
+		stats.WaitlistGetWaitlistEntryCompleted = true
+		utils.LogFlow(logger, "GetWaitlistEntry", "End")
+	}
+
+	// DeleteWaitlistentry Flow
+	if config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows {
+		utils.LogFlow(logger, "DeleteWaitlistEntry", "Start")
+
+		for _, id := range ids {
+			if err := api.DeleteWaitlistEntry(ctx, logger, id, conn); err != nil {
+				logger.Printf("%s. Delete waitlist entry failed for waitlist entry id: %s",
+					err.Error(), id)
+				stats.WaitlistDeleteWaitlistEntryErrors++
+				continue
+			}
+			stats.WaitlistDeleteWaitlistEntrySuccess++
+		}
+		stats.WaitlistDeleteWaitlistEntryCompleted = true
+		utils.LogFlow(logger, "DeleteWaitlistEntry", "End")
+	}
+	logStats(stats, logger, config)
+}