| /* |
| 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 contains common BookingService based helper functions. |
| package utils |
| |
| import ( |
| "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/v3waitlist" |
| ) |
| |
| const logFile = "http_test_client_log_" |
| |
| // SlotKey is a struct representing a unique service. |
| type SlotKey struct { |
| MerchantID string |
| ServiceID string |
| StaffID string |
| 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(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(logger *log.Logger, allServices []*fpb.Service, numTestServices int) []*fpb.Service { |
| reducedServices := make([]*fpb.Service, 0, numTestServices) |
| |
| if len(allServices) <= numTestServices { |
| reducedServices = allServices |
| } else { |
| for _, n := range rand.Perm(len(allServices))[0:numTestServices] { |
| reducedServices = append(reducedServices, allServices[n]) |
| } |
| } |
| logger.Printf("Selected %d services out of a possible %d", len(reducedServices), len(allServices)) |
| return reducedServices |
| } |
| |
| // ParseServiceFeed returns a slice of services for each service in the feed. |
| 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 { |
| // If no ticket types return nil as there is nothing to build. |
| if len(tickets) == 0 { |
| return nil |
| } |
| |
| // Treated as a set. |
| ticketMap := make(map[string]*fpb.TicketType) |
| for _, ticketType := range tickets { |
| if _, ok := ticketMap[ticketType.GetTicketTypeId()]; !ok { |
| // Ticket type ids should be unique to a merchant service pair. |
| // If they're not unique they will get dropped silently here. |
| // We enforce minimal feed validation in the test client so it's |
| // up to you to ensure UIDs. |
| ticketMap[ticketType.GetTicketTypeId()] = ticketType |
| } |
| } |
| |
| filteredTickets := tickets |
| if len(availability.GetTicketTypeId()) != 0 { |
| // Clear slice but preserve allocated memory. |
| filteredTickets = filteredTickets[:0] |
| for _, ticketTypeID := range availability.GetTicketTypeId() { |
| if ticketType, ok := ticketMap[ticketTypeID]; ok { |
| filteredTickets = append(filteredTickets, ticketType) |
| } |
| } |
| } |
| |
| // If no ticket types return nil as there is nothing to build. |
| if len(filteredTickets) == 0 { |
| return nil |
| } |
| |
| lineItem := &mpb.LineItem{ |
| ServiceId: availability.GetServiceId(), |
| StartSec: availability.GetStartSec(), |
| DurationSec: availability.GetDurationSec(), |
| Price: &mpb.Price{}, |
| } |
| for i := 0; i < int(availability.GetSpotsOpen()); i++ { |
| // This is deterministic which is fine given that we just want to get a mix of ticket types. |
| ticketTypeIndex := rand.Intn(len(filteredTickets)) |
| // Calculate price of line item. |
| if lineItem.GetPrice().GetCurrencyCode() == "" && filteredTickets[ticketTypeIndex].GetPrice().GetCurrencyCode() != "" { |
| lineItem.Price.CurrencyCode = filteredTickets[ticketTypeIndex].GetPrice().GetCurrencyCode() |
| } |
| lineItem.Price.PriceMicros += filteredTickets[ticketTypeIndex].GetPrice().GetPriceMicros() |
| |
| // Add ticket to line item. |
| lineItem.Tickets = append(lineItem.Tickets, &mpb.LineItem_OrderedTickets{ |
| TicketId: filteredTickets[ticketTypeIndex].GetTicketTypeId(), |
| Count: 1, |
| }) |
| } |
| |
| return lineItem |
| } |
| |
| // 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 { |
| 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. |
| if ticket.GetPrice() == nil || len(ticket.GetTicketTypeId()) == 0 || cmp.Diff(&fpb.Price{}, ticket.GetPrice(), cmp.Comparer(proto.Equal)) == "" { |
| 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 services, please update services and retry") |
| } |
| |
| 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) |
| } |
| } |
| 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(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) |
| if err != nil { |
| return nil, fmt.Errorf("unable to read input file: %v", err) |
| } |
| if path.Ext(availabilityFeed) == ".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(availabilityFeed) == ".pb3" { |
| if err := proto.Unmarshal(content, &feed); err != nil { |
| return nil, fmt.Errorf("unable to parse feed as pb3: %v", err) |
| } |
| } |
| |
| var finalAvailability []*fpb.Availability |
| var rawAvailability []*fpb.Availability |
| for _, sa := range feed.GetServiceAvailability() { |
| rawAvailability = append(rawAvailability, sa.GetAvailability()...) |
| } |
| if len(rawAvailability) == 0 || testSlots == 0 { |
| return finalAvailability, errors.New("no valid availability in feed, exiting workflows") |
| } |
| if len(rawAvailability) <= testSlots { |
| finalAvailability = rawAvailability |
| } else { |
| nums := rand.Perm(len(rawAvailability))[0:testSlots] |
| for _, n := range nums { |
| finalAvailability = append(finalAvailability, rawAvailability[n]) |
| if !forRescheduling { |
| continue |
| } |
| if n > 0 { |
| finalAvailability = append(finalAvailability, rawAvailability[n-1]) |
| } |
| if n < testSlots-1 { |
| finalAvailability = append(finalAvailability, rawAvailability[n+1]) |
| } |
| } |
| } |
| 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 := 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. |
| if diff := cmp.Diff(got.GetUserInformation().GetUserId(), want.GetUserInformation().GetUserId()); diff != "" { |
| return fmt.Errorf("users differ (-got +want)\n%s", diff) |
| } |
| if diff := cmp.Diff(got.GetPaymentInformation(), want.GetPaymentInformation(), cmp.Comparer(proto.Equal)); diff != "" { |
| return fmt.Errorf("payment information differs (-got +want)\n%s", diff) |
| } |
| // BookingStatus_CONFIRMED is the default case unless the slot is async, in which |
| // case the default is BookingStatus_PENDING_MERCHANT_CONFIRMATION. |
| wantStatus := mpb.BookingStatus_CONFIRMED |
| if want.GetSlot().GetConfirmationMode() == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS { |
| wantStatus = mpb.BookingStatus_PENDING_MERCHANT_CONFIRMATION |
| } |
| // If an alternate status is specified, we compare against it instead of the default. |
| if want.GetStatus() != mpb.BookingStatus_BOOKING_STATUS_UNSPECIFIED { |
| wantStatus = want.GetStatus() |
| } |
| if diff := cmp.Diff(got.GetStatus(), wantStatus); diff != "" { |
| return fmt.Errorf("status differs (-got +want)\n%s", diff) |
| } |
| 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 |
| } |
| |
| // AvailabilityToSlotTime converts an Availability object to SlotTime. |
| func AvailabilityToSlotTime(a *fpb.Availability) *mpb.SlotTime { |
| slot := &mpb.SlotTime{ |
| ServiceId: a.GetServiceId(), |
| StartSec: a.GetStartSec(), |
| DurationSec: a.GetDurationSec(), |
| AvailabilityTag: a.GetAvailabilityTag()} |
| if a.Resources != nil { |
| r := a.GetResources() |
| slot.ResourceIds = &mpb.ResourceIds{ |
| StaffId: r.GetStaffId(), |
| RoomId: r.GetRoomId(), |
| PartySize: r.GetPartySize(), |
| } |
| } |
| return slot |
| } |
| |
| // 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", sha256.Sum256([]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) { |
| confirmationMode := mpb.ConfirmationMode_CONFIRMATION_MODE_UNSPECIFIED |
| if a.GetConfirmationMode() == fpb.Availability_CONFIRMATION_MODE_SYNCHRONOUS { |
| confirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_SYNCHRONOUS |
| } else if a.GetConfirmationMode() == fpb.Availability_CONFIRMATION_MODE_ASYNCHRONOUS { |
| confirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS |
| } |
| slot := &mpb.Slot{ |
| MerchantId: a.GetMerchantId(), |
| ServiceId: a.GetServiceId(), |
| StartSec: a.GetStartSec(), |
| DurationSec: a.GetDurationSec(), |
| AvailabilityTag: a.GetAvailabilityTag(), |
| ConfirmationMode: confirmationMode, |
| } |
| if a.Resources != nil { |
| r := a.GetResources() |
| slot.Resources = &mpb.ResourceIds{ |
| StaffId: r.GetStaffId(), |
| RoomId: r.GetRoomId(), |
| PartySize: r.GetPartySize(), |
| } |
| } |
| return slot, nil |
| } |
| |
| // SplitAvailabilityByMerchant splits the list of availabilities by merchant. This is necessary if BatchAvailabilityLookup |
| // is enabled, since each BAL request handles one merchant. |
| func SplitAvailabilityByMerchant(av []*fpb.Availability) map[string][]*fpb.Availability { |
| ret := make(map[string][]*fpb.Availability) |
| for _, a := range av { |
| ret[a.GetMerchantId()] = append(ret[a.GetMerchantId()], a) |
| } |
| return ret |
| } |
| |
| // BuildBatchAvailabilityLookupRequestFrom creates a BatchAvailabilityLookupRequest from a list of input availability slots. |
| func BuildBatchAvailabilityLookupRequestFrom(av []*fpb.Availability) (*mpb.BatchAvailabilityLookupRequest, error) { |
| var st []*mpb.SlotTime |
| var m string |
| for _, a := range av { |
| if m == "" { |
| m = a.GetMerchantId() |
| } else if m != a.GetMerchantId() { |
| return nil, fmt.Errorf("BuildBatchAvailabilityLookupRequestFrom failed, got multiple merchant ids: %s, %s", m, a.GetMerchantId()) |
| } |
| st = append(st, AvailabilityToSlotTime(a)) |
| } |
| return &mpb.BatchAvailabilityLookupRequest{ |
| MerchantId: m, |
| SlotTime: st, |
| }, nil |
| } |
| |
| // BuildBatchAvailabilityLookupResponseFrom creates a BatchAvailabilityLookupResponse from a list of input availability slots. |
| func BuildBatchAvailabilityLookupResponseFrom(av []*fpb.Availability) (*mpb.BatchAvailabilityLookupResponse, error) { |
| var sta []*mpb.SlotTimeAvailability |
| var m string |
| for _, a := range av { |
| if m == "" { |
| m = a.GetMerchantId() |
| } else if m != a.GetMerchantId() { |
| return nil, fmt.Errorf("BuildBatchAvailabilityLookupRequestFrom failed, got multiple merchant ids: %s, %s", m, a.GetMerchantId()) |
| } |
| sta = append(sta, &mpb.SlotTimeAvailability{ |
| Available: a.SpotsOpen > 0, |
| SlotTime: AvailabilityToSlotTime(a), |
| }) |
| } |
| return &mpb.BatchAvailabilityLookupResponse{ |
| SlotTimeAvailability: sta, |
| }, nil |
| } |
| |
| // BuildMerchantServiceMap creates a key value pair of unique services to all of their availability slots. |
| func BuildMerchantServiceMap(av []*fpb.Availability) map[SlotKey][]*fpb.Availability { |
| m := make(map[SlotKey][]*fpb.Availability) |
| for _, a := range av { |
| key := SlotKey{ |
| MerchantID: a.GetMerchantId(), |
| ServiceID: a.GetServiceId(), |
| StaffID: a.GetResources().GetStaffId(), |
| RoomID: a.GetResources().GetRoomId(), |
| } |
| m[key] = append(m[key], a) |
| } |
| return m |
| } |
| |
| // ValidateWaitEstimate validates all the fields are populated for the wait estimate. |
| func ValidateWaitEstimate(waitEstimate *wpb.WaitEstimate) error { |
| if waitEstimate.GetPartySize() <= 0 { |
| return errors.New("party size <= 0") |
| } |
| waitLength := waitEstimate.GetWaitLength() |
| if waitLength == nil { |
| 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 nil |
| } |
| if estimatedSeatTimeRange.GetStartSeconds() <= 0 { |
| return errors.New("estimated seat time range provided and start seconds <= 0") |
| } |
| if estimatedSeatTimeRange.GetEndSeconds() <= 0 { |
| return errors.New("estimated seat time range provided and end seconds <= 0") |
| } |
| return nil |
| } |
| |
| // ValidateWaitlistEntry validates all the fields are populated for the waitlist entry. |
| func ValidateWaitlistEntry(waitlistEntry *wpb.WaitlistEntry) error { |
| if waitlistEntry.GetWaitlistEntryState() == wpb.WaitlistEntryState_WAITLIST_ENTRY_STATE_UNSPECIFIED { |
| return errors.New("waitlist entry state not specified") |
| } |
| stateTimes := waitlistEntry.GetWaitlistEntryStateTimes() |
| if stateTimes == nil { |
| return errors.New("WaitlistEntryStateTimes not specified") |
| } |
| if stateTimes.GetCreatedTimeSeconds() <= 0 { |
| return errors.New("created time seconds <= 0") |
| } |
| waitEstimate := waitlistEntry.GetWaitEstimate() |
| if waitEstimate == nil { |
| return errors.New("wait estimate not specified") |
| } |
| return ValidateWaitEstimate(waitEstimate) |
| } |