Add Support to consume/redeem points
Support Creating task/chore via api Add initial Description for tasks
This commit is contained in:
parent
50b1357dfa
commit
ac733343da
11 changed files with 368 additions and 66 deletions
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
limiter "github.com/ulule/limiter/v3"
|
||||
|
||||
chModel "donetick.com/core/internal/chore/model"
|
||||
uRepo "donetick.com/core/internal/user/repo"
|
||||
)
|
||||
|
||||
|
@ -44,6 +45,46 @@ func (h *API) GetAllChores(c *gin.Context) {
|
|||
c.JSON(200, chores)
|
||||
}
|
||||
|
||||
func (h *API) CreateChore(c *gin.Context) {
|
||||
var choreRequest chModel.ChoreReq
|
||||
|
||||
apiToken := c.GetHeader("secretkey")
|
||||
if apiToken == "" {
|
||||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
user, err := h.userRepo.GetUserByToken(c, apiToken)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&choreRequest); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
chore := &chModel.Chore{
|
||||
CreatedBy: user.ID,
|
||||
CircleID: user.CircleID,
|
||||
Name: choreRequest.Name,
|
||||
IsRolling: choreRequest.IsRolling,
|
||||
FrequencyType: choreRequest.FrequencyType,
|
||||
Frequency: choreRequest.Frequency,
|
||||
AssignStrategy: choreRequest.AssignStrategy,
|
||||
AssignedTo: user.ID,
|
||||
Assignees: []chModel.ChoreAssignees{{UserID: user.ID}},
|
||||
Description: choreRequest.Description,
|
||||
}
|
||||
|
||||
_, err = h.choreRepo.CreateChore(c, chore)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, chore)
|
||||
|
||||
}
|
||||
|
||||
func APIs(cfg *config.Config, api *API, r *gin.Engine, auth *jwt.GinJWTMiddleware, limiter *limiter.Limiter) {
|
||||
|
||||
thingsAPI := r.Group("eapi/v1/chore")
|
||||
|
|
|
@ -26,36 +26,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ThingTrigger struct {
|
||||
ID int `json:"thingID" binding:"required"`
|
||||
TriggerState string `json:"triggerState" binding:"required"`
|
||||
Condition string `json:"condition"`
|
||||
}
|
||||
|
||||
type LabelReq struct {
|
||||
LabelID int `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type ChoreReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
FrequencyType chModel.FrequencyType `json:"frequencyType"`
|
||||
ID int `json:"id"`
|
||||
DueDate string `json:"dueDate"`
|
||||
Assignees []chModel.ChoreAssignees `json:"assignees"`
|
||||
AssignStrategy string `json:"assignStrategy" binding:"required"`
|
||||
AssignedTo int `json:"assignedTo"`
|
||||
IsRolling bool `json:"isRolling"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Frequency int `json:"frequency"`
|
||||
FrequencyMetadata *chModel.FrequencyMetadata `json:"frequencyMetadata"`
|
||||
Notification bool `json:"notification"`
|
||||
NotificationMetadata *chModel.NotificationMetadata `json:"notificationMetadata"`
|
||||
Labels []string `json:"labels"`
|
||||
LabelsV2 *[]LabelReq `json:"labelsV2"`
|
||||
ThingTrigger *ThingTrigger `json:"thingTrigger"`
|
||||
Points *int `json:"points"`
|
||||
CompletionWindow *int `json:"completionWindow"`
|
||||
}
|
||||
type Handler struct {
|
||||
choreRepo *chRepo.ChoreRepository
|
||||
circleRepo *cRepo.CircleRepository
|
||||
|
@ -185,7 +155,7 @@ func (h *Handler) createChore(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
// Validate chore:
|
||||
var choreReq ChoreReq
|
||||
var choreReq chModel.ChoreReq
|
||||
if err := c.ShouldBindJSON(&choreReq); err != nil {
|
||||
log.Print(err)
|
||||
c.JSON(400, gin.H{
|
||||
|
@ -284,6 +254,7 @@ func (h *Handler) createChore(c *gin.Context) {
|
|||
CircleID: currentUser.CircleID,
|
||||
Points: choreReq.Points,
|
||||
CompletionWindow: choreReq.CompletionWindow,
|
||||
Description: choreReq.Description,
|
||||
}
|
||||
id, err := h.choreRepo.CreateChore(c, createdChore)
|
||||
createdChore.ID = id
|
||||
|
@ -302,18 +273,18 @@ func (h *Handler) createChore(c *gin.Context) {
|
|||
UserID: assignee.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
labelsV2 := make([]int, len(*choreReq.LabelsV2))
|
||||
for i, label := range *choreReq.LabelsV2 {
|
||||
labelsV2[i] = int(label.LabelID)
|
||||
if choreReq.LabelsV2 != nil {
|
||||
labelsV2 := make([]int, len(*choreReq.LabelsV2))
|
||||
for i, label := range *choreReq.LabelsV2 {
|
||||
labelsV2[i] = int(label.LabelID)
|
||||
}
|
||||
if err := h.lRepo.AssignLabelsToChore(c, createdChore.ID, currentUser.ID, currentUser.CircleID, labelsV2, []int{}); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error adding labels",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := h.lRepo.AssignLabelsToChore(c, createdChore.ID, currentUser.ID, currentUser.CircleID, labelsV2, []int{}); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error adding labels",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.choreRepo.UpdateChoreAssignees(c, choreAssignees); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error adding chore assignees",
|
||||
|
@ -342,7 +313,7 @@ func (h *Handler) editChore(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var choreReq ChoreReq
|
||||
var choreReq chModel.ChoreReq
|
||||
if err := c.ShouldBindJSON(&choreReq); err != nil {
|
||||
log.Print(err)
|
||||
c.JSON(400, gin.H{
|
||||
|
@ -545,6 +516,7 @@ func (h *Handler) editChore(c *gin.Context) {
|
|||
CreatedAt: oldChore.CreatedAt,
|
||||
Points: choreReq.Points,
|
||||
CompletionWindow: choreReq.CompletionWindow,
|
||||
Description: choreReq.Description,
|
||||
}
|
||||
if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
|
@ -589,7 +561,7 @@ func (h *Handler) editChore(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func HandleThingAssociation(choreReq ChoreReq, h *Handler, c *gin.Context, currentUser *uModel.User) bool {
|
||||
func HandleThingAssociation(choreReq chModel.ChoreReq, h *Handler, c *gin.Context, currentUser *uModel.User) bool {
|
||||
if choreReq.ThingTrigger != nil {
|
||||
thing, err := h.tRepo.GetThingByID(c, choreReq.ThingTrigger.ID)
|
||||
if err != nil {
|
||||
|
@ -744,6 +716,61 @@ func (h *Handler) updateAssignee(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func (h *Handler) updateChoreStatus(c *gin.Context) {
|
||||
type StatusReq struct {
|
||||
Status *chModel.Status `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
var statusReq StatusReq
|
||||
|
||||
if err := c.ShouldBindJSON(&statusReq); err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid request",
|
||||
})
|
||||
}
|
||||
|
||||
rawID := c.Param("id")
|
||||
id, err := strconv.Atoi(rawID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
chore, err := h.choreRepo.GetChore(c, id)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting chore",
|
||||
})
|
||||
return
|
||||
}
|
||||
if chore.CircleID != currentUser.CircleID {
|
||||
|
||||
c.JSON(403, gin.H{
|
||||
"error": "You are not allowed to start this chore",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := h.choreRepo.UpdateChoreStatus(c, chore.ID, currentUser.ID, *statusReq.Status); err != nil {
|
||||
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error starting chore",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{})
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) skipChore(c *gin.Context) {
|
||||
rawID := c.Param("id")
|
||||
id, err := strconv.Atoi(rawID)
|
||||
|
@ -778,7 +805,7 @@ func (h *Handler) skipChore(c *gin.Context) {
|
|||
}
|
||||
|
||||
nextAssigedTo := chore.AssignedTo
|
||||
if err := h.choreRepo.CompleteChore(c, chore, nil, currentUser.ID, nextDueDate, nil, nextAssigedTo); err != nil {
|
||||
if err := h.choreRepo.CompleteChore(c, chore, nil, currentUser.ID, nextDueDate, nil, nextAssigedTo, false); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error completing chore",
|
||||
})
|
||||
|
@ -1024,7 +1051,7 @@ func (h *Handler) completeChore(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := h.choreRepo.CompleteChore(c, chore, additionalNotes, currentUser.ID, nextDueDate, &completedDate, nextAssignedTo); err != nil {
|
||||
if err := h.choreRepo.CompleteChore(c, chore, additionalNotes, currentUser.ID, nextDueDate, &completedDate, nextAssignedTo, true); err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error completing chore",
|
||||
})
|
||||
|
@ -1436,6 +1463,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
|
|||
choresRoutes.DELETE("/:id/history/:history_id", h.DeleteHistory)
|
||||
choresRoutes.POST("/:id/do", h.completeChore)
|
||||
choresRoutes.POST("/:id/skip", h.skipChore)
|
||||
choresRoutes.PUT("/:id/status", h.updateChoreStatus)
|
||||
choresRoutes.PUT("/:id/assignee", h.updateAssignee)
|
||||
choresRoutes.PUT("/:id/dueDate", h.updateDueDate)
|
||||
choresRoutes.PUT("/:id/archive", h.archiveChore)
|
||||
|
|
|
@ -3,7 +3,9 @@ package model
|
|||
import (
|
||||
"time"
|
||||
|
||||
lModel "donetick.com/core/internal/label/model"
|
||||
tModel "donetick.com/core/internal/thing/model"
|
||||
thingModel "donetick.com/core/internal/thing/model"
|
||||
)
|
||||
|
||||
type FrequencyType string
|
||||
|
@ -22,6 +24,16 @@ const (
|
|||
FrequancyTypeNoRepeat FrequencyType = "no_repeat"
|
||||
)
|
||||
|
||||
type AssignmentStrategy string
|
||||
|
||||
const (
|
||||
AssignmentStrategyRandom AssignmentStrategy = "random"
|
||||
AssignmentStrategyLeastAssigned AssignmentStrategy = "least_assigned"
|
||||
AssignmentStrategyLeastCompleted AssignmentStrategy = "least_completed"
|
||||
AssignmentStrategyKeepLastAssigned AssignmentStrategy = "keep_last_assigned"
|
||||
AssignmentStrategyRandomExceptLastAssigned AssignmentStrategy = "random_except_last_assigned"
|
||||
)
|
||||
|
||||
type Chore struct {
|
||||
ID int `json:"id" gorm:"primary_key"`
|
||||
Name string `json:"name" gorm:"column:name"` // Chore description
|
||||
|
@ -32,7 +44,7 @@ type Chore struct {
|
|||
IsRolling bool `json:"isRolling" gorm:"column:is_rolling"` // Whether the chore is rolling
|
||||
AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` // Who the chore is assigned to
|
||||
Assignees []ChoreAssignees `json:"assignees" gorm:"foreignkey:ChoreID;references:ID"` // Assignees of the chore
|
||||
AssignStrategy string `json:"assignStrategy" gorm:"column:assign_strategy"` // How the chore is assigned
|
||||
AssignStrategy AssignmentStrategy `json:"assignStrategy" gorm:"column:assign_strategy"` // How the chore is assigned
|
||||
IsActive bool `json:"isActive" gorm:"column:is_active"` // Whether the chore is active
|
||||
Notification bool `json:"notification" gorm:"column:notification"` // Whether the chore has notification
|
||||
NotificationMetadata *string `json:"notificationMetadata" gorm:"column:notification_meta"` // Additional notification information
|
||||
|
@ -44,11 +56,21 @@ type Chore struct {
|
|||
CreatedBy int `json:"createdBy" gorm:"column:created_by"` // Who created the chore
|
||||
UpdatedBy int `json:"updatedBy" gorm:"column:updated_by"` // Who last updated the chore
|
||||
ThingChore *tModel.ThingChore `json:"thingChore" gorm:"foreignkey:chore_id;references:id;<-:false"` // ThingChore relationship
|
||||
Status int `json:"status" gorm:"column:status"`
|
||||
Status Status `json:"status" gorm:"column:status"`
|
||||
Priority int `json:"priority" gorm:"column:priority"`
|
||||
CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"` // Number seconds before the chore is due that it can be completed
|
||||
Points *int `json:"points,omitempty" gorm:"column:points"` // Points for completing the chore
|
||||
Description *string `json:"description,omitempty" gorm:"type:text;column:description"` // Description of the chore
|
||||
}
|
||||
|
||||
type Status int8
|
||||
|
||||
const (
|
||||
ChoreStatusNoStatus Status = 0
|
||||
ChoreStatusInProgress Status = 1
|
||||
ChoreStatusPaused Status = 2
|
||||
)
|
||||
|
||||
type ChoreAssignees struct {
|
||||
ID int `json:"-" gorm:"primary_key"`
|
||||
ChoreID int `json:"-" gorm:"column:chore_id;uniqueIndex:idx_chore_user"` // The chore this assignee is for
|
||||
|
@ -69,11 +91,9 @@ type ChoreHistory struct {
|
|||
type ChoreHistoryStatus int8
|
||||
|
||||
const (
|
||||
ChoreHistoryStatusPending ChoreHistoryStatus = 0
|
||||
ChoreHistoryStatusCompleted ChoreHistoryStatus = 1
|
||||
ChoreHistoryStatusCompletedLate ChoreHistoryStatus = 2
|
||||
ChoreHistoryStatusMissed ChoreHistoryStatus = 3
|
||||
ChoreHistoryStatusSkipped ChoreHistoryStatus = 4
|
||||
ChoreHistoryStatusPending ChoreHistoryStatus = 0
|
||||
ChoreHistoryStatusCompleted ChoreHistoryStatus = 1
|
||||
ChoreHistoryStatusSkipped ChoreHistoryStatus = 2
|
||||
)
|
||||
|
||||
type FrequencyMetadata struct {
|
||||
|
@ -126,3 +146,25 @@ type ChoreLabels struct {
|
|||
UserID int `json:"userId" gorm:"primaryKey;autoIncrement:false;not null"`
|
||||
Label Label
|
||||
}
|
||||
|
||||
type ChoreReq struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
FrequencyType FrequencyType `json:"frequencyType"`
|
||||
ID int `json:"id"`
|
||||
DueDate string `json:"dueDate"`
|
||||
Assignees []ChoreAssignees `json:"assignees"`
|
||||
AssignStrategy AssignmentStrategy `json:"assignStrategy" binding:"required"`
|
||||
AssignedTo int `json:"assignedTo"`
|
||||
IsRolling bool `json:"isRolling"`
|
||||
IsActive bool `json:"isActive"`
|
||||
Frequency int `json:"frequency"`
|
||||
FrequencyMetadata *FrequencyMetadata `json:"frequencyMetadata"`
|
||||
Notification bool `json:"notification"`
|
||||
NotificationMetadata *NotificationMetadata `json:"notificationMetadata"`
|
||||
Labels []string `json:"labels"`
|
||||
LabelsV2 *[]lModel.LabelReq `json:"labelsV2"`
|
||||
ThingTrigger *thingModel.ThingTrigger `json:"thingTrigger"`
|
||||
Points *int `json:"points"`
|
||||
CompletionWindow *int `json:"completionWindow"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
|
|
@ -85,15 +85,7 @@ func (r *ChoreRepository) IsChoreOwner(c context.Context, choreID int, userID in
|
|||
return err
|
||||
}
|
||||
|
||||
// func (r *ChoreRepository) ListChores(circleID int) ([]*chModel.Chore, error) {
|
||||
// var chores []*Chore
|
||||
// if err := r.db.WithContext(c).Find(&chores).Where("is_active = ?", true).Order("next_due_date").Error; err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return chores, nil
|
||||
// }
|
||||
|
||||
func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, note *string, userID int, dueDate *time.Time, completedDate *time.Time, nextAssignedTo int) error {
|
||||
func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, note *string, userID int, dueDate *time.Time, completedDate *time.Time, nextAssignedTo int, applyPoints bool) error {
|
||||
err := r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
|
||||
ch := &chModel.ChoreHistory{
|
||||
ChoreID: chore.ID,
|
||||
|
@ -102,17 +94,18 @@ func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore,
|
|||
AssignedTo: chore.AssignedTo,
|
||||
DueDate: chore.NextDueDate,
|
||||
Note: note,
|
||||
Points: chore.Points,
|
||||
}
|
||||
if err := tx.Create(ch).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
updates["next_due_date"] = dueDate
|
||||
updates["status"] = chModel.ChoreStatusNoStatus
|
||||
|
||||
if dueDate != nil {
|
||||
updates["assigned_to"] = nextAssignedTo
|
||||
} else {
|
||||
// one time task
|
||||
updates["is_active"] = false
|
||||
}
|
||||
// Perform the update operation once, using the prepared updates map.
|
||||
|
@ -120,8 +113,8 @@ func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore,
|
|||
return err
|
||||
}
|
||||
// Update UserCirclee Points :
|
||||
if chore.Points != nil && *chore.Points > 0 {
|
||||
if err := tx.Debug().Model(&cModel.UserCircle{}).Where("user_id = ? AND circle_id = ?", userID, chore.CircleID).Update("points", gorm.Expr("points + ?", chore.Points)).Error; err != nil {
|
||||
if applyPoints && chore.Points != nil && *chore.Points > 0 {
|
||||
if err := tx.Model(&cModel.UserCircle{}).Where("user_id = ? AND circle_id = ?", userID, chore.CircleID).Update("points", gorm.Expr("points + ?", chore.Points)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -323,3 +316,7 @@ func (r *ChoreRepository) GetChoresHistoryByUserID(c context.Context, userID int
|
|||
}
|
||||
return chores, nil
|
||||
}
|
||||
|
||||
func (r *ChoreRepository) UpdateChoreStatus(c context.Context, choreID int, userId int, status chModel.Status) error {
|
||||
return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ?", choreID).Where("created_by = ? ", userId).Update("status", status).Error
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
chRepo "donetick.com/core/internal/chore/repo"
|
||||
cModel "donetick.com/core/internal/circle/model"
|
||||
cRepo "donetick.com/core/internal/circle/repo"
|
||||
pRepo "donetick.com/core/internal/points/repo"
|
||||
uModel "donetick.com/core/internal/user/model"
|
||||
uRepo "donetick.com/core/internal/user/repo"
|
||||
"donetick.com/core/logging"
|
||||
|
@ -22,13 +23,15 @@ type Handler struct {
|
|||
circleRepo *cRepo.CircleRepository
|
||||
userRepo *uRepo.UserRepository
|
||||
choreRepo *chRepo.ChoreRepository
|
||||
pointRepo *pRepo.PointsRepository
|
||||
}
|
||||
|
||||
func NewHandler(cr *cRepo.CircleRepository, ur *uRepo.UserRepository, c *chRepo.ChoreRepository) *Handler {
|
||||
func NewHandler(cr *cRepo.CircleRepository, ur *uRepo.UserRepository, c *chRepo.ChoreRepository, pr *pRepo.PointsRepository) *Handler {
|
||||
return &Handler{
|
||||
circleRepo: cr,
|
||||
userRepo: ur,
|
||||
choreRepo: c,
|
||||
pointRepo: pr,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -424,6 +427,112 @@ func (h *Handler) AcceptJoinRequest(c *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RedeemPoints(c *gin.Context) {
|
||||
type RedeemPointsRequest struct {
|
||||
Points int `json:"points"`
|
||||
UserID int `json:"userId"`
|
||||
}
|
||||
|
||||
log := logging.FromContext(c)
|
||||
currentUser, ok := auth.CurrentUser(c)
|
||||
if !ok {
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting current user",
|
||||
})
|
||||
return
|
||||
}
|
||||
// parse body:
|
||||
var redeemReq RedeemPointsRequest
|
||||
|
||||
if err := c.ShouldBind(&redeemReq); err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid request",
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if redeemReq.Points <= 0 {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
circleIdRaw := c.Param("id")
|
||||
|
||||
circleID, err := strconv.Atoi(circleIdRaw)
|
||||
if err != nil {
|
||||
log.Error("Error redeeming points: invalid circle id")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "Invalid request: invalid circle id",
|
||||
})
|
||||
return
|
||||
}
|
||||
if circleID != currentUser.CircleID {
|
||||
log.Error("You are not a member of this circle")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "You are not a member of this circle",
|
||||
})
|
||||
return
|
||||
}
|
||||
members, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID)
|
||||
if err != nil {
|
||||
log.Error("Error getting circle admins:", err)
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error getting circle admins",
|
||||
})
|
||||
return
|
||||
}
|
||||
isAdmin := false
|
||||
isValidMember := false
|
||||
var member *cModel.UserCircleDetail
|
||||
for _, user := range members {
|
||||
if user.UserID == currentUser.ID && user.Role == "admin" {
|
||||
isAdmin = true
|
||||
}
|
||||
if user.UserID == redeemReq.UserID {
|
||||
isValidMember = true
|
||||
member = user
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
log.Error("Error redeeming points: user is not an admin of this circle")
|
||||
c.JSON(403, gin.H{
|
||||
"error": "You are not an admin of this circle",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !isValidMember {
|
||||
log.Error("Error redeeming points: user is not a member of this circle")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "User is not a member of this circle",
|
||||
})
|
||||
return
|
||||
}
|
||||
if member.Points-redeemReq.Points < 0 {
|
||||
log.Error("Error redeeming points: user does not have enough points")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "User does not have enough points",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.circleRepo.RedeemPoints(c, currentUser.CircleID, currentUser.ID, redeemReq.Points, currentUser.ID)
|
||||
if err != nil {
|
||||
log.Error("Error redeeming points:", err)
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Error redeeming points",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"res": "Points redeemed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
|
||||
log.Println("Registering routes")
|
||||
|
||||
|
@ -437,6 +546,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
|
|||
circleRoutes.POST("/join", h.JoinCircle)
|
||||
circleRoutes.DELETE("/leave", h.LeaveCircle)
|
||||
circleRoutes.DELETE("/:id/members/delete", h.DeleteCircleMember)
|
||||
circleRoutes.POST("/:id/members/points/redeem", h.RedeemPoints)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
cModel "donetick.com/core/internal/circle/model"
|
||||
pModel "donetick.com/core/internal/points"
|
||||
uModel "donetick.com/core/internal/user/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
@ -138,3 +140,26 @@ func (r *CircleRepository) AssignDefaultCircle(c context.Context, userID int) er
|
|||
|
||||
return r.db.WithContext(c).Model(&uModel.User{}).Where("id = ?", userID).Update("circle_id", defaultCircle.ID).Error
|
||||
}
|
||||
|
||||
func (r *CircleRepository) RedeemPoints(c context.Context, circleID int, userID int, points int, createdBy int) error {
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&cModel.UserCircle{}).Where("user_id = ? AND circle_id = ?", userID, circleID).Update("points_redeemed", gorm.Expr("points_redeemed + ?", points)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&pModel.PointsHistory{
|
||||
Action: pModel.PointsHistoryActionRedeem,
|
||||
CircleID: circleID,
|
||||
UserID: userID,
|
||||
Points: points,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
CreatedBy: createdBy,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,3 +7,7 @@ type Label struct {
|
|||
CircleID *int `json:"-" gorm:"column:circle_id"`
|
||||
CreatedBy int `json:"created_by" gorm:"column:created_by"`
|
||||
}
|
||||
|
||||
type LabelReq struct {
|
||||
LabelID int `json:"id" binding:"required"`
|
||||
}
|
||||
|
|
21
internal/points/model.go
Normal file
21
internal/points/model.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type PointsHistory struct {
|
||||
ID int `json:"id" gorm:"primary_key"` // Unique identifier
|
||||
Action PointsHistoryAction `json:"action" gorm:"column:action"` // Action
|
||||
Points int `json:"points" gorm:"column:points"` // Points
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // Created at
|
||||
CreatedBy int `json:"created_by" gorm:"column:created_by"` // Created by
|
||||
UserID int `json:"user_id" gorm:"column:user_id;index"` // User ID
|
||||
CircleID int `json:"circle_id" gorm:"column:circle_id;index"` // Circle ID with index
|
||||
}
|
||||
|
||||
type PointsHistoryAction int8
|
||||
|
||||
const (
|
||||
PointsHistoryActionAdd PointsHistoryAction = iota
|
||||
PointsHistoryActionRemove
|
||||
PointsHistoryActionRedeem
|
||||
)
|
24
internal/points/repo/repository.go
Normal file
24
internal/points/repo/repository.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package points
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pModel "donetick.com/core/internal/points"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PointsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPointsRepository(db *gorm.DB) *PointsRepository {
|
||||
return &PointsRepository{db}
|
||||
}
|
||||
|
||||
func (r *PointsRepository) CreatePointsHistory(c context.Context, tx *gorm.DB, pointsHistory *pModel.PointsHistory) error {
|
||||
if tx != nil {
|
||||
return tx.Model(&pModel.PointsHistory{}).Save(pointsHistory).Error
|
||||
}
|
||||
|
||||
return r.db.WithContext(c).Save(pointsHistory).Error
|
||||
}
|
|
@ -28,3 +28,9 @@ type ThingChore struct {
|
|||
TriggerState string `json:"triggerState" gorm:"column:trigger_state"`
|
||||
Condition string `json:"condition" gorm:"column:condition"`
|
||||
}
|
||||
|
||||
type ThingTrigger struct {
|
||||
ID int `json:"thingID" binding:"required"`
|
||||
TriggerState string `json:"triggerState" binding:"required"`
|
||||
Condition string `json:"condition"`
|
||||
}
|
||||
|
|
4
main.go
4
main.go
|
@ -30,6 +30,7 @@ import (
|
|||
nps "donetick.com/core/internal/notifier/service"
|
||||
"donetick.com/core/internal/notifier/service/pushover"
|
||||
telegram "donetick.com/core/internal/notifier/service/telegram"
|
||||
pRepo "donetick.com/core/internal/points/repo"
|
||||
"donetick.com/core/internal/thing"
|
||||
tRepo "donetick.com/core/internal/thing/repo"
|
||||
"donetick.com/core/internal/user"
|
||||
|
@ -81,6 +82,9 @@ func main() {
|
|||
// things
|
||||
fx.Provide(tRepo.NewThingRepository),
|
||||
|
||||
// points
|
||||
fx.Provide(pRepo.NewPointsRepository),
|
||||
|
||||
// Labels:
|
||||
fx.Provide(lRepo.NewLabelRepository),
|
||||
fx.Provide(label.NewHandler),
|
||||
|
|
Loading…
Add table
Reference in a new issue