upstream sync

This commit is contained in:
Senad Uka
2018-05-11 09:07:54 +02:00
parent 50a6362b67
commit 4852a5586c
23 changed files with 664 additions and 35 deletions

View File

@@ -0,0 +1,15 @@
package authorization
import (
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
)
func CanCreateAddress(user viewmodel.User, organization viewmodel.Organization) bool {
//rules are the same for address creation and for organization creation
return CanCreateOrganization(user, organization)
}
func CanUpdateAddress(user viewmodel.User, organization viewmodel.Organization) bool {
return CanCreateAddress(user, organization)
}

View File

@@ -0,0 +1 @@
package authorization

View File

@@ -0,0 +1,15 @@
package authorization
import (
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
)
func CanCreateContact(user viewmodel.User, organization viewmodel.Organization) bool {
//rules are the same for contact creation and for organization creation
return CanCreateOrganization(user, organization)
}
func CanUpdateContact(user viewmodel.User, organization viewmodel.Organization) bool {
return CanCreateAddress(user, organization)
}

View File

@@ -0,0 +1,68 @@
package authorization
import (
"fmt"
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
)
func isAChildOrganization(potentialParent viewmodel.Organization, potentialChild viewmodel.Organization) bool {
for _, org := range potentialParent.ChildOrgs {
if potentialChild.UUID == org.UUID {
return true
}
}
return false
}
func isSameOrganization(organizationA viewmodel.Organization, organizationB viewmodel.Organization) bool {
return organizationA.UUID == organizationB.UUID
}
func grabOrgFromUser(user viewmodel.User) (viewmodel.Organization, error) {
if len(user.Organizations) < 1 {
return viewmodel.Organization{}, fmt.Errorf("User has no organizations %v", user)
}
return user.Organizations[0], nil
}
func CanCreateOrganization(user viewmodel.User, organization viewmodel.Organization ) bool {
userRole, err := grabProfileFromUser(user)
if err != nil {
return false
}
/*
Admin BCBSI
Admin Technical Support
Super Admin Technical Support
Manage all Organizations*/
if userRole.Key == bcbsiAdmin || userRole.Key == brighterDevAdmin || userRole.Key == superAdmin{
return true
}
userOrg, err := grabOrgFromUser(user)
if err != nil{
return false
}
/*
Admin Provider
Admin Plan
Manage the authenticated Authorized User's Organization and child Organizations */
if userRole.Key == providerAdmin || userRole.Key == planAdmin{
if isSameOrganization(userOrg, organization) || isAChildOrganization(userOrg, organization) {
return true
}
return false
}
return false
}
func CanUpdateOrganization(user viewmodel.User, organization viewmodel.Organization) bool{
return CanCreateOrganization(user, organization)
}

View File

@@ -0,0 +1,62 @@
package authorization
import (
"fmt"
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
)
const (
superAdmin = "AD"
scheduler = "SP"
support = "SPT"
member = "US"
brighterDevAdmin = "BDCAD"
bcbsiAdmin = "BCBSIAD"
planAdmin = "PLANAD"
providerAdmin = "SCHDAD"
)
func grabProfileFromUser(user viewmodel.User) (viewmodel.Profile, error) {
if len(user.Profiles) < 1 {
return viewmodel.Profile{}, fmt.Errorf("User has no profiles %v", user)
}
return user.Profiles[0], nil
}
func morePrivileged(who viewmodel.Profile, towardsWhom viewmodel.Profile) bool {
order := []string{superAdmin, brighterDevAdmin, bcbsiAdmin, planAdmin, providerAdmin, support, scheduler, member}
for _, value := range order {
if value == who.Key {
return true
}
if value == towardsWhom.Key {
return false
}
}
// should hapen only in case profile key is empty
// and that's something fishy so let's deny!
return false
}
func equallyOrMorePrivileged(who viewmodel.Profile, towardsWhom viewmodel.Profile) bool {
if who.Key == towardsWhom.Key {
return true
}
return morePrivileged(who, towardsWhom)
}
func lessPrivilegedThanAdmin(who viewmodel.Profile) bool {
switch who.Key {
case member:
return true
case scheduler:
return true
case support:
return true
}
return false
}

View File

@@ -0,0 +1,86 @@
package authorization
import "bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
/*
CanCreateUser returns true if currentUser is allowed to create updatingUser according to
authorization rules
*/
func CanCreateUser(currentUser viewmodel.User, updatingUser viewmodel.User) bool {
if len(currentUser.Profiles) < 1 {
return false
}
if len(updatingUser.Profiles) < 1 {
return false
}
currentUserOrganization, err := grabOrgFromUser(currentUser)
if err != nil {
return false
}
updatingUserOrganization, err := grabOrgFromUser(updatingUser)
if err != nil {
return false
}
currentUserRole, err := grabProfileFromUser(currentUser)
if err != nil {
return false
}
updatingUserRole, err := grabProfileFromUser(updatingUser)
if err != nil {
return false
}
/*
Admin Provider
Manage all Authorized Users of the provider Organization or child organization
The (Provider) Admin can manage Authorized Users of their Parent/ Top-level Org , but not Admins
*/
currentUserHigherOrEqualOrg := isSameOrganization(currentUserOrganization, updatingUserOrganization) || isAChildOrganization(currentUserOrganization, updatingUserOrganization)
currentUserLowerOrg := isAChildOrganization(updatingUserOrganization, currentUserOrganization)
if currentUserRole.Key == providerAdmin && currentUserHigherOrEqualOrg && equallyOrMorePrivileged(currentUserRole, updatingUserRole) {
return true
}
if currentUserRole.Key == providerAdmin && currentUserLowerOrg && lessPrivilegedThanAdmin(updatingUserRole) {
return true
}
/* Admin BCBSI
Manage all Authorized Users except Admins
return false
*/
if currentUserRole.Key == bcbsiAdmin && lessPrivilegedThanAdmin(updatingUserRole) {
return true
}
/* Admin Technical Support Manage all Authorized Users except Admins */
if currentUserRole.Key == brighterDevAdmin && lessPrivilegedThanAdmin(updatingUserRole) {
return true
}
/* Admin Plan Manage all Authorized Users of a single participating Plan except Admins */
if currentUserRole.Key == planAdmin && lessPrivilegedThanAdmin(updatingUserRole) && isSameOrganization(currentUserOrganization, updatingUserOrganization) {
return true
}
/* Super Admin Technical Support
Manage all Members, INCLUDING Admins */
if currentUserRole.Key == superAdmin {
return true
}
return false
}

View File

@@ -10,6 +10,7 @@ import (
"bitbucket.org/nemt/nemt-portal-api/infra/cache"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"bitbucket.org/nemt/nemt-portal-api/server/router/routeutils"
"bitbucket.org/nemt/nemt-portal-api/server/authorization"
"github.com/labstack/echo"
)
@@ -64,6 +65,11 @@ func (c *controller) handleAddOrganization(ctx echo.Context) error {
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanCreateOrganization(authUser, org) {
return routeutils.ResponseAPIAuthorizationError(ctx)
}
org.Author.ID = authUser.ID
org.LastEditor.ID = authUser.ID
@@ -127,6 +133,15 @@ func (c *controller) handleParent(ctx echo.Context) error {
return routeutils.HandleAPIError(ctx, err)
}
organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanUpdateOrganization(authUser, organization){
return routeutils.ResponseAPIAuthorizationError(ctx)
}
resp, err := c.svc.Organization.SetParentOrganization(orgUUID, parent.UUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
@@ -152,6 +167,15 @@ func (c *controller) handleChild(ctx echo.Context) error {
return routeutils.HandleAPIError(ctx, err)
}
organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanUpdateOrganization(authUser, organization){
return routeutils.ResponseAPIAuthorizationError(ctx)
}
_, err = c.svc.Organization.SetParentOrganization(child.UUID, orgUUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
@@ -246,6 +270,18 @@ func (c *controller) handleAddAddress(ctx echo.Context) error {
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanCreateAddress(authUser, organization) {
return routeutils.ResponseAPIAuthorizationError(ctx)
}
return routeutils.ResponseAPIAuthorizationError(ctx)
address.CreatedUser.ID = authUser.ID
address.UpdatedUser.ID = authUser.ID
@@ -278,6 +314,7 @@ func (c *controller) handleRemoveContact(ctx echo.Context) error {
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
contact.UpdatedUser.ID = authUser.ID
err = c.svc.Organization.InactivateOrganizationContact(orgUUID, contact, authUser)
@@ -309,6 +346,16 @@ func (c *controller) handleAddContact(ctx echo.Context) error {
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanCreateContact(authUser, organization) {
return routeutils.ResponseAPIAuthorizationError(ctx)
}
contact.CreatedUser.ID = authUser.ID
contact.UpdatedUser.ID = authUser.ID

View File

@@ -33,6 +33,18 @@ func ResponseAPIOK(c echo.Context, data interface{}) error {
return c.JSON(http.StatusOK, data)
}
// ResponseAPIErrorWithData returns a standard API error with additional data to the response
func ResponseAPIErrorWithData(c echo.Context, status int, message string, redirect bool, data interface{}) error {
returnValue := resultWrapper{
Error: true,
Message: message,
Redirect: redirect,
Data: data,
}
return c.JSON(status, returnValue)
}
// ResponseAPIError returns a standard API error to the response
func ResponseAPIError(c echo.Context, status int, message string, redirect bool) error {
returnValue := resultWrapper{
@@ -49,6 +61,11 @@ func ResponseAPIAuthError(c echo.Context, message string, redirect bool) error {
return ResponseAPIError(c, http.StatusUnauthorized, message, redirect)
}
// ResponseAPIAuthorizationError returns a standard API auth error to the response
func ResponseAPIAuthorizationError(c echo.Context) error {
return ResponseAPIError(c, http.StatusForbidden, "Forbidden by controller", false)
}
// ResponseAPIServiceError returns a standard API service unavailable error to the response
func ResponseAPIServiceError(c echo.Context, message string) error {
return ResponseAPIError(c, http.StatusServiceUnavailable, message, false)
@@ -59,6 +76,11 @@ func ResponseAPIValidationError(c echo.Context, message string) error {
return ResponseAPIError(c, http.StatusUnprocessableEntity, message, false)
}
// ResponseAPICustomValidationError returns a standard API validation error with custom data to the response
func ResponseAPICustomValidationError(c echo.Context, message string, data interface{}) error {
return ResponseAPIErrorWithData(c, http.StatusUnprocessableEntity, message, false, data)
}
// ResponseAPIFieldValidationError returns a standard API field validation error to the response
func ResponseAPIFieldValidationError(c echo.Context, field string, message string) error {
err := errors.NewValidationError(field, message)

View File

@@ -15,6 +15,7 @@ import (
"bitbucket.org/nemt/nemt-portal-api/infra/auth"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"bitbucket.org/nemt/nemt-portal-api/server/router/routeutils"
"bitbucket.org/nemt/nemt-portal-api/server/validation"
"github.com/labstack/echo"
uuid "github.com/satori/go.uuid"
"google.golang.org/api/googleapi/transport"
@@ -231,6 +232,11 @@ func (c *controller) handle(ctx echo.Context) error {
return routeutils.ResponseAPIValidationError(ctx, "User not found")
}
//Validate ride request
if validationErrors := validation.ValidateRide(&requestRide, &user) ; len(validationErrors) > 0 {
return routeutils.ResponseAPICustomValidationError(ctx, "ride validation failed", validationErrors)
}
createdUser, err := auth.GetUserDetail(ctx, c.cfg)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
@@ -869,4 +875,4 @@ func (c *controller) handleReady(ctx echo.Context) error {
}()
return routeutils.ResponseAPIOK(ctx, nextRide)
}
}

View File

@@ -13,6 +13,7 @@ import (
"bitbucket.org/nemt/nemt-portal-api/infra/auth"
"bitbucket.org/nemt/nemt-portal-api/infra/cache"
"bitbucket.org/nemt/nemt-portal-api/infra/config"
"bitbucket.org/nemt/nemt-portal-api/server/authorization"
"bitbucket.org/nemt/nemt-portal-api/server/router/routeutils"
"github.com/labstack/echo"
)
@@ -127,6 +128,62 @@ func (c *controller) handleRemoveAddress(ctx echo.Context) error {
return routeutils.ResponseNoContent(ctx, addressID)
}
func (c *controller) handlePortalContact(ctx echo.Context) error {
userID, err := routeutils.GetAndValidateStringParam(ctx, "user_uuid", "mandatory field")
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
item, err := c.svc.Users.GetByUUID(userID, "")
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
createdUser, err := auth.GetUserDetail(ctx, c.cfg)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
if item.ID == "" {
return routeutils.ResponseAPIValidationError(ctx, "User not found")
} else {
var Contact viewmodel.Contact
if err := ctx.Bind(&Contact); err != nil {
return routeutils.HandleAPIError(ctx, err)
}
Contact.User = item
Contact.Author = createdUser
Contact, err = c.svc.Users.SaveContact(Contact)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
// Contact.User, err = c.svc.Users.GetByUUID(userID, "")
// if err != nil {
// return routeutils.HandleAPIError(ctx, err)
// }
return routeutils.ResponseAPIOK(ctx, Contact)
}
}
func (c *controller) handleRemoveContact(ctx echo.Context) error {
contactUUID, err := routeutils.GetAndValidateStringParam(ctx, "contact_uuid", "mandatory field")
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
contact := viewmodel.Contact{
ID: contactUUID,
}
contact, err = c.svc.Users.RemoveContact(contact)
if err != nil {
return routeutils.HandleAPIError(ctx, err)
}
return routeutils.ResponseNoContent(ctx, contact)
}
func (c *controller) handleMemberAddress(ctx echo.Context) error {
userID, err := routeutils.GetAndValidateStringParam(ctx, "user_uuid", "mandatory field")
if err != nil {
@@ -389,6 +446,10 @@ func (c *controller) handlePortal(ctx echo.Context) error {
return routeutils.HandleAPIError(ctx, err)
}
if !authorization.CanCreateUser(authUser, user) {
return routeutils.ResponseAPIAuthorizationError(ctx)
}
if len(user.Profiles) == 0 {
return routeutils.ResponseAPIAuthError(ctx, "profile is required", false)
}

View File

@@ -17,6 +17,8 @@ const (
userDetailRoute = "/portal/:user_uuid"
userAddressRoute = "/portal/:user_uuid/address"
userRemoveAddressRoute = "/portal/:user_uuid/address/:address_uuid"
userContactRoute = "/portal/:user_uuid/contact"
userRemoveContactRoute = "/portal/:user_uuid/contact/:contact_uuid"
portalRoute = "/portal"
portalBulkRoute = "/portal/bulk"
contacttypeRoute = "/contacttype"
@@ -41,6 +43,9 @@ func Register(r *echo.Group, cfg *config.Config, svc *applicationservice.Service
r.POST(userAddressRoute, ctrl.handlePortalAddress)
r.PUT(userRemoveAddressRoute, ctrl.handleRemoveAddress)
r.POST(userContactRoute, ctrl.handlePortalContact)
r.PUT(userRemoveContactRoute, ctrl.handleRemoveContact)
//Can be cached
r.GET(contacttypeRoute, ctrl.handleContactType)

View File

@@ -193,8 +193,6 @@ func (a *Config) objectRelation(object interface{}, currentUser viewmodel.User)
case viewmodel.User:
if obj.ID == currentUser.ID {
return "[self]"
} else {
return "[other]"
}
}
return "[other]"

183
server/validation/tnc.go Normal file
View File

@@ -0,0 +1,183 @@
package validation
import (
"time"
"fmt"
"strconv"
"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
"bitbucket.org/nemt/nemt-portal-api/infra/errors"
)
const (
tripTypeFromVisit = "From Visit"
tripTypeToVisit = "To Visit"
tripTypeFromVisitWillCall = "From Visit / Will Call"
tripTypeRoundTrip = "Round Trip"
tripTypeRountTripWillCall = "Round Trip / Will Call"
)
const (
loadingTime = 30 //in minutes
minimumLoadTime = 30 //in minutes
minimumPickupTime = 10 //in minutes
)
const (
hoursInDay = 24
hoursIn180Days = 24*180
time8Hours = 8
time10Minutes = 10
)
func ValidateRide(requestRide *viewmodel.RideRequest, user *viewmodel.User) []errors.ValidationError {
var result []errors.ValidationError
//Step #1 validation
if userID, err := strconv.Atoi(user.ID) ; err != nil || userID <= 0 {
result = append(result, errors.ValidationError{Field : "user_uuid", Message : "Step #1 - Choose a Member" })
}
if originID, err := strconv.Atoi(requestRide.Origin.ID) ; err != nil || originID <= 0 {
result = append (result, errors.ValidationError{Field : "origin.id", Message : "Step #1 - Choose a Pickup Address"})
}
if !requestRide.UserConsent {
result = append (result, errors.ValidationError{Field : "user_consent", Message : "Step #1 - Member must consent to Terms of Use"})
}
//Step #2 validation
if destinationID, err := strconv.Atoi(requestRide.Destination.ID) ; err!= nil || destinationID <= 0 {
result = append (result, errors.ValidationError{Field : "destination.id", Message : "Step #2 - Choose a Provider"})
}
//Step #3 validation
isVisitDayToday := requestRide.VisitDate.Day() == time.Now().Day() && requestRide.VisitDate.Month() == time.Now().Month() && requestRide.VisitDate.Year() == time.Now().Year()
before8Hours := time.Now().Add(-time.Hour*time8Hours)
if requestRide.VisitDate == nil {
result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Choose a Date for the Visit"})
}else{
dayBeforeToday := time.Now().Add(-time.Hour*hoursInDay)
if requestRide.VisitDate.Before(dayBeforeToday) {
result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Visit cannot occur more than one day before today"})
}
dayAfter180Days := time.Now().Add(time.Hour*hoursIn180Days)
if requestRide.VisitDate.After(dayAfter180Days) {
result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Visit cannot occur more than 180 days after today"})
}
if requestRide.VisitTime == nil {
result = append (result, errors.ValidationError{Field : "visit_time", Message : "Step #3 - Choose a Time for the Visit"})
}else{
if isVisitDayToday && requestRide.VisitTime.Before(before8Hours) {
result = append (result, errors.ValidationError{Field : "visit_time", Message : "Step #3 - Visit is more than 8 hours in the past"})
}
}
}
//Step #4 validation
if requestRide.TripType.Value == "" {
result = append (result, errors.ValidationError{Field : "trip_type.value", Message : "Step #4 - Choose a Trip Type"})
}
timeWithDurationAndLoadingTime := requestRide.VisitTime.Add(-time.Duration(requestRide.Duration)*time.Second).Add(-loadingTime*time.Minute)
after10Minutes := time.Now().Add(time.Minute*time10Minutes)
switch requestRide.TripType.Value {
case tripTypeToVisit:
if requestRide.PickupTime == nil {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"})
}else{
if requestRide.PickupTime.After(*requestRide.VisitTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"})
}
if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time before Visit Time"})
}
if isVisitDayToday && requestRide.PickupTime.Before(before8Hours) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Visit cannot occour in the past"})
}
}
case tripTypeFromVisit :
if requestRide.PickupTime == nil {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"})
}else{
timeWithMinimumPickupTime := time.Now().Add(minimumPickupTime*time.Minute)
if isVisitDayToday && requestRide.PickupTime.Before(timeWithMinimumPickupTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : fmt.Sprint("Step #4 - Time must be more than %d minutes from now",minimumPickupTime)})
}
if requestRide.PickupTime.Before(*requestRide.VisitTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time after Visit Time"})
}
timeWithMinimumLoadTime := time.Now().Add(minimumLoadTime*time.Minute)
if requestRide.PickupTime.Before(timeWithMinimumLoadTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than Minimum Load Time before Visit Time"})
}
}
case tripTypeFromVisitWillCall:
//no special validation for this case
case tripTypeRoundTrip:
if requestRide.PickupTime == nil {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"})
}else{
if requestRide.PickupTime.After(*requestRide.VisitTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"})
}
if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time before Visit Time"})
}
if isVisitDayToday && requestRide.PickupTime.Before(after10Minutes) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must be at least 10 minutes from now"})
}
}
if requestRide.ReturnTime == nil {
result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Choose a Pickup Time"})
}else{
if isVisitDayToday {
if requestRide.ReturnTime.Before(before8Hours) {
result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Return Time is more than 8 hours after Visit Time"})
}
if requestRide.ReturnTime.Before(after10Minutes) {
result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Return Time must be at least 10 minutes from now"})
}
}
}
case tripTypeRountTripWillCall:
if requestRide.PickupTime == nil {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"})
}else{
if requestRide.PickupTime.After(*requestRide.VisitTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"})
}
if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time after Visit Time"})
}
if isVisitDayToday && requestRide.PickupTime.Before(before8Hours) {
result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Visit cannot occur in the past "})
}
}
}
return result
}