diff --git a/app/database/entity/activity_logs.entity.go b/app/database/entity/activity_logs.entity.go index 79cb8aa..c64a0e3 100644 --- a/app/database/entity/activity_logs.entity.go +++ b/app/database/entity/activity_logs.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ActivityLogs struct { @@ -14,4 +15,7 @@ type ActivityLogs struct { UserId *uint `json:"user_id" gorm:"type:int4"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + + // Relations + Article *Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` } diff --git a/app/database/entity/article_approvals.entity.go b/app/database/entity/article_approvals.entity.go index 1420090..6d66c4a 100644 --- a/app/database/entity/article_approvals.entity.go +++ b/app/database/entity/article_approvals.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ArticleApprovals struct { @@ -14,4 +15,7 @@ type ArticleApprovals struct { ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + + // Relations + Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` } diff --git a/app/database/entity/article_category_details/article_category_details.entity.go b/app/database/entity/article_category_details/article_category_details.entity.go index 50f1e16..6c4fe9e 100644 --- a/app/database/entity/article_category_details/article_category_details.entity.go +++ b/app/database/entity/article_category_details/article_category_details.entity.go @@ -1,9 +1,10 @@ package article_category_details import ( - "github.com/google/uuid" "time" entity "web-medols-be/app/database/entity" + + "github.com/google/uuid" ) type ArticleCategoryDetails struct { @@ -15,4 +16,7 @@ type ArticleCategoryDetails struct { IsActive bool `json:"is_active" gorm:"type:bool"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relations + Article entity.Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` } diff --git a/app/database/entity/article_comments.entity.go b/app/database/entity/article_comments.entity.go index 7a263ff..cec3645 100644 --- a/app/database/entity/article_comments.entity.go +++ b/app/database/entity/article_comments.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ArticleComments struct { @@ -18,6 +19,9 @@ type ArticleComments struct { IsActive bool `json:"is_active" gorm:"type:bool"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relations + Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` } // statusId => 0: waiting, 1: accepted, 2: replied, 3: rejected diff --git a/app/database/entity/article_files.entity.go b/app/database/entity/article_files.entity.go index c1891de..af6eab9 100644 --- a/app/database/entity/article_files.entity.go +++ b/app/database/entity/article_files.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ArticleFiles struct { @@ -26,4 +27,7 @@ type ArticleFiles struct { IsActive bool `json:"is_active" gorm:"type:bool;default:true"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + + // Relations + Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"` } diff --git a/app/database/entity/client_approval_settings.entity.go b/app/database/entity/client_approval_settings.entity.go index c1a389c..6caa3bd 100644 --- a/app/database/entity/client_approval_settings.entity.go +++ b/app/database/entity/client_approval_settings.entity.go @@ -1,26 +1,57 @@ package entity import ( - "github.com/google/uuid" + "database/sql/driver" + "encoding/json" "time" + + "github.com/google/uuid" ) +// StringArray is a custom type for handling string arrays with JSON serialization +type StringArray []string + +// Scan implements the sql.Scanner interface +func (s *StringArray) Scan(value interface{}) error { + if value == nil { + *s = StringArray{} + return nil + } + + switch v := value.(type) { + case []byte: + return json.Unmarshal(v, s) + case string: + return json.Unmarshal([]byte(v), s) + default: + return nil + } +} + +// Value implements the driver.Valuer interface +func (s StringArray) Value() (driver.Value, error) { + if s == nil || len(s) == 0 { + return "[]", nil + } + return json.Marshal(s) +} + type ClientApprovalSettings struct { - ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` - ClientId uuid.UUID `json:"client_id" gorm:"type:UUID;not null;uniqueIndex"` - RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed - DefaultWorkflowId *uint `json:"default_workflow_id" gorm:"type:int4"` // default workflow for this client - AutoPublishArticles *bool `json:"auto_publish_articles" gorm:"type:bool;default:false"` // auto publish after creation - ApprovalExemptUsers []uint `json:"approval_exempt_users" gorm:"type:int4[]"` // user IDs exempt from approval - ApprovalExemptRoles []uint `json:"approval_exempt_roles" gorm:"type:int4[]"` // role IDs exempt from approval - ApprovalExemptCategories []uint `json:"approval_exempt_categories" gorm:"type:int4[]"` // category IDs exempt from approval - RequireApprovalFor []string `json:"require_approval_for" gorm:"type:varchar[]"` // specific content types that need approval - SkipApprovalFor []string `json:"skip_approval_for" gorm:"type:varchar[]"` // specific content types that skip approval - IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + ClientId uuid.UUID `json:"client_id" gorm:"type:UUID;not null;uniqueIndex"` + RequiresApproval *bool `json:"requires_approval" gorm:"type:bool;default:true"` // false = no approval needed + DefaultWorkflowId *uint `json:"default_workflow_id" gorm:"type:int4"` // default workflow for this client + AutoPublishArticles *bool `json:"auto_publish_articles" gorm:"type:bool;default:false"` // auto publish after creation + ApprovalExemptUsers []uint `json:"approval_exempt_users" gorm:"type:int4[]"` // user IDs exempt from approval + ApprovalExemptRoles []uint `json:"approval_exempt_roles" gorm:"type:int4[]"` // role IDs exempt from approval + ApprovalExemptCategories []uint `json:"approval_exempt_categories" gorm:"type:int4[]"` // category IDs exempt from approval + RequireApprovalFor []string `json:"require_approval_for" gorm:"type:jsonb"` // specific content types that need approval + SkipApprovalFor []string `json:"skip_approval_for" gorm:"type:jsonb"` // specific content types that skip approval + IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` // Relations - Client Clients `json:"client" gorm:"foreignKey:ClientId;constraint:OnDelete:CASCADE"` + Client Clients `json:"client" gorm:"foreignKey:ClientId;constraint:OnDelete:CASCADE"` Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:DefaultWorkflowId"` } diff --git a/app/database/index.database.go b/app/database/index.database.go index 4a21e53..f838647 100644 --- a/app/database/index.database.go +++ b/app/database/index.database.go @@ -1,13 +1,14 @@ package database import ( + "web-medols-be/app/database/entity" + "web-medols-be/app/database/entity/article_category_details" + "web-medols-be/config/config" + "github.com/rs/zerolog" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" - "web-medols-be/app/database/entity" - "web-medols-be/app/database/entity/article_category_details" - "web-medols-be/config/config" ) // Database setup database with gorm @@ -99,6 +100,7 @@ func Models() []interface{} { entity.AuditTrails{}, entity.Cities{}, entity.Clients{}, + entity.ClientApprovalSettings{}, entity.CsrfTokenRecords{}, entity.CustomStaticPages{}, entity.Districts{}, diff --git a/app/middleware/register.middleware.go b/app/middleware/register.middleware.go index 9aae6cb..814b533 100644 --- a/app/middleware/register.middleware.go +++ b/app/middleware/register.middleware.go @@ -1,14 +1,15 @@ package middleware import ( - "github.com/gofiber/fiber/v2/middleware/csrf" - "github.com/gofiber/fiber/v2/middleware/session" "log" "time" "web-medols-be/app/database" "web-medols-be/config/config" utilsSvc "web-medols-be/utils" + "github.com/gofiber/fiber/v2/middleware/csrf" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" @@ -129,7 +130,7 @@ func (m *Middleware) Register(db *database.Database) { m.App.Use(ClientMiddleware(db.DB)) m.App.Use(AuditTrailsMiddleware(db.DB)) - StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention) + // StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention) //m.App.Use(filesystem.New(filesystem.Config{ // Next: utils.IsEnabled(m.Cfg.Middleware.FileSystem.Enable), diff --git a/app/middleware/user.middleware.go b/app/middleware/user.middleware.go new file mode 100644 index 0000000..fc521b4 --- /dev/null +++ b/app/middleware/user.middleware.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "web-medols-be/app/database/entity/users" + "web-medols-be/app/module/users/repository" + utilSvc "web-medols-be/utils/service" + + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" +) + +const ( + UserContextKey = "user" + UserLevelContextKey = "user_level_id" +) + +// UserMiddleware extracts user information from JWT token and stores in context +func UserMiddleware(usersRepo repository.UsersRepository) fiber.Handler { + return func(c *fiber.Ctx) error { + // Skip if no Authorization header + authHeader := c.Get("Authorization") + if authHeader == "" { + return c.Next() + } + + // Get user info from token + // Create a default logger if not available in context + log := zerolog.Nop() + if logFromCtx, ok := c.Locals("log").(zerolog.Logger); ok { + log = logFromCtx + } + user := utilSvc.GetUserInfo(log, usersRepo, authHeader) + if user != nil { + // Store user in context + c.Locals(UserContextKey, user) + c.Locals(UserLevelContextKey, user.UserLevelId) + } + + return c.Next() + } +} + +// GetUser retrieves the user from the context +func GetUser(c *fiber.Ctx) *users.Users { + if user, ok := c.Locals(UserContextKey).(*users.Users); ok { + return user + } + return nil +} + +// GetUserLevelID retrieves the user level ID from the context +func GetUserLevelID(c *fiber.Ctx) *uint { + if userLevelId, ok := c.Locals(UserLevelContextKey).(uint); ok { + return &userLevelId + } + return nil +} diff --git a/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go b/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go index 0bd3f7d..cd4833a 100644 --- a/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go +++ b/app/module/approval_workflow_steps/repository/approval_workflow_steps.repository.go @@ -2,12 +2,13 @@ package repository import ( "fmt" - "github.com/google/uuid" - "github.com/rs/zerolog" "web-medols-be/app/database" "web-medols-be/app/database/entity" "web-medols-be/app/module/approval_workflow_steps/request" "web-medols-be/utils/paginator" + + "github.com/google/uuid" + "github.com/rs/zerolog" ) type approvalWorkflowStepsRepository struct { @@ -79,7 +80,7 @@ func (_i *approvalWorkflowStepsRepository) GetAll(clientId *uuid.UUID, req reque query = query.Where("step_name ILIKE ?", "%"+*req.StepName+"%") } - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("workflow_id ASC, step_order ASC") err = query.Count(&count).Error @@ -120,7 +121,7 @@ func (_i *approvalWorkflowStepsRepository) FindOne(clientId *uuid.UUID, id uint) query = query.Where("client_id = ?", clientId) } - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") err = query.First(&step, id).Error return step, err @@ -157,7 +158,7 @@ func (_i *approvalWorkflowStepsRepository) GetByWorkflowId(clientId *uuid.UUID, } query = query.Where("workflow_id = ?", workflowId) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("step_order ASC") err = query.Find(&steps).Error @@ -172,7 +173,7 @@ func (_i *approvalWorkflowStepsRepository) GetActiveByWorkflowId(clientId *uuid. } query = query.Where("workflow_id = ? AND is_active = ?", workflowId, true) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("step_order ASC") err = query.Find(&steps).Error @@ -187,7 +188,7 @@ func (_i *approvalWorkflowStepsRepository) FindByWorkflowAndStep(clientId *uuid. } query = query.Where("workflow_id = ? AND step_order = ?", workflowId, stepOrder) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") err = query.First(&step).Error return step, err @@ -201,7 +202,7 @@ func (_i *approvalWorkflowStepsRepository) GetNextStep(clientId *uuid.UUID, work } query = query.Where("workflow_id = ? AND step_order > ? AND is_active = ?", workflowId, currentStep, true) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("step_order ASC") err = query.First(&step).Error @@ -216,7 +217,7 @@ func (_i *approvalWorkflowStepsRepository) GetPreviousStep(clientId *uuid.UUID, } query = query.Where("workflow_id = ? AND step_order < ? AND is_active = ?", workflowId, currentStep, true) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("step_order DESC") err = query.First(&step).Error @@ -279,7 +280,7 @@ func (_i *approvalWorkflowStepsRepository) GetStepsByUserLevel(clientId *uuid.UU } query = query.Where("required_user_level_id = ? AND is_active = ?", userLevelId, true) - query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") + query = query.Preload("Workflow").Preload("RequiredUserLevel") query = query.Order("workflow_id ASC, step_order ASC") err = query.Find(&steps).Error @@ -369,4 +370,4 @@ func (_i *approvalWorkflowStepsRepository) CheckStepDependencies(clientId *uuid. canDelete = len(dependencies) == 0 return canDelete, dependencies, nil -} \ No newline at end of file +} diff --git a/app/module/approval_workflows/request/approval_workflows.request.go b/app/module/approval_workflows/request/approval_workflows.request.go index 77e19e9..5e1d592 100644 --- a/app/module/approval_workflows/request/approval_workflows.request.go +++ b/app/module/approval_workflows/request/approval_workflows.request.go @@ -20,52 +20,73 @@ type ApprovalWorkflowsQueryRequest struct { } type ApprovalWorkflowsCreateRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description" validate:"required"` - IsActive *bool `json:"isActive"` - IsDefault *bool `json:"isDefault"` + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + IsActive *bool `json:"isActive"` + IsDefault *bool `json:"isDefault"` + RequiresApproval *bool `json:"requiresApproval"` + AutoPublish *bool `json:"autoPublish"` + Steps []ApprovalWorkflowStepRequest `json:"steps"` } func (req ApprovalWorkflowsCreateRequest) ToEntity() *entity.ApprovalWorkflows { return &entity.ApprovalWorkflows{ - Name: req.Name, - Description: &req.Description, - IsActive: req.IsActive, - IsDefault: req.IsDefault, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + Name: req.Name, + Description: &req.Description, + IsActive: req.IsActive, + IsDefault: req.IsDefault, + RequiresApproval: req.RequiresApproval, + AutoPublish: req.AutoPublish, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } func (req ApprovalWorkflowsCreateRequest) ToStepsEntity() []*entity.ApprovalWorkflowSteps { - // Return empty slice since basic create request doesn't include steps - return []*entity.ApprovalWorkflowSteps{} + steps := make([]*entity.ApprovalWorkflowSteps, len(req.Steps)) + for i, stepReq := range req.Steps { + steps[i] = &entity.ApprovalWorkflowSteps{ + StepOrder: stepReq.StepOrder, + StepName: stepReq.StepName, + RequiredUserLevelId: stepReq.RequiredUserLevelId, + CanSkip: stepReq.CanSkip, + AutoApproveAfterHours: stepReq.AutoApproveAfterHours, + IsActive: stepReq.IsActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + } + return steps } type ApprovalWorkflowsUpdateRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description" validate:"required"` - IsActive *bool `json:"isActive"` - IsDefault *bool `json:"isDefault"` + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + IsActive *bool `json:"isActive"` + IsDefault *bool `json:"isDefault"` + RequiresApproval *bool `json:"requiresApproval"` + AutoPublish *bool `json:"autoPublish"` } func (req ApprovalWorkflowsUpdateRequest) ToEntity() *entity.ApprovalWorkflows { return &entity.ApprovalWorkflows{ - Name: req.Name, - Description: &req.Description, - IsActive: req.IsActive, - IsDefault: req.IsDefault, - UpdatedAt: time.Now(), + Name: req.Name, + Description: &req.Description, + IsActive: req.IsActive, + IsDefault: req.IsDefault, + RequiresApproval: req.RequiresApproval, + AutoPublish: req.AutoPublish, + UpdatedAt: time.Now(), } } type ApprovalWorkflowStepRequest struct { - StepOrder int `json:"stepOrder" validate:"required"` - StepName string `json:"stepName" validate:"required"` - RequiredUserLevelId uint `json:"requiredUserLevelId" validate:"required"` - CanSkip *bool `json:"canSkip"` - AutoApproveAfterHours *int `json:"autoApproveAfterHours"` - IsActive *bool `json:"isActive"` + StepOrder int `json:"stepOrder" validate:"required"` + StepName string `json:"stepName" validate:"required"` + RequiredUserLevelId uint `json:"requiredUserLevelId" validate:"required"` + CanSkip *bool `json:"canSkip"` + AutoApproveAfterHours *int `json:"autoApproveAfterHours"` + IsActive *bool `json:"isActive"` } func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps { @@ -83,21 +104,25 @@ func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.Approva } type ApprovalWorkflowsWithStepsCreateRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description" validate:"required"` - IsActive *bool `json:"isActive"` - IsDefault *bool `json:"isDefault"` - Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + IsActive *bool `json:"isActive"` + IsDefault *bool `json:"isDefault"` + RequiresApproval *bool `json:"requiresApproval"` + AutoPublish *bool `json:"autoPublish"` + Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` } func (req ApprovalWorkflowsWithStepsCreateRequest) ToEntity() *entity.ApprovalWorkflows { return &entity.ApprovalWorkflows{ - Name: req.Name, - Description: &req.Description, - IsActive: req.IsActive, - IsDefault: req.IsDefault, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + Name: req.Name, + Description: &req.Description, + IsActive: req.IsActive, + IsDefault: req.IsDefault, + RequiresApproval: req.RequiresApproval, + AutoPublish: req.AutoPublish, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } @@ -119,11 +144,13 @@ func (req ApprovalWorkflowsWithStepsCreateRequest) ToStepsEntity() []*entity.App } type ApprovalWorkflowsWithStepsUpdateRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description" validate:"required"` - IsActive *bool `json:"isActive"` - IsDefault *bool `json:"isDefault"` - Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` + Name string `json:"name" validate:"required"` + Description string `json:"description" validate:"required"` + IsActive *bool `json:"isActive"` + IsDefault *bool `json:"isDefault"` + RequiresApproval *bool `json:"requiresApproval"` + AutoPublish *bool `json:"autoPublish"` + Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` } type ApprovalWorkflowsQueryRequestContext struct { @@ -165,4 +192,4 @@ func (req ApprovalWorkflowsQueryRequestContext) ToParamRequest() ApprovalWorkflo IsActive: isActive, IsDefault: isDefault, } -} \ No newline at end of file +} diff --git a/app/module/article_approval_flows/controller/article_approval_flows.controller.go b/app/module/article_approval_flows/controller/article_approval_flows.controller.go index bc537ec..840dd26 100644 --- a/app/module/article_approval_flows/controller/article_approval_flows.controller.go +++ b/app/module/article_approval_flows/controller/article_approval_flows.controller.go @@ -1,13 +1,16 @@ package controller import ( - "github.com/gofiber/fiber/v2" - "github.com/rs/zerolog" "strconv" "web-medols-be/app/middleware" "web-medols-be/app/module/article_approval_flows/request" "web-medols-be/app/module/article_approval_flows/service" + usersRepository "web-medols-be/app/module/users/repository" "web-medols-be/utils/paginator" + utilSvc "web-medols-be/utils/service" + + "github.com/gofiber/fiber/v2" + "github.com/rs/zerolog" utilRes "web-medols-be/utils/response" utilVal "web-medols-be/utils/validator" @@ -15,6 +18,7 @@ import ( type articleApprovalFlowsController struct { articleApprovalFlowsService service.ArticleApprovalFlowsService + UsersRepo usersRepository.UsersRepository Log zerolog.Logger } @@ -34,9 +38,10 @@ type ArticleApprovalFlowsController interface { GetApprovalAnalytics(c *fiber.Ctx) error } -func NewArticleApprovalFlowsController(articleApprovalFlowsService service.ArticleApprovalFlowsService, log zerolog.Logger) ArticleApprovalFlowsController { +func NewArticleApprovalFlowsController(articleApprovalFlowsService service.ArticleApprovalFlowsService, usersRepo usersRepository.UsersRepository, log zerolog.Logger) ArticleApprovalFlowsController { return &articleApprovalFlowsController{ articleApprovalFlowsService: articleApprovalFlowsService, + UsersRepo: usersRepo, Log: log, } } @@ -61,13 +66,13 @@ func (_i *articleApprovalFlowsController) All(c *fiber.Ctx) error { } reqContext := request.ArticleApprovalFlowsQueryRequestContext{ - ArticleId: c.Query("articleId"), - WorkflowId: c.Query("workflowId"), - StatusId: c.Query("statusId"), - SubmittedBy: c.Query("submittedBy"), - CurrentStep: c.Query("currentStep"), - DateFrom: c.Query("dateFrom"), - DateTo: c.Query("dateTo"), + ArticleId: c.Query("articleId"), + WorkflowId: c.Query("workflowId"), + StatusId: c.Query("statusId"), + SubmittedBy: c.Query("submittedBy"), + CurrentStep: c.Query("currentStep"), + DateFrom: c.Query("dateFrom"), + DateTo: c.Query("dateTo"), } req := reqContext.ToParamRequest() req.Pagination = paginate @@ -129,6 +134,7 @@ func (_i *articleApprovalFlowsController) Show(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req body request.SubmitForApprovalRequest true "Submit for approval data" // @Success 201 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -148,12 +154,23 @@ func (_i *articleApprovalFlowsController) SubmitForApproval(c *fiber.Ctx) error // Get ClientId from context clientId := middleware.GetClientID(c) + // Get Authorization token from header and extract user ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + var workflowId *uint if req.WorkflowId != nil { workflowIdVal := uint(*req.WorkflowId) workflowId = &workflowIdVal } - articleApprovalFlowsData, err := _i.articleApprovalFlowsService.SubmitArticleForApproval(clientId, uint(req.ArticleId), 0, workflowId) + articleApprovalFlowsData, err := _i.articleApprovalFlowsService.SubmitArticleForApproval(clientId, uint(req.ArticleId), user.ID, workflowId) if err != nil { return err } @@ -171,6 +188,7 @@ func (_i *articleApprovalFlowsController) SubmitForApproval(c *fiber.Ctx) error // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "ArticleApprovalFlows ID" // @Param req body request.ApprovalActionRequest true "Approval action data" // @Success 200 {object} response.Response @@ -196,7 +214,18 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) - err = _i.articleApprovalFlowsService.ApproveStep(clientId, uint(id), 0, req.Message) + // Get Authorization token from header and extract user ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + err = _i.articleApprovalFlowsService.ApproveStep(clientId, uint(id), user.ID, req.Message) if err != nil { return err } @@ -214,6 +243,7 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "ArticleApprovalFlows ID" // @Param req body request.RejectionRequest true "Rejection data" // @Success 200 {object} response.Response @@ -239,7 +269,18 @@ func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) - err = _i.articleApprovalFlowsService.RejectArticle(clientId, uint(id), 0, req.Reason) + // Get Authorization token from header and extract user ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + err = _i.articleApprovalFlowsService.RejectArticle(clientId, uint(id), user.ID, req.Reason) if err != nil { return err } @@ -257,6 +298,7 @@ func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "ArticleApprovalFlows ID" // @Param req body request.RevisionRequest true "Revision request data" // @Success 200 {object} response.Response @@ -282,7 +324,18 @@ func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) - err = _i.articleApprovalFlowsService.RequestRevision(clientId, uint(id), 0, req.Message) + // Get Authorization token from header and extract user ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + err = _i.articleApprovalFlowsService.RequestRevision(clientId, uint(id), user.ID, req.Message) if err != nil { return err } @@ -300,6 +353,7 @@ func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "ArticleApprovalFlows ID" // @Param req body request.ResubmitRequest true "Resubmit data" // @Success 200 {object} response.Response @@ -325,7 +379,18 @@ func (_i *articleApprovalFlowsController) Resubmit(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) - err = _i.articleApprovalFlowsService.ResubmitAfterRevision(clientId, uint(id), 0) + // Get Authorization token from header and extract user ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + err = _i.articleApprovalFlowsService.ResubmitAfterRevision(clientId, uint(id), user.ID) if err != nil { return err } @@ -343,6 +408,9 @@ func (_i *articleApprovalFlowsController) Resubmit(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param includePreview query bool false "Include article preview" +// @Param urgentOnly query bool false "Show only urgent articles" // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -358,7 +426,22 @@ func (_i *articleApprovalFlowsController) GetMyApprovalQueue(c *fiber.Ctx) error // Get ClientId from context clientId := middleware.GetClientID(c) - approvalQueueData, paging, err := _i.articleApprovalFlowsService.GetMyApprovalQueue(clientId, uint(0), paginate.Page, paginate.Limit, false, false) + // Get Authorization token from header and extract user level ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + + // Optional parameters + includePreview := c.QueryBool("includePreview", false) + urgentOnly := c.QueryBool("urgentOnly", false) + + approvalQueueData, paging, err := _i.articleApprovalFlowsService.GetMyApprovalQueue(clientId, user.UserLevelId, paginate.Page, paginate.Limit, includePreview, urgentOnly) if err != nil { return err } @@ -377,7 +460,7 @@ func (_i *articleApprovalFlowsController) GetMyApprovalQueue(c *fiber.Ctx) error // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" -// @Param userLevelId query int false "User Level ID filter" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -390,20 +473,22 @@ func (_i *articleApprovalFlowsController) GetPendingApprovals(c *fiber.Ctx) erro return err } - // userLevelId parameter is not used in current implementation - // userLevelId := 0 - // if userLevelIdStr := c.Query("userLevelId"); userLevelIdStr != "" { - // userLevelId, err = strconv.Atoi(userLevelIdStr) - // if err != nil { - // return utilRes.ErrorBadRequest(c, "Invalid userLevelId format") - // } - // } - // Get ClientId from context clientId := middleware.GetClientID(c) + // Get Authorization token from header and extract user level ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") + } + + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } + filters := make(map[string]interface{}) - pendingApprovalsData, paging, err := _i.articleApprovalFlowsService.GetPendingApprovals(clientId, uint(0), paginate.Page, paginate.Limit, filters) + pendingApprovalsData, paging, err := _i.articleApprovalFlowsService.GetPendingApprovals(clientId, user.UserLevelId, paginate.Page, paginate.Limit, filters) if err != nil { return err } @@ -475,28 +560,31 @@ func (_i *articleApprovalFlowsController) GetApprovalHistory(c *fiber.Ctx) error // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" -// @Param userLevelId query int false "User Level ID filter" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError // @Failure 500 {object} response.InternalServerError // @Router /article-approval-flows/dashboard-stats [get] func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error { - userLevelId := 0 - var err error - if userLevelIdStr := c.Query("userLevelId"); userLevelIdStr != "" { - userLevelId, err = strconv.Atoi(userLevelIdStr) - if err != nil { - return utilRes.ErrorBadRequest(c, "Invalid userLevelId format") - } + // Get ClientId from context + clientId := middleware.GetClientID(c) + + // Get Authorization token from header and extract user level ID + authToken := c.Get("Authorization") + if authToken == "" { + return utilRes.ErrorBadRequest(c, "Authorization token required") } - // Get ClientId from context - // clientId := middleware.GetClientID(c) + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + return utilRes.ErrorBadRequest(c, "Invalid authorization token") + } // TODO: Implement GetDashboardStats method in service - _ = userLevelId // suppress unused variable warning - // dashboardStatsData, err := _i.articleApprovalFlowsService.GetDashboardStats(clientId, userLevelId) + _ = clientId // suppress unused variable warning + _ = user.UserLevelId // suppress unused variable warning + // dashboardStatsData, err := _i.articleApprovalFlowsService.GetDashboardStats(clientId, user.UserLevelId) // if err != nil { // return err // } @@ -514,6 +602,7 @@ func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError @@ -543,6 +632,7 @@ func (_i *articleApprovalFlowsController) GetWorkloadStats(c *fiber.Ctx) error { // @Tags ArticleApprovalFlows // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param period query string false "Period filter (daily, weekly, monthly)" // @Param startDate query string false "Start date filter (YYYY-MM-DD)" // @Param endDate query string false "End date filter (YYYY-MM-DD)" @@ -571,4 +661,4 @@ func (_i *articleApprovalFlowsController) GetApprovalAnalytics(c *fiber.Ctx) err Messages: utilRes.Messages{"Approval analytics successfully retrieved"}, Data: nil, }) -} \ No newline at end of file +} diff --git a/app/module/article_approval_flows/service/article_approval_flows.service.go b/app/module/article_approval_flows/service/article_approval_flows.service.go index 4723f70..04ef22d 100644 --- a/app/module/article_approval_flows/service/article_approval_flows.service.go +++ b/app/module/article_approval_flows/service/article_approval_flows.service.go @@ -2,26 +2,29 @@ package service import ( "errors" - "github.com/google/uuid" - "github.com/rs/zerolog" "time" "web-medols-be/app/database/entity" + approvalWorkflowStepsRepo "web-medols-be/app/module/approval_workflow_steps/repository" + approvalWorkflowsRepo "web-medols-be/app/module/approval_workflows/repository" "web-medols-be/app/module/article_approval_flows/repository" "web-medols-be/app/module/article_approval_flows/request" - approvalWorkflowsRepo "web-medols-be/app/module/approval_workflows/repository" - approvalWorkflowStepsRepo "web-medols-be/app/module/approval_workflow_steps/repository" approvalStepLogsRepo "web-medols-be/app/module/article_approval_step_logs/repository" articlesRepo "web-medols-be/app/module/articles/repository" + usersRepo "web-medols-be/app/module/users/repository" "web-medols-be/utils/paginator" + + "github.com/google/uuid" + "github.com/rs/zerolog" ) type articleApprovalFlowsService struct { - ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository - ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository - ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository + ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository + ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository + ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository ArticleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository - ArticlesRepository articlesRepo.ArticlesRepository - Log zerolog.Logger + ArticlesRepository articlesRepo.ArticlesRepository + UsersRepository usersRepo.UsersRepository + Log zerolog.Logger } // ArticleApprovalFlowsService define interface of IArticleApprovalFlowsService @@ -63,15 +66,17 @@ func NewArticleApprovalFlowsService( approvalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository, articleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository, articlesRepository articlesRepo.ArticlesRepository, + usersRepository usersRepo.UsersRepository, log zerolog.Logger, ) ArticleApprovalFlowsService { return &articleApprovalFlowsService{ - ArticleApprovalFlowsRepository: articleApprovalFlowsRepository, - ApprovalWorkflowsRepository: approvalWorkflowsRepository, - ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository, - ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository, - ArticlesRepository: articlesRepository, - Log: log, + ArticleApprovalFlowsRepository: articleApprovalFlowsRepository, + ApprovalWorkflowsRepository: approvalWorkflowsRepository, + ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository, + ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository, + ArticlesRepository: articlesRepository, + UsersRepository: usersRepository, + Log: log, } } @@ -138,7 +143,7 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U ArticleId: articleId, WorkflowId: workflow.ID, CurrentStep: 1, - StatusId: 1, // pending + StatusId: 1, // pending SubmittedById: submittedById, SubmittedAt: time.Now(), } @@ -148,30 +153,24 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U return nil, err } - // Update article status and workflow info - articleUpdate := &entity.Articles{ - WorkflowId: &workflow.ID, - CurrentApprovalStep: &flow.CurrentStep, - StatusId: &[]int{1}[0], // pending approval - } - - err = _i.ArticlesRepository.Update(clientId, articleId, articleUpdate) + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, articleId) if err != nil { return nil, err } - // Create initial step log - stepLog := &entity.ArticleApprovalStepLogs{ - ApprovalFlowId: flow.ID, - StepOrder: 1, - StepName: firstStep.StepName, - Action: "submitted", - Message: &[]string{"Article submitted for approval"}[0], - ProcessedAt: time.Now(), - UserLevelId: firstStep.RequiredUserLevelId, + // Update only the necessary fields + currentArticle.WorkflowId = &workflow.ID + currentArticle.CurrentApprovalStep = &flow.CurrentStep + currentArticle.StatusId = &[]int{1}[0] // pending approval + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, articleId, currentArticle) + if err != nil { + return nil, err } - _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) + // Process auto-skip logic based on user level + err = _i.processAutoSkipSteps(clientId, flow, submittedById) if err != nil { return nil, err } @@ -179,6 +178,160 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U return flow, nil } +// processAutoSkipSteps handles automatic step skipping based on user level +func (_i *articleApprovalFlowsService) processAutoSkipSteps(clientId *uuid.UUID, flow *entity.ArticleApprovalFlows, submittedById uint) error { + // Get user level of the submitter + userLevelId, err := _i.getUserLevelId(clientId, submittedById) + if err != nil { + return err + } + + // Get all workflow steps + steps, err := _i.ApprovalWorkflowStepsRepository.GetByWorkflowId(clientId, flow.WorkflowId) + if err != nil { + return err + } + + // Sort steps by step order + sortStepsByOrder(steps) + + // Process each step to determine if it should be auto-skipped + for _, step := range steps { + shouldSkip := _i.shouldSkipStep(userLevelId, step.RequiredUserLevelId) + + if shouldSkip { + // Create skip log + stepLog := &entity.ArticleApprovalStepLogs{ + ApprovalFlowId: flow.ID, + StepOrder: step.StepOrder, + StepName: step.StepName, + ApprovedById: &submittedById, + Action: "auto_skip", + Message: &[]string{"Step auto-skipped due to user level"}[0], + ProcessedAt: time.Now(), + UserLevelId: step.RequiredUserLevelId, + } + + _, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) + if err != nil { + return err + } + + // Update flow to next step (handle step order starting from 0) + nextStepOrder := step.StepOrder + 1 + flow.CurrentStep = nextStepOrder + } else { + // Stop at first step that cannot be skipped + break + } + } + + // Update flow with final current step + err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) + if err != nil { + return err + } + + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err + } + + // Update only the necessary fields + currentArticle.CurrentApprovalStep = &flow.CurrentStep + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) + if err != nil { + return err + } + + // Check if all steps were skipped (workflow complete) + // Find the highest step order + maxStepOrder := 0 + for _, step := range steps { + if step.StepOrder > maxStepOrder { + maxStepOrder = step.StepOrder + } + } + + if flow.CurrentStep > maxStepOrder { + // All steps completed, mark as approved + flow.StatusId = 2 // approved + flow.CompletedAt = &[]time.Time{time.Now()}[0] + + err = _i.ArticleApprovalFlowsRepository.Update(flow.ID, flow) + if err != nil { + return err + } + + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err + } + + // Update only the necessary fields + currentArticle.StatusId = &[]int{2}[0] // approved + currentArticle.CurrentApprovalStep = nil + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) + if err != nil { + return err + } + } + + return nil +} + +// getUserLevelId gets the user level ID for a given user +func (_i *articleApprovalFlowsService) getUserLevelId(clientId *uuid.UUID, userId uint) (uint, error) { + // Get user from database to retrieve user level + user, err := _i.UsersRepository.FindOne(clientId, userId) + if err != nil { + _i.Log.Error().Err(err).Uint("userId", userId).Msg("Failed to find user") + return 0, err + } + + if user.UserLevel == nil { + _i.Log.Error().Uint("userId", userId).Msg("User has no user level") + return 0, errors.New("user has no user level") + } + + _i.Log.Info(). + Uint("userId", userId). + Uint("userLevelId", user.UserLevel.ID). + Str("userLevelName", user.UserLevel.Name). + Msg("Retrieved user level from database") + + return user.UserLevel.ID, nil +} + +// shouldSkipStep determines if a step should be auto-skipped based on user level +func (_i *articleApprovalFlowsService) shouldSkipStep(userLevelId, requiredLevelId uint) bool { + // Get user level details to compare level numbers + // User level with lower level_number (higher authority) can skip steps requiring higher level_number + // For now, we'll use a simple comparison based on IDs + // In production, this should compare level_number fields + + // Simple logic: if user level ID is less than required level ID, they can skip + // This assumes level 1 (ID=1) has higher authority than level 2 (ID=2), etc. + return userLevelId < requiredLevelId +} + +// sortStepsByOrder sorts workflow steps by their step order +func sortStepsByOrder(steps []*entity.ApprovalWorkflowSteps) { + // Simple bubble sort for step order + n := len(steps) + for i := 0; i < n-1; i++ { + for j := 0; j < n-i-1; j++ { + if steps[j].StepOrder > steps[j+1].StepOrder { + steps[j], steps[j+1] = steps[j+1], steps[j] + } + } + } +} + func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId uint, approvedById uint, message string) (err error) { // Get approval flow flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) @@ -239,13 +392,17 @@ func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId u return err } - // Update article status - articleUpdate := &entity.Articles{ - StatusId: &[]int{2}[0], // approved - CurrentApprovalStep: nil, + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err } - err = _i.ArticlesRepository.Update(clientId, flow.ArticleId, articleUpdate) + // Update only the necessary fields + currentArticle.StatusId = &[]int{2}[0] // approved + currentArticle.CurrentApprovalStep = nil + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } @@ -261,12 +418,16 @@ func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId u return err } - // Update article current step - articleUpdate := &entity.Articles{ - CurrentApprovalStep: &nextStep.StepOrder, + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err } - err = _i.ArticlesRepository.Update(clientId, flow.ArticleId, articleUpdate) + // Update only the necessary fields + currentArticle.CurrentApprovalStep = &nextStep.StepOrder + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } @@ -325,13 +486,17 @@ func (_i *articleApprovalFlowsService) RejectArticle(clientId *uuid.UUID, flowId return err } - // Update article status - articleUpdate := &entity.Articles{ - StatusId: &[]int{3}[0], // rejected - CurrentApprovalStep: nil, + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err } - err = _i.ArticlesRepository.Update(clientId, flow.ArticleId, articleUpdate) + // Update only the necessary fields + currentArticle.StatusId = &[]int{3}[0] // rejected + currentArticle.CurrentApprovalStep = nil + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } @@ -379,9 +544,9 @@ func (_i *articleApprovalFlowsService) RequestRevision(clientId *uuid.UUID, flow // Update approval flow status flowUpdate := &entity.ArticleApprovalFlows{ - StatusId: 4, // revision_requested - RevisionRequested: &[]bool{true}[0], - RevisionMessage: &revisionMessage, + StatusId: 4, // revision_requested + RevisionRequested: &[]bool{true}[0], + RevisionMessage: &revisionMessage, } err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate) @@ -389,12 +554,16 @@ func (_i *articleApprovalFlowsService) RequestRevision(clientId *uuid.UUID, flow return err } - // Update article status - articleUpdate := &entity.Articles{ - StatusId: &[]int{4}[0], // revision_requested + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err } - err = _i.ArticlesRepository.Update(clientId, flow.ArticleId, articleUpdate) + // Update only the necessary fields + currentArticle.StatusId = &[]int{4}[0] // revision_requested + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } @@ -420,7 +589,7 @@ func (_i *articleApprovalFlowsService) ResubmitAfterRevision(clientId *uuid.UUID // Reset approval flow to pending flowUpdate := &entity.ArticleApprovalFlows{ StatusId: 1, // pending - RevisionRequested: &[]bool{false}[0], + RevisionRequested: &[]bool{false}[0], RevisionMessage: nil, CurrentStep: 1, // restart from first step } @@ -430,13 +599,17 @@ func (_i *articleApprovalFlowsService) ResubmitAfterRevision(clientId *uuid.UUID return err } - // Update article status - articleUpdate := &entity.Articles{ - StatusId: &[]int{1}[0], // pending approval - CurrentApprovalStep: &[]int{1}[0], + // Get current article data first + currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId) + if err != nil { + return err } - err = _i.ArticlesRepository.Update(clientId, flow.ArticleId, articleUpdate) + // Update only the necessary fields + currentArticle.StatusId = &[]int{1}[0] // pending approval + currentArticle.CurrentApprovalStep = &[]int{1}[0] + + err = _i.ArticlesRepository.UpdateSkipNull(clientId, flow.ArticleId, currentArticle) if err != nil { return err } @@ -603,4 +776,4 @@ func (_i *articleApprovalFlowsService) GetNextStepPreview(clientId *uuid.UUID, f } return nextStep, nil -} \ No newline at end of file +} diff --git a/app/module/articles/articles.module.go b/app/module/articles/articles.module.go index 74525a3..09d106b 100644 --- a/app/module/articles/articles.module.go +++ b/app/module/articles/articles.module.go @@ -1,17 +1,21 @@ package articles import ( - "github.com/gofiber/fiber/v2" - "go.uber.org/fx" + "web-medols-be/app/middleware" "web-medols-be/app/module/articles/controller" "web-medols-be/app/module/articles/repository" "web-medols-be/app/module/articles/service" + usersRepo "web-medols-be/app/module/users/repository" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" ) // ArticlesRouter struct of ArticlesRouter type ArticlesRouter struct { App fiber.Router Controller *controller.Controller + UsersRepo usersRepo.UsersRepository } // NewArticlesModule register bulky of Articles module @@ -30,10 +34,11 @@ var NewArticlesModule = fx.Options( ) // NewArticlesRouter init ArticlesRouter -func NewArticlesRouter(fiber *fiber.App, controller *controller.Controller) *ArticlesRouter { +func NewArticlesRouter(fiber *fiber.App, controller *controller.Controller, usersRepo usersRepo.UsersRepository) *ArticlesRouter { return &ArticlesRouter{ App: fiber, Controller: controller, + UsersRepo: usersRepo, } } @@ -44,6 +49,8 @@ func (_i *ArticlesRouter) RegisterArticlesRoutes() { // define routes _i.App.Route("/articles", func(router fiber.Router) { + // Add user middleware to extract user level from JWT token + router.Use(middleware.UserMiddleware(_i.UsersRepo)) router.Get("/", articlesController.All) router.Get("/old-id/:id", articlesController.ShowByOldId) router.Get("/:id", articlesController.Show) @@ -57,10 +64,11 @@ func (_i *ArticlesRouter) RegisterArticlesRoutes() { router.Get("/statistic/summary", articlesController.SummaryStats) router.Get("/statistic/user-levels", articlesController.ArticlePerUserLevelStats) router.Get("/statistic/monthly", articlesController.ArticleMonthlyStats) - + // Dynamic approval system routes router.Post("/:id/submit-approval", articlesController.SubmitForApproval) router.Get("/:id/approval-status", articlesController.GetApprovalStatus) router.Get("/pending-approval", articlesController.GetPendingApprovals) + router.Get("/waiting-for-approval", articlesController.GetArticlesWaitingForApproval) }) } diff --git a/app/module/articles/controller/articles.controller.go b/app/module/articles/controller/articles.controller.go index efbbc69..797d241 100644 --- a/app/module/articles/controller/articles.controller.go +++ b/app/module/articles/controller/articles.controller.go @@ -38,6 +38,7 @@ type ArticlesController interface { SubmitForApproval(c *fiber.Ctx) error GetApprovalStatus(c *fiber.Ctx) error GetPendingApprovals(c *fiber.Ctx) error + GetArticlesWaitingForApproval(c *fiber.Ctx) error } func NewArticlesController(articlesService service.ArticlesService, log zerolog.Logger) ArticlesController { @@ -53,6 +54,7 @@ func NewArticlesController(articlesService service.ArticlesService, log zerolog. // @Tags Articles // @Security Bearer // @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req query request.ArticlesQueryRequest false "query parameters" // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response @@ -84,9 +86,12 @@ func (_i *articlesController) All(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) + // Get Authorization token from header + authToken := c.Get("Authorization") _i.Log.Info().Interface("clientId", clientId).Msg("") + _i.Log.Info().Str("authToken", authToken).Msg("") - articlesData, paging, err := _i.articlesService.All(clientId, req) + articlesData, paging, err := _i.articlesService.All(clientId, authToken, req) if err != nil { return err } @@ -188,6 +193,9 @@ func (_i *articlesController) Save(c *fiber.Ctx) error { // Get ClientId from context clientId := middleware.GetClientID(c) + _i.Log.Info().Interface("clientId", clientId).Msg("") + _i.Log.Info().Interface("authToken", authToken).Msg("") + dataResult, err := _i.articlesService.Save(clientId, *req, authToken) if err != nil { return err @@ -613,3 +621,46 @@ func (_i *articlesController) GetPendingApprovals(c *fiber.Ctx) error { Meta: paging, }) } + +// GetArticlesWaitingForApproval +// @Summary Get articles waiting for approval by current user level +// @Description API for getting articles that are waiting for approval by the current user's level +// @Tags Articles +// @Security Bearer +// @Param X-Client-Key header string true "Client Key" +// @Param page query int false "Page number" default(1) +// @Param limit query int false "Items per page" default(10) +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /articles/waiting-for-approval [get] +func (_i *articlesController) GetArticlesWaitingForApproval(c *fiber.Ctx) error { + page, err := strconv.Atoi(c.Query("page", "1")) + if err != nil { + return err + } + + limit, err := strconv.Atoi(c.Query("limit", "10")) + if err != nil { + return err + } + + // Get ClientId from context + clientId := middleware.GetClientID(c) + + // Get user level from middleware + userLevelId := middleware.GetUserLevelID(c) + + responses, paging, err := _i.articlesService.GetArticlesWaitingForApproval(clientId, *userLevelId, page, limit) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Articles waiting for approval retrieved successfully"}, + Data: responses, + Meta: paging, + }) +} diff --git a/app/module/articles/repository/articles.repository.go b/app/module/articles/repository/articles.repository.go index c356184..28c5e0a 100644 --- a/app/module/articles/repository/articles.repository.go +++ b/app/module/articles/repository/articles.repository.go @@ -22,7 +22,7 @@ type articlesRepository struct { // ArticlesRepository define interface of IArticlesRepository type ArticlesRepository interface { - GetAll(clientId *uuid.UUID, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) + GetAll(clientId *uuid.UUID, userLevelId *uint, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) GetAllPublishSchedule(clientId *uuid.UUID) (articless []*entity.Articles, err error) FindOne(clientId *uuid.UUID, id uint) (articles *entity.Articles, err error) FindByFilename(clientId *uuid.UUID, thumbnailName string) (articleReturn *entity.Articles, err error) @@ -44,7 +44,7 @@ func NewArticlesRepository(db *database.Database, log zerolog.Logger) ArticlesRe } // implement interface of IArticlesRepository -func (_i *articlesRepository) GetAll(clientId *uuid.UUID, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) { +func (_i *articlesRepository) GetAll(clientId *uuid.UUID, userLevelId *uint, req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) { var count int64 query := _i.DB.DB.Model(&entity.Articles{}) @@ -54,6 +54,59 @@ func (_i *articlesRepository) GetAll(clientId *uuid.UUID, req request.ArticlesQu query = query.Where("client_id = ?", clientId) } + // Add approval workflow filtering based on user level + if userLevelId != nil { + // Complete filtering logic for article visibility + query = query.Where(` + ( + -- Articles that don't require approval + (bypass_approval = true OR approval_exempt = true) + OR + -- Articles that are published + (is_publish = true) + OR + -- Articles where this user level is an approver in current step + ( + workflow_id IS NOT NULL + AND current_approval_step > 0 + AND EXISTS ( + SELECT 1 FROM article_approval_flows aaf + JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id + WHERE aaf.article_id = articles.id + AND aaf.status_id = 1 + AND aws.required_user_level_id = ? + AND aws.step_order = aaf.current_step + ) + ) + OR + -- Articles created by users at same or lower hierarchy + EXISTS ( + SELECT 1 FROM users u + JOIN user_levels ul ON u.user_level_id = ul.id + WHERE u.id = articles.created_by_id + AND ul.level_number >= ( + SELECT ul2.level_number FROM user_levels ul2 WHERE ul2.id = ? + ) + ) + OR + -- Articles where this user level is ANY approver in the workflow + ( + workflow_id IS NOT NULL + AND EXISTS ( + SELECT 1 FROM article_approval_flows aaf + WHERE aaf.article_id = articles.id + AND aaf.status_id IN (1, 4) + AND EXISTS ( + SELECT 1 FROM approval_workflow_steps aws + WHERE aws.workflow_id = aaf.workflow_id + AND aws.required_user_level_id = ? + ) + ) + ) + ) + `, *userLevelId, *userLevelId, *userLevelId) + } + if req.CategoryId != nil { query = query.Joins("JOIN article_category_details acd ON acd.article_id = articles.id"). Where("acd.category_id = ?", req.CategoryId) @@ -207,6 +260,12 @@ func (_i *articlesRepository) Update(clientId *uuid.UUID, id uint, articles *ent if err != nil { return err } + + // Remove fields that could cause foreign key constraint violations + // delete(articlesMap, "workflow_id") + // delete(articlesMap, "id") + // delete(articlesMap, "created_at") + return _i.DB.DB.Model(&entity.Articles{}). Where(&entity.Articles{ID: id}). Updates(articlesMap).Error @@ -224,9 +283,16 @@ func (_i *articlesRepository) UpdateSkipNull(clientId *uuid.UUID, id uint, artic } } + // Create a copy to avoid modifying the original struct + updateData := *articles + // Clear fields that could cause foreign key constraint violations + updateData.WorkflowId = nil + updateData.ID = 0 + updateData.CreatedAt = time.Time{} + return _i.DB.DB.Model(&entity.Articles{}). Where(&entity.Articles{ID: id}). - Updates(articles).Error + Updates(&updateData).Error } func (_i *articlesRepository) Delete(clientId *uuid.UUID, id uint) error { @@ -241,7 +307,9 @@ func (_i *articlesRepository) Delete(clientId *uuid.UUID, id uint) error { } } - return _i.DB.DB.Delete(&entity.Articles{}, id).Error + // Use soft delete by setting is_active to false + isActive := false + return _i.DB.DB.Model(&entity.Articles{}).Where("id = ?", id).Update("is_active", isActive).Error } func (_i *articlesRepository) SummaryStats(clientId *uuid.UUID, userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error) { diff --git a/app/module/articles/service/articles.service.go b/app/module/articles/service/articles.service.go index e1857b8..019b78e 100644 --- a/app/module/articles/service/articles.service.go +++ b/app/module/articles/service/articles.service.go @@ -47,15 +47,15 @@ type articlesService struct { Cfg *config.Config UsersRepo usersRepository.UsersRepository MinioStorage *minioStorage.MinioStorage - + // Dynamic approval system dependencies - ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository - ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository + ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository + ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository } // ArticlesService define interface of IArticlesService type ArticlesService interface { - All(clientId *uuid.UUID, req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error) + All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articles []*response.ArticlesResponse, paging paginator.Pagination, err error) Show(clientId *uuid.UUID, id uint) (articles *response.ArticlesResponse, err error) ShowByOldId(clientId *uuid.UUID, oldId uint) (articles *response.ArticlesResponse, err error) Save(clientId *uuid.UUID, req request.ArticlesCreateRequest, authToken string) (articles *entity.Articles, err error) @@ -71,12 +71,13 @@ type ArticlesService interface { ArticleMonthlyStats(clientId *uuid.UUID, authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error) PublishScheduling(clientId *uuid.UUID, id uint, publishSchedule string) error ExecuteScheduling() error - + // Dynamic approval system methods SubmitForApproval(clientId *uuid.UUID, articleId uint, submittedById uint, workflowId *uint) error GetApprovalStatus(clientId *uuid.UUID, articleId uint) (*response.ArticleApprovalStatusResponse, error) + GetArticlesWaitingForApproval(clientId *uuid.UUID, userLevelId uint, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) - + // No-approval system methods CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) AutoApproveArticle(clientId *uuid.UUID, articleId uint, reason string) error @@ -115,7 +116,17 @@ func NewArticlesService( } // All implement interface of ArticlesService -func (_i *articlesService) All(clientId *uuid.UUID, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) { +func (_i *articlesService) All(clientId *uuid.UUID, authToken string, req request.ArticlesQueryRequest) (articless []*response.ArticlesResponse, paging paginator.Pagination, err error) { + // Extract userLevelId from authToken + var userLevelId *uint + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user != nil { + userLevelId = &user.UserLevelId + _i.Log.Info().Interface("userLevelId", userLevelId).Msg("Extracted userLevelId from auth token") + } + } + if req.Category != nil { findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *req.Category) if err != nil { @@ -124,7 +135,7 @@ func (_i *articlesService) All(clientId *uuid.UUID, req request.ArticlesQueryReq req.CategoryId = &findCategory.ID } - results, paging, err := _i.Repo.GetAll(clientId, req) + results, paging, err := _i.Repo.GetAll(clientId, userLevelId, req) if err != nil { return } @@ -170,7 +181,7 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR newReq := req.ToEntity() var userLevelNumber int - var userParentLevelId int + var approvalLevelId int if req.CreatedById != nil { createdBy, err := _i.UsersRepo.FindOne(clientId, *req.CreatedById) if err != nil { @@ -178,16 +189,16 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR } newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber - if createdBy.UserLevel.ParentLevelId != nil { - userParentLevelId = *createdBy.UserLevel.ParentLevelId - } + + // Find the next higher level for approval (level_number should be smaller) + approvalLevelId = _i.findNextApprovalLevel(clientId, userLevelNumber) } else { createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) newReq.CreatedById = &createdBy.ID userLevelNumber = createdBy.UserLevel.LevelNumber - if createdBy.UserLevel.ParentLevelId != nil { - userParentLevelId = *createdBy.UserLevel.ParentLevelId - } + + // Find the next higher level for approval (level_number should be smaller) + approvalLevelId = _i.findNextApprovalLevel(clientId, userLevelNumber) } isDraft := true @@ -219,19 +230,29 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR newReq.CreatedAt = parsedTime } - // Approval + // Dynamic Approval Workflow System statusIdOne := 1 statusIdTwo := 2 isPublishFalse := false + + // Get user info for approval logic createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + + // Check if user level requires approval if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false { + // User level doesn't require approval - auto publish newReq.NeedApprovalFrom = nil newReq.StatusId = &statusIdTwo + newReq.IsPublish = &isPublishFalse + newReq.PublishedAt = nil + newReq.BypassApproval = &[]bool{true}[0] } else { - newReq.NeedApprovalFrom = &userParentLevelId + // User level requires approval - set to pending + newReq.NeedApprovalFrom = &approvalLevelId newReq.StatusId = &statusIdOne newReq.IsPublish = &isPublishFalse newReq.PublishedAt = nil + newReq.BypassApproval = &[]bool{false}[0] } saveArticleRes, err := _i.Repo.Create(clientId, newReq) @@ -239,30 +260,63 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR return nil, err } - // Approval - var articleApproval *entity.ArticleApprovals - + // Dynamic Approval Workflow Assignment if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { - articleApproval = &entity.ArticleApprovals{ + // Get default workflow for the client + defaultWorkflow, err := _i.ApprovalWorkflowsRepo.GetDefault(clientId) + if err == nil && defaultWorkflow != nil { + // Assign workflow to article + saveArticleRes.WorkflowId = &defaultWorkflow.ID + saveArticleRes.CurrentApprovalStep = &[]int{1}[0] // Start at step 1 + + // Update article with workflow info + err = _i.Repo.Update(clientId, saveArticleRes.ID, saveArticleRes) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to update article with workflow") + } + + // Create approval flow + approvalFlow := &entity.ArticleApprovalFlows{ + ArticleId: saveArticleRes.ID, + WorkflowId: defaultWorkflow.ID, + CurrentStep: 1, + StatusId: 1, // In Progress + SubmittedById: *newReq.CreatedById, + SubmittedAt: time.Now(), + ClientId: clientId, + } + + _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create approval flow") + } + } + + // Create legacy approval record for backward compatibility + articleApproval := &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdOne, Message: "Need Approval", - ApprovalAtLevel: &userLevelNumber, + ApprovalAtLevel: &approvalLevelId, + } + _, err = _i.ArticleApprovalsRepo.Create(articleApproval) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create legacy approval record") } } else { - articleApproval = &entity.ArticleApprovals{ + // Auto-publish for users who don't require approval + articleApproval := &entity.ArticleApprovals{ ArticleId: saveArticleRes.ID, ApprovalBy: *newReq.CreatedById, StatusId: statusIdTwo, Message: "Publish Otomatis", ApprovalAtLevel: nil, } - } - - _, err = _i.ArticleApprovalsRepo.Create(articleApproval) - if err != nil { - return nil, err + _, err = _i.ArticleApprovalsRepo.Create(articleApproval) + if err != nil { + _i.Log.Error().Err(err).Msg("Failed to create auto-approval record") + } } var categoryIds []string @@ -395,14 +449,7 @@ func (_i *articlesService) Update(clientId *uuid.UUID, id uint, req request.Arti } func (_i *articlesService) Delete(clientId *uuid.UUID, id uint) error { - result, err := _i.Repo.FindOne(clientId, id) - if err != nil { - return err - } - - isActive := false - result.IsActive = &isActive - return _i.Repo.Update(clientId, id, result) + return _i.Repo.Delete(clientId, id) } func (_i *articlesService) Viewer(clientId *uuid.UUID, c *fiber.Ctx) (err error) { @@ -709,11 +756,12 @@ func (_i *articlesService) SubmitForApproval(clientId *uuid.UUID, articleId uint // Create approval flow approvalFlow := &entity.ArticleApprovalFlows{ - ArticleId: articleId, - WorkflowId: *workflowId, - CurrentStep: 1, - StatusId: 1, // 1 = In Progress - ClientId: clientId, + ArticleId: articleId, + WorkflowId: *workflowId, + CurrentStep: 1, + StatusId: 1, // 1 = In Progress + ClientId: clientId, + SubmittedById: submittedById, } _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) @@ -787,19 +835,19 @@ func (_i *articlesService) GetApprovalStatus(clientId *uuid.UUID, articleId uint status = "revision_requested" } - // Get current approver info - var currentApprover *string - var nextStep *string - if approvalFlow.CurrentStep <= totalSteps && approvalFlow.StatusId == 1 { - if approvalFlow.CurrentStep < totalSteps { - // Array indexing starts from 0, so subtract 1 from CurrentStep - nextStepIndex := approvalFlow.CurrentStep - 1 - if nextStepIndex >= 0 && nextStepIndex < len(workflowSteps) { - nextStepInfo := workflowSteps[nextStepIndex] - nextStep = &nextStepInfo.RequiredUserLevel.Name - } + // Get current approver info + var currentApprover *string + var nextStep *string + if approvalFlow.CurrentStep <= totalSteps && approvalFlow.StatusId == 1 { + if approvalFlow.CurrentStep < totalSteps { + // Array indexing starts from 0, so subtract 1 from CurrentStep + nextStepIndex := approvalFlow.CurrentStep - 1 + if nextStepIndex >= 0 && nextStepIndex < len(workflowSteps) { + nextStepInfo := workflowSteps[nextStepIndex] + nextStep = &nextStepInfo.RequiredUserLevel.Name } } + } return &response.ArticleApprovalStatusResponse{ ArticleId: articleId, @@ -877,20 +925,20 @@ func (_i *articlesService) GetPendingApprovals(clientId *uuid.UUID, userLevelId } response := &response.ArticleApprovalQueueResponse{ - ID: article.ID, - Title: article.Title, - Slug: article.Slug, - Description: article.Description, - CategoryName: categoryName, - AuthorName: authorName, - SubmittedAt: flow.CreatedAt, - CurrentStep: flow.CurrentStep, - TotalSteps: len(workflowSteps), - Priority: priority, - DaysInQueue: daysInQueue, - WorkflowName: workflow.Name, - CanApprove: true, // TODO: Implement based on user permissions - EstimatedTime: "2-3 days", // TODO: Calculate based on historical data + ID: article.ID, + Title: article.Title, + Slug: article.Slug, + Description: article.Description, + CategoryName: categoryName, + AuthorName: authorName, + SubmittedAt: flow.CreatedAt, + CurrentStep: flow.CurrentStep, + TotalSteps: len(workflowSteps), + Priority: priority, + DaysInQueue: daysInQueue, + WorkflowName: workflow.Name, + CanApprove: true, // TODO: Implement based on user permissions + EstimatedTime: "2-3 days", // TODO: Calculate based on historical data } responses = append(responses, response) @@ -899,6 +947,40 @@ func (_i *articlesService) GetPendingApprovals(clientId *uuid.UUID, userLevelId return responses, paging, nil } +// GetArticlesWaitingForApproval gets articles that are waiting for approval by a specific user level +func (_i *articlesService) GetArticlesWaitingForApproval(clientId *uuid.UUID, userLevelId uint, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error) { + // Use the existing repository method with proper filtering + pagination := paginator.Pagination{ + Page: page, + Limit: limit, + } + req := request.ArticlesQueryRequest{ + Pagination: &pagination, + } + + articles, paging, err := _i.Repo.GetAll(clientId, &userLevelId, req) + if err != nil { + return nil, paging, err + } + + // Build response + var responses []*response.ArticleApprovalQueueResponse + for _, article := range articles { + response := &response.ArticleApprovalQueueResponse{ + ID: article.ID, + Title: article.Title, + Slug: article.Slug, + Description: article.Description, + SubmittedAt: article.CreatedAt, + CurrentStep: 1, // Will be updated with actual step + CanApprove: true, + } + responses = append(responses, response) + } + + return responses, paging, nil +} + // CheckApprovalRequired checks if an article requires approval based on client settings func (_i *articlesService) CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) { // Get article to check category and other properties @@ -920,7 +1002,7 @@ func (_i *articlesService) CheckApprovalRequired(clientId *uuid.UUID, articleId // Check client-level settings (this would require the client approval settings service) // For now, we'll use a simple check // TODO: Integrate with ClientApprovalSettingsService - + // Check if workflow is set to no approval if article.WorkflowId != nil { workflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, *article.WorkflowId) @@ -947,9 +1029,9 @@ func (_i *articlesService) AutoApproveArticle(clientId *uuid.UUID, articleId uin // Update article status to approved updates := map[string]interface{}{ - "status_id": 2, // Assuming 2 = approved - "is_publish": true, - "published_at": time.Now(), + "status_id": 2, // Assuming 2 = approved + "is_publish": true, + "published_at": time.Now(), "current_approval_step": 0, // Reset approval step } @@ -964,7 +1046,7 @@ func (_i *articlesService) AutoApproveArticle(clientId *uuid.UUID, articleId uin if currentApprovalStep, ok := updates["current_approval_step"].(int); ok { articleUpdate.CurrentApprovalStep = ¤tApprovalStep } - + err = _i.Repo.Update(clientId, articleId, articleUpdate) if err != nil { return err @@ -1033,7 +1115,7 @@ func (_i *articlesService) SetArticleApprovalExempt(clientId *uuid.UUID, article if currentApprovalStep, ok := updates["current_approval_step"].(int); ok { articleUpdate.CurrentApprovalStep = ¤tApprovalStep } - + err := _i.Repo.Update(clientId, articleId, articleUpdate) if err != nil { return err @@ -1048,3 +1130,21 @@ func (_i *articlesService) SetArticleApprovalExempt(clientId *uuid.UUID, article return nil } + +// findNextApprovalLevel finds the next higher level for approval +func (_i *articlesService) findNextApprovalLevel(clientId *uuid.UUID, currentLevelNumber int) int { + // For now, we'll use a simple logic based on level numbers + // Level 3 (POLRES) -> Level 2 (POLDAS) -> Level 1 (POLDAS) + + switch currentLevelNumber { + case 3: // POLRES + return 2 // Should be approved by POLDAS (Level 2) + case 2: // POLDAS + return 1 // Should be approved by Level 1 + case 1: // Highest level + return 0 // No approval needed, can publish directly + default: + _i.Log.Warn().Int("currentLevel", currentLevelNumber).Msg("Unknown level, no approval needed") + return 0 + } +} diff --git a/app/module/client_approval_settings/README.md b/app/module/client_approval_settings/README.md deleted file mode 100644 index 7a6f01a..0000000 --- a/app/module/client_approval_settings/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Client Approval Settings Module - -Module ini mengatur konfigurasi approval per client, memungkinkan setiap client untuk memiliki setting approval yang berbeda. - -## βœ… Status: SELESAI & SIAP DIGUNAKAN - -### 🎯 Fitur Utama: -- **Konfigurasi Approval per Client** - Setiap client bisa mengatur apakah memerlukan approval atau tidak -- **Auto Publish Articles** - Artikel bisa di-publish otomatis jika tidak memerlukan approval -- **Exemption Rules** - User, role, atau category tertentu bisa di-exempt dari approval -- **Default Workflow** - Set workflow default untuk client -- **Dynamic Toggle** - Bisa mengaktifkan/menonaktifkan approval secara dinamis - -### πŸ“ Struktur File: -``` -app/module/client_approval_settings/ -β”œβ”€β”€ client_approval_settings.module.go # Module & Router -β”œβ”€β”€ controller/ -β”‚ └── client_approval_settings.controller.go -β”œβ”€β”€ request/ -β”‚ └── client_approval_settings.request.go -β”œβ”€β”€ response/ -β”‚ └── client_approval_settings.response.go -β”œβ”€β”€ repository/ -β”‚ β”œβ”€β”€ client_approval_settings.repository.go -β”‚ └── client_approval_settings.repository.impl.go -β”œβ”€β”€ service/ -β”‚ └── client_approval_settings.service.go -β”œβ”€β”€ mapper/ -β”‚ └── client_approval_settings.mapper.go -└── README.md -``` - -### πŸ”— API Endpoints: -- `GET /api/v1/client-approval-settings` - Get settings -- `PUT /api/v1/client-approval-settings` - Update settings -- `DELETE /api/v1/client-approval-settings` - Delete settings -- `POST /api/v1/client-approval-settings/toggle-approval` - Toggle approval -- `POST /api/v1/client-approval-settings/enable-approval` - Enable approval -- `POST /api/v1/client-approval-settings/disable-approval` - Disable approval -- `PUT /api/v1/client-approval-settings/default-workflow` - Set default workflow -- `POST /api/v1/client-approval-settings/exempt-users` - Manage exempt users -- `POST /api/v1/client-approval-settings/exempt-roles` - Manage exempt roles -- `POST /api/v1/client-approval-settings/exempt-categories` - Manage exempt categories - -### πŸ”§ Integration: -- βœ… Terdaftar di Router (`app/router/api.go`) -- βœ… Terdaftar di Main (`main.go`) -- βœ… Terintegrasi dengan Articles Service -- βœ… Menggunakan Entity yang sudah ada -- βœ… Dependency Injection lengkap - -### πŸ’‘ Cara Penggunaan: - -1. **Get Settings Client**: - ```bash - GET /api/v1/client-approval-settings - Headers: X-Client-Key: - ``` - -2. **Disable Approval untuk Client**: - ```bash - POST /api/v1/client-approval-settings/toggle-approval - Body: {"requiresApproval": false} - ``` - -3. **Set Auto Publish**: - ```bash - PUT /api/v1/client-approval-settings - Body: {"autoPublishArticles": true} - ``` - -4. **Add Exempt User**: - ```bash - POST /api/v1/client-approval-settings/exempt-users - Body: {"userId": 123, "reason": "Admin user"} - ``` - -### πŸŽ‰ Module ini sekarang siap digunakan untuk mengatur approval system yang dinamis per client! diff --git a/app/module/client_approval_settings/client_approval_settings.module.go b/app/module/client_approval_settings/client_approval_settings.module.go index efd3564..f87d2c7 100644 --- a/app/module/client_approval_settings/client_approval_settings.module.go +++ b/app/module/client_approval_settings/client_approval_settings.module.go @@ -12,46 +12,54 @@ import ( // ClientApprovalSettingsRouter struct of ClientApprovalSettingsRouter type ClientApprovalSettingsRouter struct { App fiber.Router - Controller controller.ClientApprovalSettingsController + Controller *controller.Controller } // NewClientApprovalSettingsModule register bulky of ClientApprovalSettings module var NewClientApprovalSettingsModule = fx.Options( + // register repository of ClientApprovalSettings module fx.Provide(repository.NewClientApprovalSettingsRepository), + + // register service of ClientApprovalSettings module fx.Provide(service.NewClientApprovalSettingsService), - fx.Provide(controller.NewClientApprovalSettingsController), + + // register controller of ClientApprovalSettings module + fx.Provide(controller.NewController), + + // register router of ClientApprovalSettings module fx.Provide(NewClientApprovalSettingsRouter), ) -// NewClientApprovalSettingsRouter create new ClientApprovalSettingsRouter -func NewClientApprovalSettingsRouter( - app *fiber.App, - controller controller.ClientApprovalSettingsController, -) *ClientApprovalSettingsRouter { +// NewClientApprovalSettingsRouter init ClientApprovalSettingsRouter +func NewClientApprovalSettingsRouter(fiber *fiber.App, controller *controller.Controller) *ClientApprovalSettingsRouter { return &ClientApprovalSettingsRouter{ - App: app, + App: fiber, Controller: controller, } } // RegisterClientApprovalSettingsRoutes register routes of ClientApprovalSettings -func (r *ClientApprovalSettingsRouter) RegisterClientApprovalSettingsRoutes() { - // Group routes under /api/v1/client-approval-settings - api := r.App.Group("/api/v1/client-approval-settings") +func (_i *ClientApprovalSettingsRouter) RegisterClientApprovalSettingsRoutes() { + // define controllers + clientApprovalSettingsController := _i.Controller.ClientApprovalSettings - // Basic CRUD routes - api.Get("/", r.Controller.GetSettings) - api.Put("/", r.Controller.UpdateSettings) - api.Delete("/", r.Controller.DeleteSettings) + // define routes + _i.App.Route("/client-approval-settings", func(router fiber.Router) { + // Basic CRUD routes + router.Post("/", clientApprovalSettingsController.CreateSettings) + router.Get("/", clientApprovalSettingsController.GetSettings) + router.Put("/", clientApprovalSettingsController.UpdateSettings) + router.Delete("/", clientApprovalSettingsController.DeleteSettings) - // Approval management routes - api.Post("/toggle-approval", r.Controller.ToggleApproval) - api.Post("/enable-approval", r.Controller.EnableApproval) - api.Post("/disable-approval", r.Controller.DisableApproval) - api.Put("/default-workflow", r.Controller.SetDefaultWorkflow) + // Approval management routes + router.Post("/toggle-approval", clientApprovalSettingsController.ToggleApproval) + router.Post("/enable-approval", clientApprovalSettingsController.EnableApproval) + router.Post("/disable-approval", clientApprovalSettingsController.DisableApproval) + router.Put("/default-workflow", clientApprovalSettingsController.SetDefaultWorkflow) - // Exemption management routes - api.Post("/exempt-users", r.Controller.ManageExemptUsers) - api.Post("/exempt-roles", r.Controller.ManageExemptRoles) - api.Post("/exempt-categories", r.Controller.ManageExemptCategories) + // Exemption management routes + router.Post("/exempt-users", clientApprovalSettingsController.ManageExemptUsers) + router.Post("/exempt-roles", clientApprovalSettingsController.ManageExemptRoles) + router.Post("/exempt-categories", clientApprovalSettingsController.ManageExemptCategories) + }) } diff --git a/app/module/client_approval_settings/controller/client_approval_settings.controller.go b/app/module/client_approval_settings/controller/client_approval_settings.controller.go index 2f0efda..38fc859 100644 --- a/app/module/client_approval_settings/controller/client_approval_settings.controller.go +++ b/app/module/client_approval_settings/controller/client_approval_settings.controller.go @@ -19,6 +19,7 @@ type clientApprovalSettingsController struct { } type ClientApprovalSettingsController interface { + CreateSettings(c *fiber.Ctx) error GetSettings(c *fiber.Ctx) error UpdateSettings(c *fiber.Ctx) error DeleteSettings(c *fiber.Ctx) error @@ -41,6 +42,39 @@ func NewClientApprovalSettingsController( } } +// CreateSettings ClientApprovalSettings +// @Summary Create Client Approval Settings +// @Description API for creating client approval settings +// @Tags ClientApprovalSettings +// @Security Bearer +// @Param X-Client-Key header string true "Insert the X-Client-Key" +// @Param payload body request.CreateClientApprovalSettingsRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /client-approval-settings [post] +func (_i *clientApprovalSettingsController) CreateSettings(c *fiber.Ctx) error { + req := new(request.CreateClientApprovalSettingsRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + // Get ClientId from context + clientId := middleware.GetClientID(c) + + settings, err := _i.clientApprovalSettingsService.Create(clientId, *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Client approval settings created successfully"}, + Data: settings, + }) +} + // GetSettings ClientApprovalSettings // @Summary Get Client Approval Settings // @Description API for getting client approval settings diff --git a/app/module/client_approval_settings/controller/controller.go b/app/module/client_approval_settings/controller/controller.go new file mode 100644 index 0000000..dd5d80f --- /dev/null +++ b/app/module/client_approval_settings/controller/controller.go @@ -0,0 +1,17 @@ +package controller + +import ( + "web-medols-be/app/module/client_approval_settings/service" + + "github.com/rs/zerolog" +) + +type Controller struct { + ClientApprovalSettings ClientApprovalSettingsController +} + +func NewController(ClientApprovalSettingsService service.ClientApprovalSettingsService, log zerolog.Logger) *Controller { + return &Controller{ + ClientApprovalSettings: NewClientApprovalSettingsController(ClientApprovalSettingsService, log), + } +} diff --git a/app/module/user_levels/controller/user_levels.controller.go b/app/module/user_levels/controller/user_levels.controller.go index c2b6a9c..2723c70 100644 --- a/app/module/user_levels/controller/user_levels.controller.go +++ b/app/module/user_levels/controller/user_levels.controller.go @@ -3,6 +3,7 @@ package controller import ( "strconv" "strings" + "web-medols-be/app/middleware" "web-medols-be/app/module/user_levels/request" "web-medols-be/app/module/user_levels/service" "web-medols-be/utils/paginator" @@ -38,6 +39,7 @@ func NewUserLevelsController(userLevelsService service.UserLevelsService) UserLe // @Description API for getting all UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param req query request.UserLevelsQueryRequest false "query parameters" // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response @@ -60,7 +62,10 @@ func (_i *userLevelsController) All(c *fiber.Ctx) error { req := reqContext.ToParamRequest() req.Pagination = paginate - userLevelsData, paging, err := _i.userLevelsService.All(req) + // Get ClientId from context + clientId := middleware.GetClientID(c) + + userLevelsData, paging, err := _i.userLevelsService.All(clientId, req) if err != nil { return err } @@ -78,6 +83,7 @@ func (_i *userLevelsController) All(c *fiber.Ctx) error { // @Description API for getting one UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param id path int true "UserLevels ID" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -90,7 +96,10 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error { return err } - userLevelsData, err := _i.userLevelsService.Show(uint(id)) + // Get ClientId from context + clientId := middleware.GetClientID(c) + + userLevelsData, err := _i.userLevelsService.Show(clientId, uint(id)) if err != nil { return err } @@ -106,6 +115,7 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error { // @Description API for getting one UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param alias path string true "UserLevels Alias" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -114,7 +124,11 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error { // @Router /user-levels/alias/{alias} [get] func (_i *userLevelsController) ShowByAlias(c *fiber.Ctx) error { alias := c.Params("alias") - userLevelsData, err := _i.userLevelsService.ShowByAlias(alias) + + // Get ClientId from context + clientId := middleware.GetClientID(c) + + userLevelsData, err := _i.userLevelsService.ShowByAlias(clientId, alias) if err != nil { return err } @@ -129,7 +143,9 @@ func (_i *userLevelsController) ShowByAlias(c *fiber.Ctx) error { // @Description API for create UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param payload body request.UserLevelsCreateRequest true "Required payload" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -142,7 +158,10 @@ func (_i *userLevelsController) Save(c *fiber.Ctx) error { return err } - dataResult, err := _i.userLevelsService.Save(*req) + // Get ClientId from context + clientId := middleware.GetClientID(c) + + dataResult, err := _i.userLevelsService.Save(clientId, *req) if err != nil { return err } @@ -159,7 +178,9 @@ func (_i *userLevelsController) Save(c *fiber.Ctx) error { // @Description API for update UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param payload body request.UserLevelsUpdateRequest true "Required payload" // @Param id path int true "UserLevels ID" // @Success 200 {object} response.Response @@ -178,7 +199,10 @@ func (_i *userLevelsController) Update(c *fiber.Ctx) error { return err } - err = _i.userLevelsService.Update(uint(id), *req) + // Get ClientId from context + clientId := middleware.GetClientID(c) + + err = _i.userLevelsService.Update(clientId, uint(id), *req) if err != nil { return err } @@ -194,6 +218,8 @@ func (_i *userLevelsController) Update(c *fiber.Ctx) error { // @Description API for delete UserLevels // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param id path int true "UserLevels ID" // @Success 200 {object} response.Response @@ -208,7 +234,10 @@ func (_i *userLevelsController) Delete(c *fiber.Ctx) error { return err } - err = _i.userLevelsService.Delete(uint(id)) + // Get ClientId from context + clientId := middleware.GetClientID(c) + + err = _i.userLevelsService.Delete(clientId, uint(id)) if err != nil { return err } @@ -224,6 +253,7 @@ func (_i *userLevelsController) Delete(c *fiber.Ctx) error { // @Description API for Enable Approval of Article // @Tags UserLevels // @Security Bearer +// @Param X-Client-Key header string true "Client Key" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param payload body request.UserLevelsApprovalRequest true "Required payload" @@ -238,13 +268,16 @@ func (_i *userLevelsController) EnableApproval(c *fiber.Ctx) error { return err } + // Get ClientId from context + clientId := middleware.GetClientID(c) + ids := strings.Split(req.Ids, ",") for _, id := range ids { idUint, err := strconv.ParseUint(id, 10, 64) if err != nil { return err } - err = _i.userLevelsService.EnableApproval(uint(idUint), req.IsApprovalActive) + err = _i.userLevelsService.EnableApproval(clientId, uint(idUint), req.IsApprovalActive) if err != nil { return err } diff --git a/app/module/user_levels/repository/user_levels.repository.go b/app/module/user_levels/repository/user_levels.repository.go index 0965848..d68828e 100644 --- a/app/module/user_levels/repository/user_levels.repository.go +++ b/app/module/user_levels/repository/user_levels.repository.go @@ -8,6 +8,8 @@ import ( "web-medols-be/app/module/user_levels/request" "web-medols-be/utils/paginator" utilSvc "web-medols-be/utils/service" + + "github.com/google/uuid" ) type userLevelsRepository struct { @@ -16,12 +18,12 @@ type userLevelsRepository struct { // UserLevelsRepository define interface of IUserLevelsRepository type UserLevelsRepository interface { - GetAll(req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error) - FindOne(id uint) (userLevels *entity.UserLevels, err error) - FindOneByAlias(alias string) (userLevels *entity.UserLevels, err error) - Create(userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error) - Update(id uint, userLevels *entity.UserLevels) (err error) - Delete(id uint) (err error) + GetAll(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error) + FindOne(clientId *uuid.UUID, id uint) (userLevels *entity.UserLevels, err error) + FindOneByAlias(clientId *uuid.UUID, alias string) (userLevels *entity.UserLevels, err error) + Create(clientId *uuid.UUID, userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error) + Update(clientId *uuid.UUID, id uint, userLevels *entity.UserLevels) (err error) + Delete(clientId *uuid.UUID, id uint) (err error) } func NewUserLevelsRepository(db *database.Database) UserLevelsRepository { @@ -31,12 +33,17 @@ func NewUserLevelsRepository(db *database.Database) UserLevelsRepository { } // implement interface of IUserLevelsRepository -func (_i *userLevelsRepository) GetAll(req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error) { +func (_i *userLevelsRepository) GetAll(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error) { var count int64 query := _i.DB.DB.Model(&entity.UserLevels{}) query = query.Where("is_active = ?", true) + // Filter by client_id if provided + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + if req.Name != nil && *req.Name != "" { name := strings.ToLower(*req.Name) query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(name)+"%") @@ -73,28 +80,52 @@ func (_i *userLevelsRepository) GetAll(req request.UserLevelsQueryRequest) (user return } -func (_i *userLevelsRepository) FindOne(id uint) (userLevels *entity.UserLevels, err error) { - if err := _i.DB.DB.First(&userLevels, id).Error; err != nil { +func (_i *userLevelsRepository) FindOne(clientId *uuid.UUID, id uint) (userLevels *entity.UserLevels, err error) { + query := _i.DB.DB.Where("id = ?", id) + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + if err := query.First(&userLevels).Error; err != nil { return nil, err } return userLevels, nil } -func (_i *userLevelsRepository) FindOneByAlias(alias string) (userLevels *entity.UserLevels, err error) { - if err := _i.DB.DB.Where("alias_name = ?", strings.ToLower(alias)).First(&userLevels).Error; err != nil { +func (_i *userLevelsRepository) FindOneByAlias(clientId *uuid.UUID, alias string) (userLevels *entity.UserLevels, err error) { + query := _i.DB.DB.Where("alias_name = ?", strings.ToLower(alias)) + if clientId != nil { + query = query.Where("client_id = ?", clientId) + } + if err := query.First(&userLevels).Error; err != nil { return nil, err } return userLevels, nil } -func (_i *userLevelsRepository) Create(userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error) { +func (_i *userLevelsRepository) Create(clientId *uuid.UUID, userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error) { + // Set client ID + if clientId != nil { + userLevels.ClientId = clientId + } + result := _i.DB.DB.Create(userLevels) return userLevels, result.Error } -func (_i *userLevelsRepository) Update(id uint, userLevels *entity.UserLevels) (err error) { +func (_i *userLevelsRepository) Update(clientId *uuid.UUID, id uint, userLevels *entity.UserLevels) (err error) { + // Validate client access + if clientId != nil { + var count int64 + if err := _i.DB.DB.Model(&entity.UserLevels{}).Where("id = ? AND client_id = ?", id, clientId).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fmt.Errorf("access denied to this resource") + } + } + userLevelsMap, err := utilSvc.StructToMap(userLevels) if err != nil { return err @@ -104,6 +135,17 @@ func (_i *userLevelsRepository) Update(id uint, userLevels *entity.UserLevels) ( Updates(userLevelsMap).Error } -func (_i *userLevelsRepository) Delete(id uint) error { +func (_i *userLevelsRepository) Delete(clientId *uuid.UUID, id uint) error { + // Validate client access + if clientId != nil { + var count int64 + if err := _i.DB.DB.Model(&entity.UserLevels{}).Where("id = ? AND client_id = ?", id, clientId).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fmt.Errorf("access denied to this resource") + } + } + return _i.DB.DB.Delete(&entity.UserLevels{}, id).Error } diff --git a/app/module/user_levels/service/user_levels.service.go b/app/module/user_levels/service/user_levels.service.go index bfd9f85..0dc96c7 100644 --- a/app/module/user_levels/service/user_levels.service.go +++ b/app/module/user_levels/service/user_levels.service.go @@ -1,13 +1,15 @@ package service import ( - "github.com/rs/zerolog" "web-medols-be/app/database/entity" "web-medols-be/app/module/user_levels/mapper" "web-medols-be/app/module/user_levels/repository" "web-medols-be/app/module/user_levels/request" "web-medols-be/app/module/user_levels/response" "web-medols-be/utils/paginator" + + "github.com/google/uuid" + "github.com/rs/zerolog" ) // UserLevelsService @@ -18,13 +20,13 @@ type userLevelsService struct { // UserLevelsService define interface of IUserLevelsService type UserLevelsService interface { - All(req request.UserLevelsQueryRequest) (userLevels []*response.UserLevelsResponse, paging paginator.Pagination, err error) - Show(id uint) (userLevels *response.UserLevelsResponse, err error) - ShowByAlias(alias string) (userLevels *response.UserLevelsResponse, err error) - Save(req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error) - Update(id uint, req request.UserLevelsUpdateRequest) (err error) - Delete(id uint) error - EnableApproval(id uint, isApprovalActive bool) (err error) + All(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevels []*response.UserLevelsResponse, paging paginator.Pagination, err error) + Show(clientId *uuid.UUID, id uint) (userLevels *response.UserLevelsResponse, err error) + ShowByAlias(clientId *uuid.UUID, alias string) (userLevels *response.UserLevelsResponse, err error) + Save(clientId *uuid.UUID, req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error) + Update(clientId *uuid.UUID, id uint, req request.UserLevelsUpdateRequest) (err error) + Delete(clientId *uuid.UUID, id uint) error + EnableApproval(clientId *uuid.UUID, id uint, isApprovalActive bool) (err error) } // NewUserLevelsService init UserLevelsService @@ -37,8 +39,8 @@ func NewUserLevelsService(repo repository.UserLevelsRepository, log zerolog.Logg } // All implement interface of UserLevelsService -func (_i *userLevelsService) All(req request.UserLevelsQueryRequest) (userLevelss []*response.UserLevelsResponse, paging paginator.Pagination, err error) { - results, paging, err := _i.Repo.GetAll(req) +func (_i *userLevelsService) All(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevelss []*response.UserLevelsResponse, paging paginator.Pagination, err error) { + results, paging, err := _i.Repo.GetAll(clientId, req) if err != nil { return } @@ -50,8 +52,8 @@ func (_i *userLevelsService) All(req request.UserLevelsQueryRequest) (userLevels return } -func (_i *userLevelsService) Show(id uint) (userLevels *response.UserLevelsResponse, err error) { - result, err := _i.Repo.FindOne(id) +func (_i *userLevelsService) Show(clientId *uuid.UUID, id uint) (userLevels *response.UserLevelsResponse, err error) { + result, err := _i.Repo.FindOne(clientId, id) if err != nil { return nil, err } @@ -59,8 +61,8 @@ func (_i *userLevelsService) Show(id uint) (userLevels *response.UserLevelsRespo return mapper.UserLevelsResponseMapper(result), nil } -func (_i *userLevelsService) ShowByAlias(alias string) (userLevels *response.UserLevelsResponse, err error) { - result, err := _i.Repo.FindOneByAlias(alias) +func (_i *userLevelsService) ShowByAlias(clientId *uuid.UUID, alias string) (userLevels *response.UserLevelsResponse, err error) { + result, err := _i.Repo.FindOneByAlias(clientId, alias) if err != nil { return nil, err } @@ -68,10 +70,14 @@ func (_i *userLevelsService) ShowByAlias(alias string) (userLevels *response.Use return mapper.UserLevelsResponseMapper(result), nil } -func (_i *userLevelsService) Save(req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error) { +func (_i *userLevelsService) Save(clientId *uuid.UUID, req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error) { _i.Log.Info().Interface("data", req).Msg("") - saveUserLevelsRes, err := _i.Repo.Create(req.ToEntity()) + entity := req.ToEntity() + // Set ClientId on entity + entity.ClientId = clientId + + saveUserLevelsRes, err := _i.Repo.Create(clientId, entity) if err != nil { return nil, err } @@ -79,32 +85,36 @@ func (_i *userLevelsService) Save(req request.UserLevelsCreateRequest) (userLeve return saveUserLevelsRes, nil } -func (_i *userLevelsService) Update(id uint, req request.UserLevelsUpdateRequest) (err error) { +func (_i *userLevelsService) Update(clientId *uuid.UUID, id uint, req request.UserLevelsUpdateRequest) (err error) { //_i.Log.Info().Interface("data", req).Msg("") _i.Log.Info().Interface("data", req.ToEntity()).Msg("") - return _i.Repo.Update(id, req.ToEntity()) + // Set ClientId on entity + entity := req.ToEntity() + entity.ClientId = clientId + + return _i.Repo.Update(clientId, id, entity) } -func (_i *userLevelsService) Delete(id uint) error { - result, err := _i.Repo.FindOne(id) +func (_i *userLevelsService) Delete(clientId *uuid.UUID, id uint) error { + result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } isActive := false result.IsActive = &isActive - return _i.Repo.Update(id, result) + return _i.Repo.Update(clientId, id, result) } -func (_i *userLevelsService) EnableApproval(id uint, isApprovalActive bool) (err error) { - result, err := _i.Repo.FindOne(id) +func (_i *userLevelsService) EnableApproval(clientId *uuid.UUID, id uint, isApprovalActive bool) (err error) { + result, err := _i.Repo.FindOne(clientId, id) if err != nil { return err } *result.IsApprovalActive = isApprovalActive - return _i.Repo.Update(id, result) + return _i.Repo.Update(clientId, id, result) } diff --git a/app/module/users/mapper/users.mapper.go b/app/module/users/mapper/users.mapper.go index d226c71..7fc50f9 100644 --- a/app/module/users/mapper/users.mapper.go +++ b/app/module/users/mapper/users.mapper.go @@ -4,11 +4,13 @@ import ( "web-medols-be/app/database/entity/users" userLevelsRepository "web-medols-be/app/module/user_levels/repository" res "web-medols-be/app/module/users/response" + + "github.com/google/uuid" ) -func UsersResponseMapper(usersReq *users.Users, userLevelsRepo userLevelsRepository.UserLevelsRepository) (usersRes *res.UsersResponse) { +func UsersResponseMapper(usersReq *users.Users, userLevelsRepo userLevelsRepository.UserLevelsRepository, clientId *uuid.UUID) (usersRes *res.UsersResponse) { if usersReq != nil { - findUserLevel, _ := userLevelsRepo.FindOne(usersReq.UserLevelId) + findUserLevel, _ := userLevelsRepo.FindOne(clientId, usersReq.UserLevelId) userLevelGroup := "" if findUserLevel != nil { userLevelGroup = findUserLevel.AliasName diff --git a/app/module/users/service/users.service.go b/app/module/users/service/users.service.go index b8357fd..aab79af 100644 --- a/app/module/users/service/users.service.go +++ b/app/module/users/service/users.service.go @@ -1,13 +1,9 @@ package service import ( - paseto "aidanwoods.dev/go-paseto" "encoding/base64" "encoding/json" "fmt" - "github.com/Nerzal/gocloak/v13" - "github.com/google/uuid" - "github.com/rs/zerolog" "strings" "time" "web-medols-be/app/database/entity" @@ -20,6 +16,11 @@ import ( "web-medols-be/config/config" "web-medols-be/utils/paginator" utilSvc "web-medols-be/utils/service" + + paseto "aidanwoods.dev/go-paseto" + "github.com/Nerzal/gocloak/v13" + "github.com/google/uuid" + "github.com/rs/zerolog" ) // UsersService @@ -73,7 +74,7 @@ func (_i *usersService) All(clientId *uuid.UUID, req request.UsersQueryRequest) } for _, result := range results { - users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo)) + users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId)) } return @@ -85,7 +86,7 @@ func (_i *usersService) Show(clientId *uuid.UUID, id uint) (users *response.User return nil, err } - return mapper.UsersResponseMapper(result, _i.UserLevelsRepo), nil + return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil } func (_i *usersService) ShowByUsername(clientId *uuid.UUID, username string) (users *response.UsersResponse, err error) { @@ -94,13 +95,13 @@ func (_i *usersService) ShowByUsername(clientId *uuid.UUID, username string) (us return nil, err } - return mapper.UsersResponseMapper(result, _i.UserLevelsRepo), nil + return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil } func (_i *usersService) ShowUserInfo(clientId *uuid.UUID, authToken string) (users *response.UsersResponse, err error) { userInfo := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken) - return mapper.UsersResponseMapper(userInfo, _i.UserLevelsRepo), nil + return mapper.UsersResponseMapper(userInfo, _i.UserLevelsRepo, clientId), nil } func (_i *usersService) Save(clientId *uuid.UUID, req request.UsersCreateRequest, authToken string) (userReturn *users.Users, err error) { diff --git a/config/toml/config.toml b/config/toml/config.toml index 47d85fb..df4133f 100644 --- a/config/toml/config.toml +++ b/config/toml/config.toml @@ -13,9 +13,9 @@ body-limit = 1048576000 # "100 * 1024 * 1024" [db.postgres] dsn = "postgresql://medols_user:MedolsDB@2025@38.47.180.165:5432/medols_db" # ://:@:/ -log-mode = "NONE" -migrate = true -seed = true +log-mode = "ERROR" +migrate = false +seed = false [logger] log-dir = "debug.log" diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index df34844..037a115 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -2417,6 +2417,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "description": "Period filter (daily, weekly, monthly)", @@ -2485,10 +2492,11 @@ const docTemplate = `{ "required": true }, { - "type": "integer", - "description": "User Level ID filter", - "name": "userLevelId", - "in": "query" + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" } ], "responses": { @@ -2640,6 +2648,25 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "boolean", + "description": "Include article preview", + "name": "includePreview", + "in": "query" + }, + { + "type": "boolean", + "description": "Show only urgent articles", + "name": "urgentOnly", + "in": "query" + }, { "type": "integer", "name": "count", @@ -2730,10 +2757,11 @@ const docTemplate = `{ "required": true }, { - "type": "integer", - "description": "User Level ID filter", - "name": "userLevelId", - "in": "query" + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" }, { "type": "integer", @@ -2824,6 +2852,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Submit for approval data", "name": "req", @@ -2881,6 +2916,13 @@ const docTemplate = `{ "name": "X-Client-Key", "in": "header", "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" } ], "responses": { @@ -2987,6 +3029,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3052,6 +3101,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3117,6 +3173,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3182,6 +3245,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -6747,6 +6817,13 @@ const docTemplate = `{ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "name": "category", @@ -7488,6 +7565,69 @@ const docTemplate = `{ } } }, + "/articles/waiting-for-approval": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting articles that are waiting for approval by the current user's level", + "tags": [ + "Articles" + ], + "summary": "Get articles waiting for approval by current user level", + "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Items per page", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/articles/{id}": { "get": { "security": [ @@ -8184,6 +8324,62 @@ const docTemplate = `{ } } }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating client approval settings", + "tags": [ + "ClientApprovalSettings" + ], + "summary": "Create Client Approval Settings", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Client-Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateClientApprovalSettingsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, "delete": { "security": [ { @@ -12353,6 +12549,13 @@ const docTemplate = `{ ], "summary": "Get all UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "integer", "name": "levelNumber", @@ -12453,12 +12656,26 @@ const docTemplate = `{ ], "summary": "Create UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", "name": "X-Csrf-Token", "in": "header" }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Required payload", "name": "payload", @@ -12510,6 +12727,13 @@ const docTemplate = `{ ], "summary": "Get one UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "UserLevels Alias", @@ -12559,6 +12783,13 @@ const docTemplate = `{ ], "summary": "EnableApproval Articles", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", @@ -12623,6 +12854,13 @@ const docTemplate = `{ ], "summary": "Get one UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "integer", "description": "UserLevels ID", @@ -12670,12 +12908,26 @@ const docTemplate = `{ ], "summary": "update UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", "name": "X-Csrf-Token", "in": "header" }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Required payload", "name": "payload", @@ -12732,6 +12984,20 @@ const docTemplate = `{ ], "summary": "delete UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "description": "Insert the X-Csrf-Token", @@ -14870,6 +15136,9 @@ const docTemplate = `{ "name" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14881,6 +15150,15 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "requiresApproval": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ApprovalWorkflowStepRequest" + } } } }, @@ -14891,6 +15169,9 @@ const docTemplate = `{ "name" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14902,6 +15183,9 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "requiresApproval": { + "type": "boolean" } } }, @@ -14913,6 +15197,9 @@ const docTemplate = `{ "steps" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14925,6 +15212,9 @@ const docTemplate = `{ "name": { "type": "string" }, + "requiresApproval": { + "type": "boolean" + }, "steps": { "type": "array", "minItems": 1, @@ -14942,6 +15232,9 @@ const docTemplate = `{ "steps" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14954,6 +15247,9 @@ const docTemplate = `{ "name": { "type": "string" }, + "requiresApproval": { + "type": "boolean" + }, "steps": { "type": "array", "minItems": 1, @@ -15609,6 +15905,57 @@ const docTemplate = `{ } } }, + "request.CreateClientApprovalSettingsRequest": { + "type": "object", + "required": [ + "requires_approval" + ], + "properties": { + "approval_exempt_categories": { + "type": "array", + "items": { + "type": "integer" + } + }, + "approval_exempt_roles": { + "type": "array", + "items": { + "type": "integer" + } + }, + "approval_exempt_users": { + "type": "array", + "items": { + "type": "integer" + } + }, + "auto_publish_articles": { + "type": "boolean" + }, + "default_workflow_id": { + "type": "integer", + "minimum": 1 + }, + "is_active": { + "type": "boolean" + }, + "require_approval_for": { + "type": "array", + "items": { + "type": "string" + } + }, + "requires_approval": { + "type": "boolean" + }, + "skip_approval_for": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "request.CustomStaticPagesCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 73357ea..66516eb 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -2406,6 +2406,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "description": "Period filter (daily, weekly, monthly)", @@ -2474,10 +2481,11 @@ "required": true }, { - "type": "integer", - "description": "User Level ID filter", - "name": "userLevelId", - "in": "query" + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" } ], "responses": { @@ -2629,6 +2637,25 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "boolean", + "description": "Include article preview", + "name": "includePreview", + "in": "query" + }, + { + "type": "boolean", + "description": "Show only urgent articles", + "name": "urgentOnly", + "in": "query" + }, { "type": "integer", "name": "count", @@ -2719,10 +2746,11 @@ "required": true }, { - "type": "integer", - "description": "User Level ID filter", - "name": "userLevelId", - "in": "query" + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" }, { "type": "integer", @@ -2813,6 +2841,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Submit for approval data", "name": "req", @@ -2870,6 +2905,13 @@ "name": "X-Client-Key", "in": "header", "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" } ], "responses": { @@ -2976,6 +3018,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3041,6 +3090,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3106,6 +3162,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -3171,6 +3234,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "integer", "description": "ArticleApprovalFlows ID", @@ -6736,6 +6806,13 @@ "in": "header", "required": true }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "name": "category", @@ -7477,6 +7554,69 @@ } } }, + "/articles/waiting-for-approval": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting articles that are waiting for approval by the current user's level", + "tags": [ + "Articles" + ], + "summary": "Get articles waiting for approval by current user level", + "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "type": "integer", + "default": 1, + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Items per page", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/articles/{id}": { "get": { "security": [ @@ -8173,6 +8313,62 @@ } } }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for creating client approval settings", + "tags": [ + "ClientApprovalSettings" + ], + "summary": "Create Client Approval Settings", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Client-Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateClientApprovalSettingsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, "delete": { "security": [ { @@ -12342,6 +12538,13 @@ ], "summary": "Get all UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "integer", "name": "levelNumber", @@ -12442,12 +12645,26 @@ ], "summary": "Create UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", "name": "X-Csrf-Token", "in": "header" }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Required payload", "name": "payload", @@ -12499,6 +12716,13 @@ ], "summary": "Get one UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "UserLevels Alias", @@ -12548,6 +12772,13 @@ ], "summary": "EnableApproval Articles", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", @@ -12612,6 +12843,13 @@ ], "summary": "Get one UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "integer", "description": "UserLevels ID", @@ -12659,12 +12897,26 @@ ], "summary": "update UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, { "type": "string", "description": "Insert the X-Csrf-Token", "name": "X-Csrf-Token", "in": "header" }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "description": "Required payload", "name": "payload", @@ -12721,6 +12973,20 @@ ], "summary": "delete UserLevels", "parameters": [ + { + "type": "string", + "description": "Client Key", + "name": "X-Client-Key", + "in": "header", + "required": true + }, + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, { "type": "string", "description": "Insert the X-Csrf-Token", @@ -14859,6 +15125,9 @@ "name" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14870,6 +15139,15 @@ }, "name": { "type": "string" + }, + "requiresApproval": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/request.ApprovalWorkflowStepRequest" + } } } }, @@ -14880,6 +15158,9 @@ "name" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14891,6 +15172,9 @@ }, "name": { "type": "string" + }, + "requiresApproval": { + "type": "boolean" } } }, @@ -14902,6 +15186,9 @@ "steps" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14914,6 +15201,9 @@ "name": { "type": "string" }, + "requiresApproval": { + "type": "boolean" + }, "steps": { "type": "array", "minItems": 1, @@ -14931,6 +15221,9 @@ "steps" ], "properties": { + "autoPublish": { + "type": "boolean" + }, "description": { "type": "string" }, @@ -14943,6 +15236,9 @@ "name": { "type": "string" }, + "requiresApproval": { + "type": "boolean" + }, "steps": { "type": "array", "minItems": 1, @@ -15598,6 +15894,57 @@ } } }, + "request.CreateClientApprovalSettingsRequest": { + "type": "object", + "required": [ + "requires_approval" + ], + "properties": { + "approval_exempt_categories": { + "type": "array", + "items": { + "type": "integer" + } + }, + "approval_exempt_roles": { + "type": "array", + "items": { + "type": "integer" + } + }, + "approval_exempt_users": { + "type": "array", + "items": { + "type": "integer" + } + }, + "auto_publish_articles": { + "type": "boolean" + }, + "default_workflow_id": { + "type": "integer", + "minimum": 1 + }, + "is_active": { + "type": "boolean" + }, + "require_approval_for": { + "type": "array", + "items": { + "type": "string" + } + }, + "requires_approval": { + "type": "boolean" + }, + "skip_approval_for": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "request.CustomStaticPagesCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0cdb082..75e5267 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -112,6 +112,8 @@ definitions: type: object request.ApprovalWorkflowsCreateRequest: properties: + autoPublish: + type: boolean description: type: string isActive: @@ -120,12 +122,20 @@ definitions: type: boolean name: type: string + requiresApproval: + type: boolean + steps: + items: + $ref: '#/definitions/request.ApprovalWorkflowStepRequest' + type: array required: - description - name type: object request.ApprovalWorkflowsUpdateRequest: properties: + autoPublish: + type: boolean description: type: string isActive: @@ -134,12 +144,16 @@ definitions: type: boolean name: type: string + requiresApproval: + type: boolean required: - description - name type: object request.ApprovalWorkflowsWithStepsCreateRequest: properties: + autoPublish: + type: boolean description: type: string isActive: @@ -148,6 +162,8 @@ definitions: type: boolean name: type: string + requiresApproval: + type: boolean steps: items: $ref: '#/definitions/request.ApprovalWorkflowStepRequest' @@ -160,6 +176,8 @@ definitions: type: object request.ApprovalWorkflowsWithStepsUpdateRequest: properties: + autoPublish: + type: boolean description: type: string isActive: @@ -168,6 +186,8 @@ definitions: type: boolean name: type: string + requiresApproval: + type: boolean steps: items: $ref: '#/definitions/request.ApprovalWorkflowStepRequest' @@ -626,6 +646,40 @@ definitions: - stepOrder - workflowId type: object + request.CreateClientApprovalSettingsRequest: + properties: + approval_exempt_categories: + items: + type: integer + type: array + approval_exempt_roles: + items: + type: integer + type: array + approval_exempt_users: + items: + type: integer + type: array + auto_publish_articles: + type: boolean + default_workflow_id: + minimum: 1 + type: integer + is_active: + type: boolean + require_approval_for: + items: + type: string + type: array + requires_approval: + type: boolean + skip_approval_for: + items: + type: string + type: array + required: + - requires_approval + type: object request.CustomStaticPagesCreateRequest: properties: description: @@ -2906,6 +2960,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: ArticleApprovalFlows ID in: path name: id @@ -2948,6 +3007,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: ArticleApprovalFlows ID in: path name: id @@ -2990,6 +3054,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: ArticleApprovalFlows ID in: path name: id @@ -3032,6 +3101,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: ArticleApprovalFlows ID in: path name: id @@ -3074,6 +3148,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: Period filter (daily, weekly, monthly) in: query name: period @@ -3117,10 +3196,11 @@ paths: name: X-Client-Key required: true type: string - - description: User Level ID filter - in: query - name: userLevelId - type: integer + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string responses: "200": description: OK @@ -3215,6 +3295,19 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string + - description: Include article preview + in: query + name: includePreview + type: boolean + - description: Show only urgent articles + in: query + name: urgentOnly + type: boolean - in: query name: count type: integer @@ -3270,10 +3363,11 @@ paths: name: X-Client-Key required: true type: string - - description: User Level ID filter - in: query - name: userLevelId - type: integer + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - in: query name: count type: integer @@ -3329,6 +3423,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: Submit for approval data in: body name: req @@ -3366,6 +3465,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string responses: "200": description: OK @@ -5623,6 +5727,11 @@ paths: name: X-Client-Key required: true type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - in: query name: category type: string @@ -6301,6 +6410,48 @@ paths: summary: Viewer Articles Thumbnail tags: - Articles + /articles/waiting-for-approval: + get: + description: API for getting articles that are waiting for approval by the current + user's level + parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string + - default: 1 + description: Page number + in: query + name: page + type: integer + - default: 10 + description: Items per page + in: query + name: limit + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get articles waiting for approval by current user level + tags: + - Articles /cities: get: description: API for getting all Cities @@ -6539,6 +6690,42 @@ paths: summary: Get Client Approval Settings tags: - ClientApprovalSettings + post: + description: API for creating client approval settings + parameters: + - description: Insert the X-Client-Key + in: header + name: X-Client-Key + required: true + type: string + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.CreateClientApprovalSettingsRequest' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Create Client Approval Settings + tags: + - ClientApprovalSettings put: description: API for updating client approval settings parameters: @@ -9188,6 +9375,11 @@ paths: get: description: API for getting all UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - in: query name: levelNumber type: integer @@ -9249,10 +9441,20 @@ paths: post: description: API for create UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - description: Insert the X-Csrf-Token in: header name: X-Csrf-Token type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: Required payload in: body name: payload @@ -9285,6 +9487,16 @@ paths: delete: description: API for delete UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: Insert the X-Csrf-Token in: header name: X-Csrf-Token @@ -9323,6 +9535,11 @@ paths: get: description: API for getting one UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - description: UserLevels ID in: path name: id @@ -9353,10 +9570,20 @@ paths: put: description: API for update UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - description: Insert the X-Csrf-Token in: header name: X-Csrf-Token type: string + - default: Bearer + description: Insert your access token + in: header + name: Authorization + type: string - description: Required payload in: body name: payload @@ -9394,6 +9621,11 @@ paths: get: description: API for getting one UserLevels parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - description: UserLevels Alias in: path name: alias @@ -9425,6 +9657,11 @@ paths: post: description: API for Enable Approval of Article parameters: + - description: Client Key + in: header + name: X-Client-Key + required: true + type: string - description: Insert the X-Csrf-Token in: header name: X-Csrf-Token diff --git a/plan/approval-workflow-architecture.md b/plan/approval-workflow-architecture.md new file mode 100644 index 0000000..9b008a7 --- /dev/null +++ b/plan/approval-workflow-architecture.md @@ -0,0 +1,347 @@ +# Approval Workflow Architecture & Module Relationships + +## πŸ“‹ **Overview** + +Sistem approval workflow di MEDOLS menggunakan arsitektur multi-module yang saling terintegrasi untuk mengelola proses persetujuan artikel secara dinamis. Setiap module memiliki peran spesifik dalam alur approval yang kompleks. + +## πŸ—οΈ **Module Architecture** + +### **1. Core Workflow Modules** + +#### **`approval_workflows`** - Master Workflow Definition +- **Purpose**: Mendefinisikan template workflow approval +- **Key Fields**: + - `id`: Primary key + - `name`: Nama workflow (e.g., "Standard Approval", "Fast Track") + - `description`: Deskripsi workflow + - `is_default`: Apakah workflow default untuk client + - `is_active`: Status aktif workflow + - `client_id`: Client yang memiliki workflow + +#### **`approval_workflow_steps`** - Workflow Steps Definition +- **Purpose**: Mendefinisikan step-step dalam workflow +- **Key Fields**: + - `id`: Primary key + - `workflow_id`: Foreign key ke `approval_workflows` + - `step_order`: Urutan step (1, 2, 3, dst.) + - `step_name`: Nama step (e.g., "Level 2 Review", "Level 1 Final Approval") + - `required_user_level_id`: User level yang harus approve step ini + - `is_required`: Apakah step wajib atau optional + +### **2. Execution Modules** + +#### **`article_approval_flows`** - Active Approval Instances +- **Purpose**: Instance aktif dari workflow yang sedang berjalan +- **Key Fields**: + - `id`: Primary key + - `article_id`: Foreign key ke `articles` + - `workflow_id`: Foreign key ke `approval_workflows` + - `current_step`: Step saat ini yang sedang menunggu approval + - `status_id`: Status (1=pending, 2=approved, 3=rejected, 4=revision_requested) + - `submitted_by_id`: User yang submit artikel + - `submitted_at`: Waktu submit + - `completed_at`: Waktu selesai (jika approved/rejected) + +#### **`article_approval_step_logs`** - Step Execution History +- **Purpose**: Log setiap step yang telah dieksekusi +- **Key Fields**: + - `id`: Primary key + - `approval_flow_id`: Foreign key ke `article_approval_flows` + - `step_order`: Urutan step yang dieksekusi + - `step_name`: Nama step + - `approved_by_id`: User yang approve step ini + - `action`: Aksi yang dilakukan (approve, reject, auto_skip) + - `message`: Pesan dari approver + - `processed_at`: Waktu eksekusi + +### **3. Legacy & Configuration Modules** + +#### **`article_approvals`** - Legacy Approval System +- **Purpose**: Sistem approval legacy untuk backward compatibility +- **Key Fields**: + - `id`: Primary key + - `article_id`: Foreign key ke `articles` + - `approval_by`: User yang approve + - `status_id`: Status approval + - `approval_at_level`: Level yang approve + - `message`: Pesan approval + +#### **`client_approval_settings`** - Client Configuration +- **Purpose**: Konfigurasi approval untuk setiap client +- **Key Fields**: + - `id`: Primary key + - `client_id`: Foreign key ke `clients` + - `workflow_id`: Workflow default untuk client + - `auto_approve_levels`: Level yang auto-approve + - `notification_settings`: Konfigurasi notifikasi + +## πŸ”„ **Approval Workflow Process Flow** + +### **Phase 1: Setup & Configuration** + +```mermaid +graph TD + A[Client Setup] --> B[Create Approval Workflow] + B --> C[Define Workflow Steps] + C --> D[Set Required User Levels] + D --> E[Configure Client Settings] + E --> F[Activate Workflow] +``` + +**Steps:** +1. **Client Registration**: Client baru dibuat di sistem +2. **Workflow Creation**: Admin membuat workflow approval +3. **Step Definition**: Mendefinisikan step-step dengan user level requirement +4. **Client Configuration**: Set workflow default untuk client + +### **Phase 2: Article Submission** + +```mermaid +graph TD + A[User Creates Article] --> B{User Level Requires Approval?} + B -->|Yes| C[Create Article Approval Flow] + B -->|No| D[Auto Publish] + C --> E[Set Current Step = 1] + E --> F[Status = Pending] + F --> G[Create Legacy Approval Record] + G --> H[Notify Approvers] +``` + +**Database Changes:** +- `articles`: `workflow_id`, `current_approval_step = 1`, `status_id = 1` +- `article_approval_flows`: New record dengan `current_step = 1` +- `article_approvals`: Legacy record untuk backward compatibility + +### **Phase 3: Step-by-Step Approval** + +```mermaid +graph TD + A[Approver Reviews Article] --> B{Approve or Reject?} + B -->|Approve| C[Create Step Log] + B -->|Reject| D[Update Status to Rejected] + C --> E{More Steps?} + E -->|Yes| F[Move to Next Step] + E -->|No| G[Complete Approval] + F --> H[Update Current Step] + H --> I[Notify Next Approver] + G --> J[Update Article Status to Approved] + D --> K[Notify Submitter] +``` + +**Database Changes per Step:** +- `article_approval_step_logs`: New log record +- `article_approval_flows`: Update `current_step` dan `status_id` +- `articles`: Update `current_approval_step` + +### **Phase 4: Completion** + +```mermaid +graph TD + A[All Steps Approved] --> B[Update Final Status] + B --> C[Set Completed At] + C --> D[Publish Article] + D --> E[Send Notifications] + E --> F[Update Analytics] +``` + +## πŸ“Š **Module Relationships Diagram** + +```mermaid +erDiagram + CLIENTS ||--o{ APPROVAL_WORKFLOWS : "has" + CLIENTS ||--o{ CLIENT_APPROVAL_SETTINGS : "configures" + + APPROVAL_WORKFLOWS ||--o{ APPROVAL_WORKFLOW_STEPS : "contains" + APPROVAL_WORKFLOWS ||--o{ ARTICLE_APPROVAL_FLOWS : "instantiates" + + ARTICLES ||--o{ ARTICLE_APPROVAL_FLOWS : "has" + ARTICLES ||--o{ ARTICLE_APPROVALS : "has_legacy" + + ARTICLE_APPROVAL_FLOWS ||--o{ ARTICLE_APPROVAL_STEP_LOGS : "logs" + ARTICLE_APPROVAL_FLOWS }o--|| APPROVAL_WORKFLOWS : "uses" + + USERS ||--o{ ARTICLE_APPROVAL_FLOWS : "submits" + USERS ||--o{ ARTICLE_APPROVAL_STEP_LOGS : "approves" + + USER_LEVELS ||--o{ APPROVAL_WORKFLOW_STEPS : "requires" + USER_LEVELS ||--o{ USERS : "defines" +``` + +## πŸ”§ **Technical Implementation Details** + +### **1. Dynamic Workflow Assignment** + +```go +// Ketika artikel dibuat +if userLevel.RequiresApproval { + // 1. Cari workflow default untuk client + workflow := getDefaultWorkflow(clientId) + + // 2. Buat approval flow instance + approvalFlow := ArticleApprovalFlows{ + ArticleId: articleId, + WorkflowId: workflow.ID, + CurrentStep: 1, + StatusId: 1, // pending + } + + // 3. Buat legacy record + legacyApproval := ArticleApprovals{ + ArticleId: articleId, + ApprovalAtLevel: nextApprovalLevel, + } +} +``` + +### **2. Step Progression Logic** + +```go +// Ketika step di-approve +func ApproveStep(flowId, approvedById, message) { + // 1. Log step yang di-approve + stepLog := ArticleApprovalStepLogs{ + ApprovalFlowId: flowId, + StepOrder: currentStep, + ApprovedById: approvedById, + Action: "approve", + } + + // 2. Cek apakah ada next step + nextStep := getNextStep(workflowId, currentStep) + + if nextStep != nil { + // 3. Pindah ke step berikutnya + updateCurrentStep(flowId, nextStep.StepOrder) + } else { + // 4. Complete approval + completeApproval(flowId) + } +} +``` + +### **3. User Level Hierarchy** + +```go +// Level hierarchy (level_number field) +Level 1: Highest Authority (POLDAS) +Level 2: Mid Authority (POLDAS) +Level 3: Lower Authority (POLRES) + +// Approval flow +Level 3 β†’ Level 2 β†’ Level 1 β†’ Approved +``` + +## πŸ“ˆ **Data Flow Examples** + +### **Example 1: Standard 3-Step Approval** + +``` +1. User Level 3 creates article + β”œβ”€β”€ article_approval_flows: {current_step: 1, status: pending} + β”œβ”€β”€ article_approvals: {approval_at_level: 2} + └── articles: {current_approval_step: 1, status: pending} + +2. User Level 2 approves + β”œβ”€β”€ article_approval_step_logs: {step_order: 1, action: approve} + β”œβ”€β”€ article_approval_flows: {current_step: 2, status: pending} + └── articles: {current_approval_step: 2} + +3. User Level 1 approves + β”œβ”€β”€ article_approval_step_logs: {step_order: 2, action: approve} + β”œβ”€β”€ article_approval_flows: {status: approved, completed_at: now} + └── articles: {status: approved, published: true} +``` + +### **Example 2: Auto-Skip Logic** + +``` +1. User Level 1 creates article (highest authority) + β”œβ”€β”€ Check: Can skip all steps? + β”œβ”€β”€ article_approval_flows: {status: approved, completed_at: now} + └── articles: {status: approved, published: true} +``` + +## πŸš€ **API Endpoints Flow** + +### **Article Creation** +``` +POST /api/articles +β”œβ”€β”€ Check user level +β”œβ”€β”€ Create article +β”œβ”€β”€ Assign workflow (if needed) +β”œβ”€β”€ Create approval flow +└── Return article data +``` + +### **Approval Process** +``` +PUT /api/article-approval-flows/{id}/approve +β”œβ”€β”€ Validate approver permissions +β”œβ”€β”€ Create step log +β”œβ”€β”€ Check for next step +β”œβ”€β”€ Update flow status +└── Send notifications +``` + +### **Status Checking** +``` +GET /api/articles/{id}/approval-status +β”œβ”€β”€ Get current flow +β”œβ”€β”€ Get step logs +β”œβ”€β”€ Get next step info +└── Return status data +``` + +## πŸ” **Debugging & Monitoring** + +### **Key Queries for Monitoring** + +```sql +-- Active approval flows +SELECT aaf.*, a.title, ul.name as current_approver_level +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id + AND aaf.current_step = aws.step_order +JOIN user_levels ul ON aws.required_user_level_id = ul.id +WHERE aaf.status_id = 1; + +-- Approval history +SELECT aasl.*, u.name as approver_name, ul.name as level_name +FROM article_approval_step_logs aasl +JOIN users u ON aasl.approved_by_id = u.id +JOIN user_levels ul ON aasl.user_level_id = ul.id +WHERE aasl.approval_flow_id = ?; +``` + +## ⚠️ **Common Issues & Solutions** + +### **Issue 1: Step Not Progressing** +- **Cause**: `getUserLevelId` returning wrong level +- **Solution**: Fix user level mapping logic + +### **Issue 2: Wrong Approval Level** +- **Cause**: `findNextApprovalLevel` logic incorrect +- **Solution**: Fix level hierarchy comparison + +### **Issue 3: Missing Step Logs** +- **Cause**: Step log not created during approval +- **Solution**: Ensure step log creation in `ApproveStep` + +## πŸ“ **Best Practices** + +1. **Always create step logs** for audit trail +2. **Validate user permissions** before approval +3. **Use transactions** for multi-table updates +4. **Implement proper error handling** for edge cases +5. **Log all approval actions** for debugging +6. **Test with different user level combinations** + +## 🎯 **Summary** + +Sistem approval workflow MEDOLS menggunakan arsitektur modular yang memisahkan: +- **Configuration** (workflows, steps, settings) +- **Execution** (flows, logs) +- **Legacy Support** (article_approvals) + +Setiap module memiliki tanggung jawab spesifik, dan mereka bekerja sama untuk menciptakan sistem approval yang fleksibel dan dapat di-audit. diff --git a/plan/approval-workflow-flow-diagram.md b/plan/approval-workflow-flow-diagram.md new file mode 100644 index 0000000..670a168 --- /dev/null +++ b/plan/approval-workflow-flow-diagram.md @@ -0,0 +1,255 @@ +# Approval Workflow Flow Diagram + +## πŸ”„ **Complete Approval Process Flow** + +### **1. System Setup Phase** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CLIENT │───▢│ APPROVAL_WORKFLOWS │───▢│ APPROVAL_WORKFLOW β”‚ +β”‚ CREATION β”‚ β”‚ (Master Template) β”‚ β”‚ STEPS β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ CLIENT_APPROVAL_ β”‚ + β”‚ SETTINGS β”‚ + β”‚ (Configuration) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### **2. Article Submission Phase** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USER LEVEL 3 │───▢│ CREATE ARTICLE │───▢│ CHECK USER LEVEL β”‚ +β”‚ (POLRES) β”‚ β”‚ β”‚ β”‚ REQUIRES APPROVAL? β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ YES - CREATE β”‚ + β”‚ APPROVAL FLOW β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ ARTICLE_APPROVAL_ β”‚ + β”‚ FLOWS β”‚ + β”‚ (current_step: 1) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### **3. Step-by-Step Approval Process** + +``` +STEP 1: Level 2 Approval +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USER LEVEL 2 │───▢│ REVIEW ARTICLE │───▢│ APPROVE/REJECT β”‚ +β”‚ (POLDAS) β”‚ β”‚ (Step 1) β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ CREATE STEP LOG β”‚ + β”‚ (ARTICLE_APPROVAL_ β”‚ + β”‚ STEP_LOGS) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ MOVE TO STEP 2 β”‚ + β”‚ (current_step: 2) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +STEP 2: Level 1 Approval +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USER LEVEL 1 │───▢│ REVIEW ARTICLE │───▢│ APPROVE/REJECT β”‚ +β”‚ (POLDAS) β”‚ β”‚ (Step 2) β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ CREATE STEP LOG β”‚ + β”‚ (ARTICLE_APPROVAL_ β”‚ + β”‚ STEP_LOGS) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ COMPLETE APPROVAL β”‚ + β”‚ (status: approved) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“Š **Database State Changes** + +### **Initial State (Article Created)** +``` +ARTICLES: +β”œβ”€β”€ id: 101 +β”œβ”€β”€ title: "Sample Article" +β”œβ”€β”€ workflow_id: 1 +β”œβ”€β”€ current_approval_step: 1 +β”œβ”€β”€ status_id: 1 (pending) +└── created_by_id: 3 + +ARTICLE_APPROVAL_FLOWS: +β”œβ”€β”€ id: 1 +β”œβ”€β”€ article_id: 101 +β”œβ”€β”€ workflow_id: 1 +β”œβ”€β”€ current_step: 1 +β”œβ”€β”€ status_id: 1 (pending) +β”œβ”€β”€ submitted_by_id: 3 +└── submitted_at: 2025-01-15 10:00:00 + +ARTICLE_APPROVALS (Legacy): +β”œβ”€β”€ id: 1 +β”œβ”€β”€ article_id: 101 +β”œβ”€β”€ approval_at_level: 2 +β”œβ”€β”€ status_id: 1 (pending) +└── message: "Need Approval" +``` + +### **After Step 1 Approval (Level 2)** +``` +ARTICLES: +β”œβ”€β”€ current_approval_step: 2 ← UPDATED +└── status_id: 1 (pending) + +ARTICLE_APPROVAL_FLOWS: +β”œβ”€β”€ current_step: 2 ← UPDATED +└── status_id: 1 (pending) + +ARTICLE_APPROVAL_STEP_LOGS: ← NEW RECORD +β”œβ”€β”€ id: 1 +β”œβ”€β”€ approval_flow_id: 1 +β”œβ”€β”€ step_order: 1 +β”œβ”€β”€ step_name: "Level 2 Review" +β”œβ”€β”€ approved_by_id: 2 +β”œβ”€β”€ action: "approve" +β”œβ”€β”€ message: "Approved by Level 2" +└── processed_at: 2025-01-15 11:00:00 +``` + +### **After Step 2 Approval (Level 1) - Final** +``` +ARTICLES: +β”œβ”€β”€ current_approval_step: null ← UPDATED +β”œβ”€β”€ status_id: 2 (approved) ← UPDATED +β”œβ”€β”€ is_publish: true ← UPDATED +└── published_at: 2025-01-15 12:00:00 ← UPDATED + +ARTICLE_APPROVAL_FLOWS: +β”œβ”€β”€ current_step: 2 +β”œβ”€β”€ status_id: 2 (approved) ← UPDATED +└── completed_at: 2025-01-15 12:00:00 ← UPDATED + +ARTICLE_APPROVAL_STEP_LOGS: ← NEW RECORD +β”œβ”€β”€ id: 2 +β”œβ”€β”€ approval_flow_id: 1 +β”œβ”€β”€ step_order: 2 +β”œβ”€β”€ step_name: "Level 1 Final Approval" +β”œβ”€β”€ approved_by_id: 1 +β”œβ”€β”€ action: "approve" +β”œβ”€β”€ message: "Final approval by Level 1" +└── processed_at: 2025-01-15 12:00:00 +``` + +## πŸ”§ **Module Interaction Matrix** + +| Module | Purpose | Reads From | Writes To | Triggers | +|--------|---------|------------|-----------|----------| +| `articles` | Article Management | - | `articles` | Creates approval flow | +| `approval_workflows` | Workflow Templates | `approval_workflows` | - | Defines steps | +| `approval_workflow_steps` | Step Definitions | `approval_workflow_steps` | - | Defines requirements | +| `article_approval_flows` | Active Instances | `approval_workflows` | `article_approval_flows` | Manages progression | +| `article_approval_step_logs` | Audit Trail | `article_approval_flows` | `article_approval_step_logs` | Logs actions | +| `article_approvals` | Legacy Support | - | `article_approvals` | Backward compatibility | +| `client_approval_settings` | Configuration | `clients` | `client_approval_settings` | Sets defaults | + +## 🚨 **Error Handling Flow** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ERROR │───▢│ LOG ERROR │───▢│ ROLLBACK β”‚ +β”‚ OCCURS β”‚ β”‚ (Zerolog) β”‚ β”‚ TRANSACTION β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ NOTIFY USER β”‚ + β”‚ (Error Response) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ”„ **Auto-Skip Logic Flow** + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ USER LEVEL 1 │───▢│ CHECK CAN SKIP │───▢│ SKIP ALL STEPS β”‚ +β”‚ (HIGHEST) β”‚ β”‚ ALL STEPS? β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ AUTO APPROVE β”‚ + β”‚ (status: approved) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“ˆ **Performance Considerations** + +### **Database Indexes Needed** +```sql +-- For fast approval flow lookups +CREATE INDEX idx_article_approval_flows_status ON article_approval_flows(status_id); +CREATE INDEX idx_article_approval_flows_current_step ON article_approval_flows(current_step); + +-- For step log queries +CREATE INDEX idx_article_approval_step_logs_flow_id ON article_approval_step_logs(approval_flow_id); +CREATE INDEX idx_article_approval_step_logs_processed_at ON article_approval_step_logs(processed_at); + +-- For user level queries +CREATE INDEX idx_users_user_level_id ON users(user_level_id); +CREATE INDEX idx_user_levels_level_number ON user_levels(level_number); +``` + +### **Caching Strategy** +``` +1. Workflow Templates β†’ Cache in Redis +2. User Level Mappings β†’ Cache in Memory +3. Active Approval Flows β†’ Cache with TTL +4. Step Requirements β†’ Cache per Workflow +``` + +## 🎯 **Key Success Metrics** + +1. **Approval Time**: Average time from submission to approval +2. **Step Completion Rate**: Percentage of steps completed successfully +3. **Error Rate**: Percentage of failed approval processes +4. **User Satisfaction**: Feedback on approval process +5. **System Performance**: Response times for approval actions + +## πŸ” **Debugging Checklist** + +- [ ] Check if workflow exists and is active +- [ ] Verify user level permissions +- [ ] Confirm step order is correct +- [ ] Validate foreign key relationships +- [ ] Check transaction rollback scenarios +- [ ] Monitor step log creation +- [ ] Verify notification delivery +- [ ] Test edge cases (auto-skip, rejection, etc.) + +## πŸ“ **Summary** + +Sistem approval workflow MEDOLS menggunakan pendekatan modular yang memungkinkan: + +1. **Fleksibilitas**: Workflow dapat dikonfigurasi per client +2. **Auditability**: Setiap action dicatat dalam step logs +3. **Scalability**: Dapat menangani multiple workflow types +4. **Backward Compatibility**: Legacy system tetap berfungsi +5. **User Experience**: Auto-skip untuk user level tinggi + +Dengan arsitektur ini, sistem dapat menangani berbagai skenario approval dengan efisien dan dapat diandalkan. diff --git a/plan/database-relationships-detailed.md b/plan/database-relationships-detailed.md new file mode 100644 index 0000000..86cb4ac --- /dev/null +++ b/plan/database-relationships-detailed.md @@ -0,0 +1,440 @@ +# Database Relationships & Entity Details + +## πŸ—„οΈ **Entity Relationship Overview** + +### **Core Entities** + +#### **1. CLIENTS** +```sql +CREATE TABLE clients ( + id UUID PRIMARY KEY, + name VARCHAR NOT NULL, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **2. USER_LEVELS** +```sql +CREATE TABLE user_levels ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + alias_name VARCHAR NOT NULL, + level_number INTEGER NOT NULL, -- 1=Highest, 2=Mid, 3=Lowest + parent_level_id INTEGER REFERENCES user_levels(id), + province_id INTEGER, + group VARCHAR, + is_approval_active BOOLEAN DEFAULT false, + client_id UUID REFERENCES clients(id), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **3. USERS** +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + email VARCHAR UNIQUE NOT NULL, + user_level_id INTEGER REFERENCES user_levels(id), + client_id UUID REFERENCES clients(id), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### **Workflow Configuration Entities** + +#### **4. APPROVAL_WORKFLOWS** +```sql +CREATE TABLE approval_workflows ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + description TEXT, + is_default BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + client_id UUID REFERENCES clients(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **5. APPROVAL_WORKFLOW_STEPS** +```sql +CREATE TABLE approval_workflow_steps ( + id SERIAL PRIMARY KEY, + workflow_id INTEGER REFERENCES approval_workflows(id) ON DELETE CASCADE, + step_order INTEGER NOT NULL, -- 1, 2, 3, etc. + step_name VARCHAR NOT NULL, + required_user_level_id INTEGER REFERENCES user_levels(id), + is_required BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **6. CLIENT_APPROVAL_SETTINGS** +```sql +CREATE TABLE client_approval_settings ( + id SERIAL PRIMARY KEY, + client_id UUID REFERENCES clients(id), + workflow_id INTEGER REFERENCES approval_workflows(id), + auto_approve_levels JSONB, -- Array of level numbers + notification_settings JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### **Article & Approval Execution Entities** + +#### **7. ARTICLES** +```sql +CREATE TABLE articles ( + id SERIAL PRIMARY KEY, + title VARCHAR NOT NULL, + slug VARCHAR UNIQUE, + description TEXT, + html_description TEXT, + workflow_id INTEGER REFERENCES approval_workflows(id), + current_approval_step INTEGER, + status_id INTEGER, -- 1=pending, 2=approved, 3=rejected + is_publish BOOLEAN DEFAULT false, + published_at TIMESTAMP, + created_by_id INTEGER REFERENCES users(id), + client_id UUID REFERENCES clients(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **8. ARTICLE_APPROVAL_FLOWS** +```sql +CREATE TABLE article_approval_flows ( + id SERIAL PRIMARY KEY, + article_id INTEGER REFERENCES articles(id) ON DELETE CASCADE, + workflow_id INTEGER REFERENCES approval_workflows(id), + current_step INTEGER DEFAULT 1, + status_id INTEGER DEFAULT 1, -- 1=pending, 2=approved, 3=rejected, 4=revision_requested + submitted_by_id INTEGER REFERENCES users(id), + submitted_at TIMESTAMP DEFAULT NOW(), + completed_at TIMESTAMP, + rejection_reason TEXT, + revision_requested BOOLEAN DEFAULT false, + revision_message TEXT, + client_id UUID REFERENCES clients(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **9. ARTICLE_APPROVAL_STEP_LOGS** +```sql +CREATE TABLE article_approval_step_logs ( + id SERIAL PRIMARY KEY, + approval_flow_id INTEGER REFERENCES article_approval_flows(id) ON DELETE CASCADE, + step_order INTEGER NOT NULL, + step_name VARCHAR NOT NULL, + approved_by_id INTEGER REFERENCES users(id), + action VARCHAR NOT NULL, -- approve, reject, auto_skip, request_revision + message TEXT, + processed_at TIMESTAMP DEFAULT NOW(), + user_level_id INTEGER REFERENCES user_levels(id), + created_at TIMESTAMP DEFAULT NOW() +); +``` + +#### **10. ARTICLE_APPROVALS (Legacy)** +```sql +CREATE TABLE article_approvals ( + id SERIAL PRIMARY KEY, + article_id INTEGER REFERENCES articles(id) ON DELETE CASCADE, + approval_by INTEGER REFERENCES users(id), + status_id INTEGER, -- 1=pending, 2=approved, 3=rejected + message TEXT, + approval_at_level INTEGER REFERENCES user_levels(id), + approval_at TIMESTAMP DEFAULT NOW(), + created_at TIMESTAMP DEFAULT NOW() +); +``` + +## πŸ”— **Relationship Details** + +### **One-to-Many Relationships** + +``` +CLIENTS (1) ──→ (N) USER_LEVELS +CLIENTS (1) ──→ (N) USERS +CLIENTS (1) ──→ (N) APPROVAL_WORKFLOWS +CLIENTS (1) ──→ (N) ARTICLES +CLIENTS (1) ──→ (N) CLIENT_APPROVAL_SETTINGS + +USER_LEVELS (1) ──→ (N) USERS +USER_LEVELS (1) ──→ (N) APPROVAL_WORKFLOW_STEPS +USER_LEVELS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS + +APPROVAL_WORKFLOWS (1) ──→ (N) APPROVAL_WORKFLOW_STEPS +APPROVAL_WORKFLOWS (1) ──→ (N) ARTICLE_APPROVAL_FLOWS +APPROVAL_WORKFLOWS (1) ──→ (N) ARTICLES + +ARTICLES (1) ──→ (N) ARTICLE_APPROVAL_FLOWS +ARTICLES (1) ──→ (N) ARTICLE_APPROVALS + +ARTICLE_APPROVAL_FLOWS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS + +USERS (1) ──→ (N) ARTICLES (as creator) +USERS (1) ──→ (N) ARTICLE_APPROVAL_FLOWS (as submitter) +USERS (1) ──→ (N) ARTICLE_APPROVAL_STEP_LOGS (as approver) +USERS (1) ──→ (N) ARTICLE_APPROVALS (as approver) +``` + +### **Many-to-Many Relationships (Implicit)** + +``` +USERS ←→ USER_LEVELS (through user_level_id) +ARTICLES ←→ APPROVAL_WORKFLOWS (through workflow_id) +ARTICLE_APPROVAL_FLOWS ←→ APPROVAL_WORKFLOW_STEPS (through workflow_id and step_order) +``` + +## πŸ“Š **Data Flow Examples** + +### **Example 1: Complete Approval Process** + +#### **Step 1: Setup Data** +```sql +-- Client +INSERT INTO clients (id, name) VALUES ('b1ce6602-07ad-46c2-85eb-0cd6decfefa3', 'Test Client'); + +-- User Levels +INSERT INTO user_levels (name, alias_name, level_number, is_approval_active, client_id) VALUES +('POLDAS', 'Poldas', 1, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'), +('POLDAS', 'Poldas', 2, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'), +('POLRES', 'Polres', 3, true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'); + +-- Users +INSERT INTO users (name, email, user_level_id, client_id) VALUES +('Admin Level 1', 'admin1@test.com', 1, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'), +('Admin Level 2', 'admin2@test.com', 2, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'), +('User Level 3', 'user3@test.com', 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'); + +-- Workflow +INSERT INTO approval_workflows (name, description, is_default, client_id) VALUES +('Standard Approval', 'Standard 3-step approval process', true, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'); + +-- Workflow Steps +INSERT INTO approval_workflow_steps (workflow_id, step_order, step_name, required_user_level_id) VALUES +(1, 1, 'Level 2 Review', 2), +(1, 2, 'Level 1 Final Approval', 1); +``` + +#### **Step 2: Article Creation** +```sql +-- Article created by User Level 3 +INSERT INTO articles (title, slug, workflow_id, current_approval_step, status_id, created_by_id, client_id) VALUES +('Test Article', 'test-article', 1, 1, 1, 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'); + +-- Approval Flow +INSERT INTO article_approval_flows (article_id, workflow_id, current_step, status_id, submitted_by_id, client_id) VALUES +(1, 1, 1, 1, 3, 'b1ce6602-07ad-46c2-85eb-0cd6decfefa3'); + +-- Legacy Approval +INSERT INTO article_approvals (article_id, approval_by, status_id, message, approval_at_level) VALUES +(1, 3, 1, 'Need Approval', 2); +``` + +#### **Step 3: Level 2 Approval** +```sql +-- Step Log +INSERT INTO article_approval_step_logs (approval_flow_id, step_order, step_name, approved_by_id, action, message, user_level_id) VALUES +(1, 1, 'Level 2 Review', 2, 'approve', 'Approved by Level 2', 2); + +-- Update Flow +UPDATE article_approval_flows SET current_step = 2 WHERE id = 1; + +-- Update Article +UPDATE articles SET current_approval_step = 2 WHERE id = 1; +``` + +#### **Step 4: Level 1 Final Approval** +```sql +-- Step Log +INSERT INTO article_approval_step_logs (approval_flow_id, step_order, step_name, approved_by_id, action, message, user_level_id) VALUES +(1, 2, 'Level 1 Final Approval', 1, 'approve', 'Final approval by Level 1', 1); + +-- Complete Flow +UPDATE article_approval_flows SET + status_id = 2, + completed_at = NOW() +WHERE id = 1; + +-- Publish Article +UPDATE articles SET + status_id = 2, + is_publish = true, + published_at = NOW(), + current_approval_step = NULL +WHERE id = 1; +``` + +## πŸ” **Key Queries for Monitoring** + +### **Active Approval Flows** +```sql +SELECT + aaf.id as flow_id, + a.title as article_title, + aaf.current_step, + aws.step_name as current_step_name, + ul.name as required_approver_level, + u.name as submitted_by, + aaf.submitted_at, + aaf.status_id +FROM article_approval_flows aaf +JOIN articles a ON aaf.article_id = a.id +JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id + AND aaf.current_step = aws.step_order +JOIN user_levels ul ON aws.required_user_level_id = ul.id +JOIN users u ON aaf.submitted_by_id = u.id +WHERE aaf.status_id = 1 -- pending +ORDER BY aaf.submitted_at DESC; +``` + +### **Approval History** +```sql +SELECT + a.title as article_title, + aasl.step_name, + aasl.action, + aasl.message, + u.name as approver_name, + ul.name as approver_level, + aasl.processed_at +FROM article_approval_step_logs aasl +JOIN article_approval_flows aaf ON aasl.approval_flow_id = aaf.id +JOIN articles a ON aaf.article_id = a.id +JOIN users u ON aasl.approved_by_id = u.id +JOIN user_levels ul ON aasl.user_level_id = ul.id +WHERE aaf.article_id = ? -- specific article +ORDER BY aasl.step_order, aasl.processed_at; +``` + +### **Workflow Performance** +```sql +SELECT + aw.name as workflow_name, + COUNT(aaf.id) as total_flows, + COUNT(CASE WHEN aaf.status_id = 2 THEN 1 END) as approved, + COUNT(CASE WHEN aaf.status_id = 3 THEN 1 END) as rejected, + AVG(EXTRACT(EPOCH FROM (aaf.completed_at - aaf.submitted_at))/3600) as avg_hours +FROM approval_workflows aw +LEFT JOIN article_approval_flows aaf ON aw.id = aaf.workflow_id +WHERE aw.client_id = ? +GROUP BY aw.id, aw.name; +``` + +## ⚠️ **Common Data Issues** + +### **Issue 1: Orphaned Records** +```sql +-- Find articles without approval flows +SELECT a.id, a.title +FROM articles a +LEFT JOIN article_approval_flows aaf ON a.id = aaf.article_id +WHERE aaf.id IS NULL AND a.workflow_id IS NOT NULL; + +-- Find approval flows without articles +SELECT aaf.id, aaf.article_id +FROM article_approval_flows aaf +LEFT JOIN articles a ON aaf.article_id = a.id +WHERE a.id IS NULL; +``` + +### **Issue 2: Inconsistent Step Data** +```sql +-- Find flows with invalid current_step +SELECT aaf.id, aaf.current_step, aws.step_order +FROM article_approval_flows aaf +JOIN approval_workflow_steps aws ON aaf.workflow_id = aws.workflow_id +WHERE aaf.current_step != aws.step_order; +``` + +### **Issue 3: Missing Step Logs** +```sql +-- Find approved flows without step logs +SELECT aaf.id, aaf.article_id, aaf.status_id +FROM article_approval_flows aaf +LEFT JOIN article_approval_step_logs aasl ON aaf.id = aasl.approval_flow_id +WHERE aaf.status_id = 2 AND aasl.id IS NULL; +``` + +## πŸš€ **Performance Optimization** + +### **Indexes** +```sql +-- Primary indexes +CREATE INDEX idx_article_approval_flows_status ON article_approval_flows(status_id); +CREATE INDEX idx_article_approval_flows_current_step ON article_approval_flows(current_step); +CREATE INDEX idx_article_approval_flows_client_id ON article_approval_flows(client_id); +CREATE INDEX idx_article_approval_step_logs_flow_id ON article_approval_step_logs(approval_flow_id); +CREATE INDEX idx_article_approval_step_logs_processed_at ON article_approval_step_logs(processed_at); +CREATE INDEX idx_articles_workflow_id ON articles(workflow_id); +CREATE INDEX idx_articles_status_id ON articles(status_id); +CREATE INDEX idx_users_user_level_id ON users(user_level_id); +CREATE INDEX idx_user_levels_level_number ON user_levels(level_number); + +-- Composite indexes +CREATE INDEX idx_article_approval_flows_status_step ON article_approval_flows(status_id, current_step); +CREATE INDEX idx_approval_workflow_steps_workflow_order ON approval_workflow_steps(workflow_id, step_order); +``` + +### **Partitioning (for large datasets)** +```sql +-- Partition article_approval_step_logs by month +CREATE TABLE article_approval_step_logs_2025_01 PARTITION OF article_approval_step_logs +FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); +``` + +## πŸ“ **Data Validation Rules** + +### **Business Rules** +1. **Workflow Steps**: Must be sequential (1, 2, 3, etc.) +2. **User Levels**: Level number must be unique per client +3. **Approval Flows**: Can only have one active flow per article +4. **Step Logs**: Must have corresponding workflow step +5. **Status Transitions**: Only valid transitions allowed + +### **Database Constraints** +```sql +-- Ensure step order is sequential +ALTER TABLE approval_workflow_steps +ADD CONSTRAINT chk_step_order_positive CHECK (step_order > 0); + +-- Ensure level number is positive +ALTER TABLE user_levels +ADD CONSTRAINT chk_level_number_positive CHECK (level_number > 0); + +-- Ensure status values are valid +ALTER TABLE article_approval_flows +ADD CONSTRAINT chk_status_valid CHECK (status_id IN (1, 2, 3, 4)); + +-- Ensure current step is valid +ALTER TABLE article_approval_flows +ADD CONSTRAINT chk_current_step_positive CHECK (current_step > 0); +``` + +## 🎯 **Summary** + +Database design untuk sistem approval workflow MEDOLS menggunakan: + +1. **Normalized Structure**: Memisahkan konfigurasi dari eksekusi +2. **Audit Trail**: Step logs untuk tracking lengkap +3. **Flexibility**: Workflow dapat dikonfigurasi per client +4. **Performance**: Indexes untuk query yang efisien +5. **Data Integrity**: Constraints dan validasi +6. **Scalability**: Partitioning untuk data besar + +Dengan struktur ini, sistem dapat menangani berbagai skenario approval dengan efisien dan dapat diandalkan. diff --git a/plan/end-to-end-testing-scenarios.md b/plan/end-to-end-testing-scenarios.md new file mode 100644 index 0000000..f15e4e6 --- /dev/null +++ b/plan/end-to-end-testing-scenarios.md @@ -0,0 +1,785 @@ +# End-to-End Testing Scenarios - Approval Workflow System + +## Overview +Dokumentasi ini berisi skenario testing end-to-end lengkap untuk sistem approval workflow, mulai dari pembuatan client baru hingga pembuatan artikel dengan proses approval yang dinamis. + +## Base Configuration +```bash +# Base URL +BASE_URL="http://localhost:8800/api" + +# Headers +AUTH_HEADER="Authorization: Bearer YOUR_JWT_TOKEN" +CLIENT_HEADER="X-Client-Key: YOUR_CLIENT_KEY" +CONTENT_TYPE="Content-Type: application/json" +``` + +--- + +## 🏒 Scenario 1: Complete Client Setup to Article Creation + +### Step 1: Create New Client +```bash +curl -X POST "${BASE_URL}/clients" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Test Media Company", + "is_active": true + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Client created successfully"], + "data": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Test Media Company", + "is_active": true, + "created_at": "2024-01-15T10:00:00Z" + } +} +``` + +### Step 2: Create User Levels +```bash +# Create user levels for approval workflow +curl -X POST "${BASE_URL}/user-levels" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Editor", + "alias_name": "ED", + "level_number": 1, + "is_approval_active": true + }' + +curl -X POST "${BASE_URL}/user-levels" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Senior Editor", + "alias_name": "SED", + "level_number": 2, + "is_approval_active": true + }' + +curl -X POST "${BASE_URL}/user-levels" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Editor in Chief", + "alias_name": "EIC", + "level_number": 3, + "is_approval_active": true + }' +``` + +### Step 3: Create Approval Workflow +```bash +curl -X POST "${BASE_URL}/approval-workflows" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Standard 3-Level Editorial Review", + "description": "Complete editorial workflow with 3 approval levels", + "is_default": true, + "is_active": true, + "requires_approval": true, + "auto_publish": false, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +### Step 4: Create Workflow Steps +```bash +# Step 1: Editor Review +curl -X POST "${BASE_URL}/approval-workflow-steps" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "step_order": 1, + "step_name": "Editor Review", + "required_user_level_id": 1, + "can_skip": false, + "auto_approve_after_hours": 24, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' + +# Step 2: Senior Editor Review +curl -X POST "${BASE_URL}/approval-workflow-steps" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "step_order": 2, + "step_name": "Senior Editor Review", + "required_user_level_id": 2, + "can_skip": false, + "auto_approve_after_hours": 48, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' + +# Step 3: Editor in Chief +curl -X POST "${BASE_URL}/approval-workflow-steps" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "step_order": 3, + "step_name": "Editor in Chief Approval", + "required_user_level_id": 3, + "can_skip": false, + "auto_approve_after_hours": 72, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +### Step 5: Configure Client Approval Settings +```bash +curl -X POST "${BASE_URL}/client-approval-settings" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "client_id": "550e8400-e29b-41d4-a716-446655440000", + "requires_approval": true, + "default_workflow_id": 1, + "auto_publish_articles": false, + "approval_exempt_users": [], + "approval_exempt_roles": [], + "approval_exempt_categories": [], + "require_approval_for": ["article", "news", "review"], + "skip_approval_for": ["announcement", "update"], + "is_active": true + }' +``` + +### Step 6: Create Article Category +```bash +curl -X POST "${BASE_URL}/article-categories" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "title": "Technology News", + "description": "Latest technology news and updates", + "slug": "technology-news", + "status_id": 1, + "is_publish": true, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +### Step 7: Create Article +```bash +curl -X POST "${BASE_URL}/articles" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "title": "Revolutionary AI Technology Breakthrough", + "slug": "revolutionary-ai-technology-breakthrough", + "description": "A comprehensive look at the latest AI breakthrough that could change everything", + "html_description": "

A comprehensive look at the latest AI breakthrough that could change everything

", + "category_id": 1, + "type_id": 1, + "tags": "AI, Technology, Innovation, Breakthrough", + "created_by_id": 1, + "status_id": 1, + "is_draft": false, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article created successfully"], + "data": { + "id": 1, + "title": "Revolutionary AI Technology Breakthrough", + "status_id": 1, + "is_draft": false, + "approval_required": true, + "workflow_id": 1, + "created_at": "2024-01-15T10:30:00Z" + } +} +``` + +--- + +## πŸ“ Scenario 2: Complete Approval Process + +### Step 1: Submit Article for Approval +```bash +curl -X POST "${BASE_URL}/articles/1/submit-approval" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "message": "Article ready for editorial review process" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article submitted for approval successfully"], + "data": { + "id": 1, + "article_id": 1, + "workflow_id": 1, + "current_step": 1, + "status_id": 1, + "submitted_at": "2024-01-15T10:35:00Z" + } +} +``` + +### Step 2: Check Approval Status +```bash +curl -X GET "${BASE_URL}/articles/1/approval-status" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Approval status retrieved successfully"], + "data": { + "article_id": 1, + "current_status": "pending_approval", + "current_step": 1, + "total_steps": 3, + "workflow_name": "Standard 3-Level Editorial Review", + "current_step_name": "Editor Review", + "next_step_name": "Senior Editor Review", + "waiting_since": "2024-01-15T10:35:00Z" + } +} +``` + +### Step 3: Editor Approves (Step 1) +```bash +curl -X POST "${BASE_URL}/articles/1/approve" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Content quality meets editorial standards, approved for next level" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article approved successfully"], + "data": { + "current_step": 2, + "status": "moved_to_next_level", + "next_approver_level": 2, + "approved_at": "2024-01-15T11:00:00Z" + } +} +``` + +### Step 4: Senior Editor Approves (Step 2) +```bash +curl -X POST "${BASE_URL}/articles/1/approve" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Excellent content quality and structure, ready for final approval" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article approved successfully"], + "data": { + "current_step": 3, + "status": "moved_to_next_level", + "next_approver_level": 3, + "approved_at": "2024-01-15T12:00:00Z" + } +} +``` + +### Step 5: Editor in Chief Approves (Step 3 - Final) +```bash +curl -X POST "${BASE_URL}/articles/1/approve" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Final approval granted, content ready for publication" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article approved and published successfully"], + "data": { + "status": "approved", + "article_status": "published", + "is_publish": true, + "published_at": "2024-01-15T13:00:00Z", + "completion_date": "2024-01-15T13:00:00Z" + } +} +``` + +--- + +## ❌ Scenario 3: Article Rejection and Revision + +### Step 1: Submit Another Article +```bash +curl -X POST "${BASE_URL}/articles" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "title": "Product Review: New Smartphone", + "slug": "product-review-new-smartphone", + "description": "Comprehensive review of the latest smartphone", + "html_description": "

Comprehensive review of the latest smartphone

", + "category_id": 1, + "type_id": 1, + "tags": "Review, Smartphone, Technology", + "created_by_id": 1, + "status_id": 1, + "is_draft": false, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +### Step 2: Submit for Approval +```bash +curl -X POST "${BASE_URL}/articles/2/submit-approval" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "message": "Product review ready for approval" + }' +``` + +### Step 3: Editor Approves (Step 1) +```bash +curl -X POST "${BASE_URL}/articles/2/approve" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Initial review passed, good structure" + }' +``` + +### Step 4: Senior Editor Rejects (Step 2) +```bash +curl -X POST "${BASE_URL}/articles/2/reject" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Insufficient technical details and benchmark comparisons needed" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article rejected successfully"], + "data": { + "status": "rejected", + "article_status": "draft", + "rejection_reason": "Insufficient technical details and benchmark comparisons needed", + "rejected_at": "2024-01-15T14:00:00Z" + } +} +``` + +### Step 5: Request Revision +```bash +curl -X POST "${BASE_URL}/articles/2/request-revision" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Please add detailed technical specifications, benchmark comparisons, and more comprehensive testing results" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Revision requested successfully"], + "data": { + "status": "revision_requested", + "revision_message": "Please add detailed technical specifications, benchmark comparisons, and more comprehensive testing results", + "requested_at": "2024-01-15T14:15:00Z" + } +} +``` + +### Step 6: Resubmit After Revision +```bash +curl -X POST "${BASE_URL}/articles/2/resubmit" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "message": "Article revised with additional technical details and benchmark comparisons" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article resubmitted successfully"], + "data": { + "status": "pending_approval", + "current_step": 1, + "resubmitted_at": "2024-01-15T15:00:00Z" + } +} +``` + +--- + +## ⚑ Scenario 4: Dynamic Approval Toggle + +### Step 1: Check Current Settings +```bash +curl -X GET "${BASE_URL}/client-approval-settings" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 2: Disable Approval System +```bash +curl -X PUT "${BASE_URL}/client-approval-settings/1" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "requires_approval": false, + "auto_publish_articles": true, + "reason": "Breaking news mode - immediate publishing required" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Approval settings updated successfully"], + "data": { + "requires_approval": false, + "auto_publish_articles": true, + "updated_at": "2024-01-15T16:00:00Z" + } +} +``` + +### Step 3: Create Article (Should Auto-Publish) +```bash +curl -X POST "${BASE_URL}/articles" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "title": "BREAKING: Major Tech Acquisition", + "slug": "breaking-major-tech-acquisition", + "description": "Breaking news about major technology acquisition", + "html_description": "

Breaking news about major technology acquisition

", + "category_id": 1, + "type_id": 1, + "tags": "Breaking, News, Acquisition, Technology", + "created_by_id": 1, + "status_id": 1, + "is_draft": false, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +**Expected Response:** +```json +{ + "success": true, + "messages": ["Article created and published successfully"], + "data": { + "id": 3, + "title": "BREAKING: Major Tech Acquisition", + "status": "published", + "is_publish": true, + "published_at": "2024-01-15T16:05:00Z", + "approval_bypassed": true, + "bypass_reason": "approval_disabled" + } +} +``` + +### Step 4: Re-enable Approval System +```bash +curl -X PUT "${BASE_URL}/client-approval-settings/1" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "requires_approval": true, + "auto_publish_articles": false, + "default_workflow_id": 1, + "reason": "Returning to normal approval process" + }' +``` + +--- + +## πŸ“Š Scenario 5: Approval Dashboard and Monitoring + +### Step 1: Get Pending Approvals +```bash +curl -X GET "${BASE_URL}/approvals/pending?page=1&limit=10" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 2: Get My Approval Queue +```bash +curl -X GET "${BASE_URL}/approvals/my-queue?page=1&limit=10" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 3: Get Approval History for Article +```bash +curl -X GET "${BASE_URL}/articles/1/approval-history" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 4: Get My Approval Statistics +```bash +curl -X GET "${BASE_URL}/approvals/my-stats" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +--- + +## πŸ”§ Scenario 6: Workflow Management + +### Step 1: Get All Workflows +```bash +curl -X GET "${BASE_URL}/approval-workflows?page=1&limit=10" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 2: Get Workflow by ID +```bash +curl -X GET "${BASE_URL}/approval-workflows/1" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" +``` + +### Step 3: Update Workflow +```bash +curl -X PUT "${BASE_URL}/approval-workflows/1" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "name": "Updated 3-Level Editorial Review", + "description": "Updated workflow with improved efficiency", + "is_active": true + }' +``` + +### Step 4: Add New Workflow Step +```bash +curl -X POST "${BASE_URL}/approval-workflow-steps" \ + -H "${AUTH_HEADER}" \ + -H "${CLIENT_HEADER}" \ + -H "${CONTENT_TYPE}" \ + -d '{ + "workflow_id": 1, + "step_order": 2, + "step_name": "Legal Review", + "required_user_level_id": 4, + "can_skip": true, + "auto_approve_after_hours": 24, + "client_id": "550e8400-e29b-41d4-a716-446655440000" + }' +``` + +--- + +## πŸ§ͺ Test Data Setup Script + +```bash +#!/bin/bash + +# Set environment variables +BASE_URL="http://localhost:8800/api" +AUTH_HEADER="Authorization: Bearer YOUR_JWT_TOKEN" +CLIENT_HEADER="X-Client-Key: YOUR_CLIENT_KEY" +CONTENT_TYPE="Content-Type: application/json" + +# Function to make API calls +make_request() { + local method=$1 + local endpoint=$2 + local data=$3 + + if [ -n "$data" ]; then + curl -X "$method" "${BASE_URL}${endpoint}" \ + -H "$AUTH_HEADER" \ + -H "$CLIENT_HEADER" \ + -H "$CONTENT_TYPE" \ + -d "$data" + else + curl -X "$method" "${BASE_URL}${endpoint}" \ + -H "$AUTH_HEADER" \ + -H "$CLIENT_HEADER" + fi +} + +echo "Setting up test data..." + +# 1. Create client +echo "Creating client..." +make_request "POST" "/clients" '{ + "name": "Test Media Company", + "is_active": true +}' + +# 2. Create user levels +echo "Creating user levels..." +make_request "POST" "/user-levels" '{ + "name": "Editor", + "alias_name": "ED", + "level_number": 1, + "is_approval_active": true +}' + +make_request "POST" "/user-levels" '{ + "name": "Senior Editor", + "alias_name": "SED", + "level_number": 2, + "is_approval_active": true +}' + +make_request "POST" "/user-levels" '{ + "name": "Editor in Chief", + "alias_name": "EIC", + "level_number": 3, + "is_approval_active": true +}' + +# 3. Create approval workflow +echo "Creating approval workflow..." +make_request "POST" "/approval-workflows" '{ + "name": "Standard 3-Level Editorial Review", + "description": "Complete editorial workflow with 3 approval levels", + "is_default": true, + "is_active": true, + "requires_approval": true, + "auto_publish": false +}' + +echo "Test data setup completed!" +``` + +--- + +## πŸ“‹ Test Validation Checklist + +### βœ… Functional Testing +- [ ] Client creation and configuration +- [ ] User level management +- [ ] Approval workflow creation and modification +- [ ] Article creation and submission +- [ ] Complete approval process flow +- [ ] Article rejection and revision process +- [ ] Dynamic approval toggle functionality +- [ ] Approval dashboard and monitoring +- [ ] Multi-step workflow progression +- [ ] Auto-publish functionality + +### βœ… Error Handling +- [ ] Invalid client key handling +- [ ] Invalid JWT token handling +- [ ] Missing required fields validation +- [ ] Workflow step validation +- [ ] User permission validation +- [ ] Article status validation + +### βœ… Performance Testing +- [ ] Response time < 500ms for all endpoints +- [ ] Concurrent approval processing +- [ ] Large dataset pagination +- [ ] Database query optimization + +### βœ… Security Testing +- [ ] Client isolation +- [ ] User authorization +- [ ] Data validation and sanitization +- [ ] SQL injection prevention + +--- + +## πŸš€ Running the Tests + +### Prerequisites +1. Ensure the backend server is running on `http://localhost:8800` +2. Obtain valid JWT token for authentication +3. Set up client key for multi-tenant support +4. Database should be clean and ready for testing + +### Execution Steps +1. Run the test data setup script +2. Execute each scenario sequentially +3. Validate responses against expected outputs +4. Check database state after each scenario +5. Clean up test data after completion + +### Monitoring +- Monitor server logs during testing +- Check database performance metrics +- Validate all audit trails are created +- Ensure proper error handling and logging + +--- + +*This documentation provides comprehensive end-to-end testing scenarios for the approval workflow system. Each scenario includes detailed curl commands and expected responses for complete testing coverage.*