diff --git a/README.md b/README.md
index e8f6ef07e5a6f31c3018c88c6bde7552f133e474..33be81ebe50bca76ab3c015475c0fa1c7c9e9abd 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ make install
 
 2. After your changes have been merged to the `main` branch of `go-utils`, run the following command which will automatically create a new tag:
 ```
-bob release full
+bob release tag 
 ```
 and select project `bob-public-utils/bobgroup-go-utils`
 
diff --git a/api_responses/api_responses.go b/api_responses/api_responses.go
index 2138361dbda238d18db0b503e0aa228e71812b50..0f6325532844af3cc285853b644dfbbb07d85afd 100644
--- a/api_responses/api_responses.go
+++ b/api_responses/api_responses.go
@@ -274,8 +274,13 @@ func GenericJSONResponseWithMessage(code int, err error) events.APIGatewayProxyR
 	var body map[string]string
 
 	if err != nil {
-		customErr := err.(*errors.CustomError)
-		message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true})
+		var message string
+		if customErr, ok := err.(*errors.CustomError); ok {
+			message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true})
+		} else {
+			message = err.Error()
+		}
+
 		body = map[string]string{
 			"message": string_utils.Capitalize(message),
 		}
diff --git a/date_utils/date_utils.go b/date_utils/date_utils.go
index ccca80d5014f376762ba0d395f13640e78e59b97..c88b39a22077602ead42c62458808428a45db174 100644
--- a/date_utils/date_utils.go
+++ b/date_utils/date_utils.go
@@ -1,10 +1,15 @@
 package date_utils
 
 import (
-	"github.com/araddon/dateparse"
+	"fmt"
 	"reflect"
 	"strconv"
+	"strings"
 	"time"
+
+	"github.com/araddon/dateparse"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils"
 )
 
 const TimeZoneString = "Africa/Johannesburg"
@@ -271,3 +276,147 @@ func formatTimestampsWithTimeZoneInSlice(fieldValue reflect.Value, location *tim
 	}
 	return nil
 }
+
+// TradingHours represents an array of (StartTime,EndTime) pairs, one for each day of the week.
+// The array is 0 indexed, with 0 being Sunday and 6 being Saturday and 7 being public holidays.
+type TradingHours []struct {
+	StartTime string `json:"start_time"`
+	EndTime   string `json:"end_time"`
+}
+
+func (t TradingHours) Validate() error {
+	if len(t) != 8 {
+		return errors.Error("Trading hours must have 8 days, 7 for every day of the week and 1 for public holidays")
+	}
+
+	for _, day := range t {
+		if day.StartTime == "" || day.EndTime == "" {
+			// Allow empty trading hours for a day to represent closed
+			continue
+		}
+
+		if !TimeBefore(day.StartTime, day.EndTime) {
+			return errors.Error("Start time must be before end time")
+		}
+
+		if len(day.StartTime) != 5 || len(day.EndTime) != 5 {
+			return errors.Error("Time must be in the format HH:MM")
+		}
+
+		startHourMinSlice := strings.Split(day.StartTime, ":")
+		if len(startHourMinSlice) != 2 {
+			return errors.Error("Time must be in the format HH:MM")
+		}
+		startHour, startMin := startHourMinSlice[0], startHourMinSlice[1]
+		startHourInt, err := strconv.Atoi(startHour)
+		if err != nil || startHourInt < 0 || startHourInt > 23 {
+			return errors.Error("Start hour must be between 0 and 23")
+		}
+		startMinInt, err := strconv.Atoi(startMin)
+		if err != nil || !(startMinInt == 0 || startMinInt == 30) {
+			return errors.Error("Start minute must be 0 or 30")
+		}
+
+		endHourMinSlice := strings.Split(day.EndTime, ":")
+		if len(endHourMinSlice) != 2 {
+			return errors.Error("Time must be in the format HH:MM")
+		}
+		endHour, endMin := endHourMinSlice[0], endHourMinSlice[1]
+		endHourInt, err := strconv.Atoi(endHour)
+		if err != nil || endHourInt < 0 || endHourInt > 23 {
+			return errors.Error("End hour must be between 0 and 23")
+		}
+		endMinInt, err := strconv.Atoi(endMin)
+		if err != nil || !(endMinInt == 0 || endMinInt == 30 || endMinInt == 59) {
+			return errors.Error("End minute must be 0, 30 or 59")
+		}
+	}
+
+	return nil
+}
+
+func (t TradingHours) String() string {
+	var result strings.Builder
+	const numberOfDaysInWeek = 7
+	copyOfT := utils.DeepCopy(t).(TradingHours)
+	weekdays, publicHolidays := copyOfT[:numberOfDaysInWeek], copyOfT[numberOfDaysInWeek]
+	weekdays = append(weekdays, weekdays[0]) // Add the first day (Sunday) to the end because we want Monday to be first in the string
+
+	rangeStartIndex := 1
+	for i := 1; i < len(weekdays); i++ {
+		currentDay := weekdays[i]
+		nextDay := currentDay
+		if i+1 < len(weekdays) {
+			nextDay = weekdays[i+1]
+		}
+
+		// Determine times
+		var times string
+		if currentDay.StartTime != "" && currentDay.EndTime != "" {
+			startTime, err := time.Parse("15:04", currentDay.StartTime)
+			if err != nil {
+				return ""
+			}
+
+			endTime, err := time.Parse("15:04", currentDay.EndTime)
+			if err != nil {
+				return ""
+			}
+
+			times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm")
+			if currentDay.StartTime == "00:00" && currentDay.EndTime == "23:59" {
+				times = "All day"
+			}
+		} else {
+			times = "Closed"
+		}
+
+		// If we're at the last element or the next day doesn't have the same times, we end the current range
+		if i == len(weekdays)-1 || currentDay.StartTime != nextDay.StartTime || currentDay.EndTime != nextDay.EndTime {
+			if rangeStartIndex == i {
+				day := time.Weekday(rangeStartIndex).String()[:3]
+				if rangeStartIndex == numberOfDaysInWeek {
+					day = time.Sunday.String()[:3]
+				}
+				result.WriteString(fmt.Sprintf("%s: %s", day, times))
+			} else {
+				rangeStartDay := time.Weekday(rangeStartIndex).String()[:3]
+				rangeEndDay := time.Weekday(i).String()[:3]
+				if i == numberOfDaysInWeek {
+					rangeEndDay = time.Sunday.String()[:3]
+				}
+				result.WriteString(fmt.Sprintf("%s – %s: %s", rangeStartDay, rangeEndDay, times))
+			}
+
+			if i < len(weekdays)-1 {
+				result.WriteString(", ")
+			}
+
+			rangeStartIndex = i + 1
+		}
+	}
+
+	// Public holidays
+	var times string
+	if publicHolidays.StartTime != "" && publicHolidays.EndTime != "" {
+		startTime, err := time.Parse("15:04", publicHolidays.StartTime)
+		if err != nil {
+			return ""
+		}
+
+		endTime, err := time.Parse("15:04", publicHolidays.EndTime)
+		if err != nil {
+			return ""
+		}
+
+		times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm")
+		if publicHolidays.StartTime == "00:00" && publicHolidays.EndTime == "23:59" {
+			times = "All day"
+		}
+	} else {
+		times = "Closed"
+	}
+	result.WriteString(fmt.Sprintf(", Public holidays: %s", times))
+
+	return result.String()
+}
diff --git a/go.mod b/go.mod
index 341c3f2f045fec1bfe9668fcd12a1df65b2116a3..494108d1bdd90b73ee14aa6a310468ac09e3cc55 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils
 go 1.21
 
 require (
+	github.com/AfterShip/email-verifier v1.4.0
 	github.com/MindscapeHQ/raygun4go v1.1.1
 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
 	github.com/aws/aws-lambda-go v1.26.0
@@ -15,6 +16,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sqs v1.31.4
 	github.com/aws/aws-secretsmanager-caching-go v1.1.0
 	github.com/aws/smithy-go v1.20.2
+	github.com/dimuska139/go-email-normalizer/v2 v2.0.0
 	github.com/dlsniper/debugger v0.6.0
 	github.com/go-pg/pg/v10 v10.10.6
 	github.com/go-redis/redis/v8 v8.11.4
@@ -22,6 +24,8 @@ require (
 	github.com/go-resty/resty/v2 v2.7.0
 	github.com/golang-jwt/jwt/v4 v4.4.3
 	github.com/google/uuid v1.3.0
+	github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
+	github.com/jinzhu/now v1.1.5
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
 	github.com/opensearch-project/opensearch-go/v2 v2.2.0
 	github.com/pkg/errors v0.9.1
@@ -29,10 +33,10 @@ require (
 	github.com/samber/lo v1.38.1
 	github.com/sirupsen/logrus v1.8.1
 	github.com/thoas/go-funk v0.9.1
-	github.com/uptrace/bun v1.1.14
+	github.com/uptrace/bun v1.1.17
 	golang.ngrok.com/ngrok v1.4.1
-	golang.org/x/crypto v0.9.0
-	golang.org/x/text v0.9.0
+	golang.org/x/crypto v0.22.0
+	golang.org/x/text v0.14.0
 )
 
 require (
@@ -56,27 +60,32 @@ require (
 	github.com/go-pg/zerochecker v0.2.0 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/hbollon/go-edlib v1.6.0 // indirect
 	github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect
 	github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-runewidth v0.0.10 // indirect
+	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/pborman/uuid v1.2.1 // indirect
+	github.com/rivo/uniseg v0.1.0 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
+	github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
 	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
 	github.com/vmihailenco/bufpool v0.1.11 // indirect
 	github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
-	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
+	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
 	github.com/vmihailenco/tagparser v0.1.2 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	go.uber.org/multierr v1.10.0 // indirect
 	golang.ngrok.com/muxado/v2 v2.0.0 // indirect
 	golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
-	golang.org/x/net v0.10.0 // indirect
-	golang.org/x/sys v0.8.0 // indirect
-	golang.org/x/term v0.8.0 // indirect
+	golang.org/x/net v0.24.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
+	golang.org/x/term v0.19.0 // indirect
 	google.golang.org/appengine v1.6.6 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	mellium.im/sasl v0.2.1 // indirect
diff --git a/go.sum b/go.sum
index 671dcd1e917c3dba3b02e17ffe20a991f7a99005..9946076a47c7a649cc1002cee8c657f4461cea2f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/AfterShip/email-verifier v1.4.0 h1:DoQplvVFVhZUfS5fPiVnmCQDr5i1tv+ivUV0TFd2AZo=
+github.com/AfterShip/email-verifier v1.4.0/go.mod h1:JNPV1KZpTq4TArmss1NAOJsTD8JRa/ZElbCAJCEgikg=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/MindscapeHQ/raygun4go v1.1.1 h1:fk3Uknv9kQxUIwL3mywwHQRyfq3PaR9lE/e40K+OcY0=
 github.com/MindscapeHQ/raygun4go v1.1.1/go.mod h1:NW0eWi2Qs00ZcctO6owrVMY+h2HxzJVgQGDrTj2ysw4=
@@ -76,6 +78,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dimuska139/go-email-normalizer/v2 v2.0.0 h1:LH41ypO4BFast9bc8hNu6YEkRvLloHFYihSjfwiARSg=
+github.com/dimuska139/go-email-normalizer/v2 v2.0.0/go.mod h1:2Gil1j/rfUKJ5BHc/uxxyRiuk3YTg6/C3D7dz7jVQfw=
 github.com/dlsniper/debugger v0.6.0 h1:AyPoOtJviCmig9AKNRAPPw5B5UyB+cI72zY3Jb+6LlA=
 github.com/dlsniper/debugger v0.6.0/go.mod h1:FFdRcPU2Yo4P411bp5U97DHJUSUMKcqw1QMGUu0uVb8=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -130,15 +134,24 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
 github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
+github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
+github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E=
+github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible h1:zaX5fYT98jX5j4UhO/WbfY8T1HkgVrydiDMC9PWqGCo=
 github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
 github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjHe47D3bbAB9BgMrc3DYA=
 github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94=
+github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
+github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -148,8 +161,10 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -157,15 +172,20 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
@@ -187,6 +207,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/r3labs/diff/v2 v2.14.2 h1:1HVhQKwg1YnoCWzCYlOWYLG4C3yfTudZo5AcrTSgCTc=
 github.com/r3labs/diff/v2 v2.14.2/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc=
+github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
@@ -199,6 +220,8 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N
 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
 github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -209,22 +232,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
 github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
-github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM=
-github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8=
+github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk=
+github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U=
 github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
 github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
 github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
 github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
 github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
-github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
-github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
 github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
 github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@@ -242,8 +266,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
 golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
@@ -252,6 +277,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -268,14 +294,18 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
 golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -296,22 +326,30 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -321,6 +359,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -354,6 +393,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
+gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/handler_utils/api.go b/handler_utils/api.go
index 0c14596501b845e90a2facd030f94ac596b7e2ac..41d45ca5ce061bd90837292450c2935d5fac1cca 100644
--- a/handler_utils/api.go
+++ b/handler_utils/api.go
@@ -43,6 +43,28 @@ func ValidateAPIEndpoints(endpoints map[string]map[string]interface{}) (map[stri
 	return endpoints, nil
 }
 
+// ValidateWebsocketEndpoints checks that all websocket endpoints are correctly defined using one of the supported
+// handler types and returns updated endpoints with additional information
+func ValidateWebsocketEndpoints(endpoints map[string]interface{}) (map[string]interface{}, error) {
+	for websocketAction, actionFunc := range endpoints {
+		if websocketAction == "" {
+			return nil, errors.Errorf("blank action")
+		}
+		if actionFunc == nil {
+			return nil, errors.Errorf("nil handler on %s %s", websocketAction, actionFunc)
+		}
+
+		handler, err := NewHandler(actionFunc)
+		if err != nil {
+			return nil, errors.Wrapf(err, "%s has invalid handler %T", websocketAction, actionFunc)
+		}
+		// replace the endpoint value so that we can quickly call this handler
+		endpoints[websocketAction] = handler
+
+	}
+	return endpoints, nil
+}
+
 func ValidateRequestParams(request *events.APIGatewayProxyRequest, paramsStructType reflect.Type) (reflect.Value, error) {
 	paramValues := map[string]interface{}{}
 	for n, v := range request.QueryStringParameters {
diff --git a/logs/logs.go b/logs/logs.go
index 6fc4ce57eb9c27ee5a5c22626a625f64b9a1c431..c901a85ca3a37dd569e0e53f385c8e166d5985b9 100644
--- a/logs/logs.go
+++ b/logs/logs.go
@@ -369,6 +369,24 @@ func LogResponseInfo(req events.APIGatewayProxyRequest, res events.APIGatewayPro
 	InfoWithFields(fields, "Res")
 }
 
+func LogFullResponseInfo(res events.APIGatewayProxyResponse, err error) {
+	if disableLogging {
+		return
+	}
+
+	fields := map[string]interface{}{
+		"status_code": res.StatusCode,
+	}
+
+	if err != nil {
+		fields["error"] = err
+	}
+
+	fields["body"] = res.Body
+
+	InfoWithFields(fields, "Res")
+}
+
 func LogApiAudit(fields log.Fields) {
 	if disableLogging {
 		return
@@ -409,6 +427,23 @@ func LogSQSEvent(event events.SQSEvent) {
 	}, "")
 }
 
+func LogWebsocketEvent(req events.APIGatewayWebsocketProxyRequest, shouldExcludeBody bool) {
+	if disableLogging {
+		return
+	}
+
+	fields := map[string]interface{}{
+		"route":         req.RequestContext.RouteKey,
+		"connection_id": req.RequestContext.ConnectionID,
+	}
+
+	if !shouldExcludeBody {
+		fields["body"] = req.Body
+	}
+
+	InfoWithFields(fields, "Req")
+}
+
 func SetOutput(out io.Writer) {
 	log.SetOutput(out)
 }
diff --git a/redis/redis.go b/redis/redis.go
index 86958ed2e67e6e3d5b41a0e9b71add25ae2a88d2..cb305b8801abe4b3df18ecf5d789cfb5ca58396c 100644
--- a/redis/redis.go
+++ b/redis/redis.go
@@ -27,6 +27,13 @@ type ClientWithHelpers struct {
 	Available bool
 }
 
+type SetLockKeyOptions struct {
+	Value          *string
+	MaxRetries     *int
+	RetryInterval  *time.Duration
+	FallbackResult *bool
+}
+
 func GetRedisClient(isDebug bool) *ClientWithHelpers {
 	if redisClient != nil && redisClient.IsConnected() {
 		return redisClient
@@ -177,14 +184,16 @@ func (r ClientWithHelpers) SetObjectByKeyIndefinitely(key string, object interfa
 
 }
 
-func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T) *T {
+// GetObjectByKey fetches an object from Redis by key. If the key does not exist or there is an error, it returns nil.
+// If an expiry is provided, it will both fetch the key and update its expiry.
+func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T, expiry ...time.Duration) *T {
 	// Make sure we have a Redis client, and it is connected
 	if redisClient == nil || !redisClient.IsConnected() {
 		return nil
 	}
 
 	// Get the object from Redis
-	jsonString := redisClient.GetValueByKey(key)
+	jsonString := redisClient.GetValueByKey(key, expiry...)
 	if jsonString == "" {
 		return nil
 	}
@@ -198,12 +207,20 @@ func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T)
 	return &object
 }
 
-func (r ClientWithHelpers) GetValueByKey(key string) string {
+// GetValueByKey fetches a value from Redis by key. If the key does not exist or there is an error, it returns an empty
+// string. If an expiry is provided, it will both fetch the key and update its expiry.
+func (r ClientWithHelpers) GetValueByKey(key string, expiry ...time.Duration) string {
 	if !r.IsConnected() {
 		return ""
 	}
 
-	jsonString, err := r.Client.Get(ctx, key).Result()
+	var jsonString string
+	var err error
+	if len(expiry) > 0 {
+		jsonString, err = r.Client.GetEx(ctx, key, expiry[0]).Result()
+	} else {
+		jsonString, err = r.Client.Get(ctx, key).Result()
+	}
 	if err == redis.Nil { /* Key does not exist */
 		return ""
 	} else if err != nil { /* Actual error */
@@ -232,3 +249,62 @@ func (r ClientWithHelpers) RateLimit(key string, limitFn func(int) redis_rate.Li
 		return res.Allowed == 1, nil
 	}
 }
+
+// SetLockKey attempts to set a lock on the specified key.
+func (r ClientWithHelpers) SetLockKey(key string, expiration time.Duration, lockOptions ...SetLockKeyOptions) (success bool, err error) {
+	fallbackResult := false
+	if len(lockOptions) > 0 && lockOptions[0].FallbackResult != nil {
+		fallbackResult = *lockOptions[0].FallbackResult
+	}
+
+	if !r.IsConnected() {
+		return fallbackResult, errors.Error("Redis is not connected")
+	}
+
+	value := "1"
+	retries := 0
+	retryInterval := 100 * time.Millisecond
+	if len(lockOptions) > 0 {
+		// Only use the values that were set
+		if lockOptions[0].Value != nil {
+			value = *lockOptions[0].Value
+		}
+		if lockOptions[0].MaxRetries != nil {
+			retries = *lockOptions[0].MaxRetries
+		}
+		if lockOptions[0].RetryInterval != nil {
+			retryInterval = *lockOptions[0].RetryInterval
+		}
+	}
+
+	for retries >= 0 {
+		success, err = r.Client.SetNX(ctx, key, value, expiration).Result()
+		if err != nil {
+			logs.ErrorWithMsg(fmt.Sprintf("Error setting lock key %s", key), err)
+
+			// Prevent further calls in this execution from trying to connect and also timeout
+			if strings.HasSuffix(err.Error(), "i/o timeout") {
+				r.Available = false
+			}
+
+			return fallbackResult, err
+		}
+
+		if success || retries == 0 {
+			break
+		}
+
+		retries--
+		time.Sleep(retryInterval)
+	}
+
+	return success, nil
+}
+
+func (r ClientWithHelpers) KeepLockKeyAlive(key string, expiration time.Duration) {
+	if !r.IsConnected() {
+		return
+	}
+
+	_ = r.Client.Expire(ctx, key, expiration)
+}
diff --git a/slice_utils/slice_utils.go b/slice_utils/slice_utils.go
index f203d0dc8d0dd698f63a5e53a815a990fb005aff..86ce64e0ca0e8465da1140a3fa836ef1884a091d 100644
--- a/slice_utils/slice_utils.go
+++ b/slice_utils/slice_utils.go
@@ -2,6 +2,7 @@ package slice_utils
 
 import (
 	"github.com/thoas/go-funk"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils"
 )
 
 func MinimumFloat64(values []float64) (min float64) {
@@ -64,3 +65,22 @@ func ArraySlice(s []any, offset, length int) []any {
 	}
 	return s[offset:]
 }
+
+func SliceToSlicePointers[V any](s []V) []*V {
+	result := make([]*V, len(s))
+	for i, v := range s {
+		vCopy := v // Create a copy to avoid pointer to loop variable issue
+		result[i] = utils.ValueToPointer(vCopy)
+	}
+	return result
+}
+
+func SlicePointersToSlice[V any](s []*V) []V {
+	result := make([]V, len(s))
+	for i, vp := range s {
+		if vp != nil {
+			result[i] = utils.PointerToValue(vp)
+		}
+	}
+	return result
+}
diff --git a/string_utils/string_utils.go b/string_utils/string_utils.go
index 9d591700c5dcc907f9f433929d95c68b782eb078..07dd05f75a460e4fc857c1ec80f50ca3cd0de2c3 100644
--- a/string_utils/string_utils.go
+++ b/string_utils/string_utils.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"github.com/jaytaylor/html2text"
 	"github.com/samber/lo"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils"
 	"regexp"
@@ -26,6 +27,48 @@ const (
 	regexIndexSubmatchEnd   = 3
 )
 
+var WhitespaceChars = []string{
+	// Standard whitespace characters
+	"\u0009", // Character tabulation
+	"\u000A", // Line feed
+	"\u000B", // Line tabulation
+	"\u000C", // Form feed
+	"\u000D", // Carriage return
+	"\u0020", // Space
+	"\u0085", // Next line
+	"\u00A0", // No-break space
+	"\u1680", // Ogham space mark
+	"\u2000", // En quad
+	"\u2001", // Em quad
+	"\u2002", // En space
+	"\u2003", // Em space
+	"\u2004", // Three-per-em space
+	"\u2005", // Four-per-em space
+	"\u2006", // Six-per-em space
+	"\u2007", // Figure space
+	"\u2008", // Punctuation space
+	"\u2009", // Thin space
+	"\u200A", // Hair space
+	"\u2028", // Line separator
+	"\u2029", // Paragraph separator
+	"\u202F", // Narrow no-break space
+	"\u205F", // Medium mathematical space
+	"\u3000", // Ideographic space
+}
+
+var NonOfficialWhitespaceChars = []string{
+	// Characters with property White_Space=no
+	"\u180E", // Mongolian vowel separator
+	"\u200B", // Zero width space
+	"\u200C", // Zero width non-joiner
+	"\u200D", // Zero width joiner
+	"\u200E", // Left-to-right mark
+	"\u2060", // Word joiner
+	"\u202C", // Pop directional formatting
+	"\uFEFF", // Zero width no-break space
+	"\u00AD", // Soft hyphen
+}
+
 var snakeCaseRegex = regexp.MustCompile("^" + snakeCasePattern + "$")
 
 func IsSnakeCase(name string) bool {
@@ -49,7 +92,11 @@ func ReplaceNonSpacingMarks(str string) string {
 }
 
 func RemoveAllWhiteSpaces(s string) string {
-	return strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "")
+	cleanedString := strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "")
+	for _, whitespaceChar := range WhitespaceChars {
+		cleanedString = strings.ReplaceAll(cleanedString, whitespaceChar, "")
+	}
+	return cleanedString
 }
 
 func ReplaceCaseInsensitive(string, toReplace, replaceWith string) string {
@@ -307,3 +354,32 @@ func TrimSpaceForPointer(s *string) *string {
 func TrimAndToLower(s string) string {
 	return strings.ToLower(strings.TrimSpace(s))
 }
+
+func StringContainsNumbers(s string) bool {
+	// Define a regular expression to match numbers
+	regex := regexp.MustCompile("[0-9]")
+
+	// Use FindString to check if the string contains any numbers
+	return regex.MatchString(s)
+}
+
+func StringContainsOnlyNumbers(s string) bool {
+	for _, char := range s {
+		if !unicode.IsDigit(char) {
+			return false
+		}
+	}
+	return true
+}
+
+func HTMLStringToTextString(html string) (string, error) {
+	return html2text.FromString(html, html2text.Options{})
+}
+
+func HTMLStringToTextBytes(html string) ([]byte, error) {
+	text, err := HTMLStringToTextString(html)
+	if err != nil {
+		return []byte{}, err
+	}
+	return []byte(text), nil
+}
diff --git a/utils/utils.go b/utils/utils.go
index f0a27b92ac5730717bdda9fd4fe914e415e2585b..296f43419957df0635a8cd100216fbf6a8e13bdc 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -2,15 +2,20 @@ package utils
 
 import (
 	"bytes"
+	emailverifier "github.com/AfterShip/email-verifier"
+	normalizer "github.com/dimuska139/go-email-normalizer/v2"
+	"github.com/jinzhu/now"
 	"github.com/mohae/deepcopy"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
-	"net/mail"
+	"math"
+	"net"
 	"net/url"
 	"os"
 	"reflect"
 	"regexp"
 	"strings"
+	"time"
 )
 
 // GetEnv is a helper function for getting environment variables with a default
@@ -54,24 +59,85 @@ func PointerToValue[V any](value *V) V {
 	return *new(V) // zero value of V
 }
 
-func ValidateEmailAddress(email string) (string, error) {
-	if email == "" {
+func ValidateEmailAddress(email string, options ...map[string]string) (string, error) {
+	// To lower
+	cleanedEmail := strings.ToLower(strings.TrimSpace(email))
+
+	// Remove all whitespaces
+	cleanedEmail = string_utils.RemoveAllWhiteSpaces(cleanedEmail)
+
+	// Also remove unofficial whitespaces
+	for _, char := range string_utils.NonOfficialWhitespaceChars {
+		cleanedEmail = strings.ReplaceAll(cleanedEmail, char, "")
+	}
+
+	// Strip invalid characters
+	cleanedEmail = stripInvalidCharacters(cleanedEmail)
+
+	// Make sure the email is not empty
+	if cleanedEmail == "" {
 		return "", errors.Error("email address is empty")
 	}
 
-	cleanEmail := strings.ToLower(strings.TrimSpace(email))
-	cleanEmail = string_utils.RemoveAllWhiteSpaces(cleanEmail)
+	doOnlyParse := false
+	if options != nil {
+		if value, exists := options[0]["do_only_parse"]; exists && value == "true" {
+			doOnlyParse = true
+		}
+	}
+
+	// Parse and verify the email
+	verifier := emailverifier.NewVerifier()
 
-	// Remove ZWSP ("\u200B") characters with an empty string to remove it
-	cleanEmail = strings.ReplaceAll(cleanEmail, "\u200B", "")
+	if doOnlyParse {
+		result := verifier.ParseAddress(cleanedEmail)
+		if !result.Valid {
+			return cleanedEmail, errors.Error("could not parse email address")
+		}
+	} else {
+		result, err := verifier.Verify(cleanedEmail)
+		if err != nil || !result.Syntax.Valid {
+			return cleanedEmail, errors.Wrap(err, "could not parse email address")
+		}
+	}
 
-	// We validate it but still return it since in some cases we don't want to break everything if the email is bad
-	_, err := mail.ParseAddress(cleanEmail)
-	if err != nil {
-		return cleanEmail, errors.Wrap(err, "could not parse email address")
+	return cleanedEmail, nil
+}
+
+func stripInvalidCharacters(email string) string {
+	cleanEmail := email
+
+	// Replace quotes, asterisks, etc.
+	cleanEmail = strings.ReplaceAll(cleanEmail, "'", "")
+	cleanEmail = strings.ReplaceAll(cleanEmail, "*", "")
+	cleanEmail = strings.ReplaceAll(cleanEmail, "!", "")
+	cleanEmail = strings.ReplaceAll(cleanEmail, "`", "")
+
+	// Trim invalid characters, like underscore, so that it still fails if it's inside the email
+	cleanEmail = strings.Trim(cleanEmail, "_")
+	cleanEmail = strings.Trim(cleanEmail, "+")
+
+	return cleanEmail
+}
+
+func SplitAndCleanEmailAddresses(emails string) []string {
+	var destinationEmails []string
+
+	splitEmails := string_utils.SplitString(emails, []rune{',', ';'})
+	if len(splitEmails) >= 1 {
+		// Success - return these emails
+		for _, email := range splitEmails {
+			cleanedEmail, err := ValidateEmailAddress(email)
+			if err == nil && cleanedEmail != "" {
+				destinationEmails = append(destinationEmails, cleanedEmail)
+			}
+		}
+		if len(destinationEmails) > 0 {
+			return destinationEmails
+		}
 	}
 
-	return cleanEmail, nil
+	return destinationEmails
 }
 
 func StripEmail(email string) (strippedEmail string, strippedDomain string) {
@@ -100,6 +166,11 @@ func StripEmail(email string) (strippedEmail string, strippedDomain string) {
 	return strippedEmail, strippedDomain
 }
 
+func NormalizeEmail(email string) string {
+	emailNormalizer := normalizer.NewNormalizer()
+	return emailNormalizer.Normalize(email)
+}
+
 // IsUrlStrict Returns whether a URL is valid in a strict way (Must have scheme and host)
 func IsUrlStrict(str string) bool {
 	u, err := url.Parse(str)
@@ -203,3 +274,18 @@ func IsEqual(expected interface{}, actual interface{}) bool {
 	return reflect.DeepEqual(expected, actual)
 
 }
+
+func DetermineDaysLeft(fromDateLocal time.Time, toDateLocal time.Time) int {
+	fromDate := now.With(fromDateLocal).EndOfDay()
+	toDate := now.With(toDateLocal).EndOfDay()
+	return int(math.Floor(toDate.Sub(fromDate).Hours() / 24))
+}
+
+func ValidateIPAddress(ipAddress string) (cleanedIPAddress string, err error) {
+	ipAddress = strings.ToLower(strings.TrimSpace(ipAddress))
+	ip := net.ParseIP(ipAddress)
+	if ip == nil {
+		return "", errors.Error("invalid IP address")
+	}
+	return ipAddress, nil
+}
diff --git a/utils/utils_test.go b/utils/utils_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b1a07699ec800d5f43bea7aef7aef65d843a992d
--- /dev/null
+++ b/utils/utils_test.go
@@ -0,0 +1,148 @@
+package utils
+
+import (
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
+	"testing"
+)
+
+func TestStripEmail(t *testing.T) {
+	tests := []struct {
+		name         string
+		email        string
+		wantStripped string
+		wantDomain   string
+	}{
+		{
+			name:         "Test with + symbol",
+			email:        "test+extra@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test without + symbol",
+			email:        "test@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test with multiple + symbols",
+			email:        "test+extra+more@gmail.com",
+			wantStripped: "test@gmail.com",
+			wantDomain:   "gmail.com",
+		},
+		{
+			name:         "Test with different domain",
+			email:        "test+extra@yahoo.com",
+			wantStripped: "test@yahoo.com",
+			wantDomain:   "yahoo.com",
+		},
+		{
+			name:         "Test with subdomain",
+			email:        "test+extra@mail.example.com",
+			wantStripped: "test@mail.example.com",
+			wantDomain:   "mail.example.com",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotStripped, gotDomain := StripEmail(tt.email)
+			if gotStripped != tt.wantStripped {
+				t.Errorf("StripEmail() gotStripped = %v, want %v", gotStripped, tt.wantStripped)
+			}
+			if gotDomain != tt.wantDomain {
+				t.Errorf("StripEmail() gotDomain = %v, want %v", gotDomain, tt.wantDomain)
+			}
+		})
+	}
+}
+
+func TestNormalizeEmail(t *testing.T) {
+	tests := []struct {
+		name           string
+		email          string
+		wantNormalized string
+	}{
+		{
+			name:           "Test with + symbol",
+			email:          "test+extra@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test without + symbol",
+			email:          "test@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test with multiple + symbols",
+			email:          "test+extra+more@gmail.com",
+			wantNormalized: "test@gmail.com",
+		},
+		{
+			name:           "Test with different domain",
+			email:          "test-extra@yahoo.com",
+			wantNormalized: "testextra@yahoo.com",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotNormalized := NormalizeEmail(tt.email)
+			if gotNormalized != tt.wantNormalized {
+				t.Errorf("NormalizeEmail() gotNormalized = %v, want %v", gotNormalized, tt.wantNormalized)
+			}
+		})
+	}
+}
+
+func TestValidateIPAddress(t *testing.T) {
+	tests := []struct {
+		name string
+		ip   string
+		want string
+		err  error
+	}{
+		{
+			name: "Test with valid IPv4",
+			ip:   "192.168.1.1",
+			want: "192.168.1.1",
+			err:  nil,
+		},
+		{
+			name: "Test with valid IPv6",
+			ip:   "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			want: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+			err:  nil,
+		},
+		{
+			name: "Test with invalid IP",
+			ip:   "999.999.999.999",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+		{
+			name: "Test with empty string",
+			ip:   "",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+		{
+			name: "Test with non-IP string",
+			ip:   "not an ip address",
+			want: "",
+			err:  errors.Error("invalid IP address"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ValidateIPAddress(tt.ip)
+			if got != tt.want {
+				t.Errorf("ValidateIPAddress() got = %v, want %v", got, tt.want)
+			}
+			if err != nil && err.Error() != tt.err.Error() {
+				t.Errorf("ValidateIPAddress() err = %v, want %v", err, tt.err)
+			}
+		})
+	}
+}
diff --git a/websocket_utils/websocket_utils.go b/websocket_utils/websocket_utils.go
new file mode 100644
index 0000000000000000000000000000000000000000..3a77890ddbeb692af6d7008741ce98c55afa838f
--- /dev/null
+++ b/websocket_utils/websocket_utils.go
@@ -0,0 +1,73 @@
+package websocket_utils
+
+import (
+	"fmt"
+	"github.com/aws/aws-lambda-go/events"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/apigatewaymanagementapi"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils"
+	"os"
+)
+
+var (
+	sessions = map[string]*APIGateWaySessionWithHelpers{}
+)
+
+type APIGateWaySessionWithHelpers struct {
+	APIGatewaySession *apigatewaymanagementapi.ApiGatewayManagementApi
+}
+
+func GetSession(region ...string) *APIGateWaySessionWithHelpers {
+	s3Region := os.Getenv("AWS_REGION")
+
+	// Set custom region
+	if region != nil && len(region) > 0 {
+		s3Region = region[0]
+	}
+
+	// Check if session exists for region, if it does return it
+	if apiGatewaySession, ok := sessions[s3Region]; ok {
+		return apiGatewaySession
+	}
+
+	// Setup session
+	options := session.Options{
+		Config: aws.Config{
+			Region: utils.ValueToPointer(s3Region),
+		},
+	}
+	sess, err := session.NewSessionWithOptions(options)
+	if err != nil {
+		return nil
+	}
+	apiGatewaySession := NewSession(sess)
+	sessions[s3Region] = apiGatewaySession
+	return apiGatewaySession
+}
+
+func NewSession(session *session.Session) *APIGateWaySessionWithHelpers {
+	return &APIGateWaySessionWithHelpers{
+		APIGatewaySession: apigatewaymanagementapi.New(session),
+	}
+}
+
+func (s APIGateWaySessionWithHelpers) PostToConnectionIDs(data []byte, req *events.APIGatewayWebsocketProxyRequest, connectionIDs []string) error {
+	if req == nil {
+		return errors.Error("websocket request is nil")
+	}
+
+	for _, connectionID := range connectionIDs {
+		s.APIGatewaySession.Endpoint = fmt.Sprintf("https://%s/%s", req.RequestContext.DomainName, req.RequestContext.Stage)
+		_, err := s.APIGatewaySession.PostToConnection(&apigatewaymanagementapi.PostToConnectionInput{
+			ConnectionId: &connectionID,
+			Data:         data,
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/zip_utils/zip_utils.go b/zip_utils/zip_utils.go
index 7edc3e6922aad8fa93fc481ebf0b491adcae525f..9cf328a7e79f2ec3125e2d6b5796a7f571a1d9ac 100644
--- a/zip_utils/zip_utils.go
+++ b/zip_utils/zip_utils.go
@@ -3,6 +3,7 @@ package zip_utils
 import (
 	"archive/zip"
 	"bytes"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils"
 	"io"
 	"time"
 )
@@ -71,7 +72,12 @@ func readZipFile(zipFile *zip.File) ([]byte, error) {
 }
 
 func AddFileToZip(fileName string, fileBytes []byte, zipWriter *zip.Writer) error {
-	file, err := zipWriter.Create(fileName)
+	header := &zip.FileHeader{
+		Name:     fileName,
+		Method:   zip.Deflate,
+		Modified: date_utils.CurrentDate(),
+	}
+	file, err := zipWriter.CreateHeader(header)
 	if err != nil {
 		return err
 	}