diff --git a/server/db/dbModels.go b/server/db/dbModels.go index c0ac9fe..ce65b11 100644 --- a/server/db/dbModels.go +++ b/server/db/dbModels.go @@ -195,3 +195,50 @@ type VehicleAttachment struct { VehicleID string `gorm:"primaryKey" json:"vehicleId"` Title string `json:"title"` } + +type VehicleAlert struct { + Base + VehicleID string `json:"vehicleId"` + Vehicle Vehicle `json:"-"` + UserID string `json:"userId"` + User User `json:"user"` + Title string `json:"title"` + Comments string `json:"comments"` + StartDate time.Time `json:"date"` + StartOdoReading int `json:"startOdoReading"` + DistanceUnit DistanceUnit `json:"distanceUnit"` + AlertFrequency AlertFrequency `json:"alertFrequency"` + OdoFrequency int `json:"odoFrequency"` + DayFrequency int `json:"dayFrequency"` + AlertAllUsers bool `json:"alertAllUsers"` + IsActive bool `json:"isActive"` + EndDate *time.Time `json:"endDate"` + AlertType AlertType `json:"alertType"` +} +type AlertOccurance struct { + Base + VehicleID string `json:"vehicleId"` + Vehicle Vehicle `json:"-"` + VehicleAlertID string `json:"vehicleAlertId"` + VehicleAlert VehicleAlert `json:"-"` + UserID string `json:"userId"` + User User `json:"-"` + OdoReading int `json:"odoReading"` + Date *time.Time `json:"date"` + ProcessDate *time.Time `json:"processDate"` + AlertProcessType AlertType `json:"alertProcessType"` + CompleteDate *time.Time `json:"completeDate"` +} + +type Notification struct { + Base + Title string `json:"title"` + Content string `json:"content"` + UserID string `json:"userId"` + VehicleID string `json:"vehicleId"` + User User `json:"-"` + Date time.Time `json:"date"` + ReadDate *time.Time `json:"readDate"` + ParentID string `json:"parentId"` + ParentType string `json:"parentType"` +} diff --git a/server/db/dbfunctions.go b/server/db/dbfunctions.go index 363918d..ead5d10 100644 --- a/server/db/dbfunctions.go +++ b/server/db/dbfunctions.go @@ -160,6 +160,11 @@ func GetFillupsByVehicleId(id string) (*[]Fillup, error) { result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Fillup{VehicleID: id}) return &obj, result.Error } +func GetLatestFillupsByVehicleId(id string) (*Fillup, error) { + var obj Fillup + result := DB.Preload(clause.Associations).Order("date desc").First(&obj, &Fillup{VehicleID: id}) + return &obj, result.Error +} func GetFillupsByVehicleIdSince(id string, since time.Time) (*[]Fillup, error) { var obj []Fillup result := DB.Where("date >= ? AND vehicle_id = ?", since, id).Preload(clause.Associations).Order("date desc").Find(&obj) @@ -190,6 +195,11 @@ func GetExpensesByVehicleId(id string) (*[]Expense, error) { result := DB.Preload(clause.Associations).Order("date desc").Find(&obj, &Expense{VehicleID: id}) return &obj, result.Error } +func GetLatestExpenseByVehicleId(id string) (*Expense, error) { + var obj Expense + result := DB.Preload(clause.Associations).Order("date desc").First(&obj, &Expense{VehicleID: id}) + return &obj, result.Error +} func GetExpenseById(id string) (*Expense, error) { var obj Expense result := DB.Preload(clause.Associations).First(&obj, "id=?", id) @@ -271,6 +281,29 @@ func GetVehicleAttachments(vehicleId string) (*[]Attachment, error) { } return &attachments, nil } +func GeAlertById(id string) (*VehicleAlert, error) { + var alert VehicleAlert + result := DB.Preload(clause.Associations).First(&alert, "id=?", id) + return &alert, result.Error +} +func GetAlertOccurenceByAlertId(id string) (*[]AlertOccurance, error) { + var alertOccurance []AlertOccurance + result := DB.Preload(clause.Associations).Order("created_at desc").Find(&alertOccurance, "vehicle_alert_id=?", id) + return &alertOccurance, result.Error +} + +func GetUnprocessedAlertOccurances() (*[]AlertOccurance, error) { + var alertOccurance []AlertOccurance + result := DB.Preload(clause.Associations).Order("created_at desc").Find(&alertOccurance, "process_date is NULL") + return &alertOccurance, result.Error +} +func MarkAlertOccuranceAsProcessed(id string, alertProcessType AlertType, date time.Time) error { + tx := DB.Debug().Model(&AlertOccurance{}).Where("id= ?", id). + Update("alert_process_type", alertProcessType). + Update("process_date", date) + return tx.Error + +} func UpdateSettings(setting *Setting) error { tx := DB.Save(&setting) diff --git a/server/db/enums.go b/server/db/enums.go index eb6cdf0..296d06e 100644 --- a/server/db/enums.go +++ b/server/db/enums.go @@ -36,6 +36,21 @@ const ( USER ) +type AlertFrequency int + +const ( + ONETIME AlertFrequency = iota + RECURRING +) + +type AlertType int + +const ( + DISTANCE AlertType = iota + TIME + BOTH +) + type EnumDetail struct { Short string `json:"short"` Long string `json:"long"` diff --git a/server/models/alert.go b/server/models/alert.go new file mode 100644 index 0000000..f33b572 --- /dev/null +++ b/server/models/alert.go @@ -0,0 +1,21 @@ +package models + +import ( + "time" + + "github.com/akhilrex/hammond/db" +) + +type CreateAlertModel struct { + Comments string `json:"comments"` + Title string `json:"title"` + StartDate time.Time `json:"date"` + StartOdoReading int `json:"startOdoReading"` + DistanceUnit *db.DistanceUnit `json:"distanceUnit"` + AlertFrequency *db.AlertFrequency `json:"alertFrequency"` + OdoFrequency int `json:"odoFrequency"` + DayFrequency int `json:"dayFrequency"` + AlertAllUsers bool `json:"alertAllUsers"` + IsActive bool `json:"isActive"` + AlertType *db.AlertType `json:"alertType"` +} diff --git a/server/service/alertSevice.go b/server/service/alertSevice.go new file mode 100644 index 0000000..2dac650 --- /dev/null +++ b/server/service/alertSevice.go @@ -0,0 +1,164 @@ +package service + +import ( + "errors" + "time" + + "github.com/akhilrex/hammond/db" + "github.com/akhilrex/hammond/models" +) + +func CreateAlert(model models.CreateAlertModel, vehicleId, userId string) (*db.VehicleAlert, error) { + alert := db.VehicleAlert{ + VehicleID: vehicleId, + UserID: userId, + Title: model.Title, + Comments: model.Comments, + StartDate: model.StartDate, + StartOdoReading: model.StartOdoReading, + DistanceUnit: *model.DistanceUnit, + AlertFrequency: *model.AlertFrequency, + OdoFrequency: model.OdoFrequency, + DayFrequency: model.DayFrequency, + AlertAllUsers: model.AlertAllUsers, + IsActive: model.IsActive, + AlertType: *model.AlertType, + } + tx := db.DB.Create(&alert) + if tx.Error != nil { + return nil, tx.Error + } + go CreateAlertInstance(alert.ID) + return &alert, nil +} + +func CreateAlertInstance(alertId string) error { + alert, err := db.GeAlertById(alertId) + if err != nil { + return err + } + existingOccurence, err := db.GetAlertOccurenceByAlertId(alertId) + var lastOccurance db.AlertOccurance + + if len(*existingOccurence) > 0 { + lastOccurance = (*existingOccurence)[0] + + if alert.AlertFrequency == db.ONETIME { + return errors.New("Only single occurance is possible for this kind of alert") + } + } + users := []string{alert.UserID} + if alert.AlertAllUsers { + allUsers, err := db.GetVehicleUsers(alert.VehicleID) + if err != nil { + return err + } + users = make([]string, len(*allUsers)) + for i, user := range *allUsers { + users[i] = user.UserID + } + } + + for _, userId := range users { + model := db.AlertOccurance{ + VehicleID: alert.VehicleID, + UserID: userId, + VehicleAlertID: alertId, + } + + if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH { + model.OdoReading = alert.StartOdoReading + alert.OdoFrequency + if &lastOccurance != nil { + model.OdoReading = lastOccurance.OdoReading + alert.OdoFrequency + } + } + if alert.AlertType == db.TIME || alert.AlertType == db.BOTH { + date := alert.StartDate.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour) + if &lastOccurance != nil { + date = lastOccurance.Date.Add(time.Duration(alert.DayFrequency) * 24 * time.Hour) + } + model.Date = &date + } + tx := db.DB.Create(&model) + if tx.Error != nil { + return tx.Error + } + } + return nil + +} + +func ProcessAlertOccurance(occurance db.AlertOccurance, today time.Time) error { + if occurance.ProcessDate != nil { + return errors.New("Alert occurence already processed") + } + alert := occurance.VehicleAlert + if !alert.IsActive { + return errors.New("Alert is not active") + } + notification := db.Notification{ + Title: alert.Title, + Content: alert.Comments, + UserID: occurance.UserID, + VehicleID: occurance.VehicleID, + Date: today, + ParentID: occurance.ID, + ParentType: "AlertOccurance", + } + var alertProcessType db.AlertType + if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH { + odoReading, err := GetLatestOdoReadingForVehicle(occurance.VehicleID) + if err != nil { + return err + } + if odoReading >= occurance.OdoReading { + alertProcessType = db.DISTANCE + } + } + if alert.AlertType == db.TIME || alert.AlertType == db.BOTH { + if occurance.Date.Before(today) { + alertProcessType = db.TIME + } + } + + db.DB.Create(¬ification) + return db.MarkAlertOccuranceAsProcessed(occurance.ID, alertProcessType, today) + +} + +func FindAlertOccurancesToProcess(today time.Time) ([]db.AlertOccurance, error) { + occurances, err := db.GetUnprocessedAlertOccurances() + if err != nil { + return nil, err + } + if len(*occurances) == 0 { + return make([]db.AlertOccurance, 0), nil + } + + var toReturn []db.AlertOccurance + + for _, occurance := range *occurances { + alert := occurance.VehicleAlert + if !alert.IsActive { + continue + } + if alert.AlertType == db.DISTANCE || alert.AlertType == db.BOTH { + odoReading, err := GetLatestOdoReadingForVehicle(occurance.VehicleID) + if err != nil { + return nil, err + } + if odoReading >= occurance.OdoReading { + toReturn = append(toReturn, occurance) + continue + } + } + if alert.AlertType == db.TIME || alert.AlertType == db.BOTH { + if occurance.Date.Before(today) { + toReturn = append(toReturn, occurance) + continue + } + } + + } + return toReturn, nil +} diff --git a/server/service/vehicleService.go b/server/service/vehicleService.go index 5e853af..6f1ccbd 100644 --- a/server/service/vehicleService.go +++ b/server/service/vehicleService.go @@ -5,6 +5,7 @@ import ( "github.com/akhilrex/hammond/db" "github.com/akhilrex/hammond/models" + "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -243,6 +244,24 @@ func GetDistinctFuelSubtypesForVehicle(vehicleId string) ([]string, error) { return names, tx.Error } +func GetLatestOdoReadingForVehicle(vehicleId string) (int, error) { + odoReading := 0 + latestFillup, err := db.GetLatestExpenseByVehicleId(vehicleId) + if err != nil && err != gorm.ErrRecordNotFound { + return 0, err + } + odoReading = latestFillup.OdoReading + + latestExpense, err := db.GetLatestExpenseByVehicleId(vehicleId) + if err != nil && err != gorm.ErrRecordNotFound { + return 0, err + } + if latestExpense.OdoReading > odoReading { + odoReading = latestExpense.OdoReading + } + return odoReading, nil +} + func GetUserStats(userId string, model models.UserStatsQueryModel) ([]models.VehicleStatsModel, error) { vehicles, err := GetUserVehicles(userId)