diff --git a/audit/audit.go b/audit/audit.go
index 44d786ed2aa5a9b9874403a863044e2f8d32872a..1fb3c2c2ffb1c87127a6d9ab14607b85fa6161bd 100644
--- a/audit/audit.go
+++ b/audit/audit.go
@@ -3,25 +3,29 @@ package audit
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/r3labs/diff/v2"
+	"github.com/samber/lo"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
 	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
 	"reflect"
 	"regexp"
 	"strconv"
 	"strings"
-
-	"github.com/r3labs/diff/v2"
-	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection"
-	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
 )
 
 type FieldChange struct {
-	From interface{} `json:"change_from"`
-	To   interface{} `json:"change_to"`
+	From any `json:"change_from"`
+	To   any `json:"change_to"`
 }
 
-func VerifyAuditEvents(original interface{}, new interface{}) error {
+type IAuditFormatter interface {
+	FormatForAuditEvent() string
+}
+
+func VerifyAuditEvents(original any, new any) error {
 	if original != nil {
 		structValue := reflect.ValueOf(original)
 		if structValue.Kind() != reflect.Struct {
@@ -39,12 +43,12 @@ func VerifyAuditEvents(original interface{}, new interface{}) error {
 	return nil
 }
 
-func GetChanges(original interface{}, new interface{}) (map[string]interface{}, error) {
+func GetChanges(original any, new any) (map[string]any, error) {
 	// Clean audit events
 	original = cleanStruct(original)
 	new = cleanStruct(new)
 
-	changes := map[string]interface{}{}
+	changes := map[string]any{}
 	changelog, err := diff.Diff(original, new)
 	if err != nil {
 		return changes, err
@@ -64,6 +68,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 			// ["Account", "ID"]
 			// 0 = Object
 			// 1 = field
+			objectKey := ToSnakeCase(change.Path[0])
+			didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, -1)
+			if didInsert {
+				continue
+			}
 
 			ChildObjectChanges(changes, change.Path[0], change.Path[1], change.From, change.To)
 
@@ -79,6 +88,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 
 			if !string_utils.IsNumericString(indexString) {
 				// Not an array, but a deeper nested object
+				didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, -1)
+				if didInsert {
+					continue
+				}
+
 				ChildObjectChanges(changes, change.Path[len(change.Path)-2], change.Path[len(change.Path)-1], change.From, change.To)
 				continue
 			}
@@ -86,6 +100,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 			index, _ := number_utils.StringToInt64(indexString)
 			field := ToSnakeCase(change.Path[2])
 
+			didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, int(index))
+			if didInsert {
+				continue
+			}
+
 			if len(change.Path) == 5 && string_utils.IsNumericString(change.Path[3]) {
 				// The field is actually an array of objects.
 				field += fmt.Sprintf("[%s] (%s)", change.Path[3], ToSnakeCase(change.Path[4]))
@@ -93,7 +112,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 
 			arrayObject, present := changes[objectKey]
 			if present {
-				if arrayOfObjects, ok := arrayObject.([]map[string]interface{}); ok {
+				if arrayOfObjects, ok := arrayObject.([]map[string]any); ok {
 					arrayIndex := ArrayIndexForObjectIndex(arrayOfObjects, index)
 					if arrayIndex != -1 {
 						// Add field to existing object in array
@@ -104,7 +123,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 						}
 					} else {
 						// new object, append to existing array
-						fieldChange := map[string]interface{}{
+						fieldChange := map[string]any{
 							"index": index,
 							field: FieldChange{
 								From: change.From,
@@ -117,14 +136,14 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 				}
 			} else {
 				// Create array of objects
-				fieldChange := map[string]interface{}{
+				fieldChange := map[string]any{
 					"index": index,
 					field: FieldChange{
 						From: change.From,
 						To:   change.To,
 					},
 				}
-				changes[objectKey] = []map[string]interface{}{
+				changes[objectKey] = []map[string]any{
 					fieldChange,
 				}
 			}
@@ -134,7 +153,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{},
 	return changes, nil
 }
 
-func cleanStruct(object interface{}) interface{} {
+func cleanStruct(object any) any {
 	defer func() {
 		if err := recover(); err != nil {
 			logs.ErrorMsg(fmt.Sprintf("audit event panic: %+v", err))
@@ -175,9 +194,10 @@ func cleanStruct(object interface{}) interface{} {
 		structField := val.Type().Field(i)
 
 		// Determine whether the field should be included or excluded
-		value, _ := structField.Tag.Lookup("audit")
-		shouldIncludeForAudit := value == "true"
-		shouldExcludeForAudit := value == "false"
+		auditTag := structField.Tag.Get("audit")
+		auditTagOptions := strings.Split(auditTag, ",")
+		shouldIncludeForAudit := lo.Contains(auditTagOptions, "true")
+		shouldExcludeForAudit := lo.Contains(auditTagOptions, "false")
 
 		// If the audit tag is present and specified to 'true', we should always include the relation
 		if shouldIncludeForAudit {
@@ -197,14 +217,158 @@ func cleanStruct(object interface{}) interface{} {
 	return object
 }
 
-func ChildObjectChanges(changes map[string]interface{}, objectPath string, fieldPath string, changeFrom interface{}, changeTo interface{}) {
+func CheckToFormatForAuditEvent(changes map[string]any, original any, new any, field string, objectKey string, index int) (didInsert bool) {
+	originalStructField, originalFieldValue, found := getFieldFromStruct(original, field)
+	if !found {
+		return false
+	}
+	_, newFieldValue, found := getFieldFromStruct(new, field)
+	if !found {
+		return false
+	}
+	doGroupSlice := doGroupSliceForAuditEvent(originalStructField)
+
+	originalFormattedObject := checkToFormatForAuditEvent(originalFieldValue, index)
+	newFormattedObject := checkToFormatForAuditEvent(newFieldValue, index)
+	if originalFormattedObject != nil ||
+		newFormattedObject != nil {
+		if _, present := changes[objectKey]; present {
+			if doGroupSlice {
+				// The object has already been added to the changes - group the new change with the previous
+				existingChanges, ok := changes[objectKey].(FieldChange)
+				if !ok {
+					return true
+				}
+
+				if originalFormattedObject != nil {
+					existingChanges.From = appendNewChangeToExistingChanges(existingChanges.From, *originalFormattedObject)
+				}
+
+				if newFormattedObject != nil {
+					existingChanges.To = appendNewChangeToExistingChanges(existingChanges.To, *newFormattedObject)
+				}
+				changes[objectKey] = existingChanges
+			}
+
+			return true
+		}
+
+		if doGroupSlice {
+			changes[objectKey] = FieldChange{
+				From: []string{*originalFormattedObject},
+				To:   []string{*newFormattedObject},
+			}
+		} else {
+			if index > -1 {
+				objectKey = fmt.Sprintf("%s[%d]", field, index)
+			}
+
+			changes[objectKey] = FieldChange{
+				From: originalFormattedObject,
+				To:   newFormattedObject,
+			}
+		}
+
+		return true
+	}
+
+	return false
+}
+
+func appendNewChangeToExistingChanges(existingChanges any, newChange string) any {
+	existingChangesStrings, ok := existingChanges.([]string)
+	if !ok {
+		return existingChanges
+	}
+
+	if !lo.Contains(existingChangesStrings, newChange) {
+		existingChangesStrings = append(existingChangesStrings, newChange)
+	}
+
+	return existingChangesStrings
+}
+
+func getFieldFromStruct(object any, field string) (reflect.StructField, reflect.Value, bool) {
+	objectValue := reflect.ValueOf(object)
+	if objectValue.Kind() == reflect.Ptr {
+		objectValue = objectValue.Elem()
+	}
+
+	if objectValue.Kind() != reflect.Struct {
+		// Fields can only be retrieved from structs
+		return reflect.StructField{}, reflect.Value{}, false
+	}
+
+	objectStructField, found := objectValue.Type().FieldByName(field)
+	if !found {
+		return reflect.StructField{}, reflect.Value{}, false
+	}
+
+	objectFieldValue := objectValue.FieldByName(field)
+
+	return objectStructField, objectFieldValue, true
+}
+
+func doGroupSliceForAuditEvent(objectField reflect.StructField) bool {
+	auditTag, found := objectField.Tag.Lookup("audit")
+	if found {
+		auditTagOptions := strings.Split(auditTag, ",")
+		if lo.Contains(auditTagOptions, "group") {
+			return true
+		}
+	}
+
+	return false
+}
+
+func checkToExecuteAuditFormatter(fieldValue reflect.Value) *string {
+	if auditFormatterObject, ok := fieldValue.Interface().(IAuditFormatter); ok {
+		fieldString := auditFormatterObject.FormatForAuditEvent()
+		if fieldString != "" {
+			return &fieldString
+		}
+	}
+	return nil
+}
+
+func checkToFormatForAuditEvent(fieldValue reflect.Value, index int) *string {
+	if fieldValue.Kind() == reflect.Ptr {
+		fieldValue = fieldValue.Elem()
+	}
+
+	var formattedField *string
+	if fieldValue.Kind() == reflect.Struct {
+		formattedField = checkToExecuteAuditFormatter(fieldValue)
+	} else if fieldValue.Kind() == reflect.Slice &&
+		fieldValue.Len() > 0 {
+		if index >= fieldValue.Len() {
+			return nil
+		}
+
+		sliceFieldValue := fieldValue.Index(index)
+		if sliceFieldValue.Kind() == reflect.Ptr {
+			sliceFieldValue = sliceFieldValue.Elem()
+		}
+
+		if sliceFieldValue.Kind() != reflect.Struct {
+			// Not a slice of structs - ignore the format flag
+			return nil
+		}
+
+		formattedField = checkToExecuteAuditFormatter(sliceFieldValue)
+	}
+
+	return formattedField
+}
+
+func ChildObjectChanges(changes map[string]any, objectPath string, fieldPath string, changeFrom any, changeTo any) {
 
 	objectKey := ToSnakeCase(objectPath)
 	field := ToSnakeCase(fieldPath)
 
 	existingObject, present := changes[objectKey]
 	if present {
-		if object, ok := existingObject.(map[string]interface{}); ok {
+		if object, ok := existingObject.(map[string]any); ok {
 			object[field] = FieldChange{
 				From: changeFrom,
 				To:   changeTo,
@@ -212,7 +376,7 @@ func ChildObjectChanges(changes map[string]interface{}, objectPath string, field
 			changes[objectKey] = object
 		}
 	} else {
-		fieldChange := map[string]interface{}{
+		fieldChange := map[string]any{
 			field: FieldChange{
 				From: changeFrom,
 				To:   changeTo,
@@ -223,7 +387,7 @@ func ChildObjectChanges(changes map[string]interface{}, objectPath string, field
 }
 
 // ArrayIndexForObjectIndex gets the index of arrayOfObjects where the object's index field is equal to objectIndex.
-func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectIndex int64) int {
+func ArrayIndexForObjectIndex(arrayOfObjects []map[string]any, objectIndex int64) int {
 	for arrayIndex, object := range arrayOfObjects {
 		index, present := object["index"]
 		if present {
@@ -238,8 +402,8 @@ func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectInd
 
 // GetAllChanges Returns the diff, structured in json, recursively
 // Be warned, here be dragons. Debug this first to understand how it works
-func GetAllChanges(original interface{}, new interface{}) (map[string]interface{}, error) {
-	changes := map[string]interface{}{}
+func GetAllChanges(original any, new any) (map[string]any, error) {
+	changes := map[string]any{}
 	changelog, err := diff.Diff(original, new)
 	if err != nil {
 		return changes, err
@@ -402,7 +566,7 @@ func ToSnakeCase(str string) string {
 	return strings.ToLower(snake)
 }
 
-func GetIntValue(object interface{}, key string) int64 {
+func GetIntValue(object any, key string) int64 {
 	structValue := reflect.ValueOf(object)
 	if structValue.Kind() == reflect.Struct {
 		field := structValue.FieldByName(key)
@@ -412,7 +576,7 @@ func GetIntValue(object interface{}, key string) int64 {
 	return 0
 }
 
-func GetStringValue(object interface{}, key string) string {
+func GetStringValue(object any, key string) string {
 	structValue := reflect.ValueOf(object)
 	if structValue.Kind() == reflect.Struct {
 		field := structValue.FieldByName(key)