| /* |
| 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) |
| } |