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