blob: c08263087f6dd2ef84787140b829da8a704d44de [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 utils contains common BookingService based helper functions.
package utils
import (
"crypto/md5"
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"path"
"strconv"
"strings"
"time"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/google/go-cmp/cmp"
mpb "github.com/maps-booking/bookingservice"
fpb "github.com/maps-booking/feeds"
)
// SlotKey is a struct representing a unique service.
type SlotKey struct {
MerchantID string
ServiceID string
StaffID string
RoomID string
}
// LogFlow is a convenience function for logging common flows..
func LogFlow(f string, status string) {
log.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " "))
}
// 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")
defer LogFlow("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])
}
}
log.Printf("Selected %d slots out of a possible %d", len(finalAvailability), len(rawAvailability))
return finalAvailability, nil
}
// ValidateBooking performs granular comparisons between all got and want Bookings.
func ValidateBooking(got *mpb.Booking, want *mpb.Booking) error {
if got.GetBookingId() == "" {
return errors.New("booking_id is empty")
}
if diff := cmp.Diff(got.GetSlot(), want.GetSlot(), cmp.Comparer(proto.Equal)); 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 want overrides it.
wantStatus := mpb.BookingStatus_CONFIRMED
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
}
// BuildIdempotencyToken creates a unique token based on the slot and user attempting to book the slot.
func BuildIdempotencyToken(a *fpb.Availability, u string) string {
r := a.GetResources()
parts := []string{a.GetMerchantId(), a.GetServiceId(), strconv.FormatInt(a.GetStartSec(), 10), strconv.FormatInt(a.GetDurationSec(), 10), r.GetStaffId(), r.GetRoomId(), u}
key := strings.Join(parts, "")
return fmt.Sprintf("%x", md5.Sum([]byte(key)))
}
// BuildSlotFrom creates a bookingservice slot from an feed availability record.
func BuildSlotFrom(a *fpb.Availability) (*mpb.Slot, error) {
startTime, err := ptypes.TimestampProto(time.Unix(a.GetStartSec(), 0))
if err != nil {
return nil, err
}
r := a.GetResources()
return &mpb.Slot{
MerchantId: a.GetMerchantId(),
ServiceId: a.GetServiceId(),
StartTime: startTime,
Duration: ptypes.DurationProto(time.Duration(a.GetDurationSec()) * time.Second),
AvailabilityTag: a.GetAvailabilityTag(),
Resources: &mpb.Resources{
StaffId: r.GetStaffId(),
RoomId: r.GetRoomId(),
PartySize: r.GetPartySize(),
},
}, 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
}