|  | /* | 
|  | Copyright 2017 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 api contains validation wrappers over BookingService endpoints. | 
|  | package api | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "crypto/tls" | 
|  | "crypto/x509" | 
|  | "encoding/base64" | 
|  | "errors" | 
|  | "fmt" | 
|  | "io/ioutil" | 
|  | "log" | 
|  | "math/rand" | 
|  | "net/http" | 
|  | "sort" | 
|  | "strconv" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "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" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | userID    = "0" | 
|  | firstName = "Jane" | 
|  | lastName  = "Doe" | 
|  | telephone = "+1 800-789-7890" | 
|  | email     = "test@example.com" | 
|  | ) | 
|  |  | 
|  | // 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 | 
|  | } | 
|  | b, err := ioutil.ReadFile(caFile) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to read root certificates file: %v", err) | 
|  | } | 
|  | cp := x509.NewCertPool() | 
|  | if !cp.AppendCertsFromPEM(b) { | 
|  | return nil, errors.New("failed to parse root certificates, please check your roots file (ca_file flag) and try again") | 
|  | } | 
|  | return &tls.Config{ | 
|  | RootCAs:    cp, | 
|  | ServerName: fullServerName, | 
|  | }, nil | 
|  | } | 
|  |  | 
|  | // InitHTTPConnection creates and returns a new HTTPConnection object | 
|  | // with a given server address and username/password. | 
|  | func InitHTTPConnection(serverAddr string, credentialsFile string, caFile string, fullServerName string) (*HTTPConnection, error) { | 
|  | // Set up username/password. | 
|  | var credentials string | 
|  | if credentialsFile != "" { | 
|  | data, err := ioutil.ReadFile(credentialsFile) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | credentials = "Basic " + base64.StdEncoding.EncodeToString([]byte(strings.Replace(string(data), "\n", "", -1))) | 
|  | } | 
|  | config, err := setupCertConfig(caFile, fullServerName) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | protocol := "http" | 
|  | if config != nil { | 
|  | protocol = "https" | 
|  | } | 
|  | return &HTTPConnection{ | 
|  | client: &http.Client{ | 
|  | Timeout:   10 * time.Second, | 
|  | Transport: &http.Transport{TLSClientConfig: config}, | 
|  | }, | 
|  | 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 | 
|  |  | 
|  | func (b Bookings) Len() int { | 
|  | return len(b) | 
|  | } | 
|  |  | 
|  | func (b Bookings) Less(i, j int) bool { | 
|  | return b[i].GetBookingId() < b[j].GetBookingId() | 
|  | } | 
|  |  | 
|  | func (b Bookings) Swap(i, j int) { | 
|  | b[i], b[j] = b[j], b[i] | 
|  | } | 
|  |  | 
|  | // HealthCheck performs a health check. | 
|  | func HealthCheck(conn *HTTPConnection) error { | 
|  | utils.LogFlow("Health Check", "Start") | 
|  | defer utils.LogFlow("Health Check", "End") | 
|  |  | 
|  | httpReq, err := http.NewRequest("GET", conn.getURL("HealthCheck"), nil) | 
|  | httpReq.Header.Set("Authorization", conn.credentials) | 
|  |  | 
|  | // See if we get a response. | 
|  | resp, err := conn.client.Do(httpReq) | 
|  | 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) | 
|  | 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) | 
|  |  | 
|  | httpResp, err := conn.client.Do(httpReq) | 
|  | if err != nil { | 
|  | return "", fmt.Errorf("invalid response. %s yielded error: %v", rpcName, err) | 
|  | } | 
|  | defer httpResp.Body.Close() | 
|  | bodyBytes, err := ioutil.ReadAll(httpResp.Body) | 
|  | if err != nil { | 
|  | 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) | 
|  | 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 { | 
|  | 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()) | 
|  | } | 
|  | reqPB := &mpb.CheckAvailabilityRequest{ | 
|  | Slot: slot, | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. CheckAvailability yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.CheckAvailabilityResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("CheckAvailability: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if diff := cmp.Diff(resp.GetSlot(), slot, cmp.Comparer(proto.Equal)); diff != "" { | 
|  | return fmt.Errorf("invalid response. CheckAvailability slots differ (-got +want)\n%s", diff) | 
|  | } | 
|  |  | 
|  | if resp.GetCountAvailable() == 0 { | 
|  | return errors.New("no count available in response") | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // 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 { | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | 
|  | } | 
|  | httpResp, err := sendRequest("BatchAvailabilityLookup", req, conn) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. BatchAvailabilityLookup yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.BatchAvailabilityLookupResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("BatchAvailabilityLookup: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  | if len(resp.GetSlotTimeAvailability()) != len(reqPB.GetSlotTime()) { | 
|  | return fmt.Errorf("invalid response. BatchAvailabilityLookup response.size and request.size differ: %v vs %v", len(resp.GetSlotTimeAvailability()), len(reqPB.GetSlotTime())) | 
|  | } | 
|  | diffCount := 0 | 
|  | for i := 0; i < len(reqPB.GetSlotTime()); i++ { | 
|  | 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) | 
|  | diffCount++ | 
|  | } | 
|  | } | 
|  | if diffCount > 0 { | 
|  | return fmt.Errorf("invalid response. Found %v diffs", diffCount) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // CreateBooking attempts to create bookings from availability slots. | 
|  | func CreateBooking(a *fpb.Availability, conn *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()) | 
|  | } | 
|  |  | 
|  | gen := rand.New(rand.NewSource(time.Now().UnixNano())) | 
|  | // Lease currently unsupported. | 
|  | reqPB := &mpb.CreateBookingRequest{ | 
|  | Slot: slot, | 
|  | UserInformation: &mpb.UserInformation{ | 
|  | UserId:     userID, | 
|  | GivenName:  firstName, | 
|  | FamilyName: lastName, | 
|  | Telephone:  telephone, | 
|  | Email:      email, | 
|  | }, | 
|  | PaymentInformation: &mpb.PaymentInformation{ | 
|  | PrepaymentStatus: mpb.PrepaymentStatus_PREPAYMENT_NOT_PROVIDED, | 
|  | }, | 
|  | IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateBooking  yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.CreateBookingResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return nil, fmt.Errorf("CreateBooking: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if resp.GetBookingFailure() != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateBooking failed with booking failure %v", resp.GetBookingFailure()) | 
|  | } | 
|  |  | 
|  | b := resp.GetBooking() | 
|  | if iE := utils.ValidateBooking(b, &mpb.Booking{ | 
|  | Slot:               reqPB.GetSlot(), | 
|  | UserInformation:    reqPB.GetUserInformation(), | 
|  | PaymentInformation: reqPB.GetPaymentInformation(), | 
|  | }); iE != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateBooking invalid: %s", iE.Error()) | 
|  | } | 
|  |  | 
|  | // 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) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) | 
|  | } | 
|  | var idemResp mpb.CreateBookingResponse | 
|  | if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | 
|  | return nil, fmt.Errorf("CreateBooking idem: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if diff := cmp.Diff(idemResp, resp); diff != "" { | 
|  | return b, fmt.Errorf("Idempotency check invalid (-got +want)\n%s", diff) | 
|  | } | 
|  |  | 
|  | return b, nil | 
|  | } | 
|  |  | 
|  | // ListBookings calls the maps booking ListBookings rpc and compares the return with all input bookings. | 
|  | func ListBookings(tB Bookings, conn *HTTPConnection) (Bookings, error) { | 
|  | reqPB := &mpb.ListBookingsRequest{ | 
|  | UserId: userID, | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. ListBookings yielded error: %v. Abandoning all booking from this flow", err) | 
|  | } | 
|  | var resp mpb.ListBookingsResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return nil, fmt.Errorf("ListBookings: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | gB := Bookings(resp.GetBookings()) | 
|  | if len(tB) == 0 { | 
|  | log.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)) | 
|  | } | 
|  | 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)) | 
|  | continue | 
|  | } | 
|  | out = append(out, tB[i]) | 
|  | } | 
|  | log.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 { | 
|  | reqPB := &mpb.GetBookingStatusRequest{ | 
|  | BookingId: b.GetBookingId(), | 
|  | } | 
|  |  | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. GetBookingStatus yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.GetBookingStatusResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("GetBookingsStatus: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if diff := cmp.Diff(resp.GetBookingStatus(), mpb.BookingStatus_CONFIRMED); diff != "" { | 
|  | return fmt.Errorf("invalid response. BookingStatus differ (-got +want)\n%s", diff) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // CancelBooking is a clean up method that cancels all supplied bookings. | 
|  | func CancelBooking(bookingID string, conn *HTTPConnection) error { | 
|  | reqPB := &mpb.UpdateBookingRequest{ | 
|  | Booking: &mpb.Booking{ | 
|  | BookingId: bookingID, | 
|  | Status:    mpb.BookingStatus_CANCELED, | 
|  | }, | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.UpdateBookingResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("CancelBooking: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if iE := utils.ValidateBooking(resp.GetBooking(), 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 { | 
|  | var slots []*fpb.Availability | 
|  | for _, v := range utils.BuildMerchantServiceMap(av) { | 
|  | // Need at least two slots for reschedule. | 
|  | if len(v) <= 1 { | 
|  | continue | 
|  | } | 
|  | slots = v | 
|  | break | 
|  | } | 
|  |  | 
|  | if len(slots) == 0 { | 
|  | return errors.New("no suitable availability for rescheduling flow. exiting") | 
|  | } | 
|  | // Book first slot. | 
|  | newBooking, err := CreateBooking(slots[0], conn) | 
|  | if err != nil { | 
|  | return fmt.Errorf("could not complete booking, abandoning rescheduling flow: %v", err) | 
|  | } | 
|  |  | 
|  | // New slot. | 
|  | lastAvailability := slots[len(slots)-1] | 
|  |  | 
|  | reqPB := &mpb.UpdateBookingRequest{ | 
|  | Booking: &mpb.Booking{ | 
|  | BookingId: newBooking.GetBookingId(), | 
|  | Slot: &mpb.Slot{ | 
|  | StartSec:    lastAvailability.GetStartSec(), | 
|  | DurationSec: lastAvailability.GetDurationSec(), | 
|  | }, | 
|  | }, | 
|  | } | 
|  | 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) | 
|  | 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) | 
|  | } | 
|  |  | 
|  | // Update slot before performing diff. | 
|  | newBooking.GetSlot().StartSec = lastAvailability.GetStartSec() | 
|  | newBooking.GetSlot().DurationSec = lastAvailability.GetDurationSec() | 
|  |  | 
|  | 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) | 
|  | } | 
|  |  | 
|  | // 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 { | 
|  | reqPB := &mpb.CheckOrderFulfillabilityRequest{ | 
|  | MerchantId: merchantID, | 
|  | Item:       lineItems, | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. CheckOrderFulfillability yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.CheckOrderFulfillabilityResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("CheckOrderFulfillability: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | 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) | 
|  | } | 
|  | if orderFulfillability.GetUnfulfillableReason() != "" { | 
|  | return errors.New("invalid response. CheckOrderFulfillability.UnfulfillableReason should be empty") | 
|  | } | 
|  |  | 
|  | var respLineItems []*mpb.LineItem | 
|  | for _, lineItemFulfillability := range orderFulfillability.GetItemFulfillability() { | 
|  | if diff := cmp.Diff(lineItemFulfillability.GetResult(), mpb.LineItemFulfillability_CAN_FULFILL); diff != "" { | 
|  | return fmt.Errorf("invalid response. CheckOrderFulfillability.Fulfillability.ItemFulfillability.Result for LineItem %v -- differ (-got +want)\n%s", lineItemFulfillability.GetItem(), diff) | 
|  | } | 
|  | if lineItemFulfillability.GetUnfulfillableReason() != "" { | 
|  | return errors.New("invalid response. CheckOrderFulfillability.Fulfillability.ItemFulfillability.UnfulfillableReason should be empty") | 
|  | } | 
|  |  | 
|  | respLineItems = append(respLineItems, lineItemFulfillability.GetItem()) | 
|  | } | 
|  |  | 
|  | if err = utils.ValidateLineItems(respLineItems, lineItems, false); err != nil { | 
|  | return fmt.Errorf("invalid response. CheckOrderFulfillability %v", err) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // 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) { | 
|  | gen := rand.New(rand.NewSource(time.Now().UnixNano())) | 
|  |  | 
|  | reqOrder := &mpb.Order{ | 
|  | UserInformation: &mpb.UserInformation{ | 
|  | UserId:     userID, | 
|  | GivenName:  firstName, | 
|  | FamilyName: lastName, | 
|  | Telephone:  telephone, | 
|  | Email:      email, | 
|  | }, | 
|  | PaymentInformation: &mpb.PaymentInformation{ | 
|  | PrepaymentStatus: mpb.PrepaymentStatus_PREPAYMENT_NOT_PROVIDED, | 
|  | }, | 
|  | MerchantId: merchantID, | 
|  | Item:       lineItems, | 
|  | } | 
|  | reqPB := &mpb.CreateOrderRequest{ | 
|  | Order:            reqOrder, | 
|  | IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | 
|  | } | 
|  |  | 
|  | 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) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateOrder yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.CreateOrderResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return nil, fmt.Errorf("CreateOrder: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if resp.GetOrderFailure() != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateOrder contains OrderFailure for request %v", reqPB) | 
|  | } | 
|  |  | 
|  | if err = utils.ValidateOrder(*resp.GetOrder(), *reqOrder); err != nil { | 
|  | return nil, fmt.Errorf("invalid response. CreateOrder %v", err) | 
|  | } | 
|  |  | 
|  | // Perform idempotency test. | 
|  | log.Printf("Idempotency check") | 
|  | idemHTTPResp, err := sendRequest("CreateOrder", req, conn) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) | 
|  | } | 
|  | var idemResp mpb.CreateOrderResponse | 
|  | if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | 
|  | return nil, fmt.Errorf("CreateOrder idem: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if idemResp.GetOrderFailure() != nil { | 
|  | return nil, errors.New("Idempotency check invalid. CreateOrder contains OrderFailure") | 
|  | } | 
|  | if err = utils.ValidateOrder(*idemResp.GetOrder(), *resp.GetOrder()); err != nil { | 
|  | return nil, fmt.Errorf("Idempotency check invalid  %v", err) | 
|  | } | 
|  |  | 
|  | return resp.GetOrder(), nil | 
|  | } | 
|  |  | 
|  | func sendListOrdersRequest(reqPB *mpb.ListOrdersRequest, conn *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) | 
|  | if err != nil { | 
|  | return mpb.ListOrdersResponse{}, fmt.Errorf("invalid response. ListOrders yielded error: %v", err) | 
|  | } | 
|  | var resp mpb.ListOrdersResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return resp, fmt.Errorf("ListOrders: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | return resp, nil | 
|  | } | 
|  |  | 
|  | // 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 { | 
|  | if len(orders) == 0 { | 
|  | return errors.New("at least one order must be present for ListOrders to succeed") | 
|  | } | 
|  |  | 
|  | // UserId check first. | 
|  | reqPB := &mpb.ListOrdersRequest{ | 
|  | Ids: &mpb.ListOrdersRequest_UserId{userID}, | 
|  | } | 
|  | respUser, err := sendListOrdersRequest(reqPB, conn) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if err = utils.ValidateOrders(respUser.GetOrder(), orders); err != nil { | 
|  | return fmt.Errorf("invalid response. ListOrders %v for request %v", err, reqPB) | 
|  | } | 
|  |  | 
|  | // Still here? OrderId check. | 
|  | reqPB.Reset() | 
|  | var orderIDs mpb.ListOrdersRequest_OrderIds | 
|  | for _, order := range orders { | 
|  | orderIDs.OrderId = append(orderIDs.OrderId, order.GetOrderId()) | 
|  | } | 
|  | reqPB.Ids = &mpb.ListOrdersRequest_OrderIds_{&orderIDs} | 
|  |  | 
|  | respOrder, err := sendListOrdersRequest(reqPB, conn) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if err = utils.ValidateOrders(respOrder.GetOrder(), orders); err != nil { | 
|  | return fmt.Errorf("invalid response. ListOrders %v for request %v", err, reqPB) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // BatchGetWaitEstimates calls the partners API and verifies the returned WaitEstimates. | 
|  | func BatchGetWaitEstimates(s *fpb.Service, conn *HTTPConnection) error { | 
|  | rules := s.GetWaitlistRules() | 
|  |  | 
|  | ps := make([]int32, rules.GetMaxPartySize()-rules.GetMinPartySize()) | 
|  | for i := range ps { | 
|  | ps[i] = rules.GetMinPartySize() + int32(i) | 
|  | } | 
|  | reqPB := &wpb.BatchGetWaitEstimatesRequest{ | 
|  | MerchantId: s.GetMerchantId(), | 
|  | ServiceId:  s.GetServiceId(), | 
|  | PartySize:  ps, | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. BatchGetWaitEstimates yielded error: %v", err) | 
|  | } | 
|  | var resp wpb.BatchGetWaitEstimatesResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("BatchGetWaitEstimates: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if resp.GetWaitlistStatus() == wpb.WaitlistStatus_WAITLIST_STATUS_UNSPECIFIED { | 
|  | return errors.New("BatchGetWaitEstimates: waitlist status was not specified in response") | 
|  | } else if resp.GetWaitlistStatus() != wpb.WaitlistStatus_OPEN { | 
|  | // Waitlist is closed. Wait estimate should not be provided. | 
|  | if len(resp.GetWaitEstimate()) > 0 { | 
|  | return errors.New("BatchGetWaitEstimates: wait estimate must not be provided when waitlist is closed") | 
|  | } | 
|  | } | 
|  |  | 
|  | // Waitlist status is OPEN | 
|  | for _, we := range resp.GetWaitEstimate() { | 
|  | if err := utils.ValidateWaitEstimate(we); err != nil { | 
|  | return fmt.Errorf("BatchGetWaitEstimates invalid, %s, service id: %s", | 
|  | err.Error(), s.GetServiceId()) | 
|  | } else if we.GetPartySize() > rules.GetMaxPartySize() || | 
|  | we.GetPartySize() < rules.GetMinPartySize() { | 
|  | return fmt.Errorf("batchGetWaitEstimates: Party size outside of min/max for service id: %s."+ | 
|  | "returned party size: %d, min party size: %d, max party size: %d", | 
|  | s.GetServiceId(), we.GetPartySize(), rules.GetMinPartySize(), rules.GetMaxPartySize()) | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // 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) { | 
|  | gen := rand.New(rand.NewSource(time.Now().UnixNano())) | 
|  |  | 
|  | reqPB := &wpb.CreateWaitlistEntryRequest{ | 
|  | MerchantId: s.GetMerchantId(), | 
|  | ServiceId:  s.GetServiceId(), | 
|  | PartySize:  s.GetWaitlistRules().GetMaxPartySize(), | 
|  | UserInformation: &wpb.UserInformation{ | 
|  | UserId:     userID, | 
|  | GivenName:  firstName, | 
|  | FamilyName: lastName, | 
|  | Telephone:  telephone, | 
|  | Email:      email, | 
|  | }, | 
|  | IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | 
|  | } | 
|  | if s.GetWaitlistRules().GetSupportsAdditionalRequest() { | 
|  | reqPB.AdditionalRequest = "test additional request" | 
|  | } | 
|  | 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) | 
|  | if err != nil { | 
|  | return "", fmt.Errorf("invalid response. CreateWaitlistEntry  yielded error: %v", err) | 
|  | } | 
|  | var resp wpb.CreateWaitlistEntryResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return "", fmt.Errorf("CreateWaitlistEntry: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if resp.GetWaitlistBusinessLogicFailure() != nil { | 
|  | return "", fmt.Errorf("invalid response. CreateWaitlistEntry failed with business logic failure %v", | 
|  | resp.GetWaitlistBusinessLogicFailure()) | 
|  | } | 
|  |  | 
|  | if resp.GetWaitlistEntryId() == "" { | 
|  | return "", fmt.Errorf("invalid response. CreateWaitlistEntry missing waitlist entry id for service: %s", | 
|  | s.GetServiceId()) | 
|  | } | 
|  |  | 
|  | // 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) | 
|  | if err != nil { | 
|  | return "", fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) | 
|  | } | 
|  | var idemResp wpb.CreateWaitlistEntryResponse | 
|  | if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | 
|  | return "", fmt.Errorf("CreateWaitlistEntry idem: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if diff := cmp.Diff(idemResp, resp); diff != "" { | 
|  | return "", fmt.Errorf("Idempotency check invalid (-got +want)\n%s for service: %s", diff, s.GetServiceId()) | 
|  | } | 
|  |  | 
|  | return resp.GetWaitlistEntryId(), nil | 
|  | } | 
|  |  | 
|  | // GetWaitlistEntry retrieves and validates the booking for the specified | 
|  | // waitlist entry id. | 
|  | func GetWaitlistEntry(id string, conn *HTTPConnection) (*wpb.WaitlistEntry, error) { | 
|  | reqPB := &wpb.GetWaitlistEntryRequest{ | 
|  | WaitlistEntryId: id, | 
|  | } | 
|  |  | 
|  | 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) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("invalid response. GetWaitlistEntry yielded error: %v", err) | 
|  | } | 
|  | var resp wpb.GetWaitlistEntryResponse | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return nil, fmt.Errorf("GetWaitlistEntry: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | if iE := utils.ValidateWaitlistEntry(resp.GetWaitlistEntry()); iE != nil { | 
|  | return nil, fmt.Errorf("invalid response. GetWaitlistEntry: %s", iE.Error()) | 
|  | } | 
|  |  | 
|  | return resp.GetWaitlistEntry(), nil | 
|  | } | 
|  |  | 
|  | // DeleteWaitlistEntry makes a request to delete the waitlist entry. | 
|  | func DeleteWaitlistEntry(id string, conn *HTTPConnection) error { | 
|  | reqPB := &wpb.DeleteWaitlistEntryRequest{ | 
|  | WaitlistEntryId: id, | 
|  | } | 
|  |  | 
|  | 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) | 
|  | if err != nil { | 
|  | return fmt.Errorf("invalid response. DeleteWaitlistEntry yielded error: %v", err) | 
|  | } | 
|  |  | 
|  | var resp empty.Empty | 
|  | if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | 
|  | return fmt.Errorf("DeleteWaitlistEntry: Could not parse HTTP response to pb3: %v", err) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } |