blob: 4379df23183d982ba6ef1cea6d3a5b073c9315d9 [file] [log] [blame]
/*
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 (
"errors"
"fmt"
"log"
"sort"
"time"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/google/go-cmp/cmp"
"github.com/maps-booking/utils"
"golang.org/x/net/context"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc"
mpb "github.com/maps-booking/bookingservice"
fpb "github.com/maps-booking/feeds"
hpb "google.golang.org/grpc/health/grpc_health_v1"
)
const (
userID = "0"
firstName = "Jane"
lastName = "Doe"
telephone = "+18007897890"
email = "test@example.com"
service = "ext.maps.booking.partner.v2.BookingService"
)
// 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 gRPC health check.
func HealthCheck(ctx context.Context, conn *grpc.ClientConn) error {
utils.LogFlow("Health Check", "Start")
defer utils.LogFlow("Health Check", "End")
healthCli := hpb.NewHealthClient(conn)
req := &hpb.HealthCheckRequest{Service: service}
// Ignore cancel. Server will take care of it.
resp, err := healthCli.Check(ctx, req)
if err != nil {
return fmt.Errorf("could not complete health check: %v", err)
}
if diff := cmp.Diff(resp.Status, hpb.HealthCheckResponse_SERVING); diff != "" {
return fmt.Errorf("health check invalid (-got +want)\n%s", diff)
}
log.Println("health check success!")
return nil
}
// CheckAvailability beforms a maps booking availability check on all supplied availability slots. This function
// will return all slots with a valid return.
func CheckAvailability(ctx context.Context, a *fpb.Availability, c mpb.BookingServiceClient) 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())
}
req := &mpb.CheckAvailabilityRequest{
Slot: slot,
}
log.Printf("CheckAvailability Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.CheckAvailability(ctx, req)
if err != nil {
return fmt.Errorf("invalid response. CheckAvailability yielded error: %v", err)
}
log.Printf("CheckAvailability Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
if diff := cmp.Diff(resp.GetSlot(), req.GetSlot(), 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
}
// CreateBooking attempts to create bookings from availability slots.
func CreateBooking(ctx context.Context, a *fpb.Availability, c mpb.BookingServiceClient) (*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())
}
// Lease currently unsupported.
req := &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: utils.BuildIdempotencyToken(a, userID),
}
log.Printf("CreateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.CreateBooking(ctx, req)
if err != nil {
return nil, fmt.Errorf("invalid response. CreateBooking yielded error: %v", err)
}
log.Printf("CreateBooking Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
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: req.GetSlot(),
UserInformation: req.GetUserInformation(),
PaymentInformation: req.GetPaymentInformation(),
}); iE != nil {
return nil, fmt.Errorf("invalid response. CreateBooking invalid: %s", iE.Error())
}
return b, nil
}
// ListBookings calls the maps booking ListBookings rpc and compares the return with all input bookings.
func ListBookings(ctx context.Context, tB Bookings, c mpb.BookingServiceClient) (Bookings, error) {
var out Bookings
req := &mpb.ListBookingsRequest{
UserId: userID,
}
log.Printf("ListBookings Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.ListBookings(ctx, req)
if err != nil {
return out, fmt.Errorf("invalid response. ListBookings yielded error: %v. Abandoning all booking from this flow", err)
}
log.Printf("ListBookings Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
gB := Bookings(resp.GetBookings())
if len(gB) != len(tB) {
return out, fmt.Errorf("number of bookings differ, ListBookings invalid. Got: %d, Want: %d. Abandoning all bookings from this flow", len(gB), len(tB))
}
sort.Sort(gB)
sort.Sort(tB)
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])
}
return out, nil
}
// GetBookingStatus checks that all input bookings are in an acceptable state.
func GetBookingStatus(ctx context.Context, b *mpb.Booking, c mpb.BookingServiceClient) error {
req := &mpb.GetBookingStatusRequest{
BookingId: b.GetBookingId(),
}
log.Printf("GetBookingStatus Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.GetBookingStatus(ctx, req)
if err != nil {
return fmt.Errorf("invalid response. GetBookingStatus yielded error: %v", err)
}
log.Printf("GetBookingStatus Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
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(ctx context.Context, b *mpb.Booking, c mpb.BookingServiceClient) error {
b.Status = mpb.BookingStatus_CANCELED
req := &mpb.UpdateBookingRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"status"},
},
Booking: b,
}
log.Printf("UpdateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.UpdateBooking(ctx, req)
if err != nil {
return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err)
}
log.Printf("UpdateBooking Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
if iE := utils.ValidateBooking(resp.GetBooking(), req.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(ctx context.Context, av []*fpb.Availability, c mpb.BookingServiceClient) 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(ctx, slots[0], c)
if err != nil {
return fmt.Errorf("could not complete booking, abandoning rescheduling flow: %v", err)
}
slot := newBooking.GetSlot()
// New slot.
lastAvailability := slots[len(slots)-1]
startTime, err := ptypes.TimestampProto(time.Unix(lastAvailability.GetStartSec(), 0))
if err != nil {
return fmt.Errorf("could not complete update, abandoning rescheduling flow: %v", err)
}
slot.StartTime = startTime
slot.Duration = ptypes.DurationProto(time.Duration(lastAvailability.GetDurationSec()) * time.Second)
req := &mpb.UpdateBookingRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"slot.start_time", "slot.duration"},
},
Booking: newBooking,
}
log.Printf("UpdateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), req.String())
resp, err := c.UpdateBooking(ctx, req)
if err != nil {
return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err)
}
log.Printf("UpdateBooking Response. Received(unix): %s, Response %s", time.Now().UTC().Format(time.RFC850), resp.String())
if iE := utils.ValidateBooking(resp.GetBooking(), newBooking); iE != nil {
return fmt.Errorf("invalid response. UpdateBooking: %s, abandoning slot 1/1", iE.Error())
}
return CancelBooking(ctx, resp.GetBooking(), c)
}