feat : update major approval workflow process

This commit is contained in:
hanif salafi 2025-09-16 23:16:51 +07:00
parent 8d0e6d81c0
commit 95cc9df933
34 changed files with 3904 additions and 449 deletions

View File

@ -1,8 +1,9 @@
package entity package entity
import ( import (
"github.com/google/uuid"
"time" "time"
"github.com/google/uuid"
) )
type ActivityLogs struct { type ActivityLogs struct {
@ -14,4 +15,7 @@ type ActivityLogs struct {
UserId *uint `json:"user_id" gorm:"type:int4"` UserId *uint `json:"user_id" gorm:"type:int4"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
// Relations
Article *Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"`
} }

View File

@ -1,8 +1,9 @@
package entity package entity
import ( import (
"github.com/google/uuid"
"time" "time"
"github.com/google/uuid"
) )
type ArticleApprovals struct { type ArticleApprovals struct {
@ -14,4 +15,7 @@ type ArticleApprovals struct {
ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"` ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
// Relations
Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"`
} }

View File

@ -1,9 +1,10 @@
package article_category_details package article_category_details
import ( import (
"github.com/google/uuid"
"time" "time"
entity "web-medols-be/app/database/entity" entity "web-medols-be/app/database/entity"
"github.com/google/uuid"
) )
type ArticleCategoryDetails struct { type ArticleCategoryDetails struct {
@ -15,4 +16,7 @@ type ArticleCategoryDetails struct {
IsActive bool `json:"is_active" gorm:"type:bool"` IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_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"`
} }

View File

@ -1,8 +1,9 @@
package entity package entity
import ( import (
"github.com/google/uuid"
"time" "time"
"github.com/google/uuid"
) )
type ArticleComments struct { type ArticleComments struct {
@ -18,6 +19,9 @@ type ArticleComments struct {
IsActive bool `json:"is_active" gorm:"type:bool"` IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_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 // statusId => 0: waiting, 1: accepted, 2: replied, 3: rejected

View File

@ -1,8 +1,9 @@
package entity package entity
import ( import (
"github.com/google/uuid"
"time" "time"
"github.com/google/uuid"
) )
type ArticleFiles struct { type ArticleFiles struct {
@ -26,4 +27,7 @@ type ArticleFiles struct {
IsActive bool `json:"is_active" gorm:"type:bool;default:true"` IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relations
Article Articles `json:"article" gorm:"foreignKey:ArticleId;constraint:OnDelete:CASCADE"`
} }

View File

@ -1,26 +1,57 @@
package entity package entity
import ( import (
"github.com/google/uuid" "database/sql/driver"
"encoding/json"
"time" "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 { type ClientApprovalSettings struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ClientId uuid.UUID `json:"client_id" gorm:"type:UUID;not null;uniqueIndex"` 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 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 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 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 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 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 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 RequireApprovalFor []string `json:"require_approval_for" gorm:"type:jsonb"` // specific content types that need approval
SkipApprovalFor []string `json:"skip_approval_for" gorm:"type:varchar[]"` // specific content types that skip 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"` IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relations // 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"` Workflow *ApprovalWorkflows `json:"workflow" gorm:"foreignKey:DefaultWorkflowId"`
} }

View File

@ -1,13 +1,14 @@
package database package database
import ( 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" "github.com/rs/zerolog"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "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 // Database setup database with gorm
@ -99,6 +100,7 @@ func Models() []interface{} {
entity.AuditTrails{}, entity.AuditTrails{},
entity.Cities{}, entity.Cities{},
entity.Clients{}, entity.Clients{},
entity.ClientApprovalSettings{},
entity.CsrfTokenRecords{}, entity.CsrfTokenRecords{},
entity.CustomStaticPages{}, entity.CustomStaticPages{},
entity.Districts{}, entity.Districts{},

View File

@ -1,14 +1,15 @@
package middleware package middleware
import ( import (
"github.com/gofiber/fiber/v2/middleware/csrf"
"github.com/gofiber/fiber/v2/middleware/session"
"log" "log"
"time" "time"
"web-medols-be/app/database" "web-medols-be/app/database"
"web-medols-be/config/config" "web-medols-be/config/config"
utilsSvc "web-medols-be/utils" 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"
"github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors" "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(ClientMiddleware(db.DB))
m.App.Use(AuditTrailsMiddleware(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{ //m.App.Use(filesystem.New(filesystem.Config{
// Next: utils.IsEnabled(m.Cfg.Middleware.FileSystem.Enable), // Next: utils.IsEnabled(m.Cfg.Middleware.FileSystem.Enable),

View File

@ -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
}

View File

@ -2,12 +2,13 @@ package repository
import ( import (
"fmt" "fmt"
"github.com/google/uuid"
"github.com/rs/zerolog"
"web-medols-be/app/database" "web-medols-be/app/database"
"web-medols-be/app/database/entity" "web-medols-be/app/database/entity"
"web-medols-be/app/module/approval_workflow_steps/request" "web-medols-be/app/module/approval_workflow_steps/request"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
"github.com/google/uuid"
"github.com/rs/zerolog"
) )
type approvalWorkflowStepsRepository struct { 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.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") query = query.Order("workflow_id ASC, step_order ASC")
err = query.Count(&count).Error 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.Where("client_id = ?", clientId)
} }
query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") query = query.Preload("Workflow").Preload("RequiredUserLevel")
err = query.First(&step, id).Error err = query.First(&step, id).Error
return step, err return step, err
@ -157,7 +158,7 @@ func (_i *approvalWorkflowStepsRepository) GetByWorkflowId(clientId *uuid.UUID,
} }
query = query.Where("workflow_id = ?", workflowId) query = query.Where("workflow_id = ?", workflowId)
query = query.Preload("ApprovalWorkflow").Preload("RequiredUserLevel") query = query.Preload("Workflow").Preload("RequiredUserLevel")
query = query.Order("step_order ASC") query = query.Order("step_order ASC")
err = query.Find(&steps).Error 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.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") query = query.Order("step_order ASC")
err = query.Find(&steps).Error 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.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 err = query.First(&step).Error
return step, err 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.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") query = query.Order("step_order ASC")
err = query.First(&step).Error 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.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") query = query.Order("step_order DESC")
err = query.First(&step).Error 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.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") query = query.Order("workflow_id ASC, step_order ASC")
err = query.Find(&steps).Error err = query.Find(&steps).Error
@ -369,4 +370,4 @@ func (_i *approvalWorkflowStepsRepository) CheckStepDependencies(clientId *uuid.
canDelete = len(dependencies) == 0 canDelete = len(dependencies) == 0
return canDelete, dependencies, nil return canDelete, dependencies, nil
} }

View File

@ -20,52 +20,73 @@ type ApprovalWorkflowsQueryRequest struct {
} }
type ApprovalWorkflowsCreateRequest struct { type ApprovalWorkflowsCreateRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"` Description string `json:"description" validate:"required"`
IsActive *bool `json:"isActive"` IsActive *bool `json:"isActive"`
IsDefault *bool `json:"isDefault"` IsDefault *bool `json:"isDefault"`
RequiresApproval *bool `json:"requiresApproval"`
AutoPublish *bool `json:"autoPublish"`
Steps []ApprovalWorkflowStepRequest `json:"steps"`
} }
func (req ApprovalWorkflowsCreateRequest) ToEntity() *entity.ApprovalWorkflows { func (req ApprovalWorkflowsCreateRequest) ToEntity() *entity.ApprovalWorkflows {
return &entity.ApprovalWorkflows{ return &entity.ApprovalWorkflows{
Name: req.Name, Name: req.Name,
Description: &req.Description, Description: &req.Description,
IsActive: req.IsActive, IsActive: req.IsActive,
IsDefault: req.IsDefault, IsDefault: req.IsDefault,
CreatedAt: time.Now(), RequiresApproval: req.RequiresApproval,
UpdatedAt: time.Now(), AutoPublish: req.AutoPublish,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
} }
} }
func (req ApprovalWorkflowsCreateRequest) ToStepsEntity() []*entity.ApprovalWorkflowSteps { func (req ApprovalWorkflowsCreateRequest) ToStepsEntity() []*entity.ApprovalWorkflowSteps {
// Return empty slice since basic create request doesn't include steps steps := make([]*entity.ApprovalWorkflowSteps, len(req.Steps))
return []*entity.ApprovalWorkflowSteps{} 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 { type ApprovalWorkflowsUpdateRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"` Description string `json:"description" validate:"required"`
IsActive *bool `json:"isActive"` IsActive *bool `json:"isActive"`
IsDefault *bool `json:"isDefault"` IsDefault *bool `json:"isDefault"`
RequiresApproval *bool `json:"requiresApproval"`
AutoPublish *bool `json:"autoPublish"`
} }
func (req ApprovalWorkflowsUpdateRequest) ToEntity() *entity.ApprovalWorkflows { func (req ApprovalWorkflowsUpdateRequest) ToEntity() *entity.ApprovalWorkflows {
return &entity.ApprovalWorkflows{ return &entity.ApprovalWorkflows{
Name: req.Name, Name: req.Name,
Description: &req.Description, Description: &req.Description,
IsActive: req.IsActive, IsActive: req.IsActive,
IsDefault: req.IsDefault, IsDefault: req.IsDefault,
UpdatedAt: time.Now(), RequiresApproval: req.RequiresApproval,
AutoPublish: req.AutoPublish,
UpdatedAt: time.Now(),
} }
} }
type ApprovalWorkflowStepRequest struct { type ApprovalWorkflowStepRequest struct {
StepOrder int `json:"stepOrder" validate:"required"` StepOrder int `json:"stepOrder" validate:"required"`
StepName string `json:"stepName" validate:"required"` StepName string `json:"stepName" validate:"required"`
RequiredUserLevelId uint `json:"requiredUserLevelId" validate:"required"` RequiredUserLevelId uint `json:"requiredUserLevelId" validate:"required"`
CanSkip *bool `json:"canSkip"` CanSkip *bool `json:"canSkip"`
AutoApproveAfterHours *int `json:"autoApproveAfterHours"` AutoApproveAfterHours *int `json:"autoApproveAfterHours"`
IsActive *bool `json:"isActive"` IsActive *bool `json:"isActive"`
} }
func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps { func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.ApprovalWorkflowSteps {
@ -83,21 +104,25 @@ func (req ApprovalWorkflowStepRequest) ToEntity(workflowId uint) *entity.Approva
} }
type ApprovalWorkflowsWithStepsCreateRequest struct { type ApprovalWorkflowsWithStepsCreateRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"` Description string `json:"description" validate:"required"`
IsActive *bool `json:"isActive"` IsActive *bool `json:"isActive"`
IsDefault *bool `json:"isDefault"` IsDefault *bool `json:"isDefault"`
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` RequiresApproval *bool `json:"requiresApproval"`
AutoPublish *bool `json:"autoPublish"`
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"`
} }
func (req ApprovalWorkflowsWithStepsCreateRequest) ToEntity() *entity.ApprovalWorkflows { func (req ApprovalWorkflowsWithStepsCreateRequest) ToEntity() *entity.ApprovalWorkflows {
return &entity.ApprovalWorkflows{ return &entity.ApprovalWorkflows{
Name: req.Name, Name: req.Name,
Description: &req.Description, Description: &req.Description,
IsActive: req.IsActive, IsActive: req.IsActive,
IsDefault: req.IsDefault, IsDefault: req.IsDefault,
CreatedAt: time.Now(), RequiresApproval: req.RequiresApproval,
UpdatedAt: time.Now(), AutoPublish: req.AutoPublish,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
} }
} }
@ -119,11 +144,13 @@ func (req ApprovalWorkflowsWithStepsCreateRequest) ToStepsEntity() []*entity.App
} }
type ApprovalWorkflowsWithStepsUpdateRequest struct { type ApprovalWorkflowsWithStepsUpdateRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"` Description string `json:"description" validate:"required"`
IsActive *bool `json:"isActive"` IsActive *bool `json:"isActive"`
IsDefault *bool `json:"isDefault"` IsDefault *bool `json:"isDefault"`
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"` RequiresApproval *bool `json:"requiresApproval"`
AutoPublish *bool `json:"autoPublish"`
Steps []ApprovalWorkflowStepRequest `json:"steps" validate:"required,min=1"`
} }
type ApprovalWorkflowsQueryRequestContext struct { type ApprovalWorkflowsQueryRequestContext struct {
@ -165,4 +192,4 @@ func (req ApprovalWorkflowsQueryRequestContext) ToParamRequest() ApprovalWorkflo
IsActive: isActive, IsActive: isActive,
IsDefault: isDefault, IsDefault: isDefault,
} }
} }

View File

@ -1,13 +1,16 @@
package controller package controller
import ( import (
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
"strconv" "strconv"
"web-medols-be/app/middleware" "web-medols-be/app/middleware"
"web-medols-be/app/module/article_approval_flows/request" "web-medols-be/app/module/article_approval_flows/request"
"web-medols-be/app/module/article_approval_flows/service" "web-medols-be/app/module/article_approval_flows/service"
usersRepository "web-medols-be/app/module/users/repository"
"web-medols-be/utils/paginator" "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" utilRes "web-medols-be/utils/response"
utilVal "web-medols-be/utils/validator" utilVal "web-medols-be/utils/validator"
@ -15,6 +18,7 @@ import (
type articleApprovalFlowsController struct { type articleApprovalFlowsController struct {
articleApprovalFlowsService service.ArticleApprovalFlowsService articleApprovalFlowsService service.ArticleApprovalFlowsService
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger Log zerolog.Logger
} }
@ -34,9 +38,10 @@ type ArticleApprovalFlowsController interface {
GetApprovalAnalytics(c *fiber.Ctx) error 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{ return &articleApprovalFlowsController{
articleApprovalFlowsService: articleApprovalFlowsService, articleApprovalFlowsService: articleApprovalFlowsService,
UsersRepo: usersRepo,
Log: log, Log: log,
} }
} }
@ -61,13 +66,13 @@ func (_i *articleApprovalFlowsController) All(c *fiber.Ctx) error {
} }
reqContext := request.ArticleApprovalFlowsQueryRequestContext{ reqContext := request.ArticleApprovalFlowsQueryRequestContext{
ArticleId: c.Query("articleId"), ArticleId: c.Query("articleId"),
WorkflowId: c.Query("workflowId"), WorkflowId: c.Query("workflowId"),
StatusId: c.Query("statusId"), StatusId: c.Query("statusId"),
SubmittedBy: c.Query("submittedBy"), SubmittedBy: c.Query("submittedBy"),
CurrentStep: c.Query("currentStep"), CurrentStep: c.Query("currentStep"),
DateFrom: c.Query("dateFrom"), DateFrom: c.Query("dateFrom"),
DateTo: c.Query("dateTo"), DateTo: c.Query("dateTo"),
} }
req := reqContext.ToParamRequest() req := reqContext.ToParamRequest()
req.Pagination = paginate req.Pagination = paginate
@ -129,6 +134,7 @@ func (_i *articleApprovalFlowsController) Show(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req body request.SubmitForApprovalRequest true "Submit for approval data" // @Param req body request.SubmitForApprovalRequest true "Submit for approval data"
// @Success 201 {object} response.Response // @Success 201 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -148,12 +154,23 @@ func (_i *articleApprovalFlowsController) SubmitForApproval(c *fiber.Ctx) error
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 var workflowId *uint
if req.WorkflowId != nil { if req.WorkflowId != nil {
workflowIdVal := uint(*req.WorkflowId) workflowIdVal := uint(*req.WorkflowId)
workflowId = &workflowIdVal 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 { if err != nil {
return err return err
} }
@ -171,6 +188,7 @@ func (_i *articleApprovalFlowsController) SubmitForApproval(c *fiber.Ctx) error
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID" // @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.ApprovalActionRequest true "Approval action data" // @Param req body request.ApprovalActionRequest true "Approval action data"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -196,7 +214,18 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 { if err != nil {
return err return err
} }
@ -214,6 +243,7 @@ func (_i *articleApprovalFlowsController) Approve(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID" // @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.RejectionRequest true "Rejection data" // @Param req body request.RejectionRequest true "Rejection data"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -239,7 +269,18 @@ func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 { if err != nil {
return err return err
} }
@ -257,6 +298,7 @@ func (_i *articleApprovalFlowsController) Reject(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID" // @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.RevisionRequest true "Revision request data" // @Param req body request.RevisionRequest true "Revision request data"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -282,7 +324,18 @@ func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 { if err != nil {
return err return err
} }
@ -300,6 +353,7 @@ func (_i *articleApprovalFlowsController) RequestRevision(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "ArticleApprovalFlows ID" // @Param id path int true "ArticleApprovalFlows ID"
// @Param req body request.ResubmitRequest true "Resubmit data" // @Param req body request.ResubmitRequest true "Resubmit data"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -325,7 +379,18 @@ func (_i *articleApprovalFlowsController) Resubmit(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 { if err != nil {
return err return err
} }
@ -343,6 +408,9 @@ func (_i *articleApprovalFlowsController) Resubmit(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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" // @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -358,7 +426,22 @@ func (_i *articleApprovalFlowsController) GetMyApprovalQueue(c *fiber.Ctx) error
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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 { if err != nil {
return err return err
} }
@ -377,7 +460,7 @@ func (_i *articleApprovalFlowsController) GetMyApprovalQueue(c *fiber.Ctx) error
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @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 <Add access token here>)
// @Param req query paginator.Pagination false "pagination parameters" // @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -390,20 +473,22 @@ func (_i *articleApprovalFlowsController) GetPendingApprovals(c *fiber.Ctx) erro
return err 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 // Get ClientId from context
clientId := middleware.GetClientID(c) 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{}) 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 { if err != nil {
return err return err
} }
@ -475,28 +560,31 @@ func (_i *articleApprovalFlowsController) GetApprovalHistory(c *fiber.Ctx) error
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @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 <Add access token here>)
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError // @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError // @Failure 500 {object} response.InternalServerError
// @Router /article-approval-flows/dashboard-stats [get] // @Router /article-approval-flows/dashboard-stats [get]
func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error { func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error {
userLevelId := 0 // Get ClientId from context
var err error clientId := middleware.GetClientID(c)
if userLevelIdStr := c.Query("userLevelId"); userLevelIdStr != "" {
userLevelId, err = strconv.Atoi(userLevelIdStr) // Get Authorization token from header and extract user level ID
if err != nil { authToken := c.Get("Authorization")
return utilRes.ErrorBadRequest(c, "Invalid userLevelId format") if authToken == "" {
} return utilRes.ErrorBadRequest(c, "Authorization token required")
} }
// Get ClientId from context user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// clientId := middleware.GetClientID(c) if user == nil {
return utilRes.ErrorBadRequest(c, "Invalid authorization token")
}
// TODO: Implement GetDashboardStats method in service // TODO: Implement GetDashboardStats method in service
_ = userLevelId // suppress unused variable warning _ = clientId // suppress unused variable warning
// dashboardStatsData, err := _i.articleApprovalFlowsService.GetDashboardStats(clientId, userLevelId) _ = user.UserLevelId // suppress unused variable warning
// dashboardStatsData, err := _i.articleApprovalFlowsService.GetDashboardStats(clientId, user.UserLevelId)
// if err != nil { // if err != nil {
// return err // return err
// } // }
@ -514,6 +602,7 @@ func (_i *articleApprovalFlowsController) GetDashboardStats(c *fiber.Ctx) error
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError // @Failure 401 {object} response.UnauthorizedError
@ -543,6 +632,7 @@ func (_i *articleApprovalFlowsController) GetWorkloadStats(c *fiber.Ctx) error {
// @Tags ArticleApprovalFlows // @Tags ArticleApprovalFlows
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param period query string false "Period filter (daily, weekly, monthly)" // @Param period query string false "Period filter (daily, weekly, monthly)"
// @Param startDate query string false "Start date filter (YYYY-MM-DD)" // @Param startDate query string false "Start date filter (YYYY-MM-DD)"
// @Param endDate query string false "End 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"}, Messages: utilRes.Messages{"Approval analytics successfully retrieved"},
Data: nil, Data: nil,
}) })
} }

View File

@ -2,26 +2,29 @@ package service
import ( import (
"errors" "errors"
"github.com/google/uuid"
"github.com/rs/zerolog"
"time" "time"
"web-medols-be/app/database/entity" "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/repository"
"web-medols-be/app/module/article_approval_flows/request" "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" approvalStepLogsRepo "web-medols-be/app/module/article_approval_step_logs/repository"
articlesRepo "web-medols-be/app/module/articles/repository" articlesRepo "web-medols-be/app/module/articles/repository"
usersRepo "web-medols-be/app/module/users/repository"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
"github.com/google/uuid"
"github.com/rs/zerolog"
) )
type articleApprovalFlowsService struct { type articleApprovalFlowsService struct {
ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository ArticleApprovalFlowsRepository repository.ArticleApprovalFlowsRepository
ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository ApprovalWorkflowsRepository approvalWorkflowsRepo.ApprovalWorkflowsRepository
ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository ApprovalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository
ArticleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository ArticleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository
ArticlesRepository articlesRepo.ArticlesRepository ArticlesRepository articlesRepo.ArticlesRepository
Log zerolog.Logger UsersRepository usersRepo.UsersRepository
Log zerolog.Logger
} }
// ArticleApprovalFlowsService define interface of IArticleApprovalFlowsService // ArticleApprovalFlowsService define interface of IArticleApprovalFlowsService
@ -63,15 +66,17 @@ func NewArticleApprovalFlowsService(
approvalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository, approvalWorkflowStepsRepository approvalWorkflowStepsRepo.ApprovalWorkflowStepsRepository,
articleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository, articleApprovalStepLogsRepository approvalStepLogsRepo.ArticleApprovalStepLogsRepository,
articlesRepository articlesRepo.ArticlesRepository, articlesRepository articlesRepo.ArticlesRepository,
usersRepository usersRepo.UsersRepository,
log zerolog.Logger, log zerolog.Logger,
) ArticleApprovalFlowsService { ) ArticleApprovalFlowsService {
return &articleApprovalFlowsService{ return &articleApprovalFlowsService{
ArticleApprovalFlowsRepository: articleApprovalFlowsRepository, ArticleApprovalFlowsRepository: articleApprovalFlowsRepository,
ApprovalWorkflowsRepository: approvalWorkflowsRepository, ApprovalWorkflowsRepository: approvalWorkflowsRepository,
ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository, ApprovalWorkflowStepsRepository: approvalWorkflowStepsRepository,
ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository, ArticleApprovalStepLogsRepository: articleApprovalStepLogsRepository,
ArticlesRepository: articlesRepository, ArticlesRepository: articlesRepository,
Log: log, UsersRepository: usersRepository,
Log: log,
} }
} }
@ -138,7 +143,7 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U
ArticleId: articleId, ArticleId: articleId,
WorkflowId: workflow.ID, WorkflowId: workflow.ID,
CurrentStep: 1, CurrentStep: 1,
StatusId: 1, // pending StatusId: 1, // pending
SubmittedById: submittedById, SubmittedById: submittedById,
SubmittedAt: time.Now(), SubmittedAt: time.Now(),
} }
@ -148,30 +153,24 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U
return nil, err return nil, err
} }
// Update article status and workflow info // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, articleId)
WorkflowId: &workflow.ID,
CurrentApprovalStep: &flow.CurrentStep,
StatusId: &[]int{1}[0], // pending approval
}
err = _i.ArticlesRepository.Update(clientId, articleId, articleUpdate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Create initial step log // Update only the necessary fields
stepLog := &entity.ArticleApprovalStepLogs{ currentArticle.WorkflowId = &workflow.ID
ApprovalFlowId: flow.ID, currentArticle.CurrentApprovalStep = &flow.CurrentStep
StepOrder: 1, currentArticle.StatusId = &[]int{1}[0] // pending approval
StepName: firstStep.StepName,
Action: "submitted", err = _i.ArticlesRepository.UpdateSkipNull(clientId, articleId, currentArticle)
Message: &[]string{"Article submitted for approval"}[0], if err != nil {
ProcessedAt: time.Now(), return nil, err
UserLevelId: firstStep.RequiredUserLevelId,
} }
_, err = _i.ArticleApprovalStepLogsRepository.Create(clientId, stepLog) // Process auto-skip logic based on user level
err = _i.processAutoSkipSteps(clientId, flow, submittedById)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -179,6 +178,160 @@ func (_i *articleApprovalFlowsService) SubmitArticleForApproval(clientId *uuid.U
return flow, nil 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) { func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId uint, approvedById uint, message string) (err error) {
// Get approval flow // Get approval flow
flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId) flow, err := _i.ArticleApprovalFlowsRepository.FindOne(clientId, flowId)
@ -239,13 +392,17 @@ func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId u
return err return err
} }
// Update article status // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
StatusId: &[]int{2}[0], // approved if err != nil {
CurrentApprovalStep: 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 { if err != nil {
return err return err
} }
@ -261,12 +418,16 @@ func (_i *articleApprovalFlowsService) ApproveStep(clientId *uuid.UUID, flowId u
return err return err
} }
// Update article current step // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
CurrentApprovalStep: &nextStep.StepOrder, 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 { if err != nil {
return err return err
} }
@ -325,13 +486,17 @@ func (_i *articleApprovalFlowsService) RejectArticle(clientId *uuid.UUID, flowId
return err return err
} }
// Update article status // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
StatusId: &[]int{3}[0], // rejected if err != nil {
CurrentApprovalStep: 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 { if err != nil {
return err return err
} }
@ -379,9 +544,9 @@ func (_i *articleApprovalFlowsService) RequestRevision(clientId *uuid.UUID, flow
// Update approval flow status // Update approval flow status
flowUpdate := &entity.ArticleApprovalFlows{ flowUpdate := &entity.ArticleApprovalFlows{
StatusId: 4, // revision_requested StatusId: 4, // revision_requested
RevisionRequested: &[]bool{true}[0], RevisionRequested: &[]bool{true}[0],
RevisionMessage: &revisionMessage, RevisionMessage: &revisionMessage,
} }
err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate) err = _i.ArticleApprovalFlowsRepository.Update(flowId, flowUpdate)
@ -389,12 +554,16 @@ func (_i *articleApprovalFlowsService) RequestRevision(clientId *uuid.UUID, flow
return err return err
} }
// Update article status // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
StatusId: &[]int{4}[0], // revision_requested 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 { if err != nil {
return err return err
} }
@ -420,7 +589,7 @@ func (_i *articleApprovalFlowsService) ResubmitAfterRevision(clientId *uuid.UUID
// Reset approval flow to pending // Reset approval flow to pending
flowUpdate := &entity.ArticleApprovalFlows{ flowUpdate := &entity.ArticleApprovalFlows{
StatusId: 1, // pending StatusId: 1, // pending
RevisionRequested: &[]bool{false}[0], RevisionRequested: &[]bool{false}[0],
RevisionMessage: nil, RevisionMessage: nil,
CurrentStep: 1, // restart from first step CurrentStep: 1, // restart from first step
} }
@ -430,13 +599,17 @@ func (_i *articleApprovalFlowsService) ResubmitAfterRevision(clientId *uuid.UUID
return err return err
} }
// Update article status // Get current article data first
articleUpdate := &entity.Articles{ currentArticle, err := _i.ArticlesRepository.FindOne(clientId, flow.ArticleId)
StatusId: &[]int{1}[0], // pending approval if err != nil {
CurrentApprovalStep: &[]int{1}[0], 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 { if err != nil {
return err return err
} }
@ -603,4 +776,4 @@ func (_i *articleApprovalFlowsService) GetNextStepPreview(clientId *uuid.UUID, f
} }
return nextStep, nil return nextStep, nil
} }

View File

@ -1,17 +1,21 @@
package articles package articles
import ( import (
"github.com/gofiber/fiber/v2" "web-medols-be/app/middleware"
"go.uber.org/fx"
"web-medols-be/app/module/articles/controller" "web-medols-be/app/module/articles/controller"
"web-medols-be/app/module/articles/repository" "web-medols-be/app/module/articles/repository"
"web-medols-be/app/module/articles/service" "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 // ArticlesRouter struct of ArticlesRouter
type ArticlesRouter struct { type ArticlesRouter struct {
App fiber.Router App fiber.Router
Controller *controller.Controller Controller *controller.Controller
UsersRepo usersRepo.UsersRepository
} }
// NewArticlesModule register bulky of Articles module // NewArticlesModule register bulky of Articles module
@ -30,10 +34,11 @@ var NewArticlesModule = fx.Options(
) )
// NewArticlesRouter init ArticlesRouter // 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{ return &ArticlesRouter{
App: fiber, App: fiber,
Controller: controller, Controller: controller,
UsersRepo: usersRepo,
} }
} }
@ -44,6 +49,8 @@ func (_i *ArticlesRouter) RegisterArticlesRoutes() {
// define routes // define routes
_i.App.Route("/articles", func(router fiber.Router) { _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("/", articlesController.All)
router.Get("/old-id/:id", articlesController.ShowByOldId) router.Get("/old-id/:id", articlesController.ShowByOldId)
router.Get("/:id", articlesController.Show) router.Get("/:id", articlesController.Show)
@ -57,10 +64,11 @@ func (_i *ArticlesRouter) RegisterArticlesRoutes() {
router.Get("/statistic/summary", articlesController.SummaryStats) router.Get("/statistic/summary", articlesController.SummaryStats)
router.Get("/statistic/user-levels", articlesController.ArticlePerUserLevelStats) router.Get("/statistic/user-levels", articlesController.ArticlePerUserLevelStats)
router.Get("/statistic/monthly", articlesController.ArticleMonthlyStats) router.Get("/statistic/monthly", articlesController.ArticleMonthlyStats)
// Dynamic approval system routes // Dynamic approval system routes
router.Post("/:id/submit-approval", articlesController.SubmitForApproval) router.Post("/:id/submit-approval", articlesController.SubmitForApproval)
router.Get("/:id/approval-status", articlesController.GetApprovalStatus) router.Get("/:id/approval-status", articlesController.GetApprovalStatus)
router.Get("/pending-approval", articlesController.GetPendingApprovals) router.Get("/pending-approval", articlesController.GetPendingApprovals)
router.Get("/waiting-for-approval", articlesController.GetArticlesWaitingForApproval)
}) })
} }

View File

@ -38,6 +38,7 @@ type ArticlesController interface {
SubmitForApproval(c *fiber.Ctx) error SubmitForApproval(c *fiber.Ctx) error
GetApprovalStatus(c *fiber.Ctx) error GetApprovalStatus(c *fiber.Ctx) error
GetPendingApprovals(c *fiber.Ctx) error GetPendingApprovals(c *fiber.Ctx) error
GetArticlesWaitingForApproval(c *fiber.Ctx) error
} }
func NewArticlesController(articlesService service.ArticlesService, log zerolog.Logger) ArticlesController { func NewArticlesController(articlesService service.ArticlesService, log zerolog.Logger) ArticlesController {
@ -53,6 +54,7 @@ func NewArticlesController(articlesService service.ArticlesService, log zerolog.
// @Tags Articles // @Tags Articles
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key" // @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query request.ArticlesQueryRequest false "query parameters" // @Param req query request.ArticlesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters" // @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -84,9 +86,12 @@ func (_i *articlesController) All(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) clientId := middleware.GetClientID(c)
// Get Authorization token from header
authToken := c.Get("Authorization")
_i.Log.Info().Interface("clientId", clientId).Msg("") _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 { if err != nil {
return err return err
} }
@ -188,6 +193,9 @@ func (_i *articlesController) Save(c *fiber.Ctx) error {
// Get ClientId from context // Get ClientId from context
clientId := middleware.GetClientID(c) 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) dataResult, err := _i.articlesService.Save(clientId, *req, authToken)
if err != nil { if err != nil {
return err return err
@ -613,3 +621,46 @@ func (_i *articlesController) GetPendingApprovals(c *fiber.Ctx) error {
Meta: paging, 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,
})
}

View File

@ -22,7 +22,7 @@ type articlesRepository struct {
// ArticlesRepository define interface of IArticlesRepository // ArticlesRepository define interface of IArticlesRepository
type ArticlesRepository interface { 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) GetAllPublishSchedule(clientId *uuid.UUID) (articless []*entity.Articles, err error)
FindOne(clientId *uuid.UUID, id uint) (articles *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) 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 // 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 var count int64
query := _i.DB.DB.Model(&entity.Articles{}) 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) 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 { if req.CategoryId != nil {
query = query.Joins("JOIN article_category_details acd ON acd.article_id = articles.id"). query = query.Joins("JOIN article_category_details acd ON acd.article_id = articles.id").
Where("acd.category_id = ?", req.CategoryId) Where("acd.category_id = ?", req.CategoryId)
@ -207,6 +260,12 @@ func (_i *articlesRepository) Update(clientId *uuid.UUID, id uint, articles *ent
if err != nil { if err != nil {
return err 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{}). return _i.DB.DB.Model(&entity.Articles{}).
Where(&entity.Articles{ID: id}). Where(&entity.Articles{ID: id}).
Updates(articlesMap).Error 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{}). return _i.DB.DB.Model(&entity.Articles{}).
Where(&entity.Articles{ID: id}). Where(&entity.Articles{ID: id}).
Updates(articles).Error Updates(&updateData).Error
} }
func (_i *articlesRepository) Delete(clientId *uuid.UUID, id uint) 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) { func (_i *articlesRepository) SummaryStats(clientId *uuid.UUID, userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error) {

View File

@ -47,15 +47,15 @@ type articlesService struct {
Cfg *config.Config Cfg *config.Config
UsersRepo usersRepository.UsersRepository UsersRepo usersRepository.UsersRepository
MinioStorage *minioStorage.MinioStorage MinioStorage *minioStorage.MinioStorage
// Dynamic approval system dependencies // Dynamic approval system dependencies
ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository ArticleApprovalFlowsRepo articleApprovalFlowsRepository.ArticleApprovalFlowsRepository
ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository ApprovalWorkflowsRepo approvalWorkflowsRepository.ApprovalWorkflowsRepository
} }
// ArticlesService define interface of IArticlesService // ArticlesService define interface of IArticlesService
type ArticlesService interface { 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) Show(clientId *uuid.UUID, id uint) (articles *response.ArticlesResponse, err error)
ShowByOldId(clientId *uuid.UUID, oldId 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) 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) ArticleMonthlyStats(clientId *uuid.UUID, authToken string, year *int) (articleMonthlyStats []*response.ArticleMonthlyStats, err error)
PublishScheduling(clientId *uuid.UUID, id uint, publishSchedule string) error PublishScheduling(clientId *uuid.UUID, id uint, publishSchedule string) error
ExecuteScheduling() error ExecuteScheduling() error
// Dynamic approval system methods // Dynamic approval system methods
SubmitForApproval(clientId *uuid.UUID, articleId uint, submittedById uint, workflowId *uint) error SubmitForApproval(clientId *uuid.UUID, articleId uint, submittedById uint, workflowId *uint) error
GetApprovalStatus(clientId *uuid.UUID, articleId uint) (*response.ArticleApprovalStatusResponse, 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) GetPendingApprovals(clientId *uuid.UUID, userLevelId uint, page, limit int) ([]*response.ArticleApprovalQueueResponse, paginator.Pagination, error)
// No-approval system methods // No-approval system methods
CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error)
AutoApproveArticle(clientId *uuid.UUID, articleId uint, reason string) error AutoApproveArticle(clientId *uuid.UUID, articleId uint, reason string) error
@ -115,7 +116,17 @@ func NewArticlesService(
} }
// All implement interface of ArticlesService // 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 { if req.Category != nil {
findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *req.Category) findCategory, err := _i.ArticleCategoriesRepo.FindOneBySlug(clientId, *req.Category)
if err != nil { if err != nil {
@ -124,7 +135,7 @@ func (_i *articlesService) All(clientId *uuid.UUID, req request.ArticlesQueryReq
req.CategoryId = &findCategory.ID req.CategoryId = &findCategory.ID
} }
results, paging, err := _i.Repo.GetAll(clientId, req) results, paging, err := _i.Repo.GetAll(clientId, userLevelId, req)
if err != nil { if err != nil {
return return
} }
@ -170,7 +181,7 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR
newReq := req.ToEntity() newReq := req.ToEntity()
var userLevelNumber int var userLevelNumber int
var userParentLevelId int var approvalLevelId int
if req.CreatedById != nil { if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(clientId, *req.CreatedById) createdBy, err := _i.UsersRepo.FindOne(clientId, *req.CreatedById)
if err != nil { if err != nil {
@ -178,16 +189,16 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR
} }
newReq.CreatedById = &createdBy.ID newReq.CreatedById = &createdBy.ID
userLevelNumber = createdBy.UserLevel.LevelNumber 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 { } else {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.CreatedById = &createdBy.ID newReq.CreatedById = &createdBy.ID
userLevelNumber = createdBy.UserLevel.LevelNumber 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 isDraft := true
@ -219,19 +230,29 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR
newReq.CreatedAt = parsedTime newReq.CreatedAt = parsedTime
} }
// Approval // Dynamic Approval Workflow System
statusIdOne := 1 statusIdOne := 1
statusIdTwo := 2 statusIdTwo := 2
isPublishFalse := false isPublishFalse := false
// Get user info for approval logic
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Check if user level requires approval
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false { if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == false {
// User level doesn't require approval - auto publish
newReq.NeedApprovalFrom = nil newReq.NeedApprovalFrom = nil
newReq.StatusId = &statusIdTwo newReq.StatusId = &statusIdTwo
newReq.IsPublish = &isPublishFalse
newReq.PublishedAt = nil
newReq.BypassApproval = &[]bool{true}[0]
} else { } else {
newReq.NeedApprovalFrom = &userParentLevelId // User level requires approval - set to pending
newReq.NeedApprovalFrom = &approvalLevelId
newReq.StatusId = &statusIdOne newReq.StatusId = &statusIdOne
newReq.IsPublish = &isPublishFalse newReq.IsPublish = &isPublishFalse
newReq.PublishedAt = nil newReq.PublishedAt = nil
newReq.BypassApproval = &[]bool{false}[0]
} }
saveArticleRes, err := _i.Repo.Create(clientId, newReq) saveArticleRes, err := _i.Repo.Create(clientId, newReq)
@ -239,30 +260,63 @@ func (_i *articlesService) Save(clientId *uuid.UUID, req request.ArticlesCreateR
return nil, err return nil, err
} }
// Approval // Dynamic Approval Workflow Assignment
var articleApproval *entity.ArticleApprovals
if createdBy != nil && *createdBy.UserLevel.IsApprovalActive == true { 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, ArticleId: saveArticleRes.ID,
ApprovalBy: *newReq.CreatedById, ApprovalBy: *newReq.CreatedById,
StatusId: statusIdOne, StatusId: statusIdOne,
Message: "Need Approval", 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 { } else {
articleApproval = &entity.ArticleApprovals{ // Auto-publish for users who don't require approval
articleApproval := &entity.ArticleApprovals{
ArticleId: saveArticleRes.ID, ArticleId: saveArticleRes.ID,
ApprovalBy: *newReq.CreatedById, ApprovalBy: *newReq.CreatedById,
StatusId: statusIdTwo, StatusId: statusIdTwo,
Message: "Publish Otomatis", Message: "Publish Otomatis",
ApprovalAtLevel: nil, ApprovalAtLevel: nil,
} }
} _, err = _i.ArticleApprovalsRepo.Create(articleApproval)
if err != nil {
_, err = _i.ArticleApprovalsRepo.Create(articleApproval) _i.Log.Error().Err(err).Msg("Failed to create auto-approval record")
if err != nil { }
return nil, err
} }
var categoryIds []string 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 { func (_i *articlesService) Delete(clientId *uuid.UUID, id uint) error {
result, err := _i.Repo.FindOne(clientId, id) return _i.Repo.Delete(clientId, id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(clientId, id, result)
} }
func (_i *articlesService) Viewer(clientId *uuid.UUID, c *fiber.Ctx) (err error) { 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 // Create approval flow
approvalFlow := &entity.ArticleApprovalFlows{ approvalFlow := &entity.ArticleApprovalFlows{
ArticleId: articleId, ArticleId: articleId,
WorkflowId: *workflowId, WorkflowId: *workflowId,
CurrentStep: 1, CurrentStep: 1,
StatusId: 1, // 1 = In Progress StatusId: 1, // 1 = In Progress
ClientId: clientId, ClientId: clientId,
SubmittedById: submittedById,
} }
_, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow) _, err = _i.ArticleApprovalFlowsRepo.Create(clientId, approvalFlow)
@ -787,19 +835,19 @@ func (_i *articlesService) GetApprovalStatus(clientId *uuid.UUID, articleId uint
status = "revision_requested" status = "revision_requested"
} }
// Get current approver info // Get current approver info
var currentApprover *string var currentApprover *string
var nextStep *string var nextStep *string
if approvalFlow.CurrentStep <= totalSteps && approvalFlow.StatusId == 1 { if approvalFlow.CurrentStep <= totalSteps && approvalFlow.StatusId == 1 {
if approvalFlow.CurrentStep < totalSteps { if approvalFlow.CurrentStep < totalSteps {
// Array indexing starts from 0, so subtract 1 from CurrentStep // Array indexing starts from 0, so subtract 1 from CurrentStep
nextStepIndex := approvalFlow.CurrentStep - 1 nextStepIndex := approvalFlow.CurrentStep - 1
if nextStepIndex >= 0 && nextStepIndex < len(workflowSteps) { if nextStepIndex >= 0 && nextStepIndex < len(workflowSteps) {
nextStepInfo := workflowSteps[nextStepIndex] nextStepInfo := workflowSteps[nextStepIndex]
nextStep = &nextStepInfo.RequiredUserLevel.Name nextStep = &nextStepInfo.RequiredUserLevel.Name
}
} }
} }
}
return &response.ArticleApprovalStatusResponse{ return &response.ArticleApprovalStatusResponse{
ArticleId: articleId, ArticleId: articleId,
@ -877,20 +925,20 @@ func (_i *articlesService) GetPendingApprovals(clientId *uuid.UUID, userLevelId
} }
response := &response.ArticleApprovalQueueResponse{ response := &response.ArticleApprovalQueueResponse{
ID: article.ID, ID: article.ID,
Title: article.Title, Title: article.Title,
Slug: article.Slug, Slug: article.Slug,
Description: article.Description, Description: article.Description,
CategoryName: categoryName, CategoryName: categoryName,
AuthorName: authorName, AuthorName: authorName,
SubmittedAt: flow.CreatedAt, SubmittedAt: flow.CreatedAt,
CurrentStep: flow.CurrentStep, CurrentStep: flow.CurrentStep,
TotalSteps: len(workflowSteps), TotalSteps: len(workflowSteps),
Priority: priority, Priority: priority,
DaysInQueue: daysInQueue, DaysInQueue: daysInQueue,
WorkflowName: workflow.Name, WorkflowName: workflow.Name,
CanApprove: true, // TODO: Implement based on user permissions CanApprove: true, // TODO: Implement based on user permissions
EstimatedTime: "2-3 days", // TODO: Calculate based on historical data EstimatedTime: "2-3 days", // TODO: Calculate based on historical data
} }
responses = append(responses, response) responses = append(responses, response)
@ -899,6 +947,40 @@ func (_i *articlesService) GetPendingApprovals(clientId *uuid.UUID, userLevelId
return responses, paging, nil 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 // 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) { func (_i *articlesService) CheckApprovalRequired(clientId *uuid.UUID, articleId uint, userId uint, userLevelId uint) (bool, error) {
// Get article to check category and other properties // 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) // Check client-level settings (this would require the client approval settings service)
// For now, we'll use a simple check // For now, we'll use a simple check
// TODO: Integrate with ClientApprovalSettingsService // TODO: Integrate with ClientApprovalSettingsService
// Check if workflow is set to no approval // Check if workflow is set to no approval
if article.WorkflowId != nil { if article.WorkflowId != nil {
workflow, err := _i.ApprovalWorkflowsRepo.FindOne(clientId, *article.WorkflowId) 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 // Update article status to approved
updates := map[string]interface{}{ updates := map[string]interface{}{
"status_id": 2, // Assuming 2 = approved "status_id": 2, // Assuming 2 = approved
"is_publish": true, "is_publish": true,
"published_at": time.Now(), "published_at": time.Now(),
"current_approval_step": 0, // Reset approval step "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 { if currentApprovalStep, ok := updates["current_approval_step"].(int); ok {
articleUpdate.CurrentApprovalStep = &currentApprovalStep articleUpdate.CurrentApprovalStep = &currentApprovalStep
} }
err = _i.Repo.Update(clientId, articleId, articleUpdate) err = _i.Repo.Update(clientId, articleId, articleUpdate)
if err != nil { if err != nil {
return err return err
@ -1033,7 +1115,7 @@ func (_i *articlesService) SetArticleApprovalExempt(clientId *uuid.UUID, article
if currentApprovalStep, ok := updates["current_approval_step"].(int); ok { if currentApprovalStep, ok := updates["current_approval_step"].(int); ok {
articleUpdate.CurrentApprovalStep = &currentApprovalStep articleUpdate.CurrentApprovalStep = &currentApprovalStep
} }
err := _i.Repo.Update(clientId, articleId, articleUpdate) err := _i.Repo.Update(clientId, articleId, articleUpdate)
if err != nil { if err != nil {
return err return err
@ -1048,3 +1130,21 @@ func (_i *articlesService) SetArticleApprovalExempt(clientId *uuid.UUID, article
return nil 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
}
}

View File

@ -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: <client-id>
```
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!

View File

@ -12,46 +12,54 @@ import (
// ClientApprovalSettingsRouter struct of ClientApprovalSettingsRouter // ClientApprovalSettingsRouter struct of ClientApprovalSettingsRouter
type ClientApprovalSettingsRouter struct { type ClientApprovalSettingsRouter struct {
App fiber.Router App fiber.Router
Controller controller.ClientApprovalSettingsController Controller *controller.Controller
} }
// NewClientApprovalSettingsModule register bulky of ClientApprovalSettings module // NewClientApprovalSettingsModule register bulky of ClientApprovalSettings module
var NewClientApprovalSettingsModule = fx.Options( var NewClientApprovalSettingsModule = fx.Options(
// register repository of ClientApprovalSettings module
fx.Provide(repository.NewClientApprovalSettingsRepository), fx.Provide(repository.NewClientApprovalSettingsRepository),
// register service of ClientApprovalSettings module
fx.Provide(service.NewClientApprovalSettingsService), 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), fx.Provide(NewClientApprovalSettingsRouter),
) )
// NewClientApprovalSettingsRouter create new ClientApprovalSettingsRouter // NewClientApprovalSettingsRouter init ClientApprovalSettingsRouter
func NewClientApprovalSettingsRouter( func NewClientApprovalSettingsRouter(fiber *fiber.App, controller *controller.Controller) *ClientApprovalSettingsRouter {
app *fiber.App,
controller controller.ClientApprovalSettingsController,
) *ClientApprovalSettingsRouter {
return &ClientApprovalSettingsRouter{ return &ClientApprovalSettingsRouter{
App: app, App: fiber,
Controller: controller, Controller: controller,
} }
} }
// RegisterClientApprovalSettingsRoutes register routes of ClientApprovalSettings // RegisterClientApprovalSettingsRoutes register routes of ClientApprovalSettings
func (r *ClientApprovalSettingsRouter) RegisterClientApprovalSettingsRoutes() { func (_i *ClientApprovalSettingsRouter) RegisterClientApprovalSettingsRoutes() {
// Group routes under /api/v1/client-approval-settings // define controllers
api := r.App.Group("/api/v1/client-approval-settings") clientApprovalSettingsController := _i.Controller.ClientApprovalSettings
// Basic CRUD routes // define routes
api.Get("/", r.Controller.GetSettings) _i.App.Route("/client-approval-settings", func(router fiber.Router) {
api.Put("/", r.Controller.UpdateSettings) // Basic CRUD routes
api.Delete("/", r.Controller.DeleteSettings) router.Post("/", clientApprovalSettingsController.CreateSettings)
router.Get("/", clientApprovalSettingsController.GetSettings)
router.Put("/", clientApprovalSettingsController.UpdateSettings)
router.Delete("/", clientApprovalSettingsController.DeleteSettings)
// Approval management routes // Approval management routes
api.Post("/toggle-approval", r.Controller.ToggleApproval) router.Post("/toggle-approval", clientApprovalSettingsController.ToggleApproval)
api.Post("/enable-approval", r.Controller.EnableApproval) router.Post("/enable-approval", clientApprovalSettingsController.EnableApproval)
api.Post("/disable-approval", r.Controller.DisableApproval) router.Post("/disable-approval", clientApprovalSettingsController.DisableApproval)
api.Put("/default-workflow", r.Controller.SetDefaultWorkflow) router.Put("/default-workflow", clientApprovalSettingsController.SetDefaultWorkflow)
// Exemption management routes // Exemption management routes
api.Post("/exempt-users", r.Controller.ManageExemptUsers) router.Post("/exempt-users", clientApprovalSettingsController.ManageExemptUsers)
api.Post("/exempt-roles", r.Controller.ManageExemptRoles) router.Post("/exempt-roles", clientApprovalSettingsController.ManageExemptRoles)
api.Post("/exempt-categories", r.Controller.ManageExemptCategories) router.Post("/exempt-categories", clientApprovalSettingsController.ManageExemptCategories)
})
} }

View File

@ -19,6 +19,7 @@ type clientApprovalSettingsController struct {
} }
type ClientApprovalSettingsController interface { type ClientApprovalSettingsController interface {
CreateSettings(c *fiber.Ctx) error
GetSettings(c *fiber.Ctx) error GetSettings(c *fiber.Ctx) error
UpdateSettings(c *fiber.Ctx) error UpdateSettings(c *fiber.Ctx) error
DeleteSettings(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 // GetSettings ClientApprovalSettings
// @Summary Get Client Approval Settings // @Summary Get Client Approval Settings
// @Description API for getting client approval settings // @Description API for getting client approval settings

View File

@ -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),
}
}

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"strconv" "strconv"
"strings" "strings"
"web-medols-be/app/middleware"
"web-medols-be/app/module/user_levels/request" "web-medols-be/app/module/user_levels/request"
"web-medols-be/app/module/user_levels/service" "web-medols-be/app/module/user_levels/service"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
@ -38,6 +39,7 @@ func NewUserLevelsController(userLevelsService service.UserLevelsService) UserLe
// @Description API for getting all UserLevels // @Description API for getting all UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param req query request.UserLevelsQueryRequest false "query parameters" // @Param req query request.UserLevelsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters" // @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -60,7 +62,10 @@ func (_i *userLevelsController) All(c *fiber.Ctx) error {
req := reqContext.ToParamRequest() req := reqContext.ToParamRequest()
req.Pagination = paginate 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 { if err != nil {
return err return err
} }
@ -78,6 +83,7 @@ func (_i *userLevelsController) All(c *fiber.Ctx) error {
// @Description API for getting one UserLevels // @Description API for getting one UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param id path int true "UserLevels ID" // @Param id path int true "UserLevels ID"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -90,7 +96,10 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error {
return err 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 { if err != nil {
return err return err
} }
@ -106,6 +115,7 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error {
// @Description API for getting one UserLevels // @Description API for getting one UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param alias path string true "UserLevels Alias" // @Param alias path string true "UserLevels Alias"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -114,7 +124,11 @@ func (_i *userLevelsController) Show(c *fiber.Ctx) error {
// @Router /user-levels/alias/{alias} [get] // @Router /user-levels/alias/{alias} [get]
func (_i *userLevelsController) ShowByAlias(c *fiber.Ctx) error { func (_i *userLevelsController) ShowByAlias(c *fiber.Ctx) error {
alias := c.Params("alias") 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 { if err != nil {
return err return err
} }
@ -129,7 +143,9 @@ func (_i *userLevelsController) ShowByAlias(c *fiber.Ctx) error {
// @Description API for create UserLevels // @Description API for create UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.UserLevelsCreateRequest true "Required payload" // @Param payload body request.UserLevelsCreateRequest true "Required payload"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError // @Failure 400 {object} response.BadRequestError
@ -142,7 +158,10 @@ func (_i *userLevelsController) Save(c *fiber.Ctx) error {
return err 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 { if err != nil {
return err return err
} }
@ -159,7 +178,9 @@ func (_i *userLevelsController) Save(c *fiber.Ctx) error {
// @Description API for update UserLevels // @Description API for update UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.UserLevelsUpdateRequest true "Required payload" // @Param payload body request.UserLevelsUpdateRequest true "Required payload"
// @Param id path int true "UserLevels ID" // @Param id path int true "UserLevels ID"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -178,7 +199,10 @@ func (_i *userLevelsController) Update(c *fiber.Ctx) error {
return err 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 { if err != nil {
return err return err
} }
@ -194,6 +218,8 @@ func (_i *userLevelsController) Update(c *fiber.Ctx) error {
// @Description API for delete UserLevels // @Description API for delete UserLevels
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevels ID" // @Param id path int true "UserLevels ID"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
@ -208,7 +234,10 @@ func (_i *userLevelsController) Delete(c *fiber.Ctx) error {
return err 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 { if err != nil {
return err return err
} }
@ -224,6 +253,7 @@ func (_i *userLevelsController) Delete(c *fiber.Ctx) error {
// @Description API for Enable Approval of Article // @Description API for Enable Approval of Article
// @Tags UserLevels // @Tags UserLevels
// @Security Bearer // @Security Bearer
// @Param X-Client-Key header string true "Client Key"
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token" // @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>) // @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.UserLevelsApprovalRequest true "Required payload" // @Param payload body request.UserLevelsApprovalRequest true "Required payload"
@ -238,13 +268,16 @@ func (_i *userLevelsController) EnableApproval(c *fiber.Ctx) error {
return err return err
} }
// Get ClientId from context
clientId := middleware.GetClientID(c)
ids := strings.Split(req.Ids, ",") ids := strings.Split(req.Ids, ",")
for _, id := range ids { for _, id := range ids {
idUint, err := strconv.ParseUint(id, 10, 64) idUint, err := strconv.ParseUint(id, 10, 64)
if err != nil { if err != nil {
return err return err
} }
err = _i.userLevelsService.EnableApproval(uint(idUint), req.IsApprovalActive) err = _i.userLevelsService.EnableApproval(clientId, uint(idUint), req.IsApprovalActive)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,6 +8,8 @@ import (
"web-medols-be/app/module/user_levels/request" "web-medols-be/app/module/user_levels/request"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
utilSvc "web-medols-be/utils/service" utilSvc "web-medols-be/utils/service"
"github.com/google/uuid"
) )
type userLevelsRepository struct { type userLevelsRepository struct {
@ -16,12 +18,12 @@ type userLevelsRepository struct {
// UserLevelsRepository define interface of IUserLevelsRepository // UserLevelsRepository define interface of IUserLevelsRepository
type UserLevelsRepository interface { type UserLevelsRepository interface {
GetAll(req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error) GetAll(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevelss []*entity.UserLevels, paging paginator.Pagination, err error)
FindOne(id uint) (userLevels *entity.UserLevels, err error) FindOne(clientId *uuid.UUID, id uint) (userLevels *entity.UserLevels, err error)
FindOneByAlias(alias string) (userLevels *entity.UserLevels, err error) FindOneByAlias(clientId *uuid.UUID, alias string) (userLevels *entity.UserLevels, err error)
Create(userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error) Create(clientId *uuid.UUID, userLevels *entity.UserLevels) (userLevelsReturn *entity.UserLevels, err error)
Update(id uint, userLevels *entity.UserLevels) (err error) Update(clientId *uuid.UUID, id uint, userLevels *entity.UserLevels) (err error)
Delete(id uint) (err error) Delete(clientId *uuid.UUID, id uint) (err error)
} }
func NewUserLevelsRepository(db *database.Database) UserLevelsRepository { func NewUserLevelsRepository(db *database.Database) UserLevelsRepository {
@ -31,12 +33,17 @@ func NewUserLevelsRepository(db *database.Database) UserLevelsRepository {
} }
// implement interface of IUserLevelsRepository // 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 var count int64
query := _i.DB.DB.Model(&entity.UserLevels{}) query := _i.DB.DB.Model(&entity.UserLevels{})
query = query.Where("is_active = ?", true) 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 != "" { if req.Name != nil && *req.Name != "" {
name := strings.ToLower(*req.Name) name := strings.ToLower(*req.Name)
query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(name)+"%") query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(name)+"%")
@ -73,28 +80,52 @@ func (_i *userLevelsRepository) GetAll(req request.UserLevelsQueryRequest) (user
return return
} }
func (_i *userLevelsRepository) FindOne(id uint) (userLevels *entity.UserLevels, err error) { func (_i *userLevelsRepository) FindOne(clientId *uuid.UUID, id uint) (userLevels *entity.UserLevels, err error) {
if err := _i.DB.DB.First(&userLevels, id).Error; err != nil { 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 nil, err
} }
return userLevels, nil return userLevels, nil
} }
func (_i *userLevelsRepository) FindOneByAlias(alias string) (userLevels *entity.UserLevels, err error) { func (_i *userLevelsRepository) FindOneByAlias(clientId *uuid.UUID, alias string) (userLevels *entity.UserLevels, err error) {
if err := _i.DB.DB.Where("alias_name = ?", strings.ToLower(alias)).First(&userLevels).Error; err != nil { 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 nil, err
} }
return userLevels, nil 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) result := _i.DB.DB.Create(userLevels)
return userLevels, result.Error 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) userLevelsMap, err := utilSvc.StructToMap(userLevels)
if err != nil { if err != nil {
return err return err
@ -104,6 +135,17 @@ func (_i *userLevelsRepository) Update(id uint, userLevels *entity.UserLevels) (
Updates(userLevelsMap).Error 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 return _i.DB.DB.Delete(&entity.UserLevels{}, id).Error
} }

View File

@ -1,13 +1,15 @@
package service package service
import ( import (
"github.com/rs/zerolog"
"web-medols-be/app/database/entity" "web-medols-be/app/database/entity"
"web-medols-be/app/module/user_levels/mapper" "web-medols-be/app/module/user_levels/mapper"
"web-medols-be/app/module/user_levels/repository" "web-medols-be/app/module/user_levels/repository"
"web-medols-be/app/module/user_levels/request" "web-medols-be/app/module/user_levels/request"
"web-medols-be/app/module/user_levels/response" "web-medols-be/app/module/user_levels/response"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
"github.com/google/uuid"
"github.com/rs/zerolog"
) )
// UserLevelsService // UserLevelsService
@ -18,13 +20,13 @@ type userLevelsService struct {
// UserLevelsService define interface of IUserLevelsService // UserLevelsService define interface of IUserLevelsService
type UserLevelsService interface { type UserLevelsService interface {
All(req request.UserLevelsQueryRequest) (userLevels []*response.UserLevelsResponse, paging paginator.Pagination, err error) All(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevels []*response.UserLevelsResponse, paging paginator.Pagination, err error)
Show(id uint) (userLevels *response.UserLevelsResponse, err error) Show(clientId *uuid.UUID, id uint) (userLevels *response.UserLevelsResponse, err error)
ShowByAlias(alias string) (userLevels *response.UserLevelsResponse, err error) ShowByAlias(clientId *uuid.UUID, alias string) (userLevels *response.UserLevelsResponse, err error)
Save(req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error) Save(clientId *uuid.UUID, req request.UserLevelsCreateRequest) (userLevels *entity.UserLevels, err error)
Update(id uint, req request.UserLevelsUpdateRequest) (err error) Update(clientId *uuid.UUID, id uint, req request.UserLevelsUpdateRequest) (err error)
Delete(id uint) error Delete(clientId *uuid.UUID, id uint) error
EnableApproval(id uint, isApprovalActive bool) (err error) EnableApproval(clientId *uuid.UUID, id uint, isApprovalActive bool) (err error)
} }
// NewUserLevelsService init UserLevelsService // NewUserLevelsService init UserLevelsService
@ -37,8 +39,8 @@ func NewUserLevelsService(repo repository.UserLevelsRepository, log zerolog.Logg
} }
// All implement interface of UserLevelsService // All implement interface of UserLevelsService
func (_i *userLevelsService) All(req request.UserLevelsQueryRequest) (userLevelss []*response.UserLevelsResponse, paging paginator.Pagination, err error) { func (_i *userLevelsService) All(clientId *uuid.UUID, req request.UserLevelsQueryRequest) (userLevelss []*response.UserLevelsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req) results, paging, err := _i.Repo.GetAll(clientId, req)
if err != nil { if err != nil {
return return
} }
@ -50,8 +52,8 @@ func (_i *userLevelsService) All(req request.UserLevelsQueryRequest) (userLevels
return return
} }
func (_i *userLevelsService) Show(id uint) (userLevels *response.UserLevelsResponse, err error) { func (_i *userLevelsService) Show(clientId *uuid.UUID, id uint) (userLevels *response.UserLevelsResponse, err error) {
result, err := _i.Repo.FindOne(id) result, err := _i.Repo.FindOne(clientId, id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -59,8 +61,8 @@ func (_i *userLevelsService) Show(id uint) (userLevels *response.UserLevelsRespo
return mapper.UserLevelsResponseMapper(result), nil return mapper.UserLevelsResponseMapper(result), nil
} }
func (_i *userLevelsService) ShowByAlias(alias string) (userLevels *response.UserLevelsResponse, err error) { func (_i *userLevelsService) ShowByAlias(clientId *uuid.UUID, alias string) (userLevels *response.UserLevelsResponse, err error) {
result, err := _i.Repo.FindOneByAlias(alias) result, err := _i.Repo.FindOneByAlias(clientId, alias)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -68,10 +70,14 @@ func (_i *userLevelsService) ShowByAlias(alias string) (userLevels *response.Use
return mapper.UserLevelsResponseMapper(result), nil 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("") _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 { if err != nil {
return nil, err return nil, err
} }
@ -79,32 +85,36 @@ func (_i *userLevelsService) Save(req request.UserLevelsCreateRequest) (userLeve
return saveUserLevelsRes, nil 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).Msg("")
_i.Log.Info().Interface("data", req.ToEntity()).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 { func (_i *userLevelsService) Delete(clientId *uuid.UUID, id uint) error {
result, err := _i.Repo.FindOne(id) result, err := _i.Repo.FindOne(clientId, id)
if err != nil { if err != nil {
return err return err
} }
isActive := false isActive := false
result.IsActive = &isActive 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) { func (_i *userLevelsService) EnableApproval(clientId *uuid.UUID, id uint, isApprovalActive bool) (err error) {
result, err := _i.Repo.FindOne(id) result, err := _i.Repo.FindOne(clientId, id)
if err != nil { if err != nil {
return err return err
} }
*result.IsApprovalActive = isApprovalActive *result.IsApprovalActive = isApprovalActive
return _i.Repo.Update(id, result) return _i.Repo.Update(clientId, id, result)
} }

View File

@ -4,11 +4,13 @@ import (
"web-medols-be/app/database/entity/users" "web-medols-be/app/database/entity/users"
userLevelsRepository "web-medols-be/app/module/user_levels/repository" userLevelsRepository "web-medols-be/app/module/user_levels/repository"
res "web-medols-be/app/module/users/response" 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 { if usersReq != nil {
findUserLevel, _ := userLevelsRepo.FindOne(usersReq.UserLevelId) findUserLevel, _ := userLevelsRepo.FindOne(clientId, usersReq.UserLevelId)
userLevelGroup := "" userLevelGroup := ""
if findUserLevel != nil { if findUserLevel != nil {
userLevelGroup = findUserLevel.AliasName userLevelGroup = findUserLevel.AliasName

View File

@ -1,13 +1,9 @@
package service package service
import ( import (
paseto "aidanwoods.dev/go-paseto"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Nerzal/gocloak/v13"
"github.com/google/uuid"
"github.com/rs/zerolog"
"strings" "strings"
"time" "time"
"web-medols-be/app/database/entity" "web-medols-be/app/database/entity"
@ -20,6 +16,11 @@ import (
"web-medols-be/config/config" "web-medols-be/config/config"
"web-medols-be/utils/paginator" "web-medols-be/utils/paginator"
utilSvc "web-medols-be/utils/service" 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 // UsersService
@ -73,7 +74,7 @@ func (_i *usersService) All(clientId *uuid.UUID, req request.UsersQueryRequest)
} }
for _, result := range results { for _, result := range results {
users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo)) users = append(users, mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId))
} }
return return
@ -85,7 +86,7 @@ func (_i *usersService) Show(clientId *uuid.UUID, id uint) (users *response.User
return nil, err 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) { 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 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) { func (_i *usersService) ShowUserInfo(clientId *uuid.UUID, authToken string) (users *response.UsersResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken) 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) { func (_i *usersService) Save(clientId *uuid.UUID, req request.UsersCreateRequest, authToken string) (userReturn *users.Users, err error) {

View File

@ -13,9 +13,9 @@ body-limit = 1048576000 # "100 * 1024 * 1024"
[db.postgres] [db.postgres]
dsn = "postgresql://medols_user:MedolsDB@2025@38.47.180.165:5432/medols_db" # <driver>://<username>:<password>@<host>:<port>/<database> dsn = "postgresql://medols_user:MedolsDB@2025@38.47.180.165:5432/medols_db" # <driver>://<username>:<password>@<host>:<port>/<database>
log-mode = "NONE" log-mode = "ERROR"
migrate = true migrate = false
seed = true seed = false
[logger] [logger]
log-dir = "debug.log" log-dir = "debug.log"

View File

@ -2417,6 +2417,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "string", "type": "string",
"description": "Period filter (daily, weekly, monthly)", "description": "Period filter (daily, weekly, monthly)",
@ -2485,10 +2492,11 @@ const docTemplate = `{
"required": true "required": true
}, },
{ {
"type": "integer", "type": "string",
"description": "User Level ID filter", "default": "Bearer \u003cAdd access token here\u003e",
"name": "userLevelId", "description": "Insert your access token",
"in": "query" "name": "Authorization",
"in": "header"
} }
], ],
"responses": { "responses": {
@ -2640,6 +2648,25 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "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", "type": "integer",
"name": "count", "name": "count",
@ -2730,10 +2757,11 @@ const docTemplate = `{
"required": true "required": true
}, },
{ {
"type": "integer", "type": "string",
"description": "User Level ID filter", "default": "Bearer \u003cAdd access token here\u003e",
"name": "userLevelId", "description": "Insert your access token",
"in": "query" "name": "Authorization",
"in": "header"
}, },
{ {
"type": "integer", "type": "integer",
@ -2824,6 +2852,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "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", "description": "Submit for approval data",
"name": "req", "name": "req",
@ -2881,6 +2916,13 @@ const docTemplate = `{
"name": "X-Client-Key", "name": "X-Client-Key",
"in": "header", "in": "header",
"required": true "required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
} }
], ],
"responses": { "responses": {
@ -2987,6 +3029,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3052,6 +3101,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3117,6 +3173,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3182,6 +3245,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -6747,6 +6817,13 @@ const docTemplate = `{
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "string", "type": "string",
"name": "category", "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}": { "/articles/{id}": {
"get": { "get": {
"security": [ "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": { "delete": {
"security": [ "security": [
{ {
@ -12353,6 +12549,13 @@ const docTemplate = `{
], ],
"summary": "Get all UserLevels", "summary": "Get all UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "integer", "type": "integer",
"name": "levelNumber", "name": "levelNumber",
@ -12453,12 +12656,26 @@ const docTemplate = `{
], ],
"summary": "Create UserLevels", "summary": "Create UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
"name": "X-Csrf-Token", "name": "X-Csrf-Token",
"in": "header" "in": "header"
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"description": "Required payload", "description": "Required payload",
"name": "payload", "name": "payload",
@ -12510,6 +12727,13 @@ const docTemplate = `{
], ],
"summary": "Get one UserLevels", "summary": "Get one UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "UserLevels Alias", "description": "UserLevels Alias",
@ -12559,6 +12783,13 @@ const docTemplate = `{
], ],
"summary": "EnableApproval Articles", "summary": "EnableApproval Articles",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
@ -12623,6 +12854,13 @@ const docTemplate = `{
], ],
"summary": "Get one UserLevels", "summary": "Get one UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "integer", "type": "integer",
"description": "UserLevels ID", "description": "UserLevels ID",
@ -12670,12 +12908,26 @@ const docTemplate = `{
], ],
"summary": "update UserLevels", "summary": "update UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
"name": "X-Csrf-Token", "name": "X-Csrf-Token",
"in": "header" "in": "header"
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"description": "Required payload", "description": "Required payload",
"name": "payload", "name": "payload",
@ -12732,6 +12984,20 @@ const docTemplate = `{
], ],
"summary": "delete UserLevels", "summary": "delete UserLevels",
"parameters": [ "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", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
@ -14870,6 +15136,9 @@ const docTemplate = `{
"name" "name"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14881,6 +15150,15 @@ const docTemplate = `{
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"requiresApproval": {
"type": "boolean"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/request.ApprovalWorkflowStepRequest"
}
} }
} }
}, },
@ -14891,6 +15169,9 @@ const docTemplate = `{
"name" "name"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14902,6 +15183,9 @@ const docTemplate = `{
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"requiresApproval": {
"type": "boolean"
} }
} }
}, },
@ -14913,6 +15197,9 @@ const docTemplate = `{
"steps" "steps"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14925,6 +15212,9 @@ const docTemplate = `{
"name": { "name": {
"type": "string" "type": "string"
}, },
"requiresApproval": {
"type": "boolean"
},
"steps": { "steps": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
@ -14942,6 +15232,9 @@ const docTemplate = `{
"steps" "steps"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14954,6 +15247,9 @@ const docTemplate = `{
"name": { "name": {
"type": "string" "type": "string"
}, },
"requiresApproval": {
"type": "boolean"
},
"steps": { "steps": {
"type": "array", "type": "array",
"minItems": 1, "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": { "request.CustomStaticPagesCreateRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -2406,6 +2406,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "string", "type": "string",
"description": "Period filter (daily, weekly, monthly)", "description": "Period filter (daily, weekly, monthly)",
@ -2474,10 +2481,11 @@
"required": true "required": true
}, },
{ {
"type": "integer", "type": "string",
"description": "User Level ID filter", "default": "Bearer \u003cAdd access token here\u003e",
"name": "userLevelId", "description": "Insert your access token",
"in": "query" "name": "Authorization",
"in": "header"
} }
], ],
"responses": { "responses": {
@ -2629,6 +2637,25 @@
"in": "header", "in": "header",
"required": true "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", "type": "integer",
"name": "count", "name": "count",
@ -2719,10 +2746,11 @@
"required": true "required": true
}, },
{ {
"type": "integer", "type": "string",
"description": "User Level ID filter", "default": "Bearer \u003cAdd access token here\u003e",
"name": "userLevelId", "description": "Insert your access token",
"in": "query" "name": "Authorization",
"in": "header"
}, },
{ {
"type": "integer", "type": "integer",
@ -2813,6 +2841,13 @@
"in": "header", "in": "header",
"required": true "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", "description": "Submit for approval data",
"name": "req", "name": "req",
@ -2870,6 +2905,13 @@
"name": "X-Client-Key", "name": "X-Client-Key",
"in": "header", "in": "header",
"required": true "required": true
},
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
} }
], ],
"responses": { "responses": {
@ -2976,6 +3018,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3041,6 +3090,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3106,6 +3162,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -3171,6 +3234,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "integer", "type": "integer",
"description": "ArticleApprovalFlows ID", "description": "ArticleApprovalFlows ID",
@ -6736,6 +6806,13 @@
"in": "header", "in": "header",
"required": true "required": true
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"type": "string", "type": "string",
"name": "category", "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}": { "/articles/{id}": {
"get": { "get": {
"security": [ "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": { "delete": {
"security": [ "security": [
{ {
@ -12342,6 +12538,13 @@
], ],
"summary": "Get all UserLevels", "summary": "Get all UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "integer", "type": "integer",
"name": "levelNumber", "name": "levelNumber",
@ -12442,12 +12645,26 @@
], ],
"summary": "Create UserLevels", "summary": "Create UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
"name": "X-Csrf-Token", "name": "X-Csrf-Token",
"in": "header" "in": "header"
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"description": "Required payload", "description": "Required payload",
"name": "payload", "name": "payload",
@ -12499,6 +12716,13 @@
], ],
"summary": "Get one UserLevels", "summary": "Get one UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "UserLevels Alias", "description": "UserLevels Alias",
@ -12548,6 +12772,13 @@
], ],
"summary": "EnableApproval Articles", "summary": "EnableApproval Articles",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
@ -12612,6 +12843,13 @@
], ],
"summary": "Get one UserLevels", "summary": "Get one UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "integer", "type": "integer",
"description": "UserLevels ID", "description": "UserLevels ID",
@ -12659,12 +12897,26 @@
], ],
"summary": "update UserLevels", "summary": "update UserLevels",
"parameters": [ "parameters": [
{
"type": "string",
"description": "Client Key",
"name": "X-Client-Key",
"in": "header",
"required": true
},
{ {
"type": "string", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
"name": "X-Csrf-Token", "name": "X-Csrf-Token",
"in": "header" "in": "header"
}, },
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header"
},
{ {
"description": "Required payload", "description": "Required payload",
"name": "payload", "name": "payload",
@ -12721,6 +12973,20 @@
], ],
"summary": "delete UserLevels", "summary": "delete UserLevels",
"parameters": [ "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", "type": "string",
"description": "Insert the X-Csrf-Token", "description": "Insert the X-Csrf-Token",
@ -14859,6 +15125,9 @@
"name" "name"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14870,6 +15139,15 @@
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"requiresApproval": {
"type": "boolean"
},
"steps": {
"type": "array",
"items": {
"$ref": "#/definitions/request.ApprovalWorkflowStepRequest"
}
} }
} }
}, },
@ -14880,6 +15158,9 @@
"name" "name"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14891,6 +15172,9 @@
}, },
"name": { "name": {
"type": "string" "type": "string"
},
"requiresApproval": {
"type": "boolean"
} }
} }
}, },
@ -14902,6 +15186,9 @@
"steps" "steps"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14914,6 +15201,9 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"requiresApproval": {
"type": "boolean"
},
"steps": { "steps": {
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
@ -14931,6 +15221,9 @@
"steps" "steps"
], ],
"properties": { "properties": {
"autoPublish": {
"type": "boolean"
},
"description": { "description": {
"type": "string" "type": "string"
}, },
@ -14943,6 +15236,9 @@
"name": { "name": {
"type": "string" "type": "string"
}, },
"requiresApproval": {
"type": "boolean"
},
"steps": { "steps": {
"type": "array", "type": "array",
"minItems": 1, "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": { "request.CustomStaticPagesCreateRequest": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -112,6 +112,8 @@ definitions:
type: object type: object
request.ApprovalWorkflowsCreateRequest: request.ApprovalWorkflowsCreateRequest:
properties: properties:
autoPublish:
type: boolean
description: description:
type: string type: string
isActive: isActive:
@ -120,12 +122,20 @@ definitions:
type: boolean type: boolean
name: name:
type: string type: string
requiresApproval:
type: boolean
steps:
items:
$ref: '#/definitions/request.ApprovalWorkflowStepRequest'
type: array
required: required:
- description - description
- name - name
type: object type: object
request.ApprovalWorkflowsUpdateRequest: request.ApprovalWorkflowsUpdateRequest:
properties: properties:
autoPublish:
type: boolean
description: description:
type: string type: string
isActive: isActive:
@ -134,12 +144,16 @@ definitions:
type: boolean type: boolean
name: name:
type: string type: string
requiresApproval:
type: boolean
required: required:
- description - description
- name - name
type: object type: object
request.ApprovalWorkflowsWithStepsCreateRequest: request.ApprovalWorkflowsWithStepsCreateRequest:
properties: properties:
autoPublish:
type: boolean
description: description:
type: string type: string
isActive: isActive:
@ -148,6 +162,8 @@ definitions:
type: boolean type: boolean
name: name:
type: string type: string
requiresApproval:
type: boolean
steps: steps:
items: items:
$ref: '#/definitions/request.ApprovalWorkflowStepRequest' $ref: '#/definitions/request.ApprovalWorkflowStepRequest'
@ -160,6 +176,8 @@ definitions:
type: object type: object
request.ApprovalWorkflowsWithStepsUpdateRequest: request.ApprovalWorkflowsWithStepsUpdateRequest:
properties: properties:
autoPublish:
type: boolean
description: description:
type: string type: string
isActive: isActive:
@ -168,6 +186,8 @@ definitions:
type: boolean type: boolean
name: name:
type: string type: string
requiresApproval:
type: boolean
steps: steps:
items: items:
$ref: '#/definitions/request.ApprovalWorkflowStepRequest' $ref: '#/definitions/request.ApprovalWorkflowStepRequest'
@ -626,6 +646,40 @@ definitions:
- stepOrder - stepOrder
- workflowId - workflowId
type: object 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: request.CustomStaticPagesCreateRequest:
properties: properties:
description: description:
@ -2906,6 +2960,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID - description: ArticleApprovalFlows ID
in: path in: path
name: id name: id
@ -2948,6 +3007,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID - description: ArticleApprovalFlows ID
in: path in: path
name: id name: id
@ -2990,6 +3054,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID - description: ArticleApprovalFlows ID
in: path in: path
name: id name: id
@ -3032,6 +3101,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: ArticleApprovalFlows ID - description: ArticleApprovalFlows ID
in: path in: path
name: id name: id
@ -3074,6 +3148,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: Period filter (daily, weekly, monthly) - description: Period filter (daily, weekly, monthly)
in: query in: query
name: period name: period
@ -3117,10 +3196,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- description: User Level ID filter - default: Bearer <Add access token here>
in: query description: Insert your access token
name: userLevelId in: header
type: integer name: Authorization
type: string
responses: responses:
"200": "200":
description: OK description: OK
@ -3215,6 +3295,19 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
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 - in: query
name: count name: count
type: integer type: integer
@ -3270,10 +3363,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- description: User Level ID filter - default: Bearer <Add access token here>
in: query description: Insert your access token
name: userLevelId in: header
type: integer name: Authorization
type: string
- in: query - in: query
name: count name: count
type: integer type: integer
@ -3329,6 +3423,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: Submit for approval data - description: Submit for approval data
in: body in: body
name: req name: req
@ -3366,6 +3465,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
responses: responses:
"200": "200":
description: OK description: OK
@ -5623,6 +5727,11 @@ paths:
name: X-Client-Key name: X-Client-Key
required: true required: true
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- in: query - in: query
name: category name: category
type: string type: string
@ -6301,6 +6410,48 @@ paths:
summary: Viewer Articles Thumbnail summary: Viewer Articles Thumbnail
tags: tags:
- Articles - 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: /cities:
get: get:
description: API for getting all Cities description: API for getting all Cities
@ -6539,6 +6690,42 @@ paths:
summary: Get Client Approval Settings summary: Get Client Approval Settings
tags: tags:
- ClientApprovalSettings - 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: put:
description: API for updating client approval settings description: API for updating client approval settings
parameters: parameters:
@ -9188,6 +9375,11 @@ paths:
get: get:
description: API for getting all UserLevels description: API for getting all UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- in: query - in: query
name: levelNumber name: levelNumber
type: integer type: integer
@ -9249,10 +9441,20 @@ paths:
post: post:
description: API for create UserLevels description: API for create UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- description: Insert the X-Csrf-Token - description: Insert the X-Csrf-Token
in: header in: header
name: X-Csrf-Token name: X-Csrf-Token
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: Required payload - description: Required payload
in: body in: body
name: payload name: payload
@ -9285,6 +9487,16 @@ paths:
delete: delete:
description: API for delete UserLevels description: API for delete UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: Insert the X-Csrf-Token - description: Insert the X-Csrf-Token
in: header in: header
name: X-Csrf-Token name: X-Csrf-Token
@ -9323,6 +9535,11 @@ paths:
get: get:
description: API for getting one UserLevels description: API for getting one UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- description: UserLevels ID - description: UserLevels ID
in: path in: path
name: id name: id
@ -9353,10 +9570,20 @@ paths:
put: put:
description: API for update UserLevels description: API for update UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- description: Insert the X-Csrf-Token - description: Insert the X-Csrf-Token
in: header in: header
name: X-Csrf-Token name: X-Csrf-Token
type: string type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- description: Required payload - description: Required payload
in: body in: body
name: payload name: payload
@ -9394,6 +9621,11 @@ paths:
get: get:
description: API for getting one UserLevels description: API for getting one UserLevels
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- description: UserLevels Alias - description: UserLevels Alias
in: path in: path
name: alias name: alias
@ -9425,6 +9657,11 @@ paths:
post: post:
description: API for Enable Approval of Article description: API for Enable Approval of Article
parameters: parameters:
- description: Client Key
in: header
name: X-Client-Key
required: true
type: string
- description: Insert the X-Csrf-Token - description: Insert the X-Csrf-Token
in: header in: header
name: X-Csrf-Token name: X-Csrf-Token

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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": "<p>A comprehensive look at the latest AI breakthrough that could change everything</p>",
"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": "<p>Comprehensive review of the latest smartphone</p>",
"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": "<p>Breaking news about major technology acquisition</p>",
"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.*